Skip to content

Instantly share code, notes, and snippets.

@buhichan
Last active December 30, 2020 09:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save buhichan/80cf4b5688832866993847b6d57721ca to your computer and use it in GitHub Desktop.
Save buhichan/80cf4b5688832866993847b6d57721ca to your computer and use it in GitHub Desktop.
model (rxjs) based form
import { Form, Col, Row, Button } from "antd"
import { FormItemProps } from "antd/lib/form"
import { useObservables } from "./use-observables"
import * as React from "react"
import { AbstractControl, ValidationInfo, FormControls, FormControlList } from "./model"
import { CloseOutlined, PlusOutlined, MinusOutlined } from "@ant-design/icons"
type FormItemRenderChildren<T, Meta> = (inputProps: { value?: T; onChange?: (v: T) => void }, behavior: Meta | null, err: ValidationInfo) => React.ReactNode
export function FormItem<T, Meta>({
field,
children,
...rest
}: Omit<FormItemProps, "name" | "children"> & {
field: AbstractControl<T, Meta>
children: FormItemRenderChildren<T, Meta>
}) {
const [error, value, meta] = useObservables(field.error, field.value, field.metadata)
return (
<Form.Item labelCol={{span:4}} wrapperCol={{span:20}} hasFeedback help={!!error ? error : undefined} validateStatus={!!error ? "error" : undefined} {...rest}>
{children(
{
value: value === null ? undefined : value,
onChange: field.change,
},
meta,
error
)}
</Form.Item>
)
}
//eslint-disable-next-line
export function FormList<Meta, Children extends AbstractControl<any, any>>({
field,
children,
...rest
}: Omit<FormItemProps, "name" | "children"> & {
field: FormControlList<Meta, Children>
children: (child: Children, arrayMeta: Meta | null, index: number) => React.ReactNode
}) {
const [items, metadata, error] = useObservables(field.children, field.metadata, field.error)
return (
<Form.Item labelCol={{span:4}} wrapperCol={{span:20}} hasFeedback help={!!error ? error : undefined} validateStatus={!!error ? "error" : undefined} {...rest}>
<Row>
<Col span={24}>
{items.map((x, i) => {
return (
<Row gutter={8} key={x.id}>
<Col>
<Button
icon={<MinusOutlined />}
onClick={() => {
field.delete(i)
}}
></Button>
</Col>
<Col span={22} key={x.id}>
{children(x.control, metadata, i)}
</Col>
</Row>
)
})}
</Col>
</Row>
<Row>
<Col span={24}>
<Button
icon={<PlusOutlined />}
onClick={() => {
//eslint-disable-next-line
field.push({} as any)
}}
></Button>
</Col>
</Row>
</Form.Item>
)
}
import { BehaviorSubject, combineLatest, empty, identity, Observable, ObservableInput, of, Subject } from "rxjs"
import { map, publishReplay, refCount, scan, startWith, switchMap } from "rxjs/operators"
export interface AbstractControl<Type, Meta = unknown> {
value: Observable<Type>
error: Observable<ValidationInfo>
change(value: Type): void
metadata: Observable<Meta>
}
// export interface FormControl<Type> extends AbstractControl<Type> {
// change(value: Type): void
// }
export type ValidationInfo = string | void | null | undefined
export type Validator<Type> = (v: Type) => ObservableInput<ValidationInfo>
export type FormControlOptions<Type, Meta> = {
validator?: Validator<Type>
middleware?: (nextValue: Type, prevValue: Type | undefined) => Type
metadata?: Observable<Meta>
}
export class FormControl<Type, Meta = never> implements AbstractControl<Type, Meta> {
constructor(public defaultValue: Type, protected options?: FormControlOptions<Type, Meta>) {}
metadata = this.options?.metadata || empty()
private change$ = new Subject<Type>()
change = (v: Type) => {
this.change$.next(v)
}
value = this.change$.pipe(
this.options?.middleware ? scan((prev, cur) => this.options!.middleware!(cur, prev)) : identity,
startWith(this.defaultValue),
publishReplay(1),
refCount()
)
error = !this.options?.validator ? (empty() as Observable<ValidationInfo>) : this.value.pipe(switchMap(this.options.validator))
}
export class FormControlList<Meta, Children extends AbstractControl<unknown>, Type = ValueOfControl<Children>> implements AbstractControl<Type[]> {
constructor(
public defaultValue: Type[],
public createChild: (x: Type) => Children,
private options?: Omit<FormControlOptions<Type[], Meta>, "middleware">
) {}
children: BehaviorSubject<
{
id: number
control: Children
}[]
> = new BehaviorSubject(
this.defaultValue.map(value => {
return {
id: this.curId++,
control: this.createChild(value),
}
})
)
currentValue = this.defaultValue
metadata = this.options?.metadata || empty()
value = this.children.pipe(
switchMap(x => {
return x.length === 0 ? of([]) : combineLatest(x.map(x => (x.control.value as unknown) as Type[]))
})
)
error = combineLatest(
this.children.pipe(
switchMap(x => {
return combineLatest(x.map(x => x.control.error))
}),
map(x => x.find(x => !!x))
),
!this.options?.validator ? of(null) : this.value.pipe(switchMap(this.options.validator))
).pipe(
map(([childErr, selfErr]) => {
return childErr || selfErr
})
)
change = (value: Type[]) => {
this.children.next(
value.map((item, i) => {
return {
id: this.curId++,
control: this.createChild(item),
}
})
)
}
private curId = 0
push(value: Type) {
this.children.next(
this.children.value.concat({
id: this.curId++,
control: this.createChild(value),
})
)
}
insert(value: Type, index: number) {
const clone = this.children.value.slice()
clone.splice(index, 0, {
id: this.curId++,
control: this.createChild(value),
})
this.children.next(clone)
}
delete(index: number) {
const clone = this.children.value.slice()
clone.splice(index, 1)
this.children.next(clone)
}
swap(indexA: number, indexB: number) {
const clone = this.children.value.slice()
const tmp = clone[indexA]
clone[indexA] = clone[indexB]
clone[indexB] = tmp
this.children.next(clone)
}
}
type ValueOfControl<T> = T extends AbstractControl<infer V> ? V : never
//eslint-disable-next-line
export class FormControls<Children extends Record<string, AbstractControl<any, any>>, Meta, Type = { [k in keyof Children]: ValueOfControl<Children[k]> }>
implements AbstractControl<Type, Meta> {
constructor(public children: Children, private options?: FormControlOptions<Type, Meta>) {}
metadata = this.options?.metadata || empty()
private formEntries = (Object.keys(this.children) as (keyof Children)[]).map(k => {
return [k, this.children[k]] as const
})
value = combineLatest(
this.formEntries.map(([k, control]) => {
return control.value.pipe(map(value => [k, value] as const))
})
).pipe(
map(kvs => {
return kvs.reduce((res, [k, v]) => {
res[k as keyof Type] = v
return res
}, {} as Type)
}),
this.options?.middleware ? scan((prev, cur) => this.options!.middleware!(cur, prev)) : identity
)
error = combineLatest([
...this.formEntries.map(([k, control]) => {
return control.error
}),
!this.options?.validator ? of(null) : this.value.pipe(switchMap(this.options.validator)),
]).pipe(
map(x => {
return x.find(x => !!x)
})
)
change = (value: Type) => {
for (const k in value) {
this.children[k]?.change(value[k])
}
}
}
import React, { useMemo } from "react";
import ReactDOM from "react-dom";
import { Button, DatePicker, Select, version, Input, Form } from "antd";
import "antd/dist/antd.css";
import { FormControl, FormControlList, FormControls } from "./model"
import { from, of } from "rxjs";
import { map, publishReplay, refCount, take, withLatestFrom } from "rxjs/operators";
import { FormItem, FormList } from "./component";
import moment from "moment";
interface IsolationRule {
product: number,
alarmRegId: number,
startTime: Date,
endTime: Date,
remark: string,
filter: {
field: string,
operator: string,
value: string
}[]
}
const AlarmDutyProductOptions = async ()=>[
{label:"产品1",value:1},
{label:"产品2",value:2},
{label:"产品3",value:3},
]
const AlarmRegOptions = async ()=>[
{label:"注册ID1",value:1},
{label:"注册ID2",value:2},
{label:"注册Id3",value:3},
]
const GetMonitorFilter = async ()=>[
{label: "ip", value:"ip"},
{label: "region", value:"region", options: [
{
label:"shanghai",
value:"sh",
},
{
label:"beijing",
value:"bj",
}
]},
]
function editBlockingRuleFormModel() {
const product = new FormControl(undefined as undefined | number, {
metadata: from(AlarmDutyProductOptions()),
validator: async v=> {
if(!v){
return "必填字段哦"
}
}
})
const alarmRegId = new FormControl(undefined as undefined | number, {
metadata: from(AlarmRegOptions()),
})
const startTime = new FormControl(moment().subtract(3,'d'))
const endTime = new FormControl(moment())
const remark = new FormControl("", {
middleware: (curv, prev)=>{
console.log('prevValue remark is ', prev)
return curv.trim()
}
})
const monitorFilterOptions = from(GetMonitorFilter()).pipe(publishReplay(1), refCount())
const filter = new FormControlList([] as IsolationRule["filter"], item => {
const field = new FormControl(item.field, {
metadata: monitorFilterOptions,
})
const operator = new FormControl(item.operator, {
metadata: of([
{ label: "相等(单值精确匹配)", value: "equal" },
{ label: "或者(多值至少精确匹配一个)", value: "or" },
{ label: "包含(多值至少包含一个)", value: "includes" },
{ label: "正则(多值至少正则匹配一个, 请注意转义)", value: "regex" },
]),
})
const valueMeta$ = field.value.pipe(
withLatestFrom(monitorFilterOptions),
map(([field, fieldOptions]) => {
const option = fieldOptions.find(x => x.value === field)
const options = option?.options
if (Array.isArray(options) && options.length > 0) {
return {
isSelect: true,
options,
}
} else {
return {
isSelect: false,
}
}
})
)
const value = new FormControl(item.value, {
metadata: valueMeta$,
})
valueMeta$.subscribe(()=>{
value.change("")
})
return new FormControls({
field,
operator,
value,
})
})
return new FormControls({
product,
alarmRegId,
startTime,
remark,
endTime,
filter,
})
}
export default function App(){
const form2 = useMemo(editBlockingRuleFormModel,[])
const [submittedValue, setSubmittedValue] = React.useState({})
return (
<form
style={{padding: 16}}
onSubmit={e => {
e.preventDefault()
form2.value.pipe(take(1)).toPromise().then(setSubmittedValue)
}}
>
<FormItem label="产品" field={form2.children.product}>
{(props, options) => {
return <Select options={options || []} {...props} />
}}
</FormItem>
<FormItem label="告警ID" field={form2.children.alarmRegId}>
{(props, options) => {
return <Select options={options || []} {...props} />
}}
</FormItem>
<FormItem label="开始时间" field={form2.children.startTime}>
{(props, options) => {
return (
<DatePicker
value={moment(props.value)}
onChange={v => {
v && props.onChange?.(v)
}}
/>
)
}}
</FormItem>
<FormItem label="失效时间" field={form2.children.endTime}>
{(props, options) => {
return (
<DatePicker
value={moment(props.value)}
onChange={v => {
v && props.onChange?.(v)
}}
/>
)
}}
</FormItem>
<FormItem label="备注" field={form2.children.remark}>
{props => {
return <Input.TextArea value={props.value} onChange={e => props.onChange?.(e.target.value)} />
}}
</FormItem>
<FormList label="过滤" field={form2.children.filter}>
{field => {
return (
<>
<FormItem label="字段" field={field.children.field}>
{(props, options) => {
return <Select {...props} options={options?.map(x => ({ label: x.label, value: x.value })) || []} />
}}
</FormItem>
<FormItem label="操作符" field={field.children.operator}>
{(props, options) => {
return <Select {...props} options={options || []} />
}}
</FormItem>
<FormItem label="值" field={field.children.value}>
{(props, options) => {
return options?.isSelect ? (
<Select
value={props.value}
options={options.options || []}
onChange={v => {
props.onChange?.(v)
}}
/>
) : (
<Input.TextArea
{...props}
onChange={e => {
console.log(e.target.value)
props.onChange?.(e.target.value)
}}
/>
)
}}
</FormItem>
</>
)
}}
</FormList>
<Button htmlType="submit">Submit</Button>
<Button onClick={()=>{
form2.change({
product: 2,
alarmRegId: 1,
startTime: moment(),
endTime: moment(),
remark: "114514",
filter: [
{
field:"ip",
operator: "equal",
value:"114.514.19.19"
}
]
})
}}>用 form.change来填充初始值</Button>
<div>
<div>你提交的值是</div>
<pre>{JSON.stringify(submittedValue,null,"\t")}</pre>
</div>
</form>
)
}
@buhichan
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment