packages/core/src/models/Form.ts (540 lines of code) (raw):
import { define, observable, batch, action, observe } from '@formily/reactive'
import {
FormPath,
FormPathPattern,
isValid,
uid,
globalThisPolyfill,
merge,
isPlainObj,
isArr,
isObj,
} from '@formily/shared'
import { Heart } from './Heart'
import { Field } from './Field'
import {
JSXComponent,
LifeCycleTypes,
HeartSubscriber,
FormPatternTypes,
IFormRequests,
IFormFeedback,
ISearchFeedback,
IFormGraph,
IFormProps,
IFieldResetOptions,
IFormFields,
IFieldFactoryProps,
IVoidFieldFactoryProps,
IFormState,
IModelGetter,
IModelSetter,
IFieldStateGetter,
IFieldStateSetter,
FormDisplayTypes,
IFormMergeStrategy,
} from '../types'
import {
createStateGetter,
createStateSetter,
createBatchStateSetter,
createBatchStateGetter,
triggerFormInitialValuesChange,
triggerFormValuesChange,
batchValidate,
batchReset,
batchSubmit,
setValidating,
setSubmitting,
setLoading,
getValidFormValues,
} from '../shared/internals'
import { isVoidField } from '../shared/checkers'
import { runEffects } from '../shared/effective'
import { ArrayField } from './ArrayField'
import { ObjectField } from './ObjectField'
import { VoidField } from './VoidField'
import { Query } from './Query'
import { Graph } from './Graph'
const DEV_TOOLS_HOOK = '__FORMILY_DEV_TOOLS_HOOK__'
export class Form<ValueType extends object = any> {
displayName = 'Form'
id: string
initialized: boolean
validating: boolean
submitting: boolean
loading: boolean
modified: boolean
pattern: FormPatternTypes
display: FormDisplayTypes
values: ValueType
initialValues: ValueType
mounted: boolean
unmounted: boolean
props: IFormProps<ValueType>
heart: Heart
graph: Graph
fields: IFormFields = {}
requests: IFormRequests = {}
indexes: Record<string, string> = {}
disposers: (() => void)[] = []
constructor(props: IFormProps<ValueType>) {
this.initialize(props)
this.makeObservable()
this.makeReactive()
this.makeValues()
this.onInit()
}
protected initialize(props: IFormProps<ValueType>) {
this.id = uid()
this.props = { ...props }
this.initialized = false
this.submitting = false
this.validating = false
this.loading = false
this.modified = false
this.mounted = false
this.unmounted = false
this.display = this.props.display || 'visible'
this.pattern = this.props.pattern || 'editable'
this.editable = this.props.editable
this.disabled = this.props.disabled
this.readOnly = this.props.readOnly
this.readPretty = this.props.readPretty
this.visible = this.props.visible
this.hidden = this.props.hidden
this.graph = new Graph(this)
this.heart = new Heart({
lifecycles: this.lifecycles,
context: this,
})
}
protected makeValues() {
this.values = getValidFormValues(this.props.values)
this.initialValues = getValidFormValues(this.props.initialValues)
}
protected makeObservable() {
define(this, {
fields: observable.shallow,
indexes: observable.shallow,
initialized: observable.ref,
validating: observable.ref,
submitting: observable.ref,
loading: observable.ref,
modified: observable.ref,
pattern: observable.ref,
display: observable.ref,
mounted: observable.ref,
unmounted: observable.ref,
values: observable,
initialValues: observable,
valid: observable.computed,
invalid: observable.computed,
errors: observable.computed,
warnings: observable.computed,
successes: observable.computed,
hidden: observable.computed,
visible: observable.computed,
editable: observable.computed,
readOnly: observable.computed,
readPretty: observable.computed,
disabled: observable.computed,
setValues: action,
setValuesIn: action,
setInitialValues: action,
setInitialValuesIn: action,
setPattern: action,
setDisplay: action,
setState: action,
deleteInitialValuesIn: action,
deleteValuesIn: action,
setSubmitting: action,
setValidating: action,
reset: action,
submit: action,
validate: action,
onMount: batch,
onUnmount: batch,
onInit: batch,
})
}
protected makeReactive() {
this.disposers.push(
observe(
this,
(change) => {
triggerFormInitialValuesChange(this, change)
triggerFormValuesChange(this, change)
},
true
)
)
}
get valid() {
return !this.invalid
}
get invalid() {
return this.errors.length > 0
}
get errors() {
return this.queryFeedbacks({
type: 'error',
})
}
get warnings() {
return this.queryFeedbacks({
type: 'warning',
})
}
get successes() {
return this.queryFeedbacks({
type: 'success',
})
}
get lifecycles() {
return runEffects(this, this.props.effects)
}
get hidden() {
return this.display === 'hidden'
}
get visible() {
return this.display === 'visible'
}
set hidden(hidden: boolean) {
if (!isValid(hidden)) return
if (hidden) {
this.display = 'hidden'
} else {
this.display = 'visible'
}
}
set visible(visible: boolean) {
if (!isValid(visible)) return
if (visible) {
this.display = 'visible'
} else {
this.display = 'none'
}
}
get editable() {
return this.pattern === 'editable'
}
set editable(editable) {
if (!isValid(editable)) return
if (editable) {
this.pattern = 'editable'
} else {
this.pattern = 'readPretty'
}
}
get readOnly() {
return this.pattern === 'readOnly'
}
set readOnly(readOnly) {
if (!isValid(readOnly)) return
if (readOnly) {
this.pattern = 'readOnly'
} else {
this.pattern = 'editable'
}
}
get disabled() {
return this.pattern === 'disabled'
}
set disabled(disabled) {
if (!isValid(disabled)) return
if (disabled) {
this.pattern = 'disabled'
} else {
this.pattern = 'editable'
}
}
get readPretty() {
return this.pattern === 'readPretty'
}
set readPretty(readPretty) {
if (!isValid(readPretty)) return
if (readPretty) {
this.pattern = 'readPretty'
} else {
this.pattern = 'editable'
}
}
/** 创建字段 **/
createField = <
Decorator extends JSXComponent,
Component extends JSXComponent
>(
props: IFieldFactoryProps<Decorator, Component>
): Field<Decorator, Component> => {
const address = FormPath.parse(props.basePath).concat(props.name)
const identifier = address.toString()
if (!identifier) return
if (!this.fields[identifier] || this.props.designable) {
batch(() => {
new Field(address, props, this, this.props.designable)
})
this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE)
}
return this.fields[identifier] as any
}
createArrayField = <
Decorator extends JSXComponent,
Component extends JSXComponent
>(
props: IFieldFactoryProps<Decorator, Component>
): ArrayField<Decorator, Component> => {
const address = FormPath.parse(props.basePath).concat(props.name)
const identifier = address.toString()
if (!identifier) return
if (!this.fields[identifier] || this.props.designable) {
batch(() => {
new ArrayField(
address,
{
...props,
value: isArr(props.value) ? props.value : [],
},
this,
this.props.designable
)
})
this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE)
}
return this.fields[identifier] as any
}
createObjectField = <
Decorator extends JSXComponent,
Component extends JSXComponent
>(
props: IFieldFactoryProps<Decorator, Component>
): ObjectField<Decorator, Component> => {
const address = FormPath.parse(props.basePath).concat(props.name)
const identifier = address.toString()
if (!identifier) return
if (!this.fields[identifier] || this.props.designable) {
batch(() => {
new ObjectField(
address,
{
...props,
value: isObj(props.value) ? props.value : {},
},
this,
this.props.designable
)
})
this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE)
}
return this.fields[identifier] as any
}
createVoidField = <
Decorator extends JSXComponent,
Component extends JSXComponent
>(
props: IVoidFieldFactoryProps<Decorator, Component>
): VoidField<Decorator, Component> => {
const address = FormPath.parse(props.basePath).concat(props.name)
const identifier = address.toString()
if (!identifier) return
if (!this.fields[identifier] || this.props.designable) {
batch(() => {
new VoidField(address, props, this, this.props.designable)
})
this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE)
}
return this.fields[identifier] as any
}
/** 状态操作模型 **/
setValues = (values: any, strategy: IFormMergeStrategy = 'merge') => {
if (!isPlainObj(values)) return
if (strategy === 'merge' || strategy === 'deepMerge') {
merge(this.values, values, {
// never reach
arrayMerge: (target, source) => source,
assign: true,
})
} else if (strategy === 'shallowMerge') {
Object.assign(this.values, values)
} else {
this.values = values as any
}
}
setInitialValues = (
initialValues: any,
strategy: IFormMergeStrategy = 'merge'
) => {
if (!isPlainObj(initialValues)) return
if (strategy === 'merge' || strategy === 'deepMerge') {
merge(this.initialValues, initialValues, {
// never reach
arrayMerge: (target, source) => source,
assign: true,
})
} else if (strategy === 'shallowMerge') {
Object.assign(this.initialValues, initialValues)
} else {
this.initialValues = initialValues as any
}
}
setValuesIn = (pattern: FormPathPattern, value: any) => {
FormPath.setIn(this.values, pattern, value)
}
deleteValuesIn = (pattern: FormPathPattern) => {
FormPath.deleteIn(this.values, pattern)
}
existValuesIn = (pattern: FormPathPattern) => {
return FormPath.existIn(this.values, pattern)
}
getValuesIn = (pattern: FormPathPattern) => {
return FormPath.getIn(this.values, pattern)
}
setInitialValuesIn = (pattern: FormPathPattern, initialValue: any) => {
FormPath.setIn(this.initialValues, pattern, initialValue)
}
deleteInitialValuesIn = (pattern: FormPathPattern) => {
FormPath.deleteIn(this.initialValues, pattern)
}
existInitialValuesIn = (pattern: FormPathPattern) => {
return FormPath.existIn(this.initialValues, pattern)
}
getInitialValuesIn = (pattern: FormPathPattern) => {
return FormPath.getIn(this.initialValues, pattern)
}
setLoading = (loading: boolean) => {
setLoading(this, loading)
}
setSubmitting = (submitting: boolean) => {
setSubmitting(this, submitting)
}
setValidating = (validating: boolean) => {
setValidating(this, validating)
}
setDisplay = (display: FormDisplayTypes) => {
this.display = display
}
setPattern = (pattern: FormPatternTypes) => {
this.pattern = pattern
}
addEffects = (id: any, effects: IFormProps['effects']) => {
if (!this.heart.hasLifeCycles(id)) {
this.heart.addLifeCycles(id, runEffects(this, effects))
}
}
removeEffects = (id: any) => {
this.heart.removeLifeCycles(id)
}
setEffects = (effects: IFormProps['effects']) => {
this.heart.setLifeCycles(runEffects(this, effects))
}
clearErrors = (pattern: FormPathPattern = '*') => {
this.query(pattern).forEach((field) => {
if (!isVoidField(field)) {
field.setFeedback({
type: 'error',
messages: [],
})
}
})
}
clearWarnings = (pattern: FormPathPattern = '*') => {
this.query(pattern).forEach((field) => {
if (!isVoidField(field)) {
field.setFeedback({
type: 'warning',
messages: [],
})
}
})
}
clearSuccesses = (pattern: FormPathPattern = '*') => {
this.query(pattern).forEach((field) => {
if (!isVoidField(field)) {
field.setFeedback({
type: 'success',
messages: [],
})
}
})
}
query = (pattern: FormPathPattern): Query => {
return new Query({
pattern,
base: '',
form: this,
})
}
queryFeedbacks = (search: ISearchFeedback): IFormFeedback[] => {
return this.query(search.address || search.path || '*').reduce(
(messages, field) => {
if (isVoidField(field)) return messages
return messages.concat(
field
.queryFeedbacks(search)
.map((feedback) => ({
...feedback,
address: field.address.toString(),
path: field.path.toString(),
}))
.filter((feedback) => feedback.messages.length > 0)
)
},
[]
)
}
notify = (type: string, payload?: any) => {
this.heart.publish(type, payload ?? this)
}
subscribe = (subscriber?: HeartSubscriber) => {
return this.heart.subscribe(subscriber)
}
unsubscribe = (id: number) => {
this.heart.unsubscribe(id)
}
/**事件钩子**/
onInit = () => {
this.initialized = true
this.notify(LifeCycleTypes.ON_FORM_INIT)
}
onMount = () => {
this.mounted = true
this.notify(LifeCycleTypes.ON_FORM_MOUNT)
if (globalThisPolyfill[DEV_TOOLS_HOOK] && !this.props.designable) {
globalThisPolyfill[DEV_TOOLS_HOOK].inject(this.id, this)
}
}
onUnmount = () => {
this.notify(LifeCycleTypes.ON_FORM_UNMOUNT)
this.query('*').forEach((field) => field.destroy(false))
this.disposers.forEach((dispose) => dispose())
this.unmounted = true
this.indexes = {}
this.heart.clear()
if (globalThisPolyfill[DEV_TOOLS_HOOK] && !this.props.designable) {
globalThisPolyfill[DEV_TOOLS_HOOK].unmount(this.id)
}
}
setState: IModelSetter<IFormState<ValueType>> = createStateSetter(this)
getState: IModelGetter<IFormState<ValueType>> = createStateGetter(this)
setFormState: IModelSetter<IFormState<ValueType>> = createStateSetter(this)
getFormState: IModelGetter<IFormState<ValueType>> = createStateGetter(this)
setFieldState: IFieldStateSetter = createBatchStateSetter(this)
getFieldState: IFieldStateGetter = createBatchStateGetter(this)
getFormGraph = () => {
return this.graph.getGraph()
}
setFormGraph = (graph: IFormGraph) => {
this.graph.setGraph(graph)
}
clearFormGraph = (pattern: FormPathPattern = '*', forceClear = true) => {
this.query(pattern).forEach((field) => {
field.destroy(forceClear)
})
}
validate = (pattern: FormPathPattern = '*') => {
return batchValidate(this, pattern)
}
submit = <T>(
onSubmit?: (values: ValueType) => Promise<T> | void
): Promise<T> => {
return batchSubmit(this, onSubmit)
}
reset = (pattern: FormPathPattern = '*', options?: IFieldResetOptions) => {
return batchReset(this, pattern, options)
}
}