From fcf3939c11699e85ddfbf2d8b51a48dff0ea3578 Mon Sep 17 00:00:00 2001 From: erikras-richard-agent Date: Wed, 6 May 2026 10:05:40 +0200 Subject: [PATCH] docs: clarify skipping pristine calculations --- README.md | 24 ++++++++++++++++---- src/decorator.test.ts | 51 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9810493..96f86b9 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,12 @@ const undecorate = decorator(form) - [API](#api) - [`createDecorator: (...calculations: Calculation[]) => Decorator`](#createdecorator-calculations-calculation--decorator) - [Types](#types) - - [`Calculation: { field: FieldPattern, updates: Updates }`](#calculation--field-fieldpattern-updates-updates-) + - [`Calculation: { field: FieldPattern, isEqual?: (any, any) => boolean, updateOnPristine?: boolean, updates: Updates }`](#calculation--field-fieldpattern-isequal-any-any--boolean-updateonpristine-boolean-updates-updates-) - [`FieldName: string`](#fieldname-string) - - [`FieldPattern: FieldName | RegExp`](#fieldpattern-fieldname--regexp) - - [`Updates: { [FieldName]: (value: any, allValues: Object, prevValues: Object) => any }`](#updates--fieldname-value-any-allvalues-object--any-) + - [`FieldPattern: FieldName | RegExp | (FieldName | RegExp)[]`](#fieldpattern-fieldname--regexp--fieldname--regexp) + - [`Updates: UpdatesByName | UpdatesForAll`](#updates-updatesbyname--updatesforall) + - [`UpdatesByName: { [FieldName]: (value: any, allValues: Object, prevValues: Object) => Promise | any }`](#updatesbyname--fieldname-value-any-allvalues-object-prevvalues-object--promise--any-) + - [`UpdatesForAll: (value: any, field: string, allValues: Object, prevValues: Object) => Promise | { [FieldName]: any }`](#updatesforall-value-any-field-string-allvalues-object-prevvalues-object--promise---fieldname-any-) @@ -126,10 +128,24 @@ A function that takes a set of calculations and returns a 🏁 Final Form ## Types -### `Calculation: { field: FieldPattern, isEqual?: (any, any) => boolean, updates: Updates }` +### `Calculation: { field: FieldPattern, isEqual?: (any, any) => boolean, updateOnPristine?: boolean, updates: Updates }` A calculation to perform, with an optional `isEqual` predicate to determine if a value has really changed (defaults to `===`). +By default, calculations run when the decorator first subscribes, even if the form is still pristine. This is useful when calculated fields should be derived from `initialValues`. Set `updateOnPristine: false` to skip that initial calculation and only run after the source field becomes dirty: + +```js +const decorator = createDecorator({ + field: 'foo', + updateOnPristine: false, + updates: { + bar: fooValue => `${fooValue}bar` + } +}) +``` + +`updateOnPristine: false` works with both object-style `updates` and function-style `updates`. + ### `FieldName: string` ### `FieldPattern: FieldName | RegExp | (FieldName | RegExp)[]` diff --git a/src/decorator.test.ts b/src/decorator.test.ts index 17d5441..8899cb6 100644 --- a/src/decorator.test.ts +++ b/src/decorator.test.ts @@ -144,6 +144,57 @@ describe('decorator', () => { expect(bar.mock.calls[1][0].value).toBe('bazbar') }) + it('should not trigger update on initialValues if updateOnPristine is false, using an updates function', () => { + const form = createForm({ + initialValues: { foo: 'test', bar: 'test' }, + onSubmit: onSubmitMock + }) + const spy = jest.fn() + const foo = jest.fn() + const bar = jest.fn() + form.subscribe(spy, { values: true }) + form.registerField('foo', foo, { value: true }) + form.registerField('bar', bar, { value: true }) + const updates = jest.fn(fooValue => ({ bar: `${fooValue}bar` })) + const decorator = createDecorator({ + field: 'foo', + updateOnPristine: false, + updates + }) + const unsubscribe = decorator(form) + expect(typeof unsubscribe).toBe('function') + + expect(updates).not.toHaveBeenCalled() + expect(spy).toHaveBeenCalledTimes(1) + expect(spy.mock.calls[0][0].values).toEqual({ foo: 'test', bar: 'test' }) + + expect(foo).toHaveBeenCalledTimes(1) + expect(foo.mock.calls[0][0].value).toBe('test') + + expect(bar).toHaveBeenCalledTimes(1) + expect(bar.mock.calls[0][0].value).toBe('test') + + // change foo (should trigger calculation on bar) + form.change('foo', 'baz') + + expect(updates).toHaveBeenCalledTimes(1) + expect(updates).toHaveBeenCalledWith( + 'baz', + 'foo', + { foo: 'baz', bar: 'test' }, + { foo: 'test', bar: 'test' } + ) + expect(spy).toHaveBeenCalledTimes(3) + expect(spy.mock.calls[1][0].values).toEqual({ foo: 'baz', bar: 'test' }) + expect(spy.mock.calls[2][0].values).toEqual({ foo: 'baz', bar: 'bazbar' }) + + expect(foo).toHaveBeenCalledTimes(2) + expect(foo.mock.calls[1][0].value).toBe('baz') + + expect(bar).toHaveBeenCalledTimes(2) + expect(bar.mock.calls[1][0].value).toBe('bazbar') + }) + it('should update one field when another changes', () => { const form = createForm({ onSubmit: onSubmitMock }) const spy = jest.fn()