当你拿到一个需求,要对接第三方的问卷题库,并且题库内容是要根据用户的回答选项控制联动显示或隐藏,也就是不同的回答会导向不同的问题组合。
你在阅读了第三方文档后,发现在选项上有两个特殊的字段:互斥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)
})
},
)