mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Currently, Signal Forms eagerly instantiates all nodes in the form tree because `childrenMap` iterates over the `value` and creates a `FieldNode` for every property. This ensures validation side-effects are run early, but creates pure overhead for fields without validation logic unless explicitly accessed. This commit makes `childrenMap` lazy by default, skipping materialization for children without schema logic. This is achieved by introducing `hasLogicRules()` and `anyChildHasLogic()` across the `LogicNode` hierarchy. Fields are now only instantiated when a direct read occurs via `getChild()` (which calls the new `ensureChildrenMap()`) or if their subtree requires eager evaluation due to existing validation rules. Fixes #67212
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
/**
|
|
* @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, applyEach, 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'}),
|
|
]);
|
|
});
|
|
|
|
it('should apply logic to each property', () => {
|
|
const s = schema<{a: string; b: string}>((p) => {
|
|
applyEach(p, (prop) => {
|
|
required(prop, {message: 'each'});
|
|
});
|
|
});
|
|
|
|
const data = signal({a: '', b: ''});
|
|
const f = form(data, s, {injector: TestBed.inject(Injector)});
|
|
|
|
expect(f.a().errors()).toEqual([jasmine.objectContaining({message: 'each'})]);
|
|
expect(f.b().errors()).toEqual([jasmine.objectContaining({message: 'each'})]);
|
|
});
|
|
|
|
it('should merge each property logic with specific property logic', () => {
|
|
const s = schema<{a: string; b: string}>((p) => {
|
|
required(p.a, {message: 'before'});
|
|
applyEach(p, (prop) => {
|
|
required(prop, {message: 'each'});
|
|
});
|
|
required(p.a, {message: 'after'});
|
|
});
|
|
|
|
const data = signal({a: '', b: ''});
|
|
const f = form(data, s, {injector: TestBed.inject(Injector)});
|
|
|
|
expect(f.a().errors()).toEqual([
|
|
jasmine.objectContaining({message: 'before'}),
|
|
jasmine.objectContaining({message: 'each'}),
|
|
jasmine.objectContaining({message: 'after'}),
|
|
]);
|
|
expect(f.b().errors()).toEqual([jasmine.objectContaining({message: 'each'})]);
|
|
});
|
|
});
|
|
|
|
describe('lazy materialization', () => {
|
|
it('should stay unmaterialized when children have no schema logic', () => {
|
|
const s = schema<{a: {b: string}}>((p) => {
|
|
// No logic applied
|
|
});
|
|
|
|
const data = signal({a: {b: 'test'}});
|
|
const f = form(data, s, {injector: TestBed.inject(Injector)});
|
|
|
|
// Cast to any to access the private helper method
|
|
expect((f() as any).structure._areChildrenMaterialized()).toBeFalse();
|
|
});
|
|
|
|
it('should materialize children when explicitly accessed', () => {
|
|
const s = schema<{a: {b: string}}>((p) => {
|
|
// No logic applied
|
|
});
|
|
|
|
const data = signal({a: {b: 'test'}});
|
|
const f = form(data, s, {injector: TestBed.inject(Injector)});
|
|
|
|
// Initially false
|
|
expect((f() as any).structure._areChildrenMaterialized()).toBeFalse();
|
|
|
|
// Accessing 'a' forces materialization of root's children
|
|
const childA = f.a;
|
|
expect((f() as any).structure._areChildrenMaterialized()).toBeTrue();
|
|
|
|
// 'a' itself should not have materialized its children ('b') yet
|
|
expect((childA() as any).structure._areChildrenMaterialized()).toBeFalse();
|
|
});
|
|
|
|
it('should eagerly materialize children when they have schema logic', () => {
|
|
const s = schema<{a: {b: string}}>((p) => {
|
|
required(p.a.b, {message: 'required'});
|
|
});
|
|
|
|
const data = signal({a: {b: 'test'}});
|
|
const f = form(data, s, {injector: TestBed.inject(Injector)});
|
|
|
|
// Because 'a.b' has rules, 'a' and 'b' must be materialized up the tree
|
|
expect((f() as any).structure._areChildrenMaterialized()).toBeTrue();
|
|
expect((f.a() as any).structure._areChildrenMaterialized()).toBeTrue();
|
|
});
|
|
});
|
|
});
|