react-icecream-form
v1.1.0
Published
Integration for react-icecream & formstate-x.
Downloads
89
Readme
react-icecream-form
集成 formstate-x & 部分交互逻辑的表单
react-icecream-form
在 react-icecream 的基础上:
- 提供
Form
(&ModalForm
、DrawerForm
) 的封装后版本,内置了表单提交的校验逻辑 - 提供
FormItem
的封装后版本,可以省略校验状态信息或 state 实例 - 提供各输入组件(
TextInput
、Select
等)的封装后版本,接受直接传入FieldState
实例 - 提供了用于构造表单状态的 hook
useFormstateX
如 3 中提到的,react-icecream-form
模块对所有 react-icecream 内置的输入组件进行了适配,使其支持直接传入 FieldState
实例,而不是通过 value
/ onChange
来绑定输入;除此之外,适配后的输入组件可以被 react-icecream-form
(如 2 中所述)提供的 FormItem
感知到,因此在搭配使用 FormItem
与输入组件时(二者一一对应),可以省略将 FieldState
实例手动传递给 FormItem
的行为;即可以写为:
import { FormItem, TextInput } from 'react-icecream-form'
// 这里 `FormItem` 会自动去使用内容中仅有的那个输入组件(`TextInput`)所对应的 state
<FormItem>
<TextInput state={state} />
</FormItem>
简单的字段输入场景
import React, { useEffect } from 'react'
import { reaction } from 'mobx'
import { observer } from 'mobx-react'
import { FieldState, Validator } from 'formstate-x'
import { TextInput, useFormstateX } from 'react-icecream-form'
export default observer(() => {
const nameState = useFormstateX(() => new FieldState('').withValidator(notEmpty), [])
useEffect(() => reaction(
() => nameState.error,
error => error && console.warn('Error:', error)
), [nameState])
return (
<>
<p style={{ margin: '0 0 1em' }}>Plz input your name: </p>
<TextInput state={nameState} />
{nameState.value && <p>Hello {nameState.value}!</p>}
</>
)
})
function notEmpty(v: string) {
return v.trim() === '' && 'empty input'
}
较复杂表单实现
import React, { useCallback } from 'react'
import { FormState, FieldState, ValueOf } from 'formstate-x'
import { observer } from 'mobx-react'
import { Form, FormItem, useFormstateX, TextInput, PasswordInput, RadioGroup, Radio, Checkbox, MultiSelect, SelectOption } from 'react-icecream-form'
type AccountType = 'default' | 'enterprise'
// 构造表单状态
function createFormState() {
const accountTypeField = new FieldState<AccountType>('default')
const usernameField = new FieldState('').withValidator(notEmpty)
const passwordField = new FieldState('').withValidator(notEmpty)
const password2Field = new FieldState('').withValidator(
notEmpty,
(v: string) => v !== passwordField.value && '密码不一致'
)
const productsField = new FieldState<string[]>([]).withValidator(
products => productsRequired(accountTypeField.value) && products.length === 0 && '不可为空'
)
const agreedField = new FieldState(false).withValidator(
agreed => !agreed && '请勾选'
)
return new FormState({
accountType: accountTypeField,
username: usernameField,
password: passwordField,
password2: password2Field,
products: productsField,
agreed: agreedField
})
}
// 表单中值的类型
type Value = ValueOf<ReturnType<typeof createFormState>>
export default observer(() => {
const state = useFormstateX(createFormState, [])
const handleSubmit = useCallback((value: Value) => {
console.log(`Submit with: ${JSON.stringify(value)}.`)
return new Promise<void>(resolve => setTimeout(resolve, 2000))
}, [])
const handleCancel = useCallback(() => (
console.log('Cancelled.')
), [])
return (
<Form
state={state}
layout="horizontal"
labelWidth="4em"
onSubmit={handleSubmit}
onCancel={handleCancel}
>
<FormItem label="账户类型" labelVerticalAlign="text" tip="企业账号需要额外选择“开通产品”">
<RadioGroup state={state.$.accountType}>
<Radio value="default">普通账号</Radio>
<Radio value="enterprise">企业账号</Radio>
</RadioGroup>
</FormItem>
<FormItem label="用户名" required>
<TextInput placeholder="请输入用户名" state={state.$.username} />
</FormItem>
<FormItem label="密码" required>
<PasswordInput placeholder="请输入密码" state={state.$.password} />
</FormItem>
<FormItem label="确认密码" required tip="两次输入密码应当一致">
<PasswordInput placeholder="请再次输入密码" state={state.$.password2} />
</FormItem>
{productsRequired(state.$.accountType.value) && (
<FormItem required label="开通产品">
<MultiSelect
placeholder="请选择需要开通的产品"
state={state.$.products}
collapsed={false}
style={{ width: '240px' }}
>
<SelectOption value="kodo">对象存储 Kodo</SelectOption>
<SelectOption value="fusion">CDN</SelectOption>
<SelectOption value="dora">智能多媒体服务 Dora</SelectOption>
<SelectOption value="qvm">云主机服务 QVM</SelectOption>
<SelectOption value="pandora">机器数据分析平台 Pandora</SelectOption>
</MultiSelect>
</FormItem>
)}
<FormItem label="">
<label style={{ color: '#999999' }}>
<Checkbox state={state.$.agreed} />
<small>我已阅读并同意服务协议和隐私政策</small>
</label>
</FormItem>
</Form>
)
})
function productsRequired(accountType: AccountType) {
return accountType === 'enterprise'
}
function notEmpty(v: string) {
return v.trim() === '' && '不可为空'
}
列表输入与校验
import React, { useCallback } from 'react'
import { FormState, ArrayFormState, FieldState, ValueOf } from 'formstate-x'
import { observer } from 'mobx-react'
import { Tooltip } from 'react-icecream'
import { AddThinIcon, CloseThinIcon } from 'react-icecream/icons'
import { Form, FormItem, useFormstateX, TextInput } from 'react-icecream-form'
// 构造单条手机号对应的表单状态
function createNumberState(initialValue: string) {
return new FieldState(initialValue).withValidator(validateNumber)
}
// 构造手机号列表对应的状态
function createNumbersState() {
return new ArrayFormState([''], createNumberState).withValidator(
numbers => numbers.length > 3 && '最多添加 3 个号码'
)
}
// 构造表单状态
function createFormState() {
return new FormState({
numbers: createNumbersState()
})
}
// 表单中值的类型
type Value = ValueOf<ReturnType<typeof createFormState>>
export default observer(() => {
const state = useFormstateX(createFormState, [])
const numbersState = state.$.numbers
const handleAddNumber = () => numbersState.append('')
const handleRemoveNumber = (i: number) => numbersState.remove(i)
const handleSubmit = (value: Value) => {
console.log(`Submit with: ${JSON.stringify(value)}.`)
return new Promise<void>(resolve => setTimeout(resolve, 2000))
}
const numbersLabel = (
<span style={{ display: 'flex', alignItems: 'center' }}>
手机号
<Tooltip title="点击添加">
<AddThinIcon
style={{ marginLeft: '.5em', cursor: 'pointer' }}
onClick={handleAddNumber}
/>
</Tooltip>
</span>
)
return (
<Form layout="horizontal" labelWidth="64px" state={state} onSubmit={handleSubmit}>
<FormItem state={numbersState} label={numbersLabel}>
{numbersState.$.map((numberState, i) => (
<FormItem key={i}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<TextInput placeholder="请输入中国大陆手机号" state={numberState} />
{numbersState.$.length > 1 && (
<Tooltip title="点击删除">
<CloseThinIcon
style={{ marginLeft: '.5em', padding: '5px', cursor: 'pointer' }}
onClick={() => handleRemoveNumber(i)}
/>
</Tooltip>
)}
</div>
</FormItem>
))}
</FormItem>
</Form>
)
})
function validateNumber(v: string) {
if (!v) return '号码不可为空'
if (!/^1\d{10}$/.test(v)) return '格式不正确'
}
支持新建或编辑的表单组件
import React, { useCallback, useState } from 'react'
import { FormState, FieldState } from 'formstate-x'
import { observer } from 'mobx-react'
import { Button } from 'react-icecream'
import { Form, FormItem, useFormstateX, TextInput } from 'react-icecream-form'
interface User {
name: string
}
function createUserState(initialValue?: User) {
return new FormState({
name: new FieldState(initialValue?.name ?? '')
})
}
interface UserFormProps {
user?: User // 当前编辑的用户,如不传则代表新建
onSubmit: (user: User) => void | Promise<void> // 表单提交的回调
}
// 用于新建或编辑用户信息的表单
function UserForm({ user, onSubmit }: UserFormProps) {
const state = useFormstateX(createUserState, [user])
return (
<Form state={state} onSubmit={onSubmit}>
<FormItem label="姓名">
<TextInput placeholder="请输入姓名" state={state.$.name} />
</FormItem>
</Form>
)
}
const motoko: User = {
name: 'Motoko'
}
export default observer(() => {
const [editing, setEditing] = useState(false)
const editTarget = editing ? motoko : undefined
const toggleEditing = useCallback(() => setEditing(v => !v), [])
const handleSubmit = useCallback((user: User) => {
const action = editing ? 'Update' : 'Create'
alert(`${action}: ${JSON.stringify(user)}`)
}, [editing])
return (
<>
<div style={{ marginBottom: '1em', paddingBottom: '1em', borderBottom: '1px solid #E5E5E5' }}>
<Button type="primary" onClick={toggleEditing}>
{editing ? '切换为新建' : `切换为编辑 ${motoko.name}`}
</Button>
</div>
<UserForm
user={editTarget}
onSubmit={handleSubmit}
/>
</>
)
})
在展示/编辑状态间切换的表单
import { FieldState, FormState } from 'formstate-x'
import React, { useState } from 'react'
import { Button } from 'react-icecream'
import { Form, FormItem, useFormstateX, TextArea } from 'react-icecream-form'
function createState(remark = '') {
return new FormState({
remark: new FieldState(remark).withValidator(v => !v && '不可为空')
})
}
export default function Demo() {
const remark = '请尽快发货'
const [editing, setEditing] = useState(false)
const state = useFormstateX(createState, [remark])
const remarkEditingView = (
<>
<TextArea style={{ marginBottom: '12px' }} state={state.$.remark} />
<Button onClick={() => setEditing(false)}>取消</Button>
</>
)
const remarkDisplayView = (
<div style={{ display: 'flex', alignItems: 'center' }}>
{remark}
<Button type="link" onClick={() => setEditing(true)}>编辑</Button>
</div>
)
return (
<Form
layout="horizontal"
labelWidth="6em"
labelColor="light"
state={state}
footer={null}
>
<FormItem label="订单 ID" labelVerticalAlign="text">8901234567</FormItem>
<FormItem label="订单备注">{editing ? remarkEditingView : remarkDisplayView}</FormItem>
</Form>
)
}
自定义输入组件
在项目中,往往会存在自定义的输入组件。如果希望这部分组件也像 react-icecream-form
提供的输入组件一样,被 FormItem
感知到,可以使用 react-icecream-form
提供的组件 InputWrapper
来实现:
import React from 'react'
import { FormState, FieldState } from 'formstate-x'
import { observer } from 'mobx-react'
import { AddThinIcon, RemoveThinIcon } from 'react-icecream/icons'
import { Form, FormItem, useFormstateX, InputGroup, InputGroupItem, InputWrapper } from 'react-icecream-form'
interface CounterProps {
state: FieldState<number>
}
const Counter = observer(function _Counter({ state }: CounterProps) {
const increase = () => state.onChange(state.value + 1)
const decrease = () => state.onChange(state.value - 1)
return (
// `InputWrapper` registers Counter's state to outer FormItem
<InputWrapper state={state}>
<InputGroup style={{ width: '120px' }}>
<InputGroupItem>
<AddThinIcon style={{ height: '100%' }} onClick={increase} />
</InputGroupItem>
<InputGroupItem style={{ flex: '1 1 auto', textAlign: 'center' }}>
{state.value}
</InputGroupItem>
<InputGroupItem>
<RemoveThinIcon style={{ height: '100%' }} onClick={decrease} />
</InputGroupItem>
</InputGroup>
</InputWrapper>
)
})
export default observer(() => {
const state = useFormstateX(() => new FormState({
count: new FieldState(0).withValidator(v => v <= 0 && 'Positive required.')
}), [])
return (
<Form state={state} onSubmit={v => alert(v.count)}>
{/* So we don't need to pass `state.$.count` to `FormItem` here */}
<FormItem label="Count">
<Counter state={state.$.count} />
</FormItem>
</Form>
)
})
注:这往往不是必须的,直接通过 FormItem
的 prop state
指定其对应的 state 实例也是常规的做法。
使用 ModalForm
react-icecream-form 也提供了 ModalForm
& DrawerForm
与 formstate-x 的绑定,使用姿势跟 Form
类似:
import React, { useCallback, useState } from 'react'
import { FormState, FieldState, ValueOf } from 'formstate-x'
import { observer } from 'mobx-react'
import { Button } from 'react-icecream'
import { ModalForm, FormItem, useFormstateX, TextInput, PasswordInput } from 'react-icecream-form'
// 构造表单状态
function createFormState() {
const usernameField = new FieldState('').validators(notEmpty)
const passwordField = new FieldState('').validators(notEmpty)
return new FormState({
username: usernameField,
password: passwordField
})
}
// 表单中值的类型
type Value = ValueOf<ReturnType<typeof createFormState>>
export default observer(() => {
const [visible, setVisible] = useState(false)
const state = useFormstateX(createFormState, [])
const handleSubmit = useCallback(async (value: Value) => {
console.log(`Submit with: ${JSON.stringify(value)}.`)
await new Promise<void>(resolve => setTimeout(resolve, 2000))
setVisible(false)
}, [])
const handleCancel = useCallback(() => {
console.log('Cancelled.')
setVisible(false)
}, [])
return (
<>
<Button onClick={() => setVisible(true)}>打开弹框</Button>
<ModalForm
visible={visible}
title="示例"
state={state}
layout="horizontal"
labelWidth="4em"
onSubmit={handleSubmit}
onCancel={handleCancel}
>
<FormItem label="用户名" required>
<TextInput placeholder="请输入用户名" state={state.$.username} />
</FormItem>
<FormItem label="密码" required>
<PasswordInput placeholder="请输入密码" state={state.$.password} />
</FormItem>
</ModalForm>
</>
)
})
function notEmpty(v: string) {
return v.trim() === '' && '不可为空'
}