怎么写“联动”问卷答题?介绍一种优雅的写法咯

154 阅读2分钟

github
阅读文档

当你拿到一个需求,要对接第三方的问卷题库,并且题库内容是要根据用户的回答选项控制联动显示或隐藏,也就是不同的回答会导向不同的问题组合。

你在阅读了第三方文档后,发现在选项上有两个特殊的字段:互斥id集合、联动id集合,都是uuid。于是你动手开始定义如下数据结构。

export const QuestionOption = typedef({
  id: string,
  name: string,
  description: string,
  picture: string,
  mutexIds: array as TypeDesc<array<typeof string>>,
  linkIds: array as TypeDesc<array<typeof string>>,
  show: bool_1,
})

export const Question = typedef({
  id: string,
  name: string,
  description: string,
  type: string as TypeDesc<QuestionType>,
  options: array as TypeDesc<array<typeof QuestionOption>>,
  show: bool_1,

  answer: string,
})

export const Questionnaire = typedef({
  id: string,
  name: string,
  description: string,
  questions: array as TypeDesc<array<typeof Question>>,
})

虽然只是定义了3个静态数据结构,但已经啥也不缺了,近乎完成了。小白也能看得懂。

于是你找到两个小弟,“法外狂徒”张三李四。你让张三去写页面UI和答题交互,同时让李四去对接API拉取适配数据,你安排的井然有序,各司其职,互不影响。在张三和李四干活的同时,你继续写剩余的“联动”答题,这部分存在与否都不影响张三李四的工作,所以后续你们三人没有任何交集了,也就不需要沟通扯皮了。

为了达到这个目的,你就想到了张三在写答题交互时肯定会对answer字段赋值,所以你只需要观察answer字段的变更,根据answer动态调整问题和选项的show调整显示或隐藏,就能实现“联动”效果了。

于是你写出如下代码,当问卷的问题数组更新时,自动对所有题目构建联动规则。(因为李四在对接接口时,肯定会对questions字段赋值。)

/**
 * 定义联动规则
 */
ruledef(
  Questionnaire,
  'questions',
  {
    questions: true,
  },
  (self) => {
    type Item = { id: string; show: bool }
    const refMap = new Map<string, Item>()

    function addItem(item: Item) {
      if (refMap.has(item.id)) {
        throw Error(`duplicate id: ${item.id}`)
      }
      refMap.set(item.id, item)
    }

    function setShow(id: string, show: bool) {
      const item = refMap.get(id)
      if (item) {
        item.show = show
      }
    }

    const DelegateQuestion = typedef({
      ref: Question,
    })
    ruledef(
      DelegateQuestion,
      'answer',
      {
        ref: {
          answer: true,
        },
      },
      ({ ref: self }) => {
        if (!self.options.length) {
          return
        }

        self.options.forEach((option) => {
          // 重置互斥为显示
          option.mutexIds.forEach((id) => setShow(id, true))
          // 重置联动为隐藏
          option.linkIds.forEach((id) => setShow(id, false))
        })

        const ids = self.answer.split(',') // 获取选中的ID数组
        // 根据选中ID更新互斥和联动
        ids.forEach((id) => {
          self.options.forEach((option) => {
            if (id === option.id) {
              // 设置互斥为隐藏
              option.mutexIds.forEach((id) => setShow(id, false))
              // 设置联动为显示
              option.linkIds.forEach((id) => setShow(id, true))
            }
          })
        })
      },
    )

    self.questions.forEach((item) => {
      typeinit(DelegateQuestion, { ref: item })
      addItem(item)
      item.options.forEach(addItem)
    })
  },
)

你在上面代码中,遍历了问题数组,对每项问题都借助委托方式定义了对字段answer的观察规则,只要张三对answer赋值,就一定会触发规则。

在规则中,你先把所有互斥项重置为显示、联动项重置为隐藏,然后获取选中的id数组,再次遍历把选中答案所关联的互斥项设置为隐藏、联动项设置为显示。

至此,就写完收工了,张三和李四应该也写完了。在张三李四看来只有纯粹的数据结构,只赋值而已,任何人都能懂会用,毫无感知有规则。

完整代码如下:

import {
  array,
  bool,
  ruledef,
  string,
  typedef,
  typeinit,
  type TypeDesc,
} from 'imsure'

const bool_1 = typedef({
  '@type': bool,
  '@value': () => true,
})

export enum QuestionType {
  text = 'text',
  number = 'number',
  radio = 'radio',
  checkbox = 'checkbox',
}

export const QuestionOption = typedef({
  id: string,
  name: string,
  description: string,
  picture: string,
  mutexIds: array as TypeDesc<array<typeof string>>,
  linkIds: array as TypeDesc<array<typeof string>>,
  show: bool_1,
})

export const Question = typedef({
  id: string,
  name: string,
  description: string,
  type: string as TypeDesc<QuestionType>,
  options: array as TypeDesc<array<typeof QuestionOption>>,
  show: bool_1,

  answer: string,
})

export const Questionnaire = typedef({
  id: string,
  name: string,
  description: string,
  questions: array as TypeDesc<array<typeof Question>>,
  '@init': (self) => {
    self.questions = self.questions
  },
})

/**
 * 定义联动规则
 */
ruledef(
  Questionnaire,
  'questions',
  {
    questions: true,
  },
  (self) => {
    type Item = { id: string; show: bool }
    const refMap = new Map<string, Item>()

    function addItem(item: Item) {
      if (refMap.has(item.id)) {
        throw Error(`duplicate id: ${item.id}`)
      }
      refMap.set(item.id, item)
    }

    function setShow(id: string, show: bool) {
      const item = refMap.get(id)
      if (item) {
        item.show = show
      }
    }

    const DelegateQuestion = typedef({
      ref: Question,
    })
    ruledef(
      DelegateQuestion,
      'answer',
      {
        ref: {
          answer: true,
        },
      },
      ({ ref: self }) => {
        if (!self.options.length) {
          return
        }

        self.options.forEach((option) => {
          // 重置互斥为显示
          option.mutexIds.forEach((id) => setShow(id, true))
          // 重置联动为隐藏
          option.linkIds.forEach((id) => setShow(id, false))
        })

        const ids = self.answer.split(',') // 获取选中的ID数组
        // 根据选中ID更新互斥和联动
        ids.forEach((id) => {
          self.options.forEach((option) => {
            if (id === option.id) {
              // 设置互斥为隐藏
              option.mutexIds.forEach((id) => setShow(id, false))
              // 设置联动为显示
              option.linkIds.forEach((id) => setShow(id, true))
            }
          })
        })
      },
    )

    self.questions.forEach((item) => {
      typeinit(DelegateQuestion, { ref: item })
      addItem(item)
      item.options.forEach(addItem)
    })
  },
)