8000 Support marking a field as dirty · angular/angular@bcc188e · GitHub
[go: up one dir, main page]

Skip to content

Commit bcc188e

Browse files
committed
Support marking a field as dirty
1 parent 8b5aeeb commit bcc188e

File tree

4 files changed

+98
-8
lines changed

4 files changed

+98
-8
lines changed

packages/forms/experimental/src/api/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export interface FieldState<T> {
7676
* A signal indicating whether the field has been touched by the user.
7777
*/
7878
readonly touched: Signal<boolean>;
79+
/**
80+
* A signal indicating whether field value has been changed by user.
81+
*/
82+
readonly dirty: Signal<boolean>;
7983
/**
8084
* A signal indicating whether the field is currently disabled.
8185
*/
@@ -102,6 +106,10 @@ export interface FieldState<T> {
102106
* Sets the touched status of the field to `true`.
103107
*/
104108
markAsTouched(): void;
109+
/**
110+
* Sets the dirty status of the field to `true`.
111+
*/
112+
markAsDirty(): void;
105113
/**
106114
* Resets the `submittedStatus` of the field and all descendant fields to unsubmitted.
107115
*/

packages/forms/experimental/src/controls/field.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ export class FieldDirective<T> {
5858
const cmp = illegallyGetComponentInstance(injector);
5959
if (this.el.nativeElement instanceof HTMLInputElement) {
6060
// Bind our field to an <input>
61-
6261
const i = this.el.nativeElement;
6362
const isCheckbox = i.type === 'checkbox';
6463

6564
i.addEventListener('input', () => {
6665
this.field().$state.value.set((!isCheckbox ? i.value : i.checked) as T);
66+
this.field().$state.markAsDirty();
6767
});
6868
i.addEventListener('blur', () => this.field().$state.markAsTouched());
6969

packages/forms/experimental/src/field_node.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@ import {deepSignal} from './util/deep_signal';
3333
*/
3434
export class FieldNode implements FieldState<unknown> {
3535
/**
36-
* Whether this specific field has been touched.
36+
* Field is considered touched when a user stops editing it for the first time (is our case on blur)
3737
*/
3838
private _touched = signal(false);
39+
/**
40+
* Field is considered dirty if a user changed the value of the field at least once.
41+
*/
42+
private _dirty = signal(false);
43+
3944
private _submittedStatus = signal<SubmittedStatus>('unsubmitted');
4045

4146
/**
@@ -136,6 +141,21 @@ export class FieldNode implements FieldState<unknown> {
136141
});
137142
}
138143

144+
/**
145+
* Whether this field is considered dirty.
146+
*
147+
* This field considers itself dirty if one of the following are true:
148+
* - it was directly dirty
149+
* - one of its children is considered dirty
150+
*/
151+
readonly dirty: Signal<boolean> = computed(() => {
152+
return this.reduceChildren(
153+
this._dirty(),
154+
(child, value) => value || child.dirty(),
155+
shortCircuitTrue,
156+
);
157+
});
158+
139159
/**
140160
* Whether this field is considered touched.
141161
*
@@ -251,6 +271,13 @@ export class FieldNode implements FieldState<unknown> {
251271
this._touched.set(true);
252272
}
253273

274+
/**
275+
* Marks this specific field as dirty.
276+
*/
277+
markAsDirty(): void {
278+
this._dirty.set(true);
279+
}
280+
254281
/**
255282
* Retrieve a child `FieldNode` of this node by property key.
256283
*/

packages/forms/experimental/test/node.spec.ts renamed to packages/forms/experimental/test/field_node.spec.ts

+61-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
19
import {computed, signal} from '@angular/core';
210
import {disabled, error, required, validate} from '../src/api/logic';
311
import {apply, applyEach, form, submit} from '../src/api/structure';
412
import {Schema} from '../src/api/types';
513
import {DISABLED_REASON, REQUIRED} from '../src/api/metadata';
614

7-
describe('Node', () => {
8-
it('is untouched initially', () => {
9-
const f = form(signal({a: 1, b: 2}));
10-
expect(f.$state.touched()).toBe(false);
11-
});
12-
15+
describe('FieldNode', () => {
1316
it('can get a child of a key that exists', () => {
1417
const f = form(signal({a: 1, b: 2}));
1518
expect(f.a).toBeDefined();
@@ -49,7 +52,59 @@ describe('Node', () => {
4952
expect(childA()).toBeDefined();
5053
});
5154

55+
describe('dirty', ()=>{
56+
it('is not dirty initially', ()=>{
57+
const f = form(signal({a: 1, b: 2}));
58+
expect(f.$state.dirty()).toBe(false);
59+
expect(f.a.$state.dirty()).toBe(false);
60+
});
61+
62+
63+
it('can be marked as dirty', () => {
64+
const f = form(signal({a: 1, b: 2}));
65+
expect(f.$state.dirty()).toBe(false);
66+
67+
f.$state.markAsDirty();
68+
expect(f.$state.dirty()).toBe(true);
69+
});
70+
71+
it('propagates from the children', () => {
72+
const f = form(signal({a: 1, b: 2}));
73+
expect(f.$state.dirty()).toBe(false);
74+
75+
f.a.$state.markAsDirty();
76+
expect(f.$state.dirty()).toBe(true);
77+
});
78+
79+
it('does not propagate down', () => {
80+
const f = form(signal({a: 1, b: 2}));
81+
82+
expect(f.a.$state.dirty()).toBe(false);
83+
f.$state.markAsDirty();
84+
expect(f.a.$state.dirty()).toBe(false);
85+
});
86+
87+
it('does not consider children that get removed', () => {
88+
const value = signal<{a: number; b?: number}>({a: 1, b: 2});
89+
const f = form(value);
90+
expect(f.$state.dirty()).toBe(false);
91+
92+
f.b!.$state.markAsDirty();
93+
expect(f.$state.dirty()).toBe(true);
94+
95+
value.set({a: 2});
96+
expect(f.$state.dirty()).toBe(false);
97+
expect(f.b).toBeUndefined();
98+
});
99+
});
100+
52101
describe('touched', () => {
102+
it('is untouched initially', () => {
103+
const f = form(signal({a: 1, b: 2}));
104+
expect(f.$state.touched()).toBe(false);
105+
});
106+
107+
53108
it('can be marked as touched', () => {
54109
const f = form(signal({a: 1, b: 2}));
55110
expect(f.$state.touched()).toBe(false);

0 commit comments

Comments
 (0)
0