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 b2a6b5a4..80f432f6 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 = ( @@ -200,7 +201,8 @@ function createForm( validating: 0, values: initialValues ? { ...initialValues } : (({}: any): FormValues) }, - lastFormState: undefined + lastFormState: undefined, + registeredFieldCounts: {} } let inBatch = 0 let validationPaused = false @@ -651,6 +653,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] + } + } + } + const resetModifiedAfterSubmit = (): void => Object.keys(state.fields).forEach( key => (state.fields[key].modifiedSinceLastSubmit = false) @@ -797,21 +832,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 @@ -835,7 +863,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 unsubscribeToFieldState = + subscriber && + subscribeToFieldState(name, subscriber, subscription, false) + let haveValidator = false const silent = fieldConfig && fieldConfig.silent const notify = () => { @@ -891,6 +932,13 @@ function createForm( } if (haveValidator) { + runValidation(undefined, () => { + notifyFormListeners() + notifyFieldListeners() + }) + } else if (subscriber !== undefined) { + notifyFormListeners() + notifyFieldListeners(name) runValidation(undefined, notify) } else { notify() @@ -907,13 +955,10 @@ function createForm( ) delete state.fields[name].validators[index] } - let hasFieldSubscribers = !!state.fieldSubscribers[name]; - if (hasFieldSubscribers) { - // state.fieldSubscribers[name] may have been removed by a mutator - delete state.fieldSubscribers[name].entries[index] - } - let lastOne = hasFieldSubscribers && !Object.keys(state.fieldSubscribers[name].entries).length + if(unsubscribeToFieldState) unsubscribeToFieldState() + let lastOne = --state.registeredFieldCounts[name] === 0 if (lastOne) { + delete state.registeredFieldCounts[name] delete state.fieldSubscribers[name] delete state.fields[name] if (validatorRemoved) { @@ -931,7 +976,7 @@ function createForm( notifyFormListeners() notifyFieldListeners() }) - } else if (lastOne) { + } else if (lastOne && subscriber !== undefined) { // values or errors may have changed notifyFormListeners() } @@ -1188,7 +1233,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 fc09fb71..8d3e6c30 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -122,7 +122,7 @@ export type Subscribers = { subscriber: Subscriber subscription: Subscription notified: boolean - } + } | void } } @@ -228,6 +228,11 @@ export interface FormApi, InitialFormValues = P 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 a782aaf5..24fbedc6 100644 --- a/src/types.js.flow +++ b/src/types.js.flow @@ -120,7 +120,7 @@ export type FieldSubscriber = Subscriber export type Subscribers = { index: number, entries: { - [number]: { + [number]: ?{ subscriber: Subscriber, subscription: Subscription, notified: boolean @@ -151,8 +151,8 @@ export type FieldConfig = { export type RegisterField = ( name: string, - subscriber: FieldSubscriber, - subscription: FieldSubscription, + subscriber?: FieldSubscriber, + subscription?: FieldSubscription, config?: FieldConfig ) => Unsubscribe @@ -232,6 +232,11 @@ export type FormApi = { subscribe: ( subscriber: FormSubscriber, subscription: FormSubscription + ) => Unsubscribe, + subscribeToFieldState: ( + name: string, + subscriber: FieldSubscriber, + subscription: FieldSubscription ) => Unsubscribe }