From e0fd4c28cd0a4805b2b7a7ca1e2b143511308271 Mon Sep 17 00:00:00 2001 From: Claudio Savino Date: Wed, 29 Mar 2023 19:23:11 +0200 Subject: [PATCH] feat: support input ref, fix: focus method support input ref, fix: focus method fix: #529, fix: #250, fix: #524 --- CHANGELOG.md | 9 ++++++ src/Base.ts | 53 +++++++++++++++++++----------------- src/Field.ts | 52 +++++++++++++++++++++++------------ src/models/FieldInterface.ts | 1 + src/models/FieldProps.ts | 11 +++++++- src/parser.ts | 21 ++++++++------ src/props.ts | 24 ++++++++++++---- src/utils.ts | 6 ++-- tests/nested.actions.ts | 6 ++-- 9 files changed, 121 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77fa4184..a7846112 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 5.6.0 (next) + +- Introduced `ref` Field prop. (handle React Refs); +- `ref` is auto-binded with the input when using `bind()` or can be defined/changed with `set()` +- `autoFocus` bheavior changed, now can be defined in field definitions or can be assigned with `set()`. +- `focus()` method reimplemented using auto-ref (fixed). +- Introduced Field `blur()` method (using auto-ref). +- Fix: #529 #250 #524 + # 5.5.2 (next) - Fix: Empty Constructor (was requiring at least an empyt object if used with only class fileds definitions) diff --git a/src/Base.ts b/src/Base.ts index de9b3a46..7926221d 100755 --- a/src/Base.ts +++ b/src/Base.ts @@ -250,34 +250,37 @@ export default class Base implements BaseInterface { const initial = this.state.get("current", "props"); const struct = pathToStruct(path); // try to get props from separated objects - const $try = (prop: string) => { + const _try = (prop: string) => { const t = _.get(initial[prop], struct); - if ((prop === "input" || prop === "output") && typeof t !== "function") - return undefined; + const isIoProp: boolean = (prop === FieldPropsEnum.input || prop === FieldPropsEnum.output); + if (isIoProp && typeof t !== "function") return undefined; return t; }; const props = { $value: _.get(initial["values"], path), - $label: $try("labels"), - $placeholder: $try("placeholders"), - $default: $try("defaults"), - $initial: $try("initials"), - $disabled: $try("disabled"), - $bindings: $try("bindings"), - $type: $try("types"), - $options: $try("options"), - $extra: $try("extra"), - $related: $try("related"), - $hooks: $try("hooks"), - $handlers: $try("handlers"), - $validatedWith: $try("validatedWith"), - $validators: $try("validators"), - $rules: $try("rules"), - $observers: $try("observers"), - $interceptors: $try("interceptors"), - $input: $try("input"), - $output: $try("output"), + $label: _try("labels"), + $placeholder: _try("placeholders"), + $default: _try("defaults"), + $initial: _try("initials"), + $disabled: _try("disabled"), + $deleted: _try("deleted"), + $type: _try("types"), + $related: _try("related"), + $rules: _try("rules"), + $options: _try("options"), + $bindings: _try("bindings"), + $extra: _try("extra"), + $hooks: _try("hooks"), + $handlers: _try("handlers"), + $validatedWith: _try("validatedWith"), + $validators: _try("validators"), + $observers: _try("observers"), + $interceptors: _try("interceptors"), + $input: _try("input"), + $output: _try("output"), + $autoFocus: _try("autoFocus"), + $ref: _try("ref"), }; const field = this.state.form.makeField({ @@ -432,7 +435,7 @@ export default class Base implements BaseInterface { $container.state.form.$changed ++; $container.initField($key, $newFieldPath, field, true); } else if (recursion) { - if (_.has(field, "fields") && !_.isNil(field.fields)) { + if (_.has(field, FieldPropsEnum.fields) && !_.isNil(field.fields)) { // handle nested fields if defined this.deepUpdate(field.fields, $path); } else { @@ -649,7 +652,7 @@ export default class Base implements BaseInterface { container.state.form.$changed ++; if (this.state.options.get(OptionsEnum.softDelete, this)) { - return this.select(fullpath).set("deleted", true); + return this.select(fullpath).set(FieldPropsEnum.deleted, true); } container.each((field) => field.debouncedValidation.cancel()); @@ -696,7 +699,7 @@ export default class Base implements BaseInterface { _.merge(this.state.disposers[type], { [$dkey]: - key === "fields" + key === FieldPropsEnum.fields ? ffn.apply((change: any) => $call(change)) : (fn as any)($instance, key, (change: any) => $call(change)), }); diff --git a/src/Field.ts b/src/Field.ts index d7ff7057..609d685b 100755 --- a/src/Field.ts +++ b/src/Field.ts @@ -30,17 +30,20 @@ const setupFieldProps = (instance: FieldInterface, props: any, data: any) => $label: props.$label || (data && data.label) || "", $placeholder: props.$placeholder || (data && data.placeholder) || "", $disabled: props.$disabled || (data && data.disabled) || false, - $bindings: props.$bindings || (data && data.bindings) || FieldPropsEnum.default, + $rules: props.$rules || (data && data.rules) || null, $related: props.$related || (data && data.related) || [], + $deleted: props.$deleted || (data && data.deleted) || false, $validators: toJS(props.$validators || (data && data.validators) || null), $validatedWith: props.$validatedWith || (data && data.validatedWith) || FieldPropsEnum.value, - $rules: props.$rules || (data && data.rules) || null, + $bindings: props.$bindings || (data && data.bindings) || FieldPropsEnum.default, $observers: props.$observers || (data && data.observers) || null, $interceptors: props.$interceptors || (data && data.interceptors) || null, $extra: props.$extra || (data && data.extra) || null, $options: props.$options || (data && data.options) || {}, $hooks: props.$hooks || (data && data.hooks) || {}, $handlers: props.$handlers || (data && data.handlers) || {}, + $autoFocus: props.$autoFocus || (data && data.autoFocus) || false, + $ref: props.$ref || (data && data.ref) || undefined, }); const setupDefaultProp = ( @@ -91,7 +94,6 @@ export default class Field extends Base implements FieldInterface { $extra: any; $related: string[] | undefined; $validatedWith: string | undefined; - $validators: any[] | undefined; $rules: string[] | undefined; @@ -99,13 +101,13 @@ export default class Field extends Base implements FieldInterface { $focused: boolean = false; $blurred: boolean = false; $deleted: boolean = false; + $autoFocus: boolean = false; + $ref: any = undefined $clearing: boolean = false; $resetting: boolean = false; - autoFocus: boolean = false; showError: boolean = false; - errorSync: string | null = null; errorAsync: string | null = null; @@ -148,7 +150,6 @@ export default class Field extends Base implements FieldInterface { $deleted: observable, $clearing: observable, $resetting: observable, - autoFocus: observable, showError: observable, errorSync: observable, errorAsync: observable, @@ -156,6 +157,7 @@ export default class Field extends Base implements FieldInterface { validationFunctionsData: observable, validationAsyncData: observable, files: observable, + autoFocus: computed, checkValidationErrors: computed, checked: computed, value: computed, @@ -194,6 +196,7 @@ export default class Field extends Base implements FieldInterface { clear: action, reset: action, focus: action, + blur: action, showErrors: action, showAsyncErrors: action, update: action @@ -216,8 +219,8 @@ export default class Field extends Base implements FieldInterface { this.observeValidationOnBlur(); this.observeValidationOnChange(); - this.initMOBXEvent("observers"); - this.initMOBXEvent("interceptors"); + this.initMOBXEvent(FieldPropsEnum.observers); + this.initMOBXEvent(FieldPropsEnum.interceptors); this.execHook(FieldPropsEnum.onInit); @@ -300,6 +303,14 @@ export default class Field extends Base implements FieldInterface { return this.$extra; } + get ref() { + return this.$ref; + } + + get autoFocus() { + return this.$autoFocus; + } + get type() { return toJS(this.$type); } @@ -437,11 +448,8 @@ export default class Field extends Base implements FieldInterface { FieldPropsEnum.onBlur, args, action(() => { - if (!this.$blurred) { - this.$blurred = true; - } - this.$focused = false; + this.$blurred = true; }) ); @@ -697,8 +705,15 @@ export default class Field extends Base implements FieldInterface { } focus(): void { - this.state.form.each((field: any) => (field.autoFocus = false)); - this.autoFocus = true; + if(this.ref && !this.focused) this.ref.focus(); + this.$focused = true; + this.$touched = true; + } + + blur(): void { + if(this.ref && this.focused) this.ref.blur(); + this.$focused = false; + this.$blurred = true; } showErrors(show: boolean = true): void { @@ -767,14 +782,17 @@ export default class Field extends Base implements FieldInterface { if (!_.isArray(this[`$${type}`])) return; let fn: any; - if (type === "observers") fn = this.observe; - if (type === "interceptors") fn = this.intercept; + if (type === FieldPropsEnum.observers) fn = this.observe; + if (type === FieldPropsEnum.interceptors) fn = this.intercept; // @ts-ignore this[`$${type}`].map((obj: any) => fn(_.omit(obj, FieldPropsEnum.path))); } bind(props = {}) { - return this.state.bindings.load(this, this.bindings, props); + return { + ...this.state.bindings.load(this, this.bindings, props), + ref: ($ref) => (this.$ref = $ref), + } } update(fields: any): void { diff --git a/src/models/FieldInterface.ts b/src/models/FieldInterface.ts index 1ffa7478..29584daa 100644 --- a/src/models/FieldInterface.ts +++ b/src/models/FieldInterface.ts @@ -16,6 +16,7 @@ export default interface FieldInterface extends BaseInterface { validationFunctionsData: any[]; debouncedValidation: any; autoFocus: boolean; + ref: any; showError: boolean; checkValidationErrors: boolean; checked: any; diff --git a/src/models/FieldProps.ts b/src/models/FieldProps.ts index 8bb174fa..0e1f366b 100644 --- a/src/models/FieldProps.ts +++ b/src/models/FieldProps.ts @@ -3,6 +3,8 @@ export enum FieldPropsEnum { id = "id", path = "path", name = "name", + fields = "fields", + ref= "ref", type = "type", value = "value", initial = "initial", @@ -10,13 +12,20 @@ export enum FieldPropsEnum { checked = "checked", label = "label", placeholder = "placeholder", + error = "error", + validatedWith = "validatedWith", + validators = "validators", + rules = "rules", related = "related", options = "options", extra = "extra", bindings = "bindings", hooks = "hooks", handlers = "handlers", - error = "error", + input="input", + output="output", + interceptors = "interceptors", + observers = "observers", // computed disabled = "disabled", deleted = "deleted", diff --git a/src/parser.ts b/src/parser.ts index de5a5327..3c745a2c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,4 +1,5 @@ import _ from "lodash"; +import { FieldPropsEnum } from "./models/FieldProps"; import { $try, isArrayOfStrings, @@ -13,11 +14,10 @@ const defaultClearValue = ({ value = undefined, type = undefined } : { value: any, type?: string }) : false | any[] | 0 | "" | null | undefined => { - if (type === "date") return null; - if (_.isDate(value)) return null; + if (_.isDate(value) || type === "date") return null; + if (_.isNumber(value) || type === "number") return 0; if (_.isArray(value)) return []; if (_.isBoolean(value)) return false; - if (_.isNumber(value)) return 0; if (_.isString(value)) return ""; return undefined; }; @@ -63,7 +63,12 @@ const parseInput = ( const parseArrayProp = (val: any, prop: string, removeNullishValuesInArrays: boolean): any => { const values = _.values(val); - if (removeNullishValuesInArrays && (prop === "value" || prop === "initial" || prop === "default")) { + const isValProp: boolean = + (prop === FieldPropsEnum.value + || prop === FieldPropsEnum.initial + || prop === FieldPropsEnum.default); + + if (removeNullishValuesInArrays && isValProp) { return _.without(values, ...[null, undefined, ""]); } return values; @@ -73,8 +78,8 @@ const parseCheckArray = (field: any, value: any, prop: string, removeNullishValu field.hasIncrementalKeys ? parseArrayProp(value, prop, removeNullishValuesInArrays) : value; const parseCheckOutput = (field: any, prop: string) => { - if (prop === "value" || prop.startsWith("value.")) { - const base = field.$output ? field.$output(field["value"]) : field["value"] + if (prop === FieldPropsEnum.value || prop.startsWith("value.")) { + const base = field.$output ? field.$output(field[FieldPropsEnum.value]) : field[FieldPropsEnum.value] return prop.startsWith("value.") ? _.get(base, prop.substring(6)) : base } return field[prop]; @@ -126,7 +131,7 @@ const handleFieldsArrayOfObjects = ($fields: any) => { fields = _.transform( fields, ($obj, field) => { - if (hasUnifiedProps({ fields: { field } }) && !_.has(field, "name")) return undefined; + if (hasUnifiedProps({ fields: { field } }) && !_.has(field, FieldPropsEnum.name)) return undefined; return Object.assign($obj, { [field.name]: field }); }, {} @@ -303,7 +308,7 @@ const pathToFieldsTree = ( const ss = s.split('.') let t = fields[0]?.fields for (let i = 0; i < ss.length; i++) { - t = t?.[ss[i]]?.['fields'] + t = t?.[ss[i]]?.[FieldPropsEnum.fields] if (!t) break; } if (t) diff --git a/src/props.ts b/src/props.ts index f4f2a48c..1c7f8123 100755 --- a/src/props.ts +++ b/src/props.ts @@ -30,6 +30,7 @@ export const props: PropsGroupsInterface = { FieldPropsEnum.error, FieldPropsEnum.deleted, FieldPropsEnum.disabled, + FieldPropsEnum.autoFocus, ], handlers: [ FieldPropsEnum.onChange, @@ -66,6 +67,7 @@ export const props: PropsGroupsInterface = { "labels", "placeholders", "disabled", + "deleted", "related", "options", "extra", @@ -73,12 +75,24 @@ export const props: PropsGroupsInterface = { "types", "hooks", "handlers", - "deleted", - "error", + "autoFocus", + "refs" + ], + functions: [ + FieldPropsEnum.observers, + FieldPropsEnum.interceptors, + FieldPropsEnum.input, + FieldPropsEnum.output, + ], + validation: [ + FieldPropsEnum.rules, + FieldPropsEnum.validators, + FieldPropsEnum.validatedWith, + ], + exceptions: [ + FieldPropsEnum.isDirty, + FieldPropsEnum.isPristine ], - functions: ["observers", "interceptors", "input", "output"], - validation: ["rules", "validators", "validatedWith"], - exceptions: ["isDirty", "isPristine"], types: { isDirty: "some", isPristine: "every", diff --git a/src/utils.ts b/src/utils.ts index b7663128..c09c99e7 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { ObservableMap, values as mobxValues, keys as mobxKeys } from "mobx"; import FieldInterface from "./models/FieldInterface"; - +import { FieldPropsEnum } from "./models/FieldProps"; import { props } from "./props"; const getObservableMapValues = (observableMap: ObservableMap): @@ -52,7 +52,7 @@ const hasProps = ($type: any, $data: any) => { break; case "all": $props = [ - "id", + FieldPropsEnum.id, ...props.computed, ...props.field, ...props.validation, @@ -117,7 +117,7 @@ const hasSeparatedProps = (initial: any) => const allowNested = (field: any, strictProps: boolean): boolean => _.isObject(field) && !_.isDate(field) && - !_.has(field, "fields") && + !_.has(field, FieldPropsEnum.fields) && (!hasSome(field, [ ...props.field, ...props.validation, diff --git a/tests/nested.actions.ts b/tests/nested.actions.ts index 4f6daa04..cf29fed0 100755 --- a/tests/nested.actions.ts +++ b/tests/nested.actions.ts @@ -13,7 +13,7 @@ describe('Form error test', () => { expect(() => $.$T.$('notIncrementalFields').del(99)).to.throw(Error)); }); -describe('Form autoFocus test', () => { - it('$T notIncrementalFields autoFocus should be true', () => - expect($.$T.$('notIncrementalFields').autoFocus).to.be.true); +describe('Form $focused test', () => { + it('$T notIncrementalFields $focused should be true', () => + expect($.$T.$('notIncrementalFields').$focused).to.be.true); });