Skip to content

Commit 2382344

Browse files
authored
feat: add "dirty" and "pristine" field states
1 parent 37b8c1d commit 2382344

File tree

8 files changed

+98
-1
lines changed

8 files changed

+98
-1
lines changed

docs/assets/field-states.png

196 KB
Loading

docs/guides/basic-concepts.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ Example:
6666
const { value, error, touched, isValidating } = field.state
6767
```
6868

69+
There are three field states can be very useful to see how the user interacts with a field. A field is _"touched"_ when the user clicks/tabs into it, _"pristine"_ until the user changes value in it, and _"dirty"_ after the value has been changed. You can check these states via the `isTouched`, `isPristine` and `isDirty` flags, as seen below.
70+
71+
```tsx
72+
const { isTouched, isPristine, isDirty } = field.state.meta
73+
```
74+
75+
![Field states](https://raw.githubusercontent.com/TanStack/form/f7afd70b502e17f4d7e83d1577823e3732ce2d43/docs/assets/field-states.png)
76+
77+
> **Important note for users coming from `React Hook Form`**: the `isDirty` flag in `TanStack/form` is different from the flag with the same name in RHF.
78+
> In RHF, `isDirty = true`, when the form's values are different from the original values. If the user changes the values in a form, and then changes them again to end up with values that match the form's default values, `isDirty` will be `true` in RHF, but `false` in `TanStack/form`.
79+
> The default values are exposed both on the form's and the field's level in `TanStack/form` (`form.options.defaultValues`, `field.options.defaultValue`), so you can write your own `isDefaultValue()` helper if you need to emulate RHF's behavior.`
80+
81+
6982
## Field API
7083

7184
The Field API is an object passed to the render prop function when creating a field. It provides methods for working with the field's state.

docs/reference/fieldApi.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ An object type representing the options for a field in a form.
131131

132132
An object type representing the metadata of a field in a form.
133133

134+
- ```tsx
135+
isPristine: boolean
136+
```
137+
- A flag that is `true` if the field's value have not been modified by the user. Opposite of `isDirty`.
138+
- ```tsx
139+
isDirty: boolean
140+
```
141+
- A flag that is `true` if the field's value have been modified by the user. Opposite of `isPristine`.
134142
- ```tsx
135143
isTouched: boolean
136144
```

