From 264b1e8ac2e761d8f922cdfd0a2b0d27d9ce4d52 Mon Sep 17 00:00:00 2001 From: Ryan Kim Date: Wed, 20 Nov 2019 22:29:02 -0600 Subject: [PATCH 1/4] Avoid notify listener calls on field register when there's no subscriber --- package.json | 2 +- src/FinalForm.fieldSubscribing.test.js | 59 +++++++++++++++++ src/FinalForm.js | 87 ++++++++++++++++++++------ src/index.d.ts | 7 ++- src/types.js.flow | 11 +++- 5 files changed, 142 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 543eef48..f391a6a0 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "bundlesize": [ { "path": "dist/final-form.umd.min.js", - "maxSize": "5.1kB" + "maxSize": "5.2kB" }, { "path": "dist/final-form.es.js", diff --git a/src/FinalForm.fieldSubscribing.test.js b/src/FinalForm.fieldSubscribing.test.js index 88bf029b..be60d7f1 100644 --- a/src/FinalForm.fieldSubscribing.test.js +++ b/src/FinalForm.fieldSubscribing.test.js @@ -888,4 +888,63 @@ describe('Field.subscribing', () => { expect(name1).toHaveBeenCalledTimes(1) expect(name2).toHaveBeenCalledTimes(1) }) + + it('should subscribe and unsubscribe to field state', () => { + const form = createForm({ onSubmit: onSubmitMock }) + const foo = jest.fn() + + form.registerField('foo') + const unsubscribe = form.subscribeToFieldState('foo', foo, { + touched: true, + value: true, + visited: true + }) + + expect(foo).not.toHaveBeenCalled() + + form.change('foo', 'new value') + + expect(foo).toHaveBeenCalledTimes(1) + expect(foo.mock.calls[0][0].value).toBe('new value') + + form.focus('foo') + expect(foo).toHaveBeenCalledTimes(2) + expect(foo.mock.calls[1][0].touched).toBe(false) + expect(foo.mock.calls[1][0].visited).toBe(true) + + form.blur('foo') + expect(foo).toHaveBeenCalledTimes(3) + expect(foo.mock.calls[2][0].touched).toBe(true) + expect(foo.mock.calls[2][0].visited).toBe(true) + + unsubscribe() + form.change('foo', 'newer value') + form.focus('foo') + form.blur('foo') + expect(foo).toHaveBeenCalledTimes(3) + }) + + it('should subscribe and unsubscribe to field state with only value subscription', () => { + const form = createForm({ onSubmit: onSubmitMock }) + const foo = jest.fn() + + form.registerField('foo') + const unsubscribe = form.subscribeToFieldState('foo', foo, { value: true }) + + expect(foo).not.toHaveBeenCalled() + + form.change('foo', 'new value') + + expect(foo).toHaveBeenCalledTimes(1) + expect(foo.mock.calls[0][0].value).toBe('new value') + + form.focus('foo') + expect(foo).toHaveBeenCalledTimes(1) + form.blur('foo') + expect(foo).toHaveBeenCalledTimes(1) + + unsubscribe() + form.change('foo', 'newer value') + expect(foo).toHaveBeenCalledTimes(1) + }) }) diff --git a/src/FinalForm.js b/src/FinalForm.js index 35a337d7..a2ccb7e6 100644 --- a/src/FinalForm.js +++ b/src/FinalForm.js @@ -52,7 +52,8 @@ type InternalState = { [string]: InternalFieldState }, fieldSubscribers: { [string]: Subscribers }, - formState: InternalFormState + formState: InternalFormState, + registeredFieldCounts: { [string]: number } } & MutableState export type StateFilter = ( @@ -197,7 +198,8 @@ function createForm( validating: 0, values: initialValues ? { ...initialValues } : (({}: any): FormValues) }, - lastFormState: undefined + lastFormState: undefined, + registeredFieldCounts: {} } let inBatch = 0 let validationPaused = false @@ -641,6 +643,39 @@ function createForm( name => state.fields[name].afterSubmit && state.fields[name].afterSubmit() ) + const subscribeToFieldState = ( + name: string, + subscriber: FieldSubscriber, + subscription: FieldSubscription = {}, + notified + ): Unsubscribe => { + if (state.fields[name] === undefined) { + throw Error( + 'You must register ' + name + ' field before you can subscribe to it!' + ) + } + + if (!state.fieldSubscribers[name]) { + state.fieldSubscribers[name] = { index: 0, entries: {} } + } + const index = state.fieldSubscribers[name].index++ + + // save field subscriber callback + state.fieldSubscribers[name].entries[index] = { + subscriber: memoize(subscriber), + subscription, + notified + } + + return () => { + delete state.fieldSubscribers[name].entries[index] + let lastOne = !Object.keys(state.fieldSubscribers[name].entries).length + if (lastOne) { + delete state.fieldSubscribers[name] + } + } + } + // generate initial errors runValidation(undefined, () => { notifyFormListeners() @@ -781,21 +816,14 @@ function createForm( registerField: ( name: string, - subscriber: FieldSubscriber, - subscription: FieldSubscription = {}, + subscriber?: FieldSubscriber, + subscription?: FieldSubscription = {}, fieldConfig?: FieldConfig ): Unsubscribe => { - if (!state.fieldSubscribers[name]) { - state.fieldSubscribers[name] = { index: 0, entries: {} } - } - const index = state.fieldSubscribers[name].index++ - - // save field subscriber callback - state.fieldSubscribers[name].entries[index] = { - subscriber: memoize(subscriber), - subscription, - notified: false + if (state.registeredFieldCounts[name] === undefined) { + state.registeredFieldCounts[name] = 0 } + const index = ++state.registeredFieldCounts[name] if (!state.fields[name]) { // create initial field state @@ -818,7 +846,20 @@ function createForm( validating: false, visited: false } + + if (subscriber === undefined) { + state.fields[name].lastFieldState = publishFieldState( + state.formState, + state.fields[name] + ) + } } + + // subscribe or don't + const unscribeToFieldState = + subscriber && + subscribeToFieldState(name, subscriber, subscription, false) + let haveValidator = false if (fieldConfig) { haveValidator = !!( @@ -865,7 +906,7 @@ function createForm( notifyFormListeners() notifyFieldListeners() }) - } else { + } else if (subscriber !== undefined) { notifyFormListeners() notifyFieldListeners(name) } @@ -881,9 +922,10 @@ function createForm( ) delete state.fields[name].validators[index] } - delete state.fieldSubscribers[name].entries[index] - let lastOne = !Object.keys(state.fieldSubscribers[name].entries).length + unscribeToFieldState && unscribeToFieldState() + let lastOne = --state.registeredFieldCounts[name] === 0 if (lastOne) { + delete state.registeredFieldCounts[name] delete state.fieldSubscribers[name] delete state.fields[name] if (validatorRemoved) { @@ -900,7 +942,7 @@ function createForm( notifyFormListeners() notifyFieldListeners() }) - } else if (lastOne) { + } else if (lastOne && subscriber !== undefined) { // values or errors may have changed notifyFormListeners() } @@ -1120,7 +1162,14 @@ function createForm( return () => { delete subscribers.entries[index] } - } + }, + + subscribeToFieldState: ( + name: string, + subscriber: FieldSubscriber, + subscription: FieldSubscription = {} + ): Unsubscribe => + subscribeToFieldState(name, subscriber, subscription, true) } return api } diff --git a/src/index.d.ts b/src/index.d.ts index 0d416b7d..cb45d5cf 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -118,7 +118,7 @@ export type Subscribers = { subscriber: Subscriber subscription: Subscription notified: boolean - } + } | void } } @@ -218,6 +218,11 @@ export interface FormApi { subscriber: FormSubscriber, subscription: FormSubscription ) => Unsubscribe + subscribeToExistingField: ( + name: string, + subscriber: FieldSubscriber, + subscription: FieldSubscription + ) => Unsubscribe } export type DebugFunction = ( diff --git a/src/types.js.flow b/src/types.js.flow index ae9a67ed..2eba20a0 100644 --- a/src/types.js.flow +++ b/src/types.js.flow @@ -116,7 +116,7 @@ export type FieldSubscriber = Subscriber export type Subscribers = { index: number, entries: { - [number]: { + [number]: ?{ subscriber: Subscriber, subscription: Subscription, notified: boolean @@ -146,8 +146,8 @@ export type FieldConfig = { export type RegisterField = ( name: string, - subscriber: FieldSubscriber, - subscription: FieldSubscription, + subscriber?: FieldSubscriber, + subscription?: FieldSubscription, config?: FieldConfig ) => Unsubscribe @@ -224,6 +224,11 @@ export type FormApi = { subscribe: ( subscriber: FormSubscriber, subscription: FormSubscription + ) => Unsubscribe, + subscribeToFieldState: ( + name: string, + subscriber: FieldSubscriber, + subscription: FieldSubscription ) => Unsubscribe } From de90b549d07ff4f330f4783720300a918cb9ff42 Mon Sep 17 00:00:00 2001 From: Ryan Kim Date: Mon, 3 Feb 2020 08:31:57 -0600 Subject: [PATCH 2/4] Fix bundle size --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f391a6a0..d3a1385e 100644 --- a/package.json +++ b/package.json @@ -93,15 +93,15 @@ "bundlesize": [ { "path": "dist/final-form.umd.min.js", - "maxSize": "5.2kB" + "maxSize": "5.24kB" }, { "path": "dist/final-form.es.js", - "maxSize": "8.9kB" + "maxSize": "9.08kB" }, { "path": "dist/final-form.cjs.js", - "maxSize": "9.0kB" + "maxSize": "9.21kB" } ], "dependencies": { From ab87c601d99f830280f62e878afb6ff023f8dcfd Mon Sep 17 00:00:00 2001 From: Ryan Kim Date: Mon, 3 Feb 2020 08:39:18 -0600 Subject: [PATCH 3/4] Bundle size fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3a1385e..cdd6ccb9 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ }, { "path": "dist/final-form.es.js", - "maxSize": "9.08kB" + "maxSize": "9.09kB" }, { "path": "dist/final-form.cjs.js", From a6e73d3dd502b8bebd03d96d52211ee76907b2e4 Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Wed, 27 May 2020 15:47:24 +0200 Subject: [PATCH 4/4] Updated bundle size maximums --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index cdd6ccb9..d9bbdedc 100644 --- a/package.json +++ b/package.json @@ -93,15 +93,15 @@ "bundlesize": [ { "path": "dist/final-form.umd.min.js", - "maxSize": "5.24kB" + "maxSize": "5.3kB" }, { "path": "dist/final-form.es.js", - "maxSize": "9.09kB" + "maxSize": "9.1kB" }, { "path": "dist/final-form.cjs.js", - "maxSize": "9.21kB" + "maxSize": "9.3kB" } ], "dependencies": {