diff --git a/CHANGELOG.md b/CHANGELOG.md index 0718c829..ad2c9cff 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 5.8.0 (next) + +- `set()` method will increase `changed` prop and trigger `onChange` Event Hook +- Mobx events (observe/intercept) allowed props guarded. +- Introduced `onSync` Hook (triggered on `onChange` Event Handler) +- Introduced `validateOnSubmit` form option (active by default). +- Fix: `ref` prop for separated props mode renamed to `refs` (plural) +- Fix: #337 # 5.7.1 (next) - fix: allow `ref` prop on `set()` diff --git a/src/Base.ts b/src/Base.ts index f41e8fa2..7cf66a36 100755 --- a/src/Base.ts +++ b/src/Base.ts @@ -16,7 +16,7 @@ import FieldInterface from "./models/FieldInterface"; import { props, allowedProps, - checkPropType, + checkPropOccurrence, throwError, isArrayOfObjects, getObservableMapValues, @@ -37,8 +37,9 @@ import { pathToFieldsTree, defaultClearValue, } from "./parser"; -import { FieldPropsEnum } from "./models/FieldProps"; +import { AllowedFieldPropsTypes, FieldPropsEnum, SeparatedPropsMode } from "./models/FieldProps"; import { OptionsEnum } from "./models/OptionsModel"; +import { ValidationHooks } from "./models/ValidatorInterface"; export default class Base implements BaseInterface { noop = () => {}; @@ -258,29 +259,29 @@ export default class Base implements BaseInterface { }; const props = { - $value: _.get(initial["values"], path), - $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"), + $value: _.get(initial[SeparatedPropsMode.values], path), + $label: _try(SeparatedPropsMode.labels), + $placeholder: _try(SeparatedPropsMode.placeholders), + $default: _try(SeparatedPropsMode.defaults), + $initial: _try(SeparatedPropsMode.initials), + $disabled: _try(SeparatedPropsMode.disabled), + $deleted: _try(SeparatedPropsMode.deleted), + $type: _try(SeparatedPropsMode.types), + $related: _try(SeparatedPropsMode.related), + $rules: _try(SeparatedPropsMode.rules), + $options: _try(SeparatedPropsMode.options), + $bindings: _try(SeparatedPropsMode.bindings), + $extra: _try(SeparatedPropsMode.extra), + $hooks: _try(SeparatedPropsMode.hooks), + $handlers: _try(SeparatedPropsMode.handlers), + $validatedWith: _try(SeparatedPropsMode.validatedWith), + $validators: _try(SeparatedPropsMode.validators), + $observers: _try(SeparatedPropsMode.observers), + $interceptors: _try(SeparatedPropsMode.interceptors), + $input: _try(SeparatedPropsMode.input), + $output: _try(SeparatedPropsMode.output), + $autoFocus: _try(SeparatedPropsMode.autoFocus), + $ref: _try(SeparatedPropsMode.refs), }; const field = this.state.form.makeField({ @@ -314,8 +315,16 @@ export default class Base implements BaseInterface { this.$submitting = true; this.$submitted += 1; - const exec = (isValid: boolean) => - isValid ? this.execHook("onSuccess", o) : this.execHook("onError", o); + if (!this.state.options.get(OptionsEnum.validateOnSubmit, this)) { + return Promise + .resolve(this) + .then(action(() => (this.$submitting = false))) + .then(() => this); + } + + const exec = (isValid: boolean) => isValid + ? this.execHook(ValidationHooks.onSuccess, o) + : this.execHook(ValidationHooks.onError, o); return ( this.validate({ @@ -345,12 +354,12 @@ export default class Base implements BaseInterface { Check Field Computed Values */ check(prop: string, deep: boolean = false): boolean { - allowedProps("computed", [prop]); + allowedProps(AllowedFieldPropsTypes.computed, [prop]); return deep - ? checkPropType({ - type: props.types[prop], - data: this.deepCheck(props.types[prop], prop, this.fields), + ? checkPropOccurrence({ + type: props.occurrences[prop], + data: this.deepCheck(props.occurrences[prop], prop, this.fields), }) : (this as any)[prop]; } @@ -365,7 +374,7 @@ export default class Base implements BaseInterface { } const $deep = this.deepCheck(type, prop, field.fields); - check.push(checkPropType({ type, data: $deep })); + check.push(checkPropOccurrence({ type, data: $deep })); return check; }, [] @@ -458,7 +467,7 @@ export default class Base implements BaseInterface { ); } - allowedProps("all", _.isArray(prop) ? prop : [prop]); + allowedProps(AllowedFieldPropsTypes.all, _.isArray(prop) ? prop : [prop]); if (_.isString(prop)) { if (strict && this.fields.size === 0) { @@ -536,14 +545,16 @@ export default class Base implements BaseInterface { set(prop: any, data?: any): void { // UPDATE CUSTOM PROP if (_.isString(prop) && !_.isUndefined(data)) { - allowedProps("field", [prop]); + allowedProps(AllowedFieldPropsTypes.editable, [prop]); const deep = (_.isObject(data) && prop === FieldPropsEnum.value) || _.isPlainObject(data); - if (deep && this.hasNestedFields) this.deepSet(prop, data, "", true); - else _.set(this, `$${prop}`, data); - + if (deep && this.hasNestedFields) return this.deepSet(prop, data, "", true); + // else _.set(this, `$${prop}`, data); if (prop === FieldPropsEnum.value) { - this.$changed ++; - this.state.form.$changed ++; + (this as any).value = parseInput((this as any).$input, { + separated: data, + }); + } else { + _.set(this, `$${prop}`, data); } return; } @@ -667,7 +678,15 @@ export default class Base implements BaseInterface { /** MobX Event (observe/intercept) */ - MOBXEvent({ path = null, key = FieldPropsEnum.value, call, type }: any): void { + MOBXEvent({ + prop = FieldPropsEnum.value, + key = null, + path = null, + call, + type + }: any): void { + let $prop = key || prop; + allowedProps(AllowedFieldPropsTypes.observable, [$prop]); const $instance = this.select(path || this.path, null, null) || this; const $call = (change: any) => @@ -685,23 +704,21 @@ export default class Base implements BaseInterface { if (type === "observer") { fn = observe; - ffn = (cb: any) => observe($instance.fields, cb); + ffn = (cb: any) => observe($instance.fields, cb); // fields } if (type === "interceptor") { - // eslint-disable-next-line - key = `$${key}`; + $prop = `$${prop}`; fn = intercept; - ffn = $instance.fields.intercept; + ffn = $instance.fields.intercept; // fields } - - const $dkey = $instance.path ? `${key}@${$instance.path}` : key; + const $dkey = $instance.path ? `${$prop}@${$instance.path}` : $prop; _.merge(this.state.disposers[type], { [$dkey]: - key === FieldPropsEnum.fields + $prop === FieldPropsEnum.fields ? ffn.apply((change: any) => $call(change)) - : (fn as any)($instance, key, (change: any) => $call(change)), + : (fn as any)($instance, $prop, (change: any) => $call(change)), }); } diff --git a/src/Field.ts b/src/Field.ts index fddf8512..a6663565 100755 --- a/src/Field.ts +++ b/src/Field.ts @@ -120,6 +120,7 @@ export default class Field extends Base implements FieldInterface { disposeValidationOnChange: any; files: any; + constructor({ key, path, @@ -225,9 +226,8 @@ export default class Field extends Base implements FieldInterface { this.execHook(FieldPropsEnum.onInit); - // handle Field onChange Hook for nested fields - this.hasNestedFields - && autorun(() => this.changed && this.execHook(FieldPropsEnum.onChange)); + // handle Field onChange Hook + autorun(() => this.changed && this.execHook(FieldPropsEnum.onChange)); } /* ------------------------------------------------------------------ */ @@ -436,10 +436,12 @@ export default class Field extends Base implements FieldInterface { this.value = e; }); - onChange = (...args: any) => + onSync = (...args: any) => this.type === "file" ? this.onDrop(...args) - : this.execHandler(FieldPropsEnum.onChange, args, this.sync); + : this.execHandler(FieldPropsEnum.onSync, args, this.sync); + + onChange = this.onSync; onToggle = (...args: any) => this.execHandler(FieldPropsEnum.onToggle, args, this.sync); diff --git a/src/Form.ts b/src/Form.ts index 43e2c7c2..5168cc23 100755 --- a/src/Form.ts +++ b/src/Form.ts @@ -7,10 +7,7 @@ import State from "./State"; import Field from "./Field"; import ValidatorInterface from "./models/ValidatorInterface"; import FieldInterface, { FieldConstructor } from "./models/FieldInterface"; -import FormInterface, { - FieldsDefinitions, - FormConfig, -} from "./models/FormInterface"; +import FormInterface, { FieldsDefinitions, FormConfig } from "./models/FormInterface"; import { FieldPropsEnum } from "./models/FieldProps"; import { OptionsEnum } from "./models/OptionsModel"; @@ -166,7 +163,7 @@ export default class Form extends Base implements FormInterface { return new Field(data); } - /** + /** DEPRECATED Init Form Fields and Nested Fields init($fields: any = null): void { diff --git a/src/Options.ts b/src/Options.ts index c7be4baf..63572953 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -27,6 +27,7 @@ export default class Options implements OptionsInterface { showErrorsOnClear: false, showErrorsOnReset: true, validateOnInit: true, + validateOnSubmit: true, validateOnBlur: true, validateOnChange: false, validateOnChangeAfterInitialBlur: false, diff --git a/src/models/FieldInterface.ts b/src/models/FieldInterface.ts index 29584daa..29f0affe 100644 --- a/src/models/FieldInterface.ts +++ b/src/models/FieldInterface.ts @@ -44,6 +44,7 @@ export default interface FieldInterface extends BaseInterface { touched: boolean; deleted: boolean; // handlers + onSync(args: any): any; onChange(args: any): any; onToggle(args: any): any; onBlur(args: any): any; diff --git a/src/models/FieldProps.ts b/src/models/FieldProps.ts index 0e1f366b..e2d0e830 100644 --- a/src/models/FieldProps.ts +++ b/src/models/FieldProps.ts @@ -43,6 +43,7 @@ export enum FieldPropsEnum { hasError = "hasError", // handlers onInit = "onInit", + onSync = "onSync", onChange = "onChange", onBlur = "onBlur", onFocus = "onFocus", @@ -61,3 +62,41 @@ export enum FieldPropsEnum { export type FieldPropsType = { [key in FieldPropsEnum]?: any; } + +export enum AllowedFieldPropsTypes { + computed = 'computed', + observable = 'observable', + editable = 'editable', + all = 'all', +} + +export enum FieldPropsOccurrence { + some = 'some', + every = 'every', +} + +export enum SeparatedPropsMode { + values = 'values', + labels = 'labels', + placeholders = 'placeholders', + defaults = 'defaults', + initials = 'initials', + disabled = 'disabled', + deleted = 'deleted', + types = 'types', + related = 'related', + rules = 'rules', + options = 'options', + bindings = 'bindings', + extra = 'extra', + hooks = 'hooks', + handlers = 'handlers', + validatedWith = 'validatedWith', + validators = 'validators', + observers = 'observers', + interceptors = 'interceptors', + input = 'input', + output = 'output', + autoFocus = 'autoFocus', + refs = 'refs', +} \ No newline at end of file diff --git a/src/models/OptionsModel.ts b/src/models/OptionsModel.ts index f2f91651..d5a197d7 100644 --- a/src/models/OptionsModel.ts +++ b/src/models/OptionsModel.ts @@ -10,6 +10,7 @@ export enum OptionsEnum { showErrorsOnClear = 'showErrorsOnClear', showErrorsOnReset = 'showErrorsOnReset', validateOnInit = 'validateOnInit', + validateOnSubmit = 'validateOnSubmit', validateOnBlur = 'validateOnBlur', validateOnChange = 'validateOnChange', validateOnChangeAfterInitialBlur = 'validateOnChangeAfterInitialBlur', @@ -47,6 +48,7 @@ export default interface OptionsModel { [OptionsEnum.showErrorsOnClear]?: boolean; [OptionsEnum.showErrorsOnReset]?: boolean; [OptionsEnum.validateOnInit]?: boolean; + [OptionsEnum.validateOnSubmit]?: boolean; [OptionsEnum.validateOnBlur]?: boolean; [OptionsEnum.validateOnChange]?: boolean; [OptionsEnum.validateOnChangeAfterInitialBlur]?: boolean; diff --git a/src/models/ValidatorInterface.ts b/src/models/ValidatorInterface.ts index 5061a01a..26a4a79c 100644 --- a/src/models/ValidatorInterface.ts +++ b/src/models/ValidatorInterface.ts @@ -38,3 +38,8 @@ export interface ValidationPluginInterface extends ValidationPluginConstructor { export interface DriversMap { [index: string]: ValidationPluginInterface; } + +export enum ValidationHooks { + onSuccess = 'onSuccess', + onError = 'onError', +} \ No newline at end of file diff --git a/src/props.ts b/src/props.ts index 95aab9ad..c767d37a 100755 --- a/src/props.ts +++ b/src/props.ts @@ -1,4 +1,4 @@ -import { FieldPropsEnum } from "./models/FieldProps"; +import { FieldPropsEnum, FieldPropsOccurrence, SeparatedPropsMode } from "./models/FieldProps"; export interface PropsGroupsInterface { editable: string[]; @@ -8,7 +8,7 @@ export interface PropsGroupsInterface { validation: string[]; exceptions: string[]; separated: string[]; - types: { + occurrences: { [index: string]: "some" | "every"; }; } @@ -62,22 +62,29 @@ export const props: PropsGroupsInterface = { FieldPropsEnum.disabled, ], separated: [ - "values", - "initials", - "defaults", - "labels", - "placeholders", - "disabled", - "deleted", - "related", - "options", - "extra", - "bindings", - "types", - "hooks", - "handlers", - "autoFocus", - "refs" + SeparatedPropsMode.values, + SeparatedPropsMode.labels, + SeparatedPropsMode.placeholders, + SeparatedPropsMode.defaults, + SeparatedPropsMode.initials, + SeparatedPropsMode.disabled, + SeparatedPropsMode.deleted, + SeparatedPropsMode.types, + SeparatedPropsMode.related, + SeparatedPropsMode.rules, + SeparatedPropsMode.options, + SeparatedPropsMode.bindings, + SeparatedPropsMode.extra, + SeparatedPropsMode.hooks, + SeparatedPropsMode.handlers, + SeparatedPropsMode.validatedWith, + SeparatedPropsMode.validators, + SeparatedPropsMode.observers, + SeparatedPropsMode.interceptors, + SeparatedPropsMode.input, + SeparatedPropsMode.output, + SeparatedPropsMode.autoFocus, + SeparatedPropsMode.refs, ], functions: [ FieldPropsEnum.observers, @@ -94,19 +101,19 @@ export const props: PropsGroupsInterface = { FieldPropsEnum.isDirty, FieldPropsEnum.isPristine ], - types: { - isDirty: "some", - isPristine: "every", - isDefault: "every", - isValid: "every", - isEmpty: "every", - hasError: "some", - focused: "some", - blurred: "some", - touched: "some", - deleted: "every", - disabled: "every", - clearing: "every", - resetting: "every", + occurrences: { + isDirty: FieldPropsOccurrence.some, + isPristine: FieldPropsOccurrence.every, + isDefault: FieldPropsOccurrence.every, + isValid: FieldPropsOccurrence.every, + isEmpty: FieldPropsOccurrence.every, + hasError: FieldPropsOccurrence.some, + focused: FieldPropsOccurrence.some, + blurred: FieldPropsOccurrence.some, + touched: FieldPropsOccurrence.some, + deleted: FieldPropsOccurrence.every, + disabled: FieldPropsOccurrence.every, + clearing: FieldPropsOccurrence.every, + resetting: FieldPropsOccurrence.every, }, }; diff --git a/src/utils.ts b/src/utils.ts index b0439bb5..40983dc8 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 { AllowedFieldPropsTypes, FieldPropsEnum, FieldPropsOccurrence } from "./models/FieldProps"; import { props } from "./props"; const getObservableMapValues = (observableMap: ObservableMap): @@ -21,17 +21,12 @@ const checkObserveItem = const checkObserve = (collection: object[]) => (change: any) => collection.map(checkObserveItem(change)); -const checkPropType = ({ type, data }: any) => { +const checkPropOccurrence = ({ type, data }: any): boolean => { let $check: any; switch (type) { - case "some": - $check = ($data: object) => _.some($data, Boolean); - break; - case "every": - $check = ($data: object) => _.every($data, Boolean); - break; - default: - $check = null; + case FieldPropsOccurrence.some: $check = ($data: object) => _.some($data, Boolean); break; + case FieldPropsOccurrence.every: $check = ($data: object) => _.every($data, Boolean); break; + default: throw new Error('Occurrence not found for specified prop'); } return $check(data); }; @@ -39,10 +34,17 @@ const checkPropType = ({ type, data }: any) => { const hasProps = ($type: any, $data: any) => { let $props; switch ($type) { - case "computed": + case AllowedFieldPropsTypes.computed: $props = props.computed; break; - case "field": + case AllowedFieldPropsTypes.observable: + $props = [ + FieldPropsEnum.fields, + ...props.computed, + ...props.editable, + ]; + break; + case AllowedFieldPropsTypes.editable: $props = [ ...props.editable, ...props.validation, @@ -50,9 +52,12 @@ const hasProps = ($type: any, $data: any) => { ...props.handlers, ]; break; - case "all": + case AllowedFieldPropsTypes.all: $props = [ FieldPropsEnum.id, + FieldPropsEnum.key, + FieldPropsEnum.name, + FieldPropsEnum.path, ...props.computed, ...props.editable, ...props.validation, @@ -109,7 +114,7 @@ const $getKeys = (fields: any) => _.union(..._.map(_.values(fields), (values) => _.keys(values))); const hasUnifiedProps = ({ fields }: any) => - !isArrayOfStrings({ fields }) && hasProps("field", $getKeys(fields)); + !isArrayOfStrings({ fields }) && hasProps(AllowedFieldPropsTypes.editable, $getKeys(fields)); const hasSeparatedProps = (initial: any) => hasSome(initial, props.separated) || hasSome(initial, props.validation); @@ -123,8 +128,7 @@ const allowNested = (field: any, strictProps: boolean): boolean => ...props.validation, ...props.functions, ...props.handlers, - ]) || - strictProps); + ]) || strictProps); const parseIntKeys = (fields: any) => _.map(getObservableMapKeys(fields), _.ary(_.toNumber, 1)); @@ -134,6 +138,7 @@ const hasIntKeys = (fields: any): boolean => const maxKey = (fields: any): number => { const max = _.max(parseIntKeys(fields)); + // @ts-ignore return _.isUndefined(max) ? 0 : max + 1; }; @@ -156,11 +161,8 @@ const $isBool = ($: any, val: any): boolean => const $try = (...args: any) => { let found: any | null = null; - args.map( - ( - val: any // eslint-disable-line - ) => found === null && !_.isUndefined(val) && (found = val) - ); + args.map(( val: any ) => + found === null && !_.isUndefined(val) && (found = val)); return found; }; @@ -168,7 +170,7 @@ const $try = (...args: any) => { export { props, checkObserve, - checkPropType, + checkPropOccurrence, hasProps, allowedProps, throwError, diff --git a/tests/fixes.values.ts b/tests/fixes.values.ts index 4de46492..09003c2f 100755 --- a/tests/fixes.values.ts +++ b/tests/fixes.values.ts @@ -159,8 +159,8 @@ describe('Check Fixes $I values', () => { }); describe('Check Fixes $M values', () => { - it('$M people[0].name value should be null', () => - expect($.$M.$('people[0].name').value).to.be.null); + it('$M people[0].name value should be empty', () => + expect($.$M.$('people[0].name').value).to.be.empty); it('$M items[0].name value should be equal to zero', () => expect($.$M.$('items[0].name').value).to.be.equal(0)); diff --git a/tests/nested.validation.ts b/tests/nested.validation.ts index 5572895e..76c1d271 100755 --- a/tests/nested.validation.ts +++ b/tests/nested.validation.ts @@ -23,6 +23,9 @@ describe('Check Nested $A validation', () => { }); describe('Check Nested $U validation', () => { + it('$U user.email value should be false', () => + expect($.$U.$('user.email').value).to.be.equal('notAnEmail')); + it('$U user.emailConfirm isValid should be false', () => expect($.$U.$('user.emailConfirm').isValid).to.be.false);