diff --git a/example/src/forms/network/form.tsx b/example/src/forms/network/form.tsx index 012c9ae..3c43b81 100644 --- a/example/src/forms/network/form.tsx +++ b/example/src/forms/network/form.tsx @@ -23,7 +23,7 @@ const Network = createForm({ }, interface: { value: "wifi", - title: 1,//"网卡类型", + title: "网卡类型", enable: true, select: () => { return [ @@ -103,7 +103,7 @@ const Network = createForm({ submit: { // 这是一个动作, title: "提交wifi", enable: (net: any) => net.interface.value === "wifi", - validate: (value: string) => value.length > 6, + // validate: (value: string) => value.length > 6, execute:async (wifi:any)=>{ await delay(2000) console.log("提交wifi=",wifi) @@ -145,9 +145,6 @@ const Network = createForm({ }, progressSubmit2: { // 这是一个动作, title: "提交进度2", - validate:async ()=>{ - return true - }, execute:action(async (fields,{getProgressbar})=>{ console.log("submit fields=",fields) const bar = getProgressbar() @@ -214,24 +211,23 @@ const Network = createForm({ }, timeoutSubmit: { title: "提交超时倒计时", - execute: computed(async () => { + execute: computed(async (scope: Dict) => { + console.log("ping=",Object.assign({},scope)); await delay(5000); },[],{timeout:2000}), }, ping: { title: "测试网络连通性", - scope:"fields.wifi", enable: computed((wifi: any) => { return wifi.ssid.value.length > 0 },{scope:"fields.wifi"}), execute: async (a: Dict) => { - await delay(2000); - console.log(a); + await delay(1000); + console.log("ping=",Object.assign({},a)); }, }, // 向导表单:上一步 previous:{ - enable: (wifi: any) => wifi.ssid.value.length > 0, execute:async ()=>{ return 1 diff --git a/example/src/forms/network/index.tsx b/example/src/forms/network/index.tsx index 04d6da3..a2678bd 100644 --- a/example/src/forms/network/index.tsx +++ b/example/src/forms/network/index.tsx @@ -90,6 +90,11 @@ const NetworkForm = ()=>{ return }} + name="fields.wifi.standardSubmit" > + {({title,visible,loading,enable,run,timeout})=>{ + return + }} + name="fields.wifi.timeoutSubmit" > {({title,visible,loading,enable,run,error,timeout})=>{ return diff --git a/packages/core/src/action.tsx b/packages/core/src/action.tsx index 0c6d333..671e3ae 100644 --- a/packages/core/src/action.tsx +++ b/packages/core/src/action.tsx @@ -34,7 +34,7 @@ import { ReactNode, useCallback, useRef, RefObject,useState} from "react"; import React from "react"; import type { FormDefine, FormStore } from "./form"; -import { AsyncComputedDefine, AsyncComputedGetter, AsyncComputedObject, ComputedDescriptorDefine, ComputedOptions, ComputedParams, Dict, RuntimeComputedOptions, computed, getValueByPath} from '@speedform/reactive'; +import { AsyncComputedDefine, AsyncComputedGetter, AsyncComputedObject, ComputedDescriptorDefine, ComputedOptions, ComputedParams, Dict, RuntimeComputedOptions, computed, getVal, getValueByPath} from '@speedform/reactive'; import { omit } from "flex-tools/object/omit"; import { getFormData } from "./serialize"; import { getId } from "./utils"; @@ -357,6 +357,8 @@ export function createUseAction(store:For execute:action(executor,options) } }) + // 读取一次以触发计算属性对象的创建 + getValueByPath(state,['actions',actionName]) } ref.current = actionName } diff --git a/packages/core/src/form.tsx b/packages/core/src/form.tsx index c4f1cb4..d061ee5 100644 --- a/packages/core/src/form.tsx +++ b/packages/core/src/form.tsx @@ -46,7 +46,7 @@ import { createFieldComponent, FormFields } from './field'; import { createFieldGroupComponent } from "./fieldGroup"; import { assignObject } from "flex-tools/object/assignObject"; import { FormActions, UseActionType, createActionComponent, createUseAction, getAction } from './action'; -import { FIELDS_STATE_KEY, VALIDATE_COMPUTED_GROUP } from './consts'; +import { ACTIONS_STATE_KEY, FIELDS_STATE_KEY, VALIDATE_COMPUTED_GROUP } from './consts'; import { defaultObject } from "flex-tools/object/defaultObject"; import { createObjectProxy } from "./utils"; import { createLoadApi, createGetValuesApi } from "./serialize"; @@ -167,6 +167,21 @@ export type FormStatus = 'idle' // actions : Actions // 表单动作 // } +/** + * 设置表单默认属性 + * title?:ComputedAttr // 动作标题 + help?:ComputedAttr // 动作帮助 + tips?:ComputedAttr // 动作提示 + visible?:ComputedAttr // 是否可见 + enable?:ComputedAttr // 是否可用 + valid?:ComputedAttr // 是否有效 + readonly?:ComputedAttr // 是否只读 + * @param define + */ + function setFormDefault(define:T){ + return defaultObject(define,defaultFormProps) as typeof defaultFormProps & T + } + /** * @@ -202,21 +217,6 @@ function createValidatorHook(valuePath:string[],options:ComputedOptions){ } -/** - * 设置表单默认属性 - * title?:ComputedAttr // 动作标题 - help?:ComputedAttr // 动作帮助 - tips?:ComputedAttr // 动作提示 - visible?:ComputedAttr // 是否可见 - enable?:ComputedAttr // 是否可用 - valid?:ComputedAttr // 是否有效 - readonly?:ComputedAttr // 是否只读 - * @param define - */ -function setFormDefault(define:T){ - return defaultObject(define,defaultFormProps) as typeof defaultFormProps & T -} - /** @@ -238,25 +238,29 @@ function setFormDefault(define:T){ * */ function createActionHook(valuePath:string[],options:ComputedOptions){ - if(valuePath.length>1 && valuePath[valuePath.length-1]=='execute'){ - // 默认不自动执行,需要手动调用action.execute.run()来执行 - options.immediate = false - // 如果没有指定scope,则默认指向fields,这样就可以直接使用fields下的字段,而不需要fields前缀 - if(options.scope){ - if(Array.isArray(options.scope)){ - // 如果scope中没有fields,则添加fields,并且保证fields在第一个位置 - if(options.scope.length>0 && options.scope[0]!=FIELDS_STATE_KEY){ - options.scope.unshift(FIELDS_STATE_KEY) - } + if(valuePath.length>1 ){ + if(valuePath[valuePath.length-1]=='execute'){ + // 默认不自动执行,需要手动调用action.execute.run()来执行 + options.immediate = false + // 如果没有指定scope,则默认指向fields,这样就可以直接使用fields下的字段,而不需要fields前缀 + if(options.scope){ + if(Array.isArray(options.scope)){ + // 如果scope中没有fields,则添加fields,并且保证fields在第一个位置 + if(options.scope.length>0 && options.scope[0]!=FIELDS_STATE_KEY){ + options.scope.unshift(FIELDS_STATE_KEY) + } + } + }else{// 如果没有指定scope,则默认指向fields + options.scope = [FIELDS_STATE_KEY] } - }else{// 如果没有指定scope,则默认指向fields, - options.scope = [FIELDS_STATE_KEY] - } - options.noReentry = true // 禁止重入 + options.noReentry = true // 禁止重入 + } } + } /** * 对所有位于fields下的的依赖均自动添加fields前缀,这样在声明依赖时就可以省略fields前缀 + * * @param valuePath * @param getter * @param options @@ -273,6 +277,7 @@ function createDepsHook(valuePath:string[],options:ComputedOptions){ } } + /** * * 冻结表单,即表单计算函数不再执行 @@ -292,6 +297,27 @@ export function getFieldName(valuePath:string[]){ valuePath.slice(0,-1).join(".") : valuePath.join(".") : '' } +export type FormObject = { + state: FormState, + useState: (fn: (state: FormState) => any) => any, + setState: (fn: (draft: FormState) => void) => void, + Form: FormComponent, + Field: ReactFC, + Group: ReactFC, + Action: ReactFC, + Submit: ReactFC, + Reset: ReactFC, + useAction: UseActionType, + fields: State['fields'], + actions: State['actions'], + getAction: (name: string) => any, + freeze: (value?: boolean) => void, + load: (data: Dict) => void, + getValues: () => Dict, + computedObjects: Dict, + watchObjects: Dict, + validate: (value?: Dict) => Promise +} /** * 创建声明表单 * @@ -310,7 +336,8 @@ export function createForm(schema: State,op // 创建表单Store对象实例 const store = createStore(schema ,{ - debug:opts.debug, + debug:opts.debug, + immediate:true, // 默认立即执行完成所有计算属性的初始化 // 计算函数作用域默认指向fields scope: ()=>[FIELDS_STATE_KEY], // 创建计算函数时的钩子函数,可以在创建前做一些不可描述的处理 @@ -322,15 +349,13 @@ export function createForm(schema: State,op // 3. 将表单actions的execute的onComputedResult指向其current createActionHook(valuePath,options) }, - onComputedDraft(draft,{computedType,valuePath}){ - // 针对计算属性 + getRootScope(draft,{computedType,valuePath}){ // 修改fields下的所有计算函数的作用域根,使之总是指向fields开头 // 这样可以保证在计算函数中,当scope->Root时,总是指向fields,否则就需要state.fields.xxx.xxx if(computedType==='Computed' && valuePath.length >0 && valuePath[0]==FIELDS_STATE_KEY){ return draft.fields } - }, - immediate:true // 默认立即执行完成所有计算属性的初始化 + } }); /** @@ -370,7 +395,7 @@ export function createForm(schema: State,op computedObjects: formStore.computedObjects, watchObjects : formStore.watchObjects, validate : createValidator(formStore) - }; + } } @@ -379,55 +404,100 @@ export function createForm(schema: State,op * * 当使用标准的表单提交模式时,使用该组件 * + * 简单用法: + * { + * console.log(data) + * }} + * onLoading={(loading)=>{ }} + * onError={(error)=>{}} + * onValid={(valid)=>{}} + * > + * + * + * 提交 + * * - *
// 表单组件 - * name="wifi"> - * // 声明字段 - * - * {({action})=>{ - * retrun - * }} - * - * // 声明子表单 - * + * 高级用法: + * + * {({timeout,loading})=>{ + * return <> + * + * + * + * + * }} + * * * * @param this * @param store * @returns */ -function createFormComponent(store: FormStore,formOptions:RequiredFormOptions): FormComponent { - +function createFormComponent(store: FormStore,formOptions:RequiredFormOptions): FormComponent { + const Action = createActionComponent(store) + const useAction = createUseAction(store) + return React.forwardRef((props: FormProps,ref:React.ForwardedRef) => { const {children } = props; - + + // + const { run } = useAction(async (scope:any,params)=>{ + await store.computedObjects.runGroup(VALIDATE_COMPUTED_GROUP) + + },{ + save:false + }) + // 提交表单 - const onSubmit = useCallback(async (ev: React.FormEvent) => { + const onSubmit = useCallback(async (ev: React.FormEvent) => { + run() // 提交前运行校验 if(formOptions.validAt==='submit'){ await store.computedObjects.runGroup(VALIDATE_COMPUTED_GROUP) - } - debugger + } await store.computedObjects.runGroup(VALIDATE_COMPUTED_GROUP) ev.preventDefault(); - if(store.state.validate){ - return true - }else{ - return false - } },[]); // 重置表单 const onReset = useCallback((e: React.FormEvent) => { },[]); + return ( - {children} + <> +
+ {children} +
+ + // name="$submit"> + // {(params)=>{ + + // return <>{ + // typeof(children)=='function' ? + // (children as any)(params) + // : + //
onSubmit(ev,run)} + // onReset={onReset} + // >{children}
+ // } + // + // }} + + // + ) }) as FormComponent } diff --git a/packages/reactive/src/computed/async.ts b/packages/reactive/src/computed/async.ts index 81b3d4c..41f4755 100644 --- a/packages/reactive/src/computed/async.ts +++ b/packages/reactive/src/computed/async.ts @@ -73,7 +73,7 @@ async function executeComputedGetter(draft:any,computedRunContex const { id,valuePath,getter,resultPath,dependValues } = computedRunContext; const { timeout=0,retry=[0,0],selfReactiveable } = computedOptions const setState = selfReactiveable ? selfReactiveable.setState.bind(selfReactiveable) : store.setState - // + const scopeDraft = getComputedScope(store,computedOptions,{draft,dependValues,valuePath,computedType:"Computed"} ) const [retryCount,retryInterval] = Array.isArray(retry) ? retry : [Number(retry),0] @@ -265,7 +265,7 @@ export function createAsyncComputedMutate(computedPar computedOptions.id = computedId const computedDesc = valuePath.join(OBJECT_PATH_DELIMITER) - store.options.log(`Create async computed: ${computedDesc} (depends=${depends.length==0 ? 'None' : joinValuePath(depends)})`); + store.options.log(()=>`Create async computed: ${computedDesc} (depends=${depends.length==0 ? 'None' : joinValuePath(depends)},scope=${computedOptions.scope})`); // 7. 创建mutate const computedRunContext:ComputedRunContext = { diff --git a/packages/reactive/src/computed/computedObjects.ts b/packages/reactive/src/computed/computedObjects.ts index 3ee53af..f2479e9 100644 --- a/packages/reactive/src/computed/computedObjects.ts +++ b/packages/reactive/src/computed/computedObjects.ts @@ -24,6 +24,22 @@ export class ComputedObjects extends MapcomputedObject.group==group).map(computedObject=> computedObject.async ? computedObject.run(options) : computedObject.run(options))) } + + async run(filter:(computedObject:ComputedObject)=>boolean,options?:RuntimeComputedOptions):Promise + async run(id:string,options?:RuntimeComputedOptions):Promise + async run():Promise{ + if(arguments.length==0){ + return Promise.all([...this.values()].map(computedObject=>computedObject.run())) + } + let filter,id='' + if(typeof(arguments[0])==='function'){ + filter = arguments[0] + }else if(typeof(arguments[0])==='string'){ + id=arguments[0] + } + const options:RuntimeComputedOptions|undefined = arguments[1] + return Promise.all([...this.values()].filter(filter).map(computedObject=>computedObject.run(options))) + } /** * 启用或禁用计算 * @param value diff --git a/packages/reactive/src/computed/sync.ts b/packages/reactive/src/computed/sync.ts index 284fdb1..1dc856e 100644 --- a/packages/reactive/src/computed/sync.ts +++ b/packages/reactive/src/computed/sync.ts @@ -89,7 +89,7 @@ export function createComputedMutate(computedParams:I const computedId = getComputedId(valuePath,computedOptions) const computedDesc = valuePath.join(OBJECT_PATH_DELIMITER) - store.options.log(`Create sync computed: ${computedDesc}`); + store.options.log(()=>`Create sync computed: ${computedDesc} (scope=${computedOptions.scope})`); const computedRunContext:ComputedRunContext = { id : computedId, diff --git a/packages/reactive/src/computed/types.ts b/packages/reactive/src/computed/types.ts index 9a175c9..8d07091 100644 --- a/packages/reactive/src/computed/types.ts +++ b/packages/reactive/src/computed/types.ts @@ -100,7 +100,7 @@ export interface ComputedProgressbar{ scope? : ComputedScope // 计算函数的第一个参数 initial? : Value // 异步计算,默认情况下,通过typeof(fn)=="async function"来判断是否是异步计算函数 - // 但是在返回Promise或者Babel转码等情况下,判断可能失效时,需要手动指定async=true + // 但是在返回Promise或者Babel转码等情况下,判断会失效时,需要手动指定async=true async?:boolean // 指定依赖,例如["key","a.b.c"]等形式 depends?:ComputedDepends diff --git a/packages/reactive/src/consts.ts b/packages/reactive/src/consts.ts index 91b692f..dae0f7f 100644 --- a/packages/reactive/src/consts.ts +++ b/packages/reactive/src/consts.ts @@ -1,7 +1,4 @@ // 当声明对象路径时,使用的分隔符 export const OBJECT_PATH_DELIMITER = '.' export const SKIP_COMPUTED= Symbol('SKIP_COMPUTED') - - - - \ No newline at end of file +export const EMPTY_DEPENDS =Symbol('EMPTY_DEPENDS') \ No newline at end of file diff --git a/packages/reactive/src/extends.ts b/packages/reactive/src/extends.ts index 6d320b4..3555ec1 100644 --- a/packages/reactive/src/extends.ts +++ b/packages/reactive/src/extends.ts @@ -13,6 +13,7 @@ import { installComputed } from "./computed"; import { installWatch } from "./watch"; import { IReactiveReadHookParams } from "./reactives/types"; import { Dict } from "./types"; +import { isValidPath } from "./utils/isValidPath"; // export function installExtends(computedParams:IComputeParams,store:IStore) { @@ -35,6 +36,7 @@ import { Dict } from "./types"; export function installExtends(computedParams:IReactiveReadHookParams,store:IStore) { // 拦截读取state的操作,在第一次读取时, const { path, value } = computedParams; + if(!isValidPath(path)) return const key = joinValuePath(path); if ( typeof value === "function" && !store._replacedKeys[key] && !isSkipComputed(value) ) { store._replacedKeys[key] = true; diff --git a/packages/reactive/src/scope.ts b/packages/reactive/src/scope.ts index fa566da..12f82ab 100644 --- a/packages/reactive/src/scope.ts +++ b/packages/reactive/src/scope.ts @@ -68,49 +68,51 @@ export type GetComputedContextOptions ={ export function getComputedScope(store:IStore,computedOptions: ComputedOptions,ctx:{draft: any,dependValues:any[],valuePath:string[],computedType:ComputedType}) { const { draft,dependValues, valuePath, computedType } = ctx; - + let rootDraft = draft; - - - // 1. 执行hook:可以在hook函数中修改计算函数的根上下文以及相关配置参数 - if (typeof store.options.onComputedDraft == "function") { - const newDraft = store.options.onComputedDraft.call(draft,draft,{computedType,valuePath}); + // 1. 执行hook:可以在hook函数中修改计算函数的根上下文以及相关配置参数 + if (typeof store.options.getRootScope == "function") { + const newDraft = store.options.getRootScope.call(draft,draft,{computedType,valuePath}); if (newDraft !== undefined) { rootDraft = newDraft; } } + const parentPath = valuePath.length>=1 ? valuePath.slice(0, valuePath.length - 1) : []; // 2. 读取计算函数的上下文配置参数 const scopeRef = getScopeOptions(draft,valuePath,computedOptions.scope, (store.options.scope && store.options.scope(computedType,valuePath))) + let scope = draft // 3. 根据配置参数获取计算函数的上下文对象 try { if(scopeRef === ComputedScopeRef.Current) { - return getValueByPath(draft, parentPath); + scope = getValueByPath(draft, parentPath); }else if (scopeRef === ComputedScopeRef.Parent) { - return getValueByPath(draft,valuePath.slice(0, valuePath.length - 2)); + scope = getValueByPath(draft,valuePath.slice(0, valuePath.length - 2)); }else if (scopeRef === ComputedScopeRef.Root) { - return rootDraft; + scope = rootDraft; }else if (scopeRef === ComputedScopeRef.Depends) { // 异步计算的依赖值 - return Array.isArray(dependValues) ? dependValues.map(dep=>typeof(dep)=='function' ? dep() : dep) : []; + scope = Array.isArray(dependValues) ? dependValues.map(dep=>typeof(dep)=='function' ? dep() : dep) : []; }else{ if (typeof scopeRef == "string") { - if(scopeRef.startsWith("@")){ - return getComputedScope(store,{...computedOptions,scope:scopeRef.slice(1)},{draft,dependValues,valuePath,computedType}) + // 当scope是以@开头的字符串时,代表是一个路径指向,如:@./user,代表其scope是由user属性值指向的对象路径 + if(scopeRef.startsWith("@")){ // + scope = getComputedScope(store,{ + ...computedOptions, + scope:getComputedScope(store,{ + ...computedOptions,scope:scopeRef.slice(1) + },ctx) + },ctx) }else{ - return getValueByPath(draft, getRelValuePath(valuePath,scopeRef)); + scope = getValueByPath(draft, getRelValuePath(valuePath,scopeRef)); } }else if (Array.isArray(scopeRef)) { // 从根对象开始的完整路径 - return getValueByPath(draft, scopeRef); - }else if (typeof scopeRef == "number") { - const endIndex = scopeRef > valuePath.length - 2 ? valuePath.length - scopeRef - 1 : 0; - return getValueByPath(draft, valuePath.slice(0, endIndex)); - }else { - return draft; + scope = getValueByPath(draft, scopeRef); } } - }catch (e) { - return draft; - } + }catch (e:any) { + store.options.log(`Error while getting computed scope ${valuePath.join(OBJECT_PATH_DELIMITER)}: ${e.message}`); + } + return scope } diff --git a/packages/reactive/src/store/types.ts b/packages/reactive/src/store/types.ts index f4c8936..b04740a 100644 --- a/packages/reactive/src/store/types.ts +++ b/packages/reactive/src/store/types.ts @@ -75,6 +75,7 @@ export interface StoreOptions{ */ onCreateComputed:(this:IStore,keyPath:string[],getter:Function,options:ComputedOptions)=> void | (()=>any) + /** * 在传递给计算函数的scope时调用 * 默认draft指向的是当前根对象,可以在此返回一个新的draft指向 @@ -82,7 +83,8 @@ export interface StoreOptions{ * 比如,return draft.fields,代表计算函数的draft指向state.fields * */ - onComputedDraft(draft:any,options:{computedType:StateComputedType, valuePath:string[]}):any + getRootScope(draft:any,options:{computedType:StateComputedType, valuePath:string[]}):any + // 输出日志信息 log:(message:any,level?:'log' | 'error' | 'warn')=>void diff --git a/packages/reactive/src/utils/getDepValues.ts b/packages/reactive/src/utils/getDepValues.ts index 20283f2..62c683d 100644 --- a/packages/reactive/src/utils/getDepValues.ts +++ b/packages/reactive/src/utils/getDepValues.ts @@ -9,7 +9,12 @@ import { getVal } from "./getVal" */ export function getDepValues(deps:any[],draft:any,curValuePath:string[]){ return deps.map((dep)=>{ - return getVal(draft,getRelValuePath(curValuePath,dep)) - }) + try{ + return getVal(draft,getRelValuePath(curValuePath,dep)) + }catch(e){ + console.warn(e) + return undefined + } + }).filter((v)=>v!==undefined) } diff --git a/packages/reactive/src/utils/getVal.ts b/packages/reactive/src/utils/getVal.ts index 208768b..44bbb71 100644 --- a/packages/reactive/src/utils/getVal.ts +++ b/packages/reactive/src/utils/getVal.ts @@ -12,9 +12,10 @@ export function getVal(obj: any, keyPath: string[]): any { if(key in parent){ val = parent[key] }else{ - throw new Error(`key ${key} not in object ${parent}`) - } - } + throw new Error(`invalid keypath: ${keyPath.join(".")}`) + } + } + parent = val }); return val; } diff --git a/packages/reactive/src/utils/getValueByPath.ts b/packages/reactive/src/utils/getValueByPath.ts index 0592151..4acd0cf 100644 --- a/packages/reactive/src/utils/getValueByPath.ts +++ b/packages/reactive/src/utils/getValueByPath.ts @@ -9,7 +9,8 @@ export function getValueByPath(state:any,path?:string | string[] | ((state:any)= } paths = Array.isArray(path) ? path : (typeof(path)=='string' ? path.split(delimiter) : []) return paths.length > 0 ? getVal(state,paths) : state - }catch{ + }catch(e){ + console.warn(e) return state } } diff --git a/packages/reactive/src/utils/isValidPath.ts b/packages/reactive/src/utils/isValidPath.ts new file mode 100644 index 0000000..d125314 --- /dev/null +++ b/packages/reactive/src/utils/isValidPath.ts @@ -0,0 +1,22 @@ +/** + * 判定是否为有效路径 + * + * 有效路径必须满足以下条件: + * + * - 字符串 + * - 字符串长度大于 0 + * - 字符串数组 + * + * + */ + + +export function isValidPath(path: string | string[]): path is string[] { + if (typeof path === "string") { + return path.length > 0; + } + if (Array.isArray(path)) { + return path.every((p) => typeof p === "string" && p.length > 0); + } + return true +} \ No newline at end of file