mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(forms): preserve logic order when apply is used on root path
In some cases the logic order was not preserved properly when using `apply`. In particular this occurs when some logic is registered on a child of the root, followed by an apply to the root, followed by further logic registered on a child. In this case the final registered logic wound up running before the applied logic.
This happened because `FieldPathNode` for a child path was caching its `LogicNodeBuilder` at creation time. This meant that if the parent's `LogicNodeBuilder` changed (e.g., due to an `apply` call), the child would still be using the old one.
This commit fixes the issue by dynamically resolving the `LogicNodeBuilder` for a child path whenever it is accessed.
(cherry picked from commit fa13a167f1)
This commit is contained in:
parent
6e004caa5b
commit
60c8e2779c
3 changed files with 79 additions and 10 deletions
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
import {FieldPathNode} from '../schema/path_node';
|
||||
|
||||
import {WritableSignal} from '@angular/core';
|
||||
import {FormFieldManager} from './manager';
|
||||
import {FieldNode} from './node';
|
||||
import {FieldNodeState} from './state';
|
||||
import {ChildFieldNodeOptions, FieldNodeOptions, FieldNodeStructure} from './structure';
|
||||
import {ValidationState, FieldValidationState} from './validation';
|
||||
import {WritableSignal} from '@angular/core';
|
||||
import {FieldValidationState, ValidationState} from './validation';
|
||||
|
||||
/**
|
||||
* Adapter allowing customization of the creation logic for a field and its associated
|
||||
|
|
|
|||
|
|
@ -37,14 +37,33 @@ export class FieldPathNode {
|
|||
FIELD_PATH_PROXY_HANDLER,
|
||||
) as unknown as FieldPath<any>;
|
||||
|
||||
/**
|
||||
* For a root path node this will contain the root logic builder. For non-root nodes,
|
||||
* they determine their logic builder from their parent so this is undefined.
|
||||
*/
|
||||
private readonly logicBuilder: LogicNodeBuilder | undefined;
|
||||
|
||||
protected constructor(
|
||||
/** The property keys used to navigate from the root path to this path. */
|
||||
readonly keys: PropertyKey[],
|
||||
/** The logic builder used to accumulate logic on this path node. */
|
||||
readonly logic: LogicNodeBuilder,
|
||||
root: FieldPathNode,
|
||||
root: FieldPathNode | undefined,
|
||||
/** The parent of this path node. */
|
||||
private readonly parent: FieldPathNode | undefined,
|
||||
/** The key of this node in its parent. */
|
||||
private readonly keyInParent: PropertyKey | undefined,
|
||||
) {
|
||||
this.root = root ?? this;
|
||||
if (!parent) {
|
||||
this.logicBuilder = LogicNodeBuilder.newRoot();
|
||||
}
|
||||
}
|
||||
|
||||
/** The logic builder used to accumulate logic on this path node. */
|
||||
get logic(): LogicNodeBuilder {
|
||||
if (this.logicBuilder) {
|
||||
return this.logicBuilder;
|
||||
}
|
||||
return this.parent!.logic.getChild(this.keyInParent!);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -60,10 +79,7 @@ export class FieldPathNode {
|
|||
*/
|
||||
getChild(key: PropertyKey): FieldPathNode {
|
||||
if (!this.children.has(key)) {
|
||||
this.children.set(
|
||||
key,
|
||||
new FieldPathNode([...this.keys, key], this.logic.getChild(key), this.root),
|
||||
);
|
||||
this.children.set(key, new FieldPathNode([...this.keys, key], this.root, this, key));
|
||||
}
|
||||
return this.children.get(key)!;
|
||||
}
|
||||
|
|
@ -85,7 +101,7 @@ export class FieldPathNode {
|
|||
|
||||
/** Creates a new root path node to be passed in to a schema function. */
|
||||
static newRoot() {
|
||||
return new FieldPathNode([], LogicNodeBuilder.newRoot(), undefined!);
|
||||
return new FieldPathNode([], undefined, undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
53
packages/forms/signals/test/node/api/structure.spec.ts
Normal file
53
packages/forms/signals/test/node/api/structure.spec.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injector, signal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {apply, form, required, schema} from '@angular/forms/signals';
|
||||
|
||||
describe('structure APIs', () => {
|
||||
describe('apply', () => {
|
||||
it('should maintain order when applying to child of root path', () => {
|
||||
const s = schema<{a: string}>((p) => {
|
||||
required(p.a, {message: 'before'});
|
||||
apply(p.a, (prop) => {
|
||||
required(prop, {message: 'apply'});
|
||||
});
|
||||
required(p.a, {message: 'after'});
|
||||
});
|
||||
|
||||
const data = signal({a: ''});
|
||||
const f = form(data, s, {injector: TestBed.inject(Injector)});
|
||||
|
||||
expect(f.a().errors()).toEqual([
|
||||
jasmine.objectContaining({message: 'before'}),
|
||||
jasmine.objectContaining({message: 'apply'}),
|
||||
jasmine.objectContaining({message: 'after'}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should maintain order when applying to root path', () => {
|
||||
const s = schema<{a: {b: string}}>((p) => {
|
||||
required(p.a.b, {message: 'before'});
|
||||
apply(p, (prop) => {
|
||||
required(prop.a.b, {message: 'apply'});
|
||||
});
|
||||
required(p.a.b, {message: 'after'});
|
||||
});
|
||||
|
||||
const data = signal({a: {b: ''}});
|
||||
const f = form(data, s, {injector: TestBed.inject(Injector)});
|
||||
|
||||
expect(f.a.b().errors()).toEqual([
|
||||
jasmine.objectContaining({message: 'before'}),
|
||||
jasmine.objectContaining({message: 'apply'}),
|
||||
jasmine.objectContaining({message: 'after'}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue