diff --git a/packages/core/src/__tests__/field.spec.ts b/packages/core/src/__tests__/field.spec.ts index 749ea8955cd..bd1d38cf6b1 100644 --- a/packages/core/src/__tests__/field.spec.ts +++ b/packages/core/src/__tests__/field.spec.ts @@ -1603,3 +1603,33 @@ test('state depend field visible value', async () => { expect(cc.visible).toBeTruthy() expect(cc.disabled).toBeFalsy() }) + +test('reactions initialValue and value', () => { + const form = attach( + createForm({ + values: { + aa: { + input: '111', + }, + }, + }) + ) + attach( + form.createObjectField({ + name: 'aa', + reactions: [ + (field) => { + field.initialValue = {} + field.initialValue.input = 123 + }, + ], + }) + ) + attach( + form.createField({ + name: 'input', + basePath: 'aa', + }) + ) + expect(form.values.aa.input).toEqual('111') +}) diff --git a/packages/core/src/models/Field.ts b/packages/core/src/models/Field.ts index 4de444da950..7ebf15cad43 100644 --- a/packages/core/src/models/Field.ts +++ b/packages/core/src/models/Field.ts @@ -59,6 +59,8 @@ import { setLoading, validateSelf, getValidFieldDefaultValue, + initializeStart, + initializeEnd, } from '../shared/internals' import { Query } from './Query' export class Field< @@ -112,11 +114,13 @@ export class Field< this.form = form this.props = props this.designable = designable + initializeStart() this.makeIndexes(address) this.initialize() this.makeObservable() this.makeReactive() this.onInit() + initializeEnd() } protected makeIndexes(address: FormPathPattern) { diff --git a/packages/core/src/models/VoidField.ts b/packages/core/src/models/VoidField.ts index b840e5c8164..76c87fd59bc 100644 --- a/packages/core/src/models/VoidField.ts +++ b/packages/core/src/models/VoidField.ts @@ -19,6 +19,8 @@ import { createStateGetter, createStateSetter, initFieldUpdate, + initializeStart, + initializeEnd, } from '../shared/internals' import { Form } from './Form' import { Query } from './Query' @@ -61,11 +63,13 @@ export class VoidField { this.form = form this.props = props this.designable = designable + initializeStart() this.makeIndexes(address) this.initialize() this.makeObservable() this.makeReactive() this.onInit() + initializeEnd() } protected makeIndexes(address: FormPathPattern) { diff --git a/packages/core/src/shared/constants.ts b/packages/core/src/shared/constants.ts index 3152cd7f4a1..03901c340ef 100644 --- a/packages/core/src/shared/constants.ts +++ b/packages/core/src/shared/constants.ts @@ -1,21 +1,21 @@ -export const ReservedProperties = new Set([ - 'form', - 'parent', - 'props', - 'caches', - 'requests', - 'disposers', - 'heart', - 'graph', - 'indexes', - 'fields', - 'lifecycles', - 'originValues', - 'componentType', - 'componentProps', - 'decoratorType', - 'decoratorProps', -]) +export const ReservedProperties = { + form: true, + parent: true, + props: true, + caches: true, + requests: true, + disposers: true, + heart: true, + graph: true, + indexes: true, + fields: true, + lifecycles: true, + originValues: true, + componentType: true, + componentProps: true, + decoratorType: true, + decoratorProps: true, +} export const RESPONSE_REQUEST_DURATION = 100 @@ -24,6 +24,7 @@ export const GlobalState = { context: [], effectStart: false, effectEnd: false, + initializing: false, } export const NumberIndexReg = /^\.(\d+)/ diff --git a/packages/core/src/shared/internals.ts b/packages/core/src/shared/internals.ts index 7340596842b..472c249136e 100644 --- a/packages/core/src/shared/internals.ts +++ b/packages/core/src/shared/internals.ts @@ -49,6 +49,7 @@ import { RESPONSE_REQUEST_DURATION, ReservedProperties, NumberIndexReg, + GlobalState, } from './constants' export const isHTMLInputEvent = (event: any, stopPropagation = true) => { @@ -160,17 +161,15 @@ export const patchFormValues = ( if (allowAssignDefaultValue(targetValue, source)) { update(path, source) } else { + if (isEmpty(source)) return + if (GlobalState.initializing) return if (isPlainObj(targetValue) && isPlainObj(source)) { each(source, (value, key) => { patch(value, path.concat(key)) }) - } else if (!isEmpty(source)) { + } else { if (targetField) { - if ( - !isVoidField(targetField) && - targetField.initialized && - !targetField.modified - ) { + if (!isVoidField(targetField) && !targetField.modified) { update(path, source) } } else { @@ -619,7 +618,7 @@ export const setModelState = (model: any, setter: any) => { Object.keys(setter || {}).forEach((key: string) => { const value = setter[key] if (isFn(value)) return - if (ReservedProperties.has(key)) return + if (ReservedProperties[key]) return if (isSkipProperty(key)) return model[key] = value }) @@ -636,7 +635,7 @@ export const getModelState = (model: any, getter?: any) => { if (isFn(value)) { return buf } - if (ReservedProperties.has(key)) return buf + if (ReservedProperties[key]) return buf if (key === 'address' || key === 'path') { buf[key] = value.toString() return buf @@ -1023,3 +1022,13 @@ export const createReactions = (field: GeneralField) => { }) }) } + +export const initializeStart = () => { + GlobalState.initializing = true +} + +export const initializeEnd = () => { + batch.endpoint(() => { + GlobalState.initializing = false + }) +} diff --git a/packages/json-schema/src/__tests__/server-validate.spec.ts b/packages/json-schema/src/__tests__/server-validate.spec.ts new file mode 100644 index 00000000000..6868da4c17f --- /dev/null +++ b/packages/json-schema/src/__tests__/server-validate.spec.ts @@ -0,0 +1,136 @@ +import { createForm, Form } from '@formily/core' +import { ISchema, Schema, SchemaKey } from '../' + +// 这是schema +const schemaJson = { + type: 'object', + title: 'xxx配置', + properties: { + string: { + type: 'string', + title: 'string', + maxLength: 5, + required: true, + }, + number: { + type: 'number', + title: 'number', + required: true, + }, + url: { + type: 'string', + title: 'url', + format: 'url', + }, + arr: { + type: 'array', + title: 'array', + maxItems: 2, + required: true, + items: { + type: 'object', + properties: { + string: { + type: 'string', + title: 'string', + required: true, + }, + }, + }, + }, + }, +} +// 这是需要校验的数据 +const schemaData = { + string: '123456', // 超过5个字 + // number 字段不存在 + url: 'xxxxx', // 不合法的url + arr: [ + { + string: '1', + }, + { + string: '2', + }, + { + // 数组超出2项 + string: '', // 没有填 + }, + ], +} + +function recursiveField( + form: Form, + schema: ISchema, + basePath?: string, + name?: SchemaKey +) { + const fieldSchema = new Schema(schema) + const fieldProps = fieldSchema.toFieldProps() + + function recursiveProperties(propBasePath?: string) { + fieldSchema.mapProperties((propSchema, propName) => { + recursiveField(form, propSchema, propBasePath, propName) + }) + } + + if (name === undefined || name === null) { + recursiveProperties(basePath) + return + } + + if (schema.type === 'object') { + const field = form.createObjectField({ + ...fieldProps, + name, + basePath, + }) + + recursiveProperties(field.address.toString()) + } else if (schema.type === 'array') { + const field = form.createArrayField({ + ...fieldProps, + name, + basePath, + }) + + const fieldAddress = field.address.toString() + const fieldValues = form.getValuesIn(fieldAddress) + fieldValues.forEach((value: any, index: number) => { + if (schema.items) { + const itemsSchema = Array.isArray(schema.items) + ? schema.items[index] || schema.items[0] + : schema.items + + recursiveField(form, itemsSchema as ISchema, fieldAddress, index) + } + }) + } else if (schema.type === 'void') { + const field = form.createVoidField({ + ...fieldProps, + name, + basePath, + }) + + recursiveProperties(field.address.toString()) + } else { + form.createField({ + ...fieldProps, + name, + basePath, + }) + } +} +test('server validate', async () => { + const form = createForm({ + values: schemaData, + }) + recursiveField(form, schemaJson) + let errors: any[] + try { + await form.validate() + } catch (e) { + errors = e + } + expect(errors).not.toBeUndefined() +}) diff --git a/packages/json-schema/src/__tests__/traverse.spec.ts b/packages/json-schema/src/__tests__/traverse.spec.ts index 7ab1ed4ee9c..dbc6a9a244d 100644 --- a/packages/json-schema/src/__tests__/traverse.spec.ts +++ b/packages/json-schema/src/__tests__/traverse.spec.ts @@ -8,12 +8,20 @@ test('traverseSchema', () => { type: 'string', required: true, 'x-validator': 'phone', + default: { + input: 123, + }, }, (value, path) => { visited.push(path) } ) - expect(visited).toEqual([['x-validator'], ['type'], ['required']]) + expect(visited).toEqual([ + ['x-validator'], + ['type'], + ['required'], + ['default'], + ]) }) test('traverse circular reference', () => { @@ -33,6 +41,7 @@ test('traverse circular reference', () => { } a.dd.mm = a traverse(a, () => {}) + traverseSchema(a as any, () => {}) }) test('traverse none circular reference', () => { @@ -50,6 +59,7 @@ test('traverse none circular reference', () => { traverse(a, (value, path) => { paths.push(path) }) + traverseSchema(a, () => {}) expect( paths.some((path) => FormPath.parse(path).includes('dd.mm')) ).toBeTruthy() diff --git a/packages/json-schema/src/shared.ts b/packages/json-schema/src/shared.ts index 99b14306c19..0d0e84965b5 100644 --- a/packages/json-schema/src/shared.ts +++ b/packages/json-schema/src/shared.ts @@ -69,14 +69,11 @@ export const hasOwnProperty = Object.prototype.hasOwnProperty export const traverse = ( target: any, - visitor: (value: any, path: Array) => void, - filter?: (value: any, path: Array) => boolean + visitor: (value: any, path: Array) => void ) => { const seenObjects = [] const root = target const traverse = (target: any, path = []) => { - if (filter?.(target, path) === false) return - if (isPlainObj(target)) { const seenIndex = seenObjects.indexOf(target) if (seenIndex > -1) { @@ -106,12 +103,41 @@ export const traverseSchema = ( if (schema['x-validator'] !== undefined) { visitor(schema['x-validator'], ['x-validator']) } - traverse(schema, visitor, (value, path) => { - if (path[0] === 'x-validator') return false - if (String(path[0]).indexOf('x-') == -1 && isFn(value)) return false - if (SchemaNestedMap[path[0]]) return false - return true - }) + const seenObjects = [] + const root = schema + const traverse = (target: any, path = []) => { + if ( + path[0] === 'x-validator' || + path[0] === 'version' || + path[0] === '_isJSONSchemaObject' + ) + return + if (String(path[0]).indexOf('x-') == -1 && isFn(target)) return + if (SchemaNestedMap[path[0]]) return + if (isPlainObj(target)) { + if (path[0] === 'default' || path[0] === 'x-value') { + visitor(target, path) + return + } + const seenIndex = seenObjects.indexOf(target) + if (seenIndex > -1) { + return + } + const addIndex = seenObjects.length + seenObjects.push(target) + if (isNoNeedCompileObject(target) && root !== target) { + visitor(target, path) + return + } + each(target, (value, key) => { + traverse(value, path.concat(key)) + }) + seenObjects.splice(addIndex, 1) + } else { + visitor(target, path) + } + } + traverse(schema) } export const isNoNeedCompileObject = (source: any) => { diff --git a/packages/json-schema/src/transformer.ts b/packages/json-schema/src/transformer.ts index 0dc5d37efdc..c21b40e88ff 100644 --- a/packages/json-schema/src/transformer.ts +++ b/packages/json-schema/src/transformer.ts @@ -132,7 +132,10 @@ const setSchemaFieldState = ( } } -const getBaseScope = (field: Field, options: ISchemaTransformerOptions) => { +const getBaseScope = ( + field: Field, + options: ISchemaTransformerOptions = {} +) => { const $observable = (target: any, deps?: any[]) => autorun.memo(() => observable(target), deps) const $props = (props: any) => field.setComponentProps(props) diff --git a/packages/react/src/components/RecursionField.tsx b/packages/react/src/components/RecursionField.tsx index 6e311e224f5..9321660596e 100644 --- a/packages/react/src/components/RecursionField.tsx +++ b/packages/react/src/components/RecursionField.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useContext, useRef } from 'react' +import React, { Fragment, useContext, useRef, useMemo } from 'react' import { isFn, isValid } from '@formily/shared' import { GeneralField } from '@formily/core' import { Schema } from '@formily/json-schema' @@ -40,7 +40,7 @@ const useBasePath = (props: IRecursionFieldProps) => { export const RecursionField: React.FC = (props) => { const basePath = useBasePath(props) - const fieldSchema = new Schema(props.schema) + const fieldSchema = useMemo(() => new Schema(props.schema), [props.schema]) const fieldProps = useFieldProps(fieldSchema) const renderProperties = (field?: GeneralField) => { if (props.onlyRenderSelf) return diff --git a/packages/reactive/docs/api/batch.md b/packages/reactive/docs/api/batch.md index e6082cf3b09..71285c465d6 100644 --- a/packages/reactive/docs/api/batch.md +++ b/packages/reactive/docs/api/batch.md @@ -11,7 +11,7 @@ interface batch { (callback?: () => T): T //In-place batch scope(callback?: () => T): T //In-situ local batch bound any>(callback: T, context?: any): T //High-level binding -} + endpoint(callback?: () => void): void //Register batch endpoint callback ``` ## Example diff --git a/packages/reactive/docs/api/batch.zh-CN.md b/packages/reactive/docs/api/batch.zh-CN.md index 87c31c4c7ad..15ffada8ef8 100644 --- a/packages/reactive/docs/api/batch.zh-CN.md +++ b/packages/reactive/docs/api/batch.zh-CN.md @@ -11,6 +11,7 @@ interface batch { (callback?: () => T): T //原地batch scope(callback?: () => T): T //原地局部batch bound any>(callback: T, context?: any): T //高阶绑定 + endpoint(callback?: () => void): void //注册批量执行结束回调 } ``` diff --git a/packages/reactive/src/__tests__/batch.spec.ts b/packages/reactive/src/__tests__/batch.spec.ts index 5dade2837e0..84025d6851a 100644 --- a/packages/reactive/src/__tests__/batch.spec.ts +++ b/packages/reactive/src/__tests__/batch.spec.ts @@ -463,3 +463,43 @@ describe('annotation batch', () => { expect(obs.cc).toEqual(41) }) }) + +describe('batch endpoint', () => { + test('normal endpoint', () => { + const tokens = [] + const inner = batch.bound(() => { + batch.endpoint(() => { + tokens.push('endpoint') + }) + tokens.push('inner') + }) + const wrapper = batch.bound(() => { + inner() + tokens.push('wrapper') + }) + wrapper() + expect(tokens).toEqual(['inner', 'wrapper', 'endpoint']) + }) + + test('unexpect endpoint', () => { + const tokens = [] + const inner = batch.bound(() => { + batch.endpoint() + tokens.push('inner') + }) + const wrapper = batch.bound(() => { + inner() + tokens.push('wrapper') + }) + wrapper() + expect(tokens).toEqual(['inner', 'wrapper']) + }) + + test('no wrapper endpoint', () => { + const tokens = [] + batch.endpoint(() => { + tokens.push('endpoint') + }) + expect(tokens).toEqual(['endpoint']) + }) +}) diff --git a/packages/reactive/src/array.ts b/packages/reactive/src/array.ts index a239ddf287c..9090bc01597 100644 --- a/packages/reactive/src/array.ts +++ b/packages/reactive/src/array.ts @@ -30,12 +30,14 @@ export class ArraySet { } forEach(callback: (value: T) => void) { - for (let index = 0; index < this.value.length; index++) { + if (this.value.length === 0) return + for (let index = 0, len = this.value.length; index < len; index++) { callback(this.value[index]) } } forEachDelete(callback: (value: T) => void) { + if (this.value.length === 0) return for (let index = 0; index < this.value.length; index++) { const item = this.value[index] this.value.splice(index, 1) diff --git a/packages/reactive/src/batch.ts b/packages/reactive/src/batch.ts index 47596efecbe..cf2be719e63 100644 --- a/packages/reactive/src/batch.ts +++ b/packages/reactive/src/batch.ts @@ -4,8 +4,18 @@ import { batchScopeStart, batchScopeEnd, } from './reaction' +import { BatchEndpoints, BatchCount } from './environment' import { createBoundaryAnnotation } from './internals' -import { IAction } from './types' +import { IBatch } from './types' +import { isFn } from './checkers' -export const batch: IAction = createBoundaryAnnotation(batchStart, batchEnd) +export const batch: IBatch = createBoundaryAnnotation(batchStart, batchEnd) batch.scope = createBoundaryAnnotation(batchScopeStart, batchScopeEnd) +batch.endpoint = (callback?: () => void) => { + if (!isFn(callback)) return + if (BatchCount.value === 0) { + callback() + } else { + BatchEndpoints.add(callback) + } +} diff --git a/packages/reactive/src/environment.ts b/packages/reactive/src/environment.ts index 813d4265bdf..50083e04912 100644 --- a/packages/reactive/src/environment.ts +++ b/packages/reactive/src/environment.ts @@ -15,5 +15,6 @@ export const BatchScope = { value: false } export const DependencyCollected = { value: false } export const PendingReactions = new ArraySet() export const PendingScopeReactions = new ArraySet() +export const BatchEndpoints = new ArraySet<() => void>() export const MakeObservableSymbol = Symbol('MakeObservableSymbol') export const ObserverListeners = new ArraySet() diff --git a/packages/reactive/src/reaction.ts b/packages/reactive/src/reaction.ts index b888b625f49..7d2430608c2 100644 --- a/packages/reactive/src/reaction.ts +++ b/packages/reactive/src/reaction.ts @@ -4,6 +4,7 @@ import { IOperation, ReactionsMap, Reaction, PropertyKey } from './types' import { ReactionStack, PendingScopeReactions, + BatchEndpoints, DependencyCollected, RawReactionsMap, PendingReactions, @@ -182,6 +183,7 @@ export const batchEnd = () => { const prevUntrackCount = UntrackCount.value UntrackCount.value = 0 executePendingReactions() + executeBatchEndpoints() UntrackCount.value = prevUntrackCount } } @@ -228,6 +230,12 @@ export const executePendingReactions = () => { }) } +export const executeBatchEndpoints = () => { + BatchEndpoints.forEachDelete((callback) => { + callback() + }) +} + export const hasDepsChange = (newDeps: any[], oldDeps: any[]) => { if (newDeps === oldDeps) return false if (newDeps.length !== oldDeps.length) return true diff --git a/packages/reactive/src/types.ts b/packages/reactive/src/types.ts index 65a749a6a2f..0a6be573f85 100644 --- a/packages/reactive/src/types.ts +++ b/packages/reactive/src/types.ts @@ -105,3 +105,7 @@ export interface IAction extends IBoundable { (callback?: () => T): T //原地action scope?: ((callback?: () => T) => T) & IBoundable //原地局部action } + +export interface IBatch extends IAction { + endpoint?: (callback?: () => void) => void +}