如果你点开了这篇文章,相信你是一个有经验的前端开发了,造轮子可能已经不能满足我们的成就感了。想当初我吭哧吭哧做了一套Angular的组件库别提多开心了,但是毕竟是自己一个人短期内开发出来的,也只有input,select之类的几个基础组件,前端小伙伴又凭什么宁愿放弃现有的成熟的组件库,转而用我的呢?既然AntDesign这样的组件库已经如此成熟,我又何必班门弄斧?但我发现一个夹缝求生的地方,像AntDesign,SemiDesign,MuiDesign这类组件库,本质是定义的样式,很多组件拿到项目中,还是需要封装一遍,尤其是做简化代码甚至低代码的工作中,将现有的组件库封装成一个可配置化的组件库就尤为重要了。
我们在这篇文章中介绍了封装表单组件需要准备哪些属性:封装表单元素,应该定义哪些状态或属性?。我们可以知道有一些表单元素有共同的属性,有一些有特异性,比如单行文本输入框text input,数字输入框number input,和多行文本输入框textarea都需要监听输入的变化,而select和autocomplete都是以选项的变化来改变值的。
本着"高内聚,低耦合"的宗旨,我们将封装分为不同的层级,这里我用不同的目录层级来表示,可以参考右边的大纲。不常用的组件后期我会持续更新,感兴趣可以先收藏。
FreeForm
在form层面,也就是field的外层需要对单个field处理如下属性:
- 【值属性】根据field的
getDefaultValue()
计算好初始值。传递defaultValue
。默认值属性相互之间是有冲突的,后面我会详细设计这其中的优先级。 - 【值属性】如果组件是初始无值的状态,将
defaultValue
赋值给value
。如果组件已经有值,则作为value
。传递value
。 - 【值属性】根据field的
onChange()
改变value
。 - 【值属性】根据field的
onInputChange()
改变inputValue
。 - 【值属性】传递
rootFormValue
。 - 【值属性】传递哪个form里哪个field的值变化了。
- 【控制类属性】根据field的
enabledOn
计算enabled
状态。传递enabled
。 - 【控制类属性】根据field的
visibleOn
计算visible
状态。传递visible
。 - 【控制类属性】根据field的
availableOn
计算available
状态。传递available
。 - 【控制类属性】根据field的
requiredOn
计算required
状态。传递required
。 - 【用户行为类属性】根据field的
onBlur()
改变touched
状态。传递touched
。 - 【用户行为类属性】根据field的
onChange()
改变dirty
状态。传递dirty
。 - 【用户行为类属性】根据field的
onChange()
时value
是否等同于defaultValue
来计算different
状态。传递different
。 - 【用户行为类属性】传递
clearable
,当clearable
为true
时,根据field的onClear()
将value
清空。 - 【用户行为类属性】传递
resettable
,当resettable
为true
时,根据field的onReset()
将value
,inputValue
,touched
,dirty
重置。 - 【用户行为类属性】传递
deletable
,当deletable
为true
时,根据field的onDelete()
将available
改为false
,意味着这个field
不可用,对应的字段将被移除。 - 【辅助类属性】传递
label
,key
,title
,disabledReason
(disabled时,title选用展示disableReason),helperText
,placeholder
。 - 【辅助类属性】传递
path
,当有嵌套form时,path
将帮助我们快速定位到field,比如:univerity.department.class.student.name
。 - 【样式类属性】传递
className
,width
,size
,tabIndex
。 - 【校验类属性】传递
validations
。 - 【校验类属性】传递
errors
。 - 【多组件组合】需要支持多field+button组合,如下图:
- 【辅助类属性】传递
label
。 - 【辅助类属性】传递
key
/name
。 - 【辅助类属性】传递
title
。 - 【辅助类属性】传递
helperText
。 - 【辅助类属性】传递
placeholder
。
jsx
const { type, getDefaultValue } = props;
const defaultValue = getDefaultValue(...);
const [value, setValue] = useState(defaultValue);
const [inputValue, setInputValue] = useState(defaultValue);
const enabled = useMemo(() => enabledOn(), [...]);
const visible = useMemo(() => visibleOn(), [...]);
const available = useMemo(() => availableOn(), [...]);
const required = useMemo(() => requiredOn(), [...]);
const [touched, setTouched] = useState(false);
const [dirty, setDirty] = useState(false);
const [different, setDifferent] = useState(false);
const handleChange = (value) => {
setValue(value);
setTouched(true);
setDifferent(!isEqual(value, defaultValue));
}
return <FormControl>
<BaseField
type={type}
value={value}
defaultValue={defaultValue}
label={label}
key={key}
title={title}
disableReason={disableReason}
helperText={helperText}
placeholder={placeholder}
path={path}
className={classname}
width={width} // '200px', '80%'
size={size} // 'sm', 'md', 'lg'
validations={validations}
tabIndex={tabIndex}
onChange={handleChange}
onInputChange={setInputValue}
{...(clearable && {onClear: handleClear})} // 将value清空
{...(resettable && {onClear: handleReset})} // 将value重置为最开始的值(不一定是defaultValue)
{...(deletable && {onClear: handleDelete})} // availble改为false
{...(mentionable && onMention: handleMention)} // @某人
onValidated={handleValidated}
/>
</FormControl>
BaseField
多个BaseField组成FreeForm。面向的是form,如果是面向开发者/使用者,参考下面的FreeFormField。
jsx
<BaseField .../>
- 【值属性】传递value。
- 【值属性】传递defaultValue。
- 【校验类属性】接受validations。将validations的结果通过
onValidated()
返回给form。validations和对应的errors的属性定义见这一篇:正在写。。。 - 【校验类属性】传递
errors
。对单独的field显示错误信息有两种方式,一个是传给原生组件,一个是封装自己的错误样式,这取决于我们用什么样的组件库。validations和对应的errors的属性定义见这一篇:正在写。。。 - 【校验类属性】传递下面各类field所需的校验类属性。
1. BaseTypeable
打字型输入。这是最基本的组件,后面很多组件都需要这个组件。
jsx
<BaseField type="text" .../> ==> <BaseTypeable type="text" .../>
<BaseField type="number" .../> ==> <BaseTypeable type="number" .../>
<BaseField type="textarea" .../> ==> <BaseTypeable type="textarea" .../>
<BaseField type="password" .../> ==> <BaseTypeable type="password" .../>
继承以上BaseField的所有属性以外,还可以:
- 【值属性】【性能】支持debounce。value change可选择防抖,比如用户type一个字符约1s后改变field的值。
- 需要支持前缀后缀,如下图:
- 【用户行为类】如果deletable为true,提供一个按钮点击返回onDelete()。(clear,reset,delete行为仅返回event,field内部不做值的处理,值的处理在form内,field外处理。)
1-1. BaseText
单行文本输入框。
js
<BaseTypeable type="text" .../> ==> <BaseText .../>
继承以上BaseTypeable的所有属性以外,还可以:
- 【校验类属性】字数限制,接受
minLength
和maxLength
,传递给adaptor。这两个属性是原生属性,我们尽量使用原生功能,更高效。 - 【校验类属性】字数显示/倒计数,如下图:
- 【校验类属性】接受
pattern
,传递给adaptor。 - 【用户行为类属性】如果
clearable
为true
,提供一个按钮点击返回onClear()
。 - 【用户行为类属性】如果
resettable
为true
,提供一个按钮点击返回onReset()
。 - 【用户行为类属性】语音输入。(如果你想做得狂拽炫酷吊炸天可以试试)
- 【用户行为类属性】接受
mode
,适用于移动端的原生属性。 - 【值属性】接受
format
1-2. BaseNumber
数字输入框。有加减按钮。
即:
js
<BaseTypeable type="number" .../> ===> <BaseNumber .../>
继承以上BaseTypeable的所有属性以外,还可以:
- 【控制类属性】可通过键盘控制增加减少,大多数组件库本来就支持。
- 【校验类属性】接受
max
,min
,和step
,原生功能。 - 【校验类属性】接受
numberType
:integer
,decimal
, - 【值属性】接受
format
格式化数字,比如货币,科学计数法,电话号码,银行卡号,如下图: - 【值属性】支持高精度,如下图,封装的时候需要装BigInt polyfill。
1-3. BasePassword
密码输入框,可以选择显示明文密码。如下图:
js
<BaseTypeable type="password" .../> ===> <BasePassword .../>
继承以上BaseTypeable的所有属性以外,还可以:
- 【用户行为类属性】是否可以显示明文密码。 密码的报错信息可能不同于其他field,需要显示校验列表,并标出哪些满足了哪些不满足。这部分参见Password组件。
1-4. BaseTextarea
多行文本输入框。
js
<BaseTypeable type="textarea" .../> ===> <BaseTextarea .../>
继承以上BaseTypeable的所有属性以外,还可以:
- 【校验类属性】字数限制,接受
minLength
和maxLength
,传递给adaptor。这两个属性是原生属性,我们尽量使用原生功能,更高效。 - 【校验类属性】字数显示/倒计数,类似于BaseText。
- 【样式类属性】接受
rows
,默认显示多少行。 - 【样式类属性】接受
resizable
,可以自定义输入框大小。 - 【用户行为类属性】可以mention@某人,mention的属性参考下面BaseMention组件。
1-5. BaseTags
tag输入框。如下图:
2. BaseBinary
Boolean类的输入。也是基础组件,后面很多复合组件也会用到这里的组件。
js
<BaseField type="radio" .../> ==> <BaseBinary type="radio" .../>
<BaseField type="checkbox" .../> ==> <BaseBinary type="checkbox" .../>
<BaseField type="switch" .../> ==> <BaseBinary type="switch" .../>
继承以上BaseField的所有属性以外,还可以:
- 【值属性】处理change event里的
checked
,改为value
。 - 【值属性】常见多个binary field组合成一个selectable 组件,这里值的处理需要符合selectable field的用法。
- 【辅助类属性】
label
是指这个field的label,而binary的单个field也有它们自己的label,具体见下面各个field。传递binary label。
2-1. BaseRadio
单个Radio。一旦勾选无法取消。
js
<BaseBinary type="radio" .../> ===> <BaseRadio .../>
继承以上BaseBinary的所有属性以外,还可以:
- 【值属性】返回值为
boolean
。 - 【辅助类属性】接受
optionLabel
,作为这一个选项的label。
2-2. BaseCheckbox
单个Checkbox。可勾选可取消,还有一个中间态(依然放在binary里面,因为中间态是给treeSelect这种复合组件用的)。 单个Radio。一旦勾选无法取消。
js
<BaseBinary type="checkbox" .../> ===> <BaseCheckbox .../>
继承以上BaseBinary的所有属性以外,还可以:
- 【值属性】接受
ternary
,当ternary
为true
时,值有三种状态,用数字0,1,2表示。否则返回值为boolean
。 - 【辅助类属性】接受
optionLabel
,作为这一个选项的label。
2-3. BaseSwitch
Switch。如下图:
js
<BaseBinary type="switch" .../> ===> <BaseSwitch .../>
继承以上BaseBinary的所有属性以外,还可以:
- 【值属性】返回值为
boolean
。 - 【辅助类属性】接受
labelLeft
,作为显示在左边的label。 - 【辅助类属性】接受
labelRight
,作为显示在右边的label。大多数情况,只需要labelRight
就可以了。
3. BaseSelectable
选择类的输入。属于复合组件了,有些会用到上面的基础组件。为了方便TypeScript定义类型,我将带有下拉框的组件分为单选和多选。
js
<BaseField type="select" .../> ==> <BaseSelectable type="select" .../>
<BaseField type="autocomplete" .../> ==> <BaseSelectable type="autocomplete" .../>
// <BaseField type="cascader" .../> ==> <BaseSelectable type="cascader" .../> 待定
<BaseField type="treeSelect" .../> ==> <BaseSelectable type="treeSelect" .../>
<BaseField type="radios" .../> ==> <BaseSelectable type="radios" .../>
<BaseField type="checkboxes" .../> ==> <BaseSelectable type="checkboxes" .../>
继承以上BaseField的所有属性以外,还可以:
- 【值属性】单选的值为单个值,多选的值为一个数组。
- 【值属性】接受value,并计算出选项中与之同等的值,即
selectedOption
/selectedOptions
。 - 【值属性】接受
allowValueOutOfValue
,当这个属性为true
时,value改变后必须是选项中的一个,否则会被还原为上一次的值。考虑到后台可能会有脏数据,value在没做任何改变时还是可以照本来的value显示。 - 【值属性】接受
getDefaultValue()
,这个方法决定了当某些依赖项(如某些其他field的值,选项)改变时,这个field默认改变value。 - 【值属性】接受
isOptionEqualToValue()
,用这个方法通过value选择对应的option。 - 【选项类属性】选项类属性较为复杂,请参考这一篇文章:正在写。。。
3-1. BaseDropdown
带下拉框的选择组件。
js
<BaseField type="dropdown" multiple={true} .../> ==> <BaseSingleDropdown .../>
<BaseField type="dropdown" multiple={false} .../> ==> <BaseMultiDropdown .../>
继承以上BaseSelectable的所有属性以外,还可以:
- 【性能】虚拟列表
- 【选项类属性】异步load options时,会有输入状态告知用户。
3-1-1. BaseSingleDropdown
带下拉框的单选组件。
js
<BaseSingleDropdown .../>
继承以上BaseDropdown的所有属性以外,还可以:
- 【值属性】当
clearable
为true
时,选项中第一位会为一个带placeholder的空选项:
js
// options[0]
{
label: '-- Select --',
value: ''
}
这样用户选择了某些值之后,还可以通过选择这个值来清空。
- 【值属性】接受
defaultSelectFirst
,当这个属性为true
时,表示初始状态或选项更新后,默认选择第一个选项。
3-1-2. BaseMultiDropdown
带下拉框的多选组件。
js
<BaseMultiDropdown .../>
继承以上BaseDropdown的所有属性以外,还可以:
- 【样式类属性】接受
limitTags
参数,定义在输入框里显示多少个选项。 - 【值属性】接受
defaultSelectAll
,意味着默认全选。
3-2. BaseAutocomplete
带下拉框和自动填充的选择组件。
js
<BaseField type="autocomplete" multiple={true} .../> ==> <BaseSingleAutocomplete .../>
<BaseField type="autocomplete" multiple={false} .../> ==> <BaseMultiAutocomplete .../>
继承以上BaseSelectable的所有属性以外,还可以:
- 【值属性】当
allowValueOutOfValue
为true
时,用户可以自由输入。 - 【值属性】接受
format
,当用户可以自由输入时,如果这个组件值的类型较复杂,可以通过这个方法得到符合格式要求的值。 - 【选项类属性】接受
optionAddable
,当用户自由输入了一个新值后,可以把这个新值加入到option list中(异步操作会发送请求,同步操作会存到状态里)。 - 【选项类属性】异步load options时,会有输入状态告知用户。既可以初始时一次性异步获取options,也可以随着输入异步获取options。
3-2-1. BaseSingleAutocomplete
带下拉框和自动填充的单选组件。
js
<BaseSingleAutocomplete .../>
继承以上BaseAutocomplete的所有属性以外,还可以:
- 【值属性】接受clearable,当这个属性为true时,选项中第一位会为一个带placeholder的空选项:
js
// options[0]
{
label: '-- Select --',
value: ''
}
这样用户选择了某些值之后,还可以通过选择这个值来清空。
- 【值属性】接受
defaultSelectFirst
,当这个属性为true
时,表示初始状态或选项更新后,默认选择第一个选项。
3-2-2. BaseMention
@功能,是由BaseSingleAutocomplete变换而成,type"@"后,随着输入可以异步查询人名。一般常用在Textarea里。如下图:
js
<BaseMention .../>
继承以上BaseSingleAutocomplete的大部分属性以外,还可以:
- 【选项类属性】异步获取人员信息,像Jira一样。
- 【值属性】选择好后,@后面其实只是可编辑可删除的文本。
3-2-3. BaseMultiAutocomplete
带下拉框和自动填充的多选组件。
js
<BaseMultiAutocomplete .../>
继承以上BaseAutocomplete的所有属性以外,还可以:
- 【样式类属性】接受
limitTags
参数,定义在输入框里显示多少个选项。 - 【值属性】接受
defaultSelectAll
,意味着默认全选。
3-3. BaseRadios
Radio组合的单选组件。
js
<BaseField type="radios" .../> ==> <BaseRadios .../>
继承以上BaseSelectable的所有属性以外,还可以:
- 【选项类属性】接受'optionsDirection',定义options的排列方向,是横向还是垂直,
row
还是column
。
3-4. BaseCheckboxes
Checkbox组合的多选组件。
js
<BaseField type="checkboxes" .../> ==> <BaseCheckboxes .../>
继承以上BaseSelectable的所有属性以外,还可以:
- 【选项类属性】接受'optionsDirection',定义options的排列方向,是横向还是垂直,
row
还是column
。
3-5. BaseTreeDropdown
下拉框是树组件的选择组件。
js
<BaseField type="tree" multiple={false} .../> ==> <BaseSingleTreeDropdown .../>
<BaseField type="tree" multiple={true} .../> ==> <BaseMultiTreeDropdown .../>
继承以上BaseSelectable的所有属性以外,还可以:
- 【选项类属性】接受searchable,类似autocomplete组件,可以将带有关键字的节点筛选出来。
- 【选项类属性】父节点展开时可以定义
lazyload
,异步获取子节点。
3-5-1. BaseTree
基础的树组件,独立存在,不在下拉框里。如果BaseTreeDropdown的searchable
为false
,则选用这个组件。
js
<BaseSingleTreeDropdown searchable={false}.../> 封装 <BaseSearchTree .../>
<BaseMultiTreeDropdown searchable={false}.../> 封装 <BaseSearchTree .../>
树组件需要带有select功能,具体设计参考这篇文章:正在写。。。
3-5-2. BaseSearchTree
带搜索功能的树组件,独立存在,不在下拉框里。将BaseTree与搜索框封装在了一起。如果BaseTreeDropdown的searchable
为true
,则选用这个组件。
js
<BaseSingleTreeDropdown searchable={true}.../> 封装 <BaseSearchTree .../>
<BaseMultiTreeDropdown searchable={true}.../> 封装 <BaseSearchTree .../>
3-5-3. BaseSingleTreeDropdown
下拉框是树组件的单选组件。
js
<BaseSingleTreeDropdown .../>
3-5-4. BaseMultiTreeDropdown
下拉框是树组件的多选组件。
js
<BaseMultiTreeDropdown .../>
4. BaseFile
文件上传组件。可以通过打开文件系统选择,也可以拖拽上传,也可以组合起来。
js
<BaseField type="fileSelect" .../> ==> <BaseFile type="fileSelect" .../>
<BaseField type="fileArea" .../> ==> <BaseFile type="fileArea" .../>
<BaseField type="fileReview" .../> ==> <BaseFile type="fileReview" .../>
继承以上BaseField的所有属性以外,还可以:
- 【值属性】如果是已有值,需要用到BaseFileReview。
- 【值属性】定义file的值,需要区分文件本身和文件名。
- 【值属性】接受
multiple
,可以选择单文件还是多文件上传。 - 【校验类属性】接受
accept
,限制上传的文件类型。 - 【校验类属性】接受
maxSize
,限制上传的文件大小。 - 【用户行为类属性】上传的方式,文件切片,是否可以中止或继续。
- 【样式类属性】上传过程中,是进度条还是圆环。
4-1. BaseFileSelect
输入框式的文件选择器。
js
<BaseFile type="fileSelect" .../> ==> <BaseFileSelect .../>
继承以上BaseFile的所有属性
4-2. BaseFileArea
Drag & Drop式的文件上传。
js
<BaseFile type="fileArea" .../> ==> <BaseFileArea .../>
继承以上BaseFile的所有属性以外,还可以:
- 【样式类属性】接受
size
,拖拽区域的大小。
4-3. BaseFileReview
上传后的文件预览。这个组件不属于formField组件,仅用来展示。
js
<BaseFile type="fileReview" .../> ==> <BaseFileReview .../>
5. BaseMoment
日期时间选择组件。
js
<BaseField type="date" .../> ==> <BaseMoment type="date" .../>
<BaseField type="week" .../> ==> <BaseMoment type="week" .../>
<BaseField type="month" .../> ==> <BaseMoment type="month" .../>
<BaseField type="time" .../> ==> <BaseMoment type="time" .../>
<BaseField type="datetime" .../> ==> <BaseMoment type="datetime" .../>
继承以上BaseField的所有属性以外,还可以:
- 【值属性】接受
format
,比如:'DD MMM YYYY'
。
5-1. BaseDate
选择日期。
js
<BaseMoment type="date" .../> ==> <BaseDate .../>
继承以上BaseMoment的所有属性
5-2. BaseWeek
选择周。
js
<BaseMoment type="week" .../> ==> <BaseWeek .../>
继承以上BaseMoment的所有属性
5-3. BaseMonth
选择月。
js
<BaseMoment type="month" .../> ==> <BaseMonth .../>
继承以上BaseMoment的所有属性
5-4. BaseTime
选择时间。
js
<BaseMoment type="time" .../> ==> <BaseTime .../>
继承以上BaseMoment的所有属性
5-5. BaseDateTime
选择日期和时间。
js
<BaseMoment type="datetime" .../> ==> <BaseDateTime .../>
继承以上BaseMoment的所有属性
6. BaseRange
范围类组件,field值会是包含两个值的数组。
js
<BaseField type="dateRange" .../> ==> <BaseRange type="dateRange" .../>
<BaseField type="numberRange" .../> ==> <BaseRange type="numberRange" .../>
继承以上BaseField的所有属性
6-1. BaseDateRange
日期范围。
js
<BaseRange type="dateRange" .../> ==> <BaseDateRange />
继承以上BaseRange的所有属性
6-2. BaseNumberRange
日期范围。
js
<BaseRange type="numberRange" .../> ==> <BaseNumberRange />
继承以上BaseRange的所有属性
7. BaseSlider
滑块组件,field值会是单个数字,或是数字数组。
js
<BaseField type="slider" .../> ==> <BaseSlider .../>
继承以上BaseField的所有属性
8. BaseColor
颜色选择器。
js
<BaseField type="color" .../> ==> <BaseColor .../>
继承以上BaseField的所有属性
9. BaseRating
五星好评选择器。
js
<BaseField type="rating" .../> ==> <BaseRating .../>
继承以上BaseField的所有属性
FormField
以上BaseField是放在FreeForm里运行的,form中会计算值,做校验等操作,并不适合单独拿出来使用,为了能让开发者更方便地使用form field,需要将每个BaseField封装成FormField,比如BaseText,开发者可以单独使用Text组件,BaseSingleDropdown对应SingleDropdown,这样做的好处如下:
- 这样的组件已经将值计算和校验等操作封装在其中了,开发者使用起来会非常轻松。
- 有利于做集成测试。
- 有利于写开发文档。
- 给未来如果需要将组件做成动态组件做准备。
总结
我们开发的重点并不是在组件本身,事实上,我打算直接用现有的组件库,由于需要通用不同的组件库,每个组件库对应的接口不同,因此需要特制Adaptor,比如在我们的框架中格式化方法叫"format",而某个组件库的同样功能的方法名叫"formatter",另一个组件库这个方法名叫"formateValue",这样Adaptor的重要性就在于此,仅仅只是通过变换名字,将不同的组件库接入进来,这种方式也是coding浪费最少的方式了。
详细Adaptor的设计这在写。。。 接下来,是FreeForm和FormArray的封装:正在写。。。