diff --git a/CHANGELOG.md b/CHANGELOG.md index 817ce285..c678ae22 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 5.5.0 (next) + +- Updated add()/del()/update() actions to handle `changed` field prop. +(not triggering anymore `onChange` when using add(), use `onAdd`/`onDel` hooks instead). +- Updated `changed` computed prop behavior for nested fields and Event Hooks triggering. +- Events Hooks now are triggered also from actions if not used Event Handlers. + +fix: #585 #531 + # 5.4.1 (next) fix: #371 #399 #408 diff --git a/src/Base.ts b/src/Base.ts index 2e4e9645..3ec688f6 100755 --- a/src/Base.ts +++ b/src/Base.ts @@ -35,6 +35,7 @@ import { parseCheckArray, parseCheckOutput, pathToFieldsTree, + defaultClearValue, } from "./parser"; import { FieldPropsEnum } from "./models/FieldProps"; import { OptionsEnum } from "./models/OptionsModel"; @@ -135,7 +136,7 @@ export default class Base implements BaseInterface { get changed(): number { return !_.isNil(this.path) && this.hasNestedFields - ? this.reduce((acc: number, field: FieldInterface) => (acc + field.changed), 0) + ? (this.reduce((acc: number, field: FieldInterface) => (acc + field.changed), 0) + this.$changed) : this.$changed; } @@ -165,7 +166,7 @@ export default class Base implements BaseInterface { onClear = (...args: any): any => this.execHandler(FieldPropsEnum.onClear, args, (e: Event) => { e.preventDefault(); - (this as any).clear(true); + (this as any).clear(true, false); }); /** @@ -174,7 +175,7 @@ export default class Base implements BaseInterface { onReset = (...args: any): any => this.execHandler(FieldPropsEnum.onReset, args, (e: Event) => { e.preventDefault(); - (this as any).reset(true); + (this as any).reset(true, false); }); /** @@ -183,7 +184,7 @@ export default class Base implements BaseInterface { onSubmit = (...args: any): any => this.execHandler(FieldPropsEnum.onSubmit, args, (e: Event, o = {}) => { e.preventDefault(); - this.submit(o); + this.submit(o, false); }); /** @@ -192,7 +193,7 @@ export default class Base implements BaseInterface { onAdd = (...args: any): any => this.execHandler(FieldPropsEnum.onAdd, args, (e: Event, val: any) => { e.preventDefault(); - this.add($isEvent(val) ? null : val); + this.add($isEvent(val) ? null : val, false); }); /** @@ -201,7 +202,7 @@ export default class Base implements BaseInterface { onDel = (...args: any): any => this.execHandler(FieldPropsEnum.onDel, args, (e: Event, path: string) => { e.preventDefault(); - this.del($isEvent(path) ? this.path : path); + this.del($isEvent(path) ? this.path : path, false); }); /****************************************************************** @@ -305,7 +306,8 @@ export default class Base implements BaseInterface { /** Submit */ - submit(o: any = {}): Promise { + submit(o: any = {}, execHook: boolean = true): Promise { + execHook && this.execHook(FieldPropsEnum.onSubmit, o); this.$submitting = true; this.$submitted += 1; @@ -395,14 +397,19 @@ export default class Base implements BaseInterface { if (!_.isNil($field) && !_.isUndefined(field)) { if (_.isArray($field.values())) { - let n: number = _.max(_.map(field.fields, (f, i) => Number(i))) ?? -1; + const n: number = _.max(_.map(field.fields, (f, i) => Number(i))) ?? -1; _.each(getObservableMapValues($field.fields), ($f) => { - if (Number($f.name) > n) $field.fields.delete($f.name); + if (Number($f.name) > n) { + $field.$changed ++; + $field.state.form.$changed ++; + $field.fields.delete($f.name); + } }); } if (field?.fields) { const fallback = this.state.options.get(OptionsEnum.fallback); - if (!fallback && $field.fields.size === 0 && this.state.struct().findIndex(s => s.startsWith($field.path.replace(/\.\d+\./, '[].') + '[]')) < 0) { + const x = this.state.struct().findIndex(s => s.startsWith($field.path.replace(/\.\d+\./, '[].') + '[]')); + if (!fallback && $field.fields.size === 0 && x < 0) { $field.value = parseInput($field.$input, { separated: _.get(raw, $path), }); @@ -421,6 +428,8 @@ export default class Base implements BaseInterface { // get full path when using update() with select() - FIX: #179 const $newFieldPath = _.trimStart([this.path, $path].join("."), "."); // init field into the container field + $container.$changed ++; + $container.state.form.$changed ++; $container.initField($key, $newFieldPath, field, true); } else if (recursion) { if (_.has(field, "fields") && !_.isNil(field.fields)) { @@ -529,7 +538,7 @@ export default class Base implements BaseInterface { if (deep && this.hasNestedFields) this.deepSet(prop, data, "", true); else _.set(this, `$${prop}`, data); - if (prop === 'value') { + if (prop === FieldPropsEnum.value) { this.$changed ++; this.state.form.$changed ++; } @@ -556,7 +565,7 @@ export default class Base implements BaseInterface { const isStrict = this.state.options.get(OptionsEnum.strictUpdate, this); if (_.isNil(data)) { - this.each((field: any) => field.clear(true)); + this.each((field: any) => field.$value = defaultClearValue({ value: field.$value })); return; } @@ -583,7 +592,7 @@ export default class Base implements BaseInterface { /** Add Field */ - add(obj: any): any { + add(obj: any, execEvent: boolean = true): any { if (isArrayOfObjects(obj)) { _.each(obj, (values) => this.update({ @@ -593,7 +602,8 @@ export default class Base implements BaseInterface { this.$changed ++; this.state.form.$changed ++; - return this.execHook(FieldPropsEnum.onChange); + execEvent && this.execHook(FieldPropsEnum.onAdd); + return; } let key; @@ -607,14 +617,14 @@ export default class Base implements BaseInterface { this.$changed ++; this.state.form.$changed ++; - this.execHook(FieldPropsEnum.onChange); + execEvent && this.execHook(FieldPropsEnum.onAdd); return field; } /** Del Field */ - del($path: string | null = null) { + del($path: string | null = null, execEvent: boolean = true) { const isStrict = this.state.options.get(OptionsEnum.strictDelete, this); const path = parsePath($path ?? this.path); const fullpath = _.trim([this.path, path].join("."), "."); @@ -627,14 +637,15 @@ export default class Base implements BaseInterface { throwError(fullpath, null, msg); } - this.$changed ++; - this.state.form.$changed ++; + container.$changed ++; + container.state.form.$changed ++; if (this.state.options.get(OptionsEnum.softDelete, this)) { return this.select(fullpath).set("deleted", true); } container.each((field) => field.debouncedValidation.cancel()); + execEvent && this.execHook(FieldPropsEnum.onDel); return container.fields.delete(last); } diff --git a/src/Field.ts b/src/Field.ts index 83d848e0..9ce5db32 100755 --- a/src/Field.ts +++ b/src/Field.ts @@ -358,11 +358,13 @@ export default class Field extends Base implements FieldInterface { } get isDirty(): boolean { - return !_.isNil(this.initial) && !_.isEqual(this.initial, this.value); + const value = this.changed ? this.value : this.initial; + return !_.isNil(this.initial) && !_.isEqual(this.initial, value); } get isPristine(): boolean { - return !_.isNil(this.initial) && _.isEqual(this.initial, this.value); + const value = this.changed ? this.value : this.initial; + return !_.isNil(this.initial) && _.isEqual(this.initial, value); } get isEmpty(): boolean { @@ -427,7 +429,8 @@ export default class Field extends Base implements FieldInterface { ? this.onDrop(...args) : this.execHandler(FieldPropsEnum.onChange, args, this.sync); - onToggle = (...args: any) => this.execHandler(FieldPropsEnum.onToggle, args, this.sync); + onToggle = (...args: any) => + this.execHandler(FieldPropsEnum.onToggle, args, this.sync); onBlur = (...args: any) => this.execHandler( @@ -653,7 +656,8 @@ export default class Field extends Base implements FieldInterface { if (deep) this.each((field: any) => field.resetValidation()); } - clear(deep: boolean = true): void { + clear(deep: boolean = true, execHook: boolean = true): void { + execHook && this.execHook(FieldPropsEnum.onClear); this.$clearing = true; this.$touched = false; this.$blurred = false; @@ -669,7 +673,8 @@ export default class Field extends Base implements FieldInterface { }) : this.resetValidation(deep); } - reset(deep: boolean = true): void { + reset(deep: boolean = true, execHook: boolean = true): void { + execHook && this.execHook(FieldPropsEnum.onReset); this.$resetting = true; this.$touched = false; this.$blurred = false; @@ -689,7 +694,6 @@ export default class Field extends Base implements FieldInterface { } focus(): void { - // eslint-disable-next-line this.state.form.each((field: any) => (field.autoFocus = false)); this.autoFocus = true; } @@ -775,7 +779,8 @@ export default class Field extends Base implements FieldInterface { throw new Error("The update() method accepts only plain objects."); } const fallback = this.state.options.get(OptionsEnum.fallback); - if (!fallback && this.fields.size === 0 && this.state.struct().findIndex(s => s.startsWith(this.path.replace(/\.\d+\./, '[].') + '[]')) < 0) { + const x = this.state.struct().findIndex(s => s.startsWith(this.path.replace(/\.\d+\./, '[].') + '[]')); + if (!fallback && this.fields.size === 0 && x < 0) { this.value = parseInput(this.$input, { separated: fields, }); diff --git a/src/Form.ts b/src/Form.ts index 8bb76687..5d92f7e9 100755 --- a/src/Form.ts +++ b/src/Form.ts @@ -199,18 +199,20 @@ export default class Form extends Base implements FormInterface { /** Clear Form Fields */ - clear(): void { + clear(deep: boolean = true, execHook: boolean = true): void { + execHook && this.execHook(FieldPropsEnum.onClear); this.$touched = false; this.$changed = 0; - this.each((field: FieldInterface) => field.clear(true)); + this.each((field: FieldInterface) => field.clear(deep)); } /** Reset Form Fields */ - reset(): void { + reset(deep: boolean = true, execHook: boolean = true): void { + execHook && this.execHook(FieldPropsEnum.onReset); this.$touched = false; this.$changed = 0; - this.each((field: FieldInterface) => field.reset(true)); + this.each((field: FieldInterface) => field.reset(deep)); } } diff --git a/src/utils.ts b/src/utils.ts index b7ece5b5..b7663128 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -159,7 +159,7 @@ const $try = (...args: any) => { args.map( ( val: any // eslint-disable-line - ) => found === null && !_.isNil(val) && (found = val) + ) => found === null && !_.isUndefined(val) && (found = val) ); return found; diff --git a/tests/computed/flat.isDirty.ts b/tests/computed/flat.isDirty.ts index 7e10c7a8..b96bed40 100755 --- a/tests/computed/flat.isDirty.ts +++ b/tests/computed/flat.isDirty.ts @@ -11,7 +11,7 @@ export default ($) => { it('$G isDirty should be false', () => expect($.$G.isDirty).to.be.false); it('$H isDirty should be false', () => expect($.$H.isDirty).to.be.false); it('$I isDirty should be false', () => expect($.$I.isDirty).to.be.false); - it('$L isDirty should be true', () => expect($.$L.isDirty).to.be.true); + it('$L isDirty should be false', () => expect($.$L.isDirty).to.be.false); // it('$M isDirty should be true', () => expect($.$M.isDirty).to.be.true); it('$N isDirty should be false', () => expect($.$N.isDirty).to.be.false); }); diff --git a/tests/computed/flat.isPristine.ts b/tests/computed/flat.isPristine.ts index ff368326..d637f5f4 100755 --- a/tests/computed/flat.isPristine.ts +++ b/tests/computed/flat.isPristine.ts @@ -11,7 +11,7 @@ export default ($) => { it('$G isPristine should be true', () => expect($.$G.isPristine).to.be.true); it('$H isPristine should be true', () => expect($.$H.isPristine).to.be.true); it('$I isPristine should be true', () => expect($.$I.isPristine).to.be.true); - it('$L isPristine should be false', () => expect($.$L.isPristine).to.be.false); + it('$L isPristine should be true', () => expect($.$L.isPristine).to.be.true); // it('$M isPristine should be false', () => expect($.$M.isPristine).to.be.false); it('$N isPristine should be true', () => expect($.$N.isPristine).to.be.true); }); diff --git a/tests/data/_.fixes.ts b/tests/data/_.fixes.ts index baf5e689..f31ba43e 100755 --- a/tests/data/_.fixes.ts +++ b/tests/data/_.fixes.ts @@ -38,8 +38,8 @@ import $454 from "./forms/fixes/form.454"; import $518 from "./forms/fixes/form.518"; import $376 from "./forms/fixes/form.376"; -// import $585 from "./forms/fixes/form.585"; -// import $531 from "./forms/fixes/form.531"; +import $585 from "./forms/fixes/form.585"; +import $531 from "./forms/fixes/form.531"; export default { $A, @@ -78,6 +78,6 @@ export default { $518, $376, - // $585, - // $531, + $585, + $531, }; diff --git a/tests/data/forms/fixes/form.e.ts b/tests/data/forms/fixes/form.e.ts index 7319e7eb..5ccbbd5e 100755 --- a/tests/data/forms/fixes/form.e.ts +++ b/tests/data/forms/fixes/form.e.ts @@ -21,6 +21,16 @@ const values = { places: ["NY", "NJ"], }; +const hooks = { + places: { + onClear(fieldset) { + it('Fixes-E $(places).clear() should call onClear() hook on fieldset', () => { + expect(fieldset.values()).to.deep.equal([]); + }) + } + } +} + class NewForm extends Form { hooks() { return { @@ -39,6 +49,6 @@ class NewForm extends Form { } } -export default new NewForm({ fields, values, extra }, { options: { +export default new NewForm({ fields, values, extra, hooks }, { options: { removeNullishValuesInArrays: true, }, name: "Fixes-E" }); diff --git a/tests/data/forms/fixes/form.t.ts b/tests/data/forms/fixes/form.t.ts index 41f3dbbc..6b2bc393 100755 --- a/tests/data/forms/fixes/form.t.ts +++ b/tests/data/forms/fixes/form.t.ts @@ -120,6 +120,8 @@ class NewForm extends Form { expect(form.$("arrayChangeAdd").isDirty).to.be.true); it(`"arrayChangeAdd" isPristine should be false`, () => expect(form.$("arrayChangeAdd").isPristine).to.be.false); + it(`"arrayChangeAdd" changed should not be 0`, () => + expect(form.$("arrayChangeAdd").changed).not.to.be.equal(0)); }); describe("Check props state after change:", () => { @@ -135,6 +137,8 @@ class NewForm extends Form { expect(form.$("arrayChangeDel").isDirty).to.be.true); it(`"arrayChangeDel" isPristine should be false`, () => expect(form.$("arrayChangeDel").isPristine).to.be.false); + it(`"arrayChangeDel" changed should should not be 0`, () => + expect(form.$("arrayChangeDel").changed).not.to.be.equal(0)); }); describe("Check props state after change:", () => { diff --git a/tests/data/forms/nested/form.r.ts b/tests/data/forms/nested/form.r.ts index 4a44fcb3..503c6d35 100755 --- a/tests/data/forms/nested/form.r.ts +++ b/tests/data/forms/nested/form.r.ts @@ -57,6 +57,11 @@ const checkFieldset = (fieldset) => expect(fieldset).to.have.property("path"))); const submit = { + onSubmit(fieldset) { + it('$R.submit() should call onSubmit callback', () => { + expect(fieldset.submitted).to.equal(1); + }) + }, onSuccess(fieldset) { checkFieldset(fieldset); }, @@ -81,5 +86,10 @@ export default new Form( { name: "Nested-R", plugins, + hooks: { + onInit(form) { + form.$('club').submit(); + } + } } ); diff --git a/tests/data/forms/nested/form.v3.ts b/tests/data/forms/nested/form.v3.ts index 6cfaa6a2..7a3aaa88 100644 --- a/tests/data/forms/nested/form.v3.ts +++ b/tests/data/forms/nested/form.v3.ts @@ -11,12 +11,12 @@ const fields = [ const hooks = { account: { - onChange(field: FieldInterface) { + onAdd(field: FieldInterface) { field.select(0).$('id').set('account-id'); } }, test: { - onChange(field: FieldInterface) { + onAdd(field: FieldInterface) { field.state.form.$('final').set('final-value') } } diff --git a/tests/fixes.submit.ts b/tests/fixes.submit.ts index 996d57b9..6afda470 100755 --- a/tests/fixes.submit.ts +++ b/tests/fixes.submit.ts @@ -22,10 +22,11 @@ describe('Form submit() decoupled callback', () => { it('$L form submitted should be 1', () => expect(form.submitted).to.equal(1)); + + it('$L form isValid should be false', () => + expect(form.isValid).to.be.false); }); - // eslint-disable-next-line - expect(form.isValid).to.be.false; done(); }, }); diff --git a/tests/fixes.values.ts b/tests/fixes.values.ts index 0f34136e..4de46492 100755 --- a/tests/fixes.values.ts +++ b/tests/fixes.values.ts @@ -408,7 +408,7 @@ describe('update to nested array items', () => { }) }); -describe('#523', () => { +describe('#523 is Dirty should be false', () => { it('', () => { const fields = [{ name: 'fieldA', @@ -428,7 +428,7 @@ describe('#523', () => { }); describe('update nested nested array items', () => { - it('', () => { + it('check isDirty after update()', () => { const fields = [ 'pricing', 'pricing.value[]', @@ -472,6 +472,7 @@ describe('update nested nested array items', () => { }) // console.debug('pricing.value.0.initial', $526.$('pricing.value.0').initial) // console.debug('pricing.value.0.prices.initial', $526.$('pricing.value.0.prices').initial) + // expect($526.$('pricing.value').changed).not.to.be.equal(0) expect($526.$('pricing.value').isDirty).to.be.equal(true) expect($526.$('pricing.value.0').isDirty).to.be.equal(true) expect($526.$('pricing.value.0.prices').isDirty).to.be.equal(true) diff --git a/tests/nested.hooks.ts b/tests/nested.hooks.ts index 9d2e31eb..3b070ee2 100644 --- a/tests/nested.hooks.ts +++ b/tests/nested.hooks.ts @@ -63,21 +63,21 @@ describe('Check form onChange hook after reset', () => { expect($.$V2.$('test.email').value).to.be.empty); }); -describe('Check form onChange hook after add/del', () => { +describe('Check form onAdd hook after add/del', () => { it('$V3 form changed should equal form $changed', () => expect($.$V3.changed).to.be.equal($.$V3.$changed)); it('$V3 form changed should equal 7', () => - expect($.$V3.changed).to.be.equal(7)); + expect($.$V3.changed).to.be.equal(8)); it('$V3 form $changed should equal 7', () => - expect($.$V3.$changed).to.be.equal(7)); + expect($.$V3.$changed).to.be.equal(8)); it('$V3 user $changed should equal 1', () => - expect($.$V3.$('user').$changed).to.be.equal(1)); + expect($.$V3.$('user').$changed).to.be.equal(1)); // it('$V3 user changed should equal 1', () => - expect($.$V3.$('user').changed).to.be.equal(1)); + expect($.$V3.$('user').changed).to.be.equal(2)); // it('$V3 user[0].name $changed should equal 1', () => expect($.$V3.$('user[0].name').$changed).to.be.equal(1));