docs/reference/formApi.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ An object representing the current state of the form.
233233
```
234234
- A boolean indicating if any of the form fields have been touched.
235235

236+
- ```tsx
237+
isPristine: boolean
238+
```
239+
- A boolean indicating if none of the form's fields' values have been modified by the user. `True` if the user have not modified any of the fields. Opposite of `isDirty`.
240+
241+
- ```tsx
242+
isDirty: boolean
243+
```
244+
- A boolean indicating if any of the form's fields' values have been modified by the user. `True` if the user have modified at least one of the fields. Opposite of `isPristine`.
245+
236246
- ```tsx
237247
isSubmitted: boolean
238248
```

packages/form-core/src/FieldApi.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ export interface FieldApiOptions<
232232

233233
export type FieldMeta = {
234234
isTouched: boolean
235+
isPristine: boolean
236+
isDirty: boolean
235237
touchedErrors: ValidationError[]
236238
errors: ValidationError[]
237239
errorMap: ValidationErrorMap
@@ -300,6 +302,8 @@ export class FieldApi<
300302
meta: this._getMeta() ?? {
301303
isValidating: false,
302304
isTouched: false,
305+
isDirty: false,
306+
isPristine: true,
303307
touchedErrors: [],
304308
errors: [],
305309
errorMap: {},
@@ -318,6 +322,8 @@ export class FieldApi<
318322
? state.meta.errors
319323
: []
320324

325+
state.meta.isPristine = !state.meta.isDirty
326+
321327
this.prevState = state
322328
this.state = state
323329
},
@@ -448,6 +454,8 @@ export class FieldApi<
448454
({
449455
isValidating: false,
450456
isTouched: false,
457+
isDirty: false,
458+
isPristine: true,
451459
touchedErrors: [],
452460
errors: [],
453461
errorMap: {},

packages/form-core/src/FormApi.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export type FormState<TFormData> = {
129129
isSubmitting: boolean
130130
// General
131131
isTouched: boolean
132+
isDirty: boolean
133+
isPristine: boolean
132134
isSubmitted: boolean
133135
isValidating: boolean
134136
isValid: boolean
@@ -152,6 +154,8 @@ function getDefaultFormState<TFormData>(
152154
isSubmitted: defaultState.isSubmitted ?? false,
153155
isSubmitting: defaultState.isSubmitting ?? false,
154156
isTouched: defaultState.isTouched ?? false,
157+
isPristine: defaultState.isPristine ?? true,
158+
isDirty: defaultState.isDirty ?? false,
155159
isValid: defaultState.isValid ?? false,
156160
isValidating: defaultState.isValidating ?? false,
157161
submissionAttempts: defaultState.submissionAttempts ?? 0,
@@ -208,6 +212,9 @@ export class FormApi<
208212

209213
const isTouched = fieldMetaValues.some((field) => field?.isTouched)
210214

215+
const isDirty = fieldMetaValues.some((field) => field?.isDirty)
216+
const isPristine = !isDirty
217+
211218
const isValidating = isFieldsValidating || state.isFormValidating
212219
state.errors = Object.values(state.errorMap).filter(
213220
(val: unknown) => val !== undefined,
@@ -226,6 +233,8 @@ export class FormApi<
226233
isValid,
227234
canSubmit,
228235
isTouched,
236+
isPristine,
237+
isDirty,
229238
}
230239

231240
this.state = state
@@ -625,6 +634,7 @@ export class FormApi<
625634
this.setFieldMeta(field, (prev) => ({
626635
...prev,
627636
isTouched: true,
637+
isDirty: true,
628638
}))
629639
}
630640

packages/form-core/src/tests/FieldApi.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ describe('field api', () => {
4646
expect(field.getMeta()).toEqual({
4747
isTouched: false,
4848
isValidating: false,
49+
isPristine: true,
50+
isDirty: false,
4951
touchedErrors: [],
5052
errors: [],
5153
errorMap: {},
@@ -57,12 +59,14 @@ describe('field api', () => {
5759
const field = new FieldApi({
5860
form,
5961
name: 'name',
60-
defaultMeta: { isTouched: true },
62+
defaultMeta: { isTouched: true, isDirty: true, isPristine: false },
6163
})
6264

6365
expect(field.getMeta()).toEqual({
6466
isTouched: true,
6567
isValidating: false,
68+
isDirty: true,
69+
isPristine: false,
6670
touchedErrors: [],
6771
errors: [],
6872
errorMap: {},

packages/form-core/src/tests/FormApi.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ describe('form api', () => {
2121
errorMap: {},
2222
isSubmitting: false,
2323
isTouched: false,
24+
isPristine: true,
25+
isDirty: false,
2426
isValid: true,
2527
isValidating: false,
2628
submissionAttempts: 0,
@@ -55,6 +57,8 @@ describe('form api', () => {
5557
isSubmitted: false,
5658
isSubmitting: false,
5759
isTouched: false,
60+
isPristine: true,
61+
isDirty: false,
5862
isValid: true,
5963
isValidating: false,
6064
submissionAttempts: 0,
@@ -87,6 +91,8 @@ describe('form api', () => {
8791
isSubmitted: false,
8892
isSubmitting: false,
8993
isTouched: false,
94+
isPristine: true,
95+
isDirty: false,
9096
isValid: true,
9197
isValidating: false,
9298
submissionAttempts: 30,
@@ -130,6 +136,8 @@ describe('form api', () => {
130136
isSubmitted: false,
131137
isSubmitting: false,
132138
isTouched: false,
139+
isPristine: true,
140+
isDirty: false,
133141
isValid: true,
134142
isValidating: false,
135143
submissionAttempts: 300,
@@ -170,6 +178,8 @@ describe('form api', () => {
170178
isSubmitted: false,
171179
isSubmitting: false,
172180
isTouched: false,
181+
isPristine: true,
182+
isDirty: false,
173183
isValid: true,
174184
isValidating: false,
175185
submissionAttempts: 0,
@@ -204,6 +214,40 @@ describe('form api', () => {
204214
expect(form.getFieldValue('name')).toEqual('other')
205215
})
206216

217+
it("should be dirty after a field's value has been set", () => {
218+
const form = new FormApi({
219+
defaultValues: {
220+
name: 'test',
221+
},
222+
})
223+
form.mount()
224+
225+
form.setFieldValue('name', 'other', { touch: true })
226+
227+
expect(form.state.isDirty).toBe(true)
228+
expect(form.state.isPristine).toBe(false)
229+
})
230+
231+
it('should be clean again after being reset from a dirty state', () => {
232+
const form = new FormApi({
233+
defaultValues: {
234+
name: 'test',
235+
},
236+
})
237+
form.mount()
238+
239+
form.setFieldMeta('name', (meta) => ({
240+
...meta,
241+
isDirty: true,
242+
isPristine: false,
243+
}))
244+
245+
form.reset()
246+
247+
expect(form.state.isDirty).toBe(false)
248+
expect(form.state.isPristine).toBe(true)
249+
})
250+
207251
it("should push an array field's value", () => {
208252
const form = new FormApi({
209253
defaultValues: {

0 commit comments

Comments
 (0)