@@ -12,6 +12,7 @@ import type {ReadonlyFieldTree, TreeValidationResult, ValidationResult} from '..
1212import { isArray } from '../util/type_guards' ;
1313import type { FieldNode } from './node' ;
1414import { shortCircuitFalse } from './util' ;
15+ import { shallowArrayEquals } from '../util/array' ;
1516
1617/**
1718 * Helper function taking validation state, and returning own state of the node.
@@ -153,35 +154,41 @@ export class FieldValidationState implements ValidationState {
153154 * The full set of synchronous tree errors visible to this field. This includes ones that are
154155 * targeted at a descendant field rather than at this field.
155156 */
156- readonly rawSyncTreeErrors : Signal < ValidationError . WithFieldTree [ ] > = computed ( ( ) => {
157- if ( this . shouldSkipValidation ( ) ) {
158- return [ ] ;
159- }
160-
161- return [
162- ...this . node . logicNode . logic . syncTreeErrors . compute ( this . node . context ) ,
163- ...( this . node . structure . parent ?. validationState . rawSyncTreeErrors ( ) ?? [ ] ) ,
164- ] ;
165- } ) ;
157+ readonly rawSyncTreeErrors : Signal < ValidationError . WithFieldTree [ ] > = computed (
158+ ( ) => {
159+ if ( this . shouldSkipValidation ( ) ) {
160+ return [ ] ;
161+ }
162+
163+ return [
164+ ...this . node . logicNode . logic . syncTreeErrors . compute ( this . node . context ) ,
165+ ...( this . node . structure . parent ?. validationState . rawSyncTreeErrors ( ) ?? [ ] ) ,
166+ ] ;
167+ } ,
168+ { equal : shallowArrayEquals } ,
169+ ) ;
166170
167171 /**
168172 * The full set of synchronous errors for this field, including synchronous tree errors and
169173 * submission errors. Submission errors are considered "synchronous" because they are imperatively
170174 * added. From the perspective of the field state they are either there or not, they are never in a
171175 * pending state.
172176 */
173- readonly syncErrors : Signal < ValidationError . WithFieldTree [ ] > = computed ( ( ) => {
174- // Short-circuit running validators if validation doesn't apply to this field.
175- if ( this . shouldSkipValidation ( ) ) {
176- return [ ] ;
177- }
178-
179- return [
180- ...this . node . logicNode . logic . syncErrors . compute ( this . node . context ) ,
181- ...this . syncTreeErrors ( ) ,
182- ...normalizeErrors ( this . node . submitState . submissionErrors ( ) ) ,
183- ] ;
184- } ) ;
177+ readonly syncErrors : Signal < ValidationError . WithFieldTree [ ] > = computed (
178+ ( ) => {
179+ // Short-circuit running validators if validation doesn't apply to this field.
180+ if ( this . shouldSkipValidation ( ) ) {
181+ return [ ] ;
182+ }
183+
184+ return [
185+ ...this . node . logicNode . logic . syncErrors . compute ( this . node . context ) ,
186+ ...this . syncTreeErrors ( ) ,
187+ ...normalizeErrors ( this . node . submitState . submissionErrors ( ) ) ,
188+ ] ;
189+ } ,
190+ { equal : shallowArrayEquals } ,
191+ ) ;
185192
186193 /**
187194 * Whether the field is considered valid according solely to its synchronous validators.
@@ -204,67 +211,81 @@ export class FieldValidationState implements ValidationState {
204211 * The synchronous tree errors visible to this field that are specifically targeted at this field
205212 * rather than a descendant.
206213 */
207- readonly syncTreeErrors : Signal < ValidationError . WithFieldTree [ ] > = computed ( ( ) =>
208- this . rawSyncTreeErrors ( ) . filter ( ( err ) => err . fieldTree === this . node . fieldTree ) ,
214+ readonly syncTreeErrors : Signal < ValidationError . WithFieldTree [ ] > = computed (
215+ ( ) => this . rawSyncTreeErrors ( ) . filter ( ( err ) => err . fieldTree === this . node . fieldTree ) ,
216+ { equal : shallowArrayEquals } ,
209217 ) ;
210218
211219 /**
212220 * The full set of asynchronous tree errors visible to this field. This includes ones that are
213221 * targeted at a descendant field rather than at this field, as well as sentinel 'pending' values
214222 * indicating that the validator is still running and an error could still occur.
215223 */
216- readonly rawAsyncErrors : Signal < ( ValidationError . WithFieldTree | 'pending' ) [ ] > = computed ( ( ) => {
217- // Short-circuit running validators if validation doesn't apply to this field.
218- if ( this . shouldSkipValidation ( ) ) {
219- return [ ] ;
220- }
221-
222- return [
223- // TODO: add field in `validateAsync` and remove this map
224- ...this . node . logicNode . logic . asyncErrors . compute ( this . node . context ) ,
225- // TODO: does it make sense to filter this to errors in this subtree?
226- ...( this . node . structure . parent ?. validationState . rawAsyncErrors ( ) ?? [ ] ) ,
227- ] ;
228- } ) ;
224+ readonly rawAsyncErrors : Signal < ( ValidationError . WithFieldTree | 'pending' ) [ ] > = computed (
225+ ( ) => {
226+ // Short-circuit running validators if validation doesn't apply to this field.
227+ if ( this . shouldSkipValidation ( ) ) {
228+ return [ ] ;
229+ }
230+
231+ return [
232+ // TODO: add field in `validateAsync` and remove this map
233+ ...this . node . logicNode . logic . asyncErrors . compute ( this . node . context ) ,
234+ // TODO: does it make sense to filter this to errors in this subtree?
235+ ...( this . node . structure . parent ?. validationState . rawAsyncErrors ( ) ?? [ ] ) ,
236+ ] ;
237+ } ,
238+ { equal : shallowArrayEquals } ,
239+ ) ;
229240
230241 /**
231242 * The asynchronous tree errors visible to this field that are specifically targeted at this field
232243 * rather than a descendant. This also includes all 'pending' sentinel values, since those could
233244 * theoretically result in errors for this field.
234245 */
235- readonly asyncErrors : Signal < ( ValidationError . WithFieldTree | 'pending' ) [ ] > = computed ( ( ) => {
236- if ( this . shouldSkipValidation ( ) ) {
237- return [ ] ;
238- }
239- return this . rawAsyncErrors ( ) . filter (
240- ( err ) => err === 'pending' || err . fieldTree === this . node . fieldTree ,
241- ) ;
242- } ) ;
246+ readonly asyncErrors : Signal < ( ValidationError . WithFieldTree | 'pending' ) [ ] > = computed (
247+ ( ) => {
248+ if ( this . shouldSkipValidation ( ) ) {
249+ return [ ] ;
250+ }
251+ return this . rawAsyncErrors ( ) . filter (
252+ ( err ) => err === 'pending' || err . fieldTree === this . node . fieldTree ,
253+ ) ;
254+ } ,
255+ { equal : shallowArrayEquals } ,
256+ ) ;
243257
244- readonly parseErrors : Signal < ValidationError . WithFormField [ ] > = computed ( ( ) =>
245- this . node . formFieldBindings ( ) . flatMap ( ( field ) => field . parseErrors ( ) ) ,
258+ readonly parseErrors : Signal < ValidationError . WithFormField [ ] > = computed (
259+ ( ) => this . node . formFieldBindings ( ) . flatMap ( ( field ) => field . parseErrors ( ) ) ,
260+ { equal : shallowArrayEquals } ,
246261 ) ;
247262
248263 /**
249264 * The combined set of all errors that currently apply to this field.
250265 */
251- readonly errors = computed ( ( ) => [
252- ...this . parseErrors ( ) ,
253- ...this . syncErrors ( ) ,
254- ...this . asyncErrors ( ) . filter ( ( err ) => err !== 'pending' ) ,
255- ] ) ;
256-
257- readonly errorSummary = computed ( ( ) => {
258- const errors = this . node . structure . reduceChildren ( this . errors ( ) , ( child , result ) => [
259- ...result ,
260- ...child . errorSummary ( ) ,
261- ] ) ;
262- // Sort by DOM order on client-side only.
263- if ( typeof ngServerMode === 'undefined' || ! ngServerMode ) {
264- untracked ( ( ) => errors . sort ( compareErrorPosition ) ) ;
265- }
266- return errors ;
267- } ) ;
266+ readonly errors = computed (
267+ ( ) => [
268+ ...this . parseErrors ( ) ,
269+ ...this . syncErrors ( ) ,
270+ ...this . asyncErrors ( ) . filter ( ( err ) => err !== 'pending' ) ,
271+ ] ,
272+ { equal : shallowArrayEquals } ,
273+ ) ;
274+
275+ readonly errorSummary = computed (
276+ ( ) => {
277+ const errors = this . node . structure . reduceChildren ( this . errors ( ) , ( child , result ) => [
278+ ...result ,
279+ ...child . errorSummary ( ) ,
280+ ] ) ;
281+ // Sort by DOM order on client-side only.
282+ if ( typeof ngServerMode === 'undefined' || ! ngServerMode ) {
283+ untracked ( ( ) => errors . sort ( compareErrorPosition ) ) ;
284+ }
285+ return errors ;
286+ } ,
287+ { equal : shallowArrayEquals } ,
288+ ) ;
268289
269290 /**
270291 * Whether this field has any asynchronous validators still pending.
0 commit comments