UNPKG

@angular/forms

Version:

Angular - directives and services for creating forms

1,744 lines (1,722 loc) 56.7 kB
/** * @license Angular v22.0.2 * (c) 2010-2026 Google LLC. https://angular.dev/ * License: MIT */ import { untracked, ɵRuntimeError as _RuntimeError, computed, runInInjectionContext, Injector, linkedSignal, signal, APP_ID, effect, InjectionToken, inject } from '@angular/core'; import { AbstractControl, FormGroup, FormArray } from '@angular/forms'; import { SIGNAL } from '@angular/core/primitives/signals'; let boundPathDepth = 0; function getBoundPathDepth() { return boundPathDepth; } function setBoundPathDepthForResolution(fn, depth) { return (...args) => { try { boundPathDepth = depth; return fn(...args); } finally { boundPathDepth = 0; } }; } function shortCircuitFalse(value) { return !value; } function shortCircuitTrue(value) { return value; } function getInjectorFromOptions(options) { if (options.kind === 'root') { return options.fieldManager.injector; } return options.parent.structure.root.structure.injector; } function isArray(value) { return Array.isArray(value); } function isObject(value) { return (typeof value === 'object' || typeof value === 'function') && value != null; } const DYNAMIC = Symbol(); const IGNORED = Symbol(); class AbstractLogic { predicates; fns = []; constructor(predicates) { this.predicates = predicates; } push(logicFn) { this.fns.push(wrapWithPredicates(this.predicates, logicFn)); } mergeIn(other) { const fns = this.predicates ? other.fns.map(fn => wrapWithPredicates(this.predicates, fn)) : other.fns; this.fns.push(...fns); } hasRules() { return this.fns.length > 0; } } class BooleanOrLogic extends AbstractLogic { get defaultValue() { return false; } compute(arg) { return this.fns.some(f => { const result = f(arg); return result && result !== IGNORED; }); } } class ArrayMergeIgnoreLogic extends AbstractLogic { ignore; static ignoreNull(predicates) { return new ArrayMergeIgnoreLogic(predicates, e => e === null); } constructor(predicates, ignore) { super(predicates); this.ignore = ignore; } get defaultValue() { return []; } compute(arg) { return this.fns.reduce((prev, f) => { const value = f(arg); if (value === undefined || value === IGNORED) { return prev; } else if (isArray(value)) { return [...prev, ...(this.ignore ? value.filter(e => !this.ignore(e)) : value)]; } else { if (this.ignore && this.ignore(value)) { return prev; } return [...prev, value]; } }, []); } } class ArrayMergeLogic extends ArrayMergeIgnoreLogic { constructor(predicates) { super(predicates, undefined); } } class MetadataMergeLogic extends AbstractLogic { key; get defaultValue() { return this.key.reducer.getInitial(); } constructor(predicates, key) { super(predicates); this.key = key; } compute(ctx) { if (this.fns.length === 0) { return this.key.reducer.getInitial(); } let acc = this.key.reducer.getInitial(); for (let i = 0; i < this.fns.length; i++) { const item = this.fns[i](ctx); if (item !== IGNORED) { acc = this.key.reducer.reduce(acc, item); } } return acc; } } function wrapWithPredicates(predicates, logicFn) { if (predicates.length === 0) { return logicFn; } return arg => { for (const predicate of predicates) { let predicateField = arg.stateOf(predicate.path); const depthDiff = untracked(predicateField.structure.pathKeys).length - predicate.depth; for (let i = 0; i < depthDiff; i++) { predicateField = predicateField.structure.parent; } if (!predicate.fn(predicateField.context)) { return IGNORED; } } return logicFn(arg); }; } class LogicContainer { predicates; hidden; disabledReasons; readonly; syncErrors; syncTreeErrors; asyncErrors; metadata = new Map(); constructor(predicates) { this.predicates = predicates; this.hidden = new BooleanOrLogic(predicates); this.disabledReasons = new ArrayMergeLogic(predicates); this.readonly = new BooleanOrLogic(predicates); this.syncErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates); this.syncTreeErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates); this.asyncErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates); } hasAnyLogic() { return this.hidden.hasRules() || this.disabledReasons.hasRules() || this.readonly.hasRules() || this.syncErrors.hasRules() || this.syncTreeErrors.hasRules() || this.asyncErrors.hasRules() || this.metadata.size > 0; } hasMetadata(key) { return this.metadata.has(key); } hasMetadataKeys() { return this.metadata.size > 0; } getMetadataKeys() { return this.metadata.keys(); } getMetadata(key) { if (!this.metadata.has(key)) { this.metadata.set(key, new MetadataMergeLogic(this.predicates, key)); } return this.metadata.get(key); } mergeIn(other) { this.hidden.mergeIn(other.hidden); this.disabledReasons.mergeIn(other.disabledReasons); this.readonly.mergeIn(other.readonly); this.syncErrors.mergeIn(other.syncErrors); this.syncTreeErrors.mergeIn(other.syncTreeErrors); this.asyncErrors.mergeIn(other.asyncErrors); for (const key of other.getMetadataKeys()) { const metadataLogic = other.metadata.get(key); this.getMetadata(key).mergeIn(metadataLogic); } } } class AbstractLogicNodeBuilder { depth; constructor(depth) { this.depth = depth; } build() { return new LeafLogicNode(this, [], 0); } } class LogicNodeBuilder extends AbstractLogicNodeBuilder { constructor(depth) { super(depth); } current; all = []; addHiddenRule(logic) { this.getCurrent().addHiddenRule(logic); } addDisabledReasonRule(logic) { this.getCurrent().addDisabledReasonRule(logic); } addReadonlyRule(logic) { this.getCurrent().addReadonlyRule(logic); } addSyncErrorRule(logic) { this.getCurrent().addSyncErrorRule(logic); } addSyncTreeErrorRule(logic) { this.getCurrent().addSyncTreeErrorRule(logic); } addAsyncErrorRule(logic) { this.getCurrent().addAsyncErrorRule(logic); } addMetadataRule(key, logic) { this.getCurrent().addMetadataRule(key, logic); } getChild(key) { if (key === DYNAMIC) { const children = this.getCurrent().children; if (children.size > (children.has(DYNAMIC) ? 1 : 0)) { this.current = undefined; } } return this.getCurrent().getChild(key); } hasLogic(builder) { if (this === builder) { return true; } return this.all.some(({ builder: subBuilder }) => subBuilder.hasLogic(builder)); } hasRules() { return this.all.length > 0; } anyChildHasLogic() { return this.all.some(({ builder }) => builder.anyChildHasLogic()); } mergeIn(other, predicate) { if (predicate) { this.all.push({ builder: other, predicate: { fn: setBoundPathDepthForResolution(predicate.fn, this.depth), path: predicate.path } }); } else { this.all.push({ builder: other }); } this.current = undefined; } getCurrent() { if (this.current === undefined) { this.current = new NonMergeableLogicNodeBuilder(this.depth); this.all.push({ builder: this.current }); } return this.current; } static newRoot() { return new LogicNodeBuilder(0); } } class NonMergeableLogicNodeBuilder extends AbstractLogicNodeBuilder { logic = new LogicContainer([]); children = new Map(); constructor(depth) { super(depth); } addHiddenRule(logic) { this.logic.hidden.push(setBoundPathDepthForResolution(logic, this.depth)); } addDisabledReasonRule(logic) { this.logic.disabledReasons.push(setBoundPathDepthForResolution(logic, this.depth)); } addReadonlyRule(logic) { this.logic.readonly.push(setBoundPathDepthForResolution(logic, this.depth)); } addSyncErrorRule(logic) { this.logic.syncErrors.push(setBoundPathDepthForResolution(logic, this.depth)); } addSyncTreeErrorRule(logic) { this.logic.syncTreeErrors.push(setBoundPathDepthForResolution(logic, this.depth)); } addAsyncErrorRule(logic) { this.logic.asyncErrors.push(setBoundPathDepthForResolution(logic, this.depth)); } addMetadataRule(key, logic) { this.logic.getMetadata(key).push(setBoundPathDepthForResolution(logic, this.depth)); } getChild(key) { if (!this.children.has(key)) { this.children.set(key, new LogicNodeBuilder(this.depth + 1)); } return this.children.get(key); } hasLogic(builder) { return this === builder; } hasRules() { return this.logic.hasAnyLogic() || this.children.size > 0; } anyChildHasLogic() { for (const child of this.children.values()) { if (child.hasRules()) { return true; } } return false; } } class LeafLogicNode { builder; predicates; depth; logic; constructor(builder, predicates, depth) { this.builder = builder; this.predicates = predicates; this.depth = depth; this.logic = builder ? createLogic(builder, predicates, depth) : new LogicContainer([]); } getChild(key) { const childBuilders = this.builder ? getAllChildBuilders(this.builder, key) : []; if (childBuilders.length === 0) { return new LeafLogicNode(undefined, [], this.depth + 1); } else if (childBuilders.length === 1) { const { builder, predicates } = childBuilders[0]; return new LeafLogicNode(builder, [...this.predicates, ...predicates.map(p => bindLevel(p, this.depth))], this.depth + 1); } else { const builtNodes = childBuilders.map(({ builder, predicates }) => new LeafLogicNode(builder, [...this.predicates, ...predicates.map(p => bindLevel(p, this.depth))], this.depth + 1)); return new CompositeLogicNode(builtNodes); } } hasLogic(builder) { if (!this.builder) { return false; } return this.builder.hasLogic(builder); } hasRules() { return this.builder ? this.builder.hasRules() : false; } anyChildHasLogic() { return this.builder ? this.builder.anyChildHasLogic() : false; } } class CompositeLogicNode { all; logic; constructor(all) { this.all = all; this.logic = new LogicContainer([]); for (const node of all) { this.logic.mergeIn(node.logic); } } getChild(key) { return new CompositeLogicNode(this.all.flatMap(child => child.getChild(key))); } hasLogic(builder) { return this.all.some(node => node.hasLogic(builder)); } hasRules() { return this.all.some(node => node.hasRules()); } anyChildHasLogic() { return this.all.some(child => child.anyChildHasLogic()); } } function getAllChildBuilders(builder, key) { if (builder instanceof LogicNodeBuilder) { return builder.all.flatMap(({ builder, predicate }) => { const children = getAllChildBuilders(builder, key); if (predicate) { return children.map(({ builder, predicates }) => ({ builder, predicates: [...predicates, predicate] })); } return children; }); } else if (builder instanceof NonMergeableLogicNodeBuilder) { return [...(key !== DYNAMIC && builder.children.has(DYNAMIC) ? [{ builder: builder.getChild(DYNAMIC), predicates: [] }] : []), ...(builder.children.has(key) ? [{ builder: builder.getChild(key), predicates: [] }] : [])]; } else { throw new _RuntimeError(1909, ngDevMode && 'Unknown LogicNodeBuilder type'); } } function createLogic(builder, predicates, depth) { const logic = new LogicContainer(predicates); if (builder instanceof LogicNodeBuilder) { const builtNodes = builder.all.map(({ builder, predicate }) => new LeafLogicNode(builder, predicate ? [...predicates, bindLevel(predicate, depth)] : predicates, depth)); for (const node of builtNodes) { logic.mergeIn(node.logic); } } else if (builder instanceof NonMergeableLogicNodeBuilder) { logic.mergeIn(builder.logic); } else { throw new _RuntimeError(1909, ngDevMode && 'Unknown LogicNodeBuilder type'); } return logic; } function bindLevel(predicate, depth) { return { ...predicate, depth: depth }; } const PATH = Symbol('PATH'); class FieldPathNode { keys; parent; keyInParent; root; children = new Map(); fieldPathProxy = new Proxy(this, FIELD_PATH_PROXY_HANDLER); logicBuilder; constructor(keys, root, parent, keyInParent) { this.keys = keys; this.parent = parent; this.keyInParent = keyInParent; this.root = root ?? this; if (!parent) { this.logicBuilder = LogicNodeBuilder.newRoot(); } } get builder() { if (this.logicBuilder) { return this.logicBuilder; } return this.parent.builder.getChild(this.keyInParent); } getChild(key) { if (!this.children.has(key)) { this.children.set(key, new FieldPathNode([...this.keys, key], this.root, this, key)); } return this.children.get(key); } mergeIn(other, predicate) { const path = other.compile(); this.builder.mergeIn(path.builder, predicate); } static unwrapFieldPath(formPath) { return formPath[PATH]; } static newRoot() { return new FieldPathNode([], undefined, undefined, undefined); } } const FIELD_PATH_PROXY_HANDLER = { get(node, property) { if (property === PATH) { return node; } return node.getChild(property).fieldPathProxy; } }; let currentCompilingNode = undefined; const compiledSchemas = new Map(); class SchemaImpl { schemaFn; constructor(schemaFn) { this.schemaFn = schemaFn; } compile() { if (compiledSchemas.has(this)) { return compiledSchemas.get(this); } const path = FieldPathNode.newRoot(); compiledSchemas.set(this, path); let prevCompilingNode = currentCompilingNode; try { currentCompilingNode = path; this.schemaFn(path.fieldPathProxy); } finally { currentCompilingNode = prevCompilingNode; } return path; } static create(schema) { if (schema instanceof SchemaImpl) { return schema; } return new SchemaImpl(schema); } static rootCompile(schema) { try { compiledSchemas.clear(); if (schema === undefined) { return FieldPathNode.newRoot(); } if (schema instanceof SchemaImpl) { return schema.compile(); } return new SchemaImpl(schema).compile(); } finally { compiledSchemas.clear(); } } } function isSchemaOrSchemaFn(value) { return value instanceof SchemaImpl || typeof value === 'function'; } function assertPathIsCurrent(path) { if (currentCompilingNode !== FieldPathNode.unwrapFieldPath(path).root) { throw new _RuntimeError(1908, ngDevMode && `A FieldPath can only be used directly within the Schema that owns it, **not** outside of it or within a sub-schema.`); } } function metadata(path, key, logic) { assertPathIsCurrent(path); const pathNode = FieldPathNode.unwrapFieldPath(path); pathNode.builder.addMetadataRule(key, logic); return key; } const MetadataReducer = { list() { return { reduce: (acc, item) => item === undefined ? acc : [...acc, item], getInitial: () => [] }; }, min() { return { reduce: (acc, item) => { if (acc === undefined || item === undefined) { return acc ?? item; } return item < acc ? item : acc; }, getInitial: () => undefined }; }, max() { return { reduce: (acc, item) => { if (acc === undefined || item === undefined) { return acc ?? item; } return item > acc ? item : acc; }, getInitial: () => undefined }; }, or() { return { reduce: (prev, next) => prev || next, getInitial: () => false }; }, and() { return { reduce: (prev, next) => prev && next, getInitial: () => true }; }, override }; function override(getInitial) { return { reduce: (_, item) => item, getInitial: () => getInitial?.() }; } const IS_ASYNC_VALIDATION_RESOURCE = Symbol('IS_ASYNC_VALIDATION_RESOURCE'); class MetadataKey { reducer; create; brand; [IS_ASYNC_VALIDATION_RESOURCE]; constructor(reducer, create) { this.reducer = reducer; this.create = create; } } function createMetadataKey(reducer) { return new MetadataKey(reducer ?? MetadataReducer.override()); } function createManagedMetadataKey(create, reducer) { return new MetadataKey(reducer ?? MetadataReducer.override(), create); } function createLimitSelectionKey() { return createMetadataKey(); } const REQUIRED = createMetadataKey(MetadataReducer.or()); const MIN = createLimitSelectionKey(); const MIN_DATE = createMetadataKey(MetadataReducer.max()); const MIN_NUMBER = createMetadataKey(MetadataReducer.max()); const MAX = createLimitSelectionKey(); const MAX_DATE = createMetadataKey(MetadataReducer.min()); const MAX_NUMBER = createMetadataKey(MetadataReducer.min()); const MIN_LENGTH = createMetadataKey(MetadataReducer.max()); const MAX_LENGTH = createMetadataKey(MetadataReducer.min()); const PATTERN = createMetadataKey(MetadataReducer.list()); function shallowArrayEquals(a, b) { if (a === b) return true; if (!a || !b) return false; if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!Object.is(a[i], b[i])) return false; } return true; } function calculateValidationSelfStatus(state) { if (state.errors().length > 0) { return 'invalid'; } if (state.pending()) { return 'unknown'; } return 'valid'; } class FieldValidationState { node; constructor(node) { this.node = node; } rawSyncTreeErrors = computed(() => { if (this.shouldSkipValidation()) { return []; } return [...this.node.logicNode.logic.syncTreeErrors.compute(this.node.context), ...(this.node.structure.parent?.validationState.rawSyncTreeErrors() ?? [])]; }, { ...(ngDevMode ? { debugName: "rawSyncTreeErrors" } : {}), equal: shallowArrayEquals }); syncErrors = computed(() => { if (this.shouldSkipValidation()) { return []; } return [...this.node.logicNode.logic.syncErrors.compute(this.node.context), ...this.syncTreeErrors(), ...normalizeErrors(this.node.submitState.submissionErrors())]; }, { ...(ngDevMode ? { debugName: "syncErrors" } : {}), equal: shallowArrayEquals }); syncValid = computed(() => { if (this.shouldSkipValidation()) { return true; } return this.node.structure.reduceChildren(this.syncErrors().length === 0, (child, value) => value && child.validationState.syncValid(), shortCircuitFalse); }, ...(ngDevMode ? [{ debugName: "syncValid" }] : [])); syncTreeErrors = computed(() => this.rawSyncTreeErrors().filter(err => err.fieldTree === this.node.fieldTree), { ...(ngDevMode ? { debugName: "syncTreeErrors" } : {}), equal: shallowArrayEquals }); rawAsyncErrors = computed(() => { if (this.shouldSkipValidation()) { return []; } return [...this.node.logicNode.logic.asyncErrors.compute(this.node.context), ...(this.node.structure.parent?.validationState.rawAsyncErrors() ?? [])]; }, { ...(ngDevMode ? { debugName: "rawAsyncErrors" } : {}), equal: shallowArrayEquals }); asyncErrors = computed(() => { if (this.shouldSkipValidation()) { return []; } return this.rawAsyncErrors().filter(err => err === 'pending' || err.fieldTree === this.node.fieldTree); }, { ...(ngDevMode ? { debugName: "asyncErrors" } : {}), equal: shallowArrayEquals }); parseErrors = computed(() => this.node.formFieldBindings().flatMap(field => field.parseErrors()), { ...(ngDevMode ? { debugName: "parseErrors" } : {}), equal: shallowArrayEquals }); errors = computed(() => [...this.parseErrors(), ...this.syncErrors(), ...this.asyncErrors().filter(err => err !== 'pending')], { ...(ngDevMode ? { debugName: "errors" } : {}), equal: shallowArrayEquals }); errorSummary = computed(() => { const errors = this.node.structure.reduceChildren(this.errors(), (child, result) => [...result, ...child.errorSummary()]); if (typeof ngServerMode === 'undefined' || !ngServerMode) { untracked(() => errors.sort(compareErrorPosition)); } return errors; }, { ...(ngDevMode ? { debugName: "errorSummary" } : {}), equal: shallowArrayEquals }); pending = computed(() => this.node.structure.reduceChildren(this.asyncErrors().includes('pending'), (child, value) => value || child.validationState.asyncErrors().includes('pending')), ...(ngDevMode ? [{ debugName: "pending" }] : [])); status = computed(() => { if (this.shouldSkipValidation()) { return 'valid'; } let ownStatus = calculateValidationSelfStatus(this); return this.node.structure.reduceChildren(ownStatus, (child, value) => { if (value === 'invalid' || child.validationState.status() === 'invalid') { return 'invalid'; } else if (value === 'unknown' || child.validationState.status() === 'unknown') { return 'unknown'; } return 'valid'; }, v => v === 'invalid'); }, ...(ngDevMode ? [{ debugName: "status" }] : [])); valid = computed(() => this.status() === 'valid', ...(ngDevMode ? [{ debugName: "valid" }] : [])); invalid = computed(() => this.status() === 'invalid', ...(ngDevMode ? [{ debugName: "invalid" }] : [])); shouldSkipValidation = computed(() => this.node.hidden() || this.node.disabled() || this.node.readonly() || this.node.structure.isOrphaned(), ...(ngDevMode ? [{ debugName: "shouldSkipValidation" }] : [])); } function normalizeErrors(error) { if (error === undefined) { return []; } if (isArray(error)) { return error; } return [error]; } function addDefaultField(errors, fieldTree) { if (isArray(errors)) { for (const error of errors) { error.fieldTree ??= fieldTree; } } else if (errors) { errors.fieldTree ??= fieldTree; } return errors; } function getFirstBoundElement(error) { if (error.formField) return error.formField.element; return error.fieldTree().formFieldBindings().reduce((el, binding) => { if (!el || !binding.element) return el ?? binding.element; return el.compareDocumentPosition(binding.element) & Node.DOCUMENT_POSITION_PRECEDING ? binding.element : el; }, undefined); } function compareErrorPosition(a, b) { const aEl = getFirstBoundElement(a); const bEl = getFirstBoundElement(b); if (aEl === bEl) return 0; if (aEl === undefined || bEl === undefined) return aEl === undefined ? 1 : -1; return aEl.compareDocumentPosition(bEl) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1; } const DEBOUNCER = createMetadataKey(); class FieldNodeContext { node; cache = new WeakMap(); constructor(node) { this.node = node; this.fieldTreeOf = this.fieldTreeOf.bind(this); this.stateOf = this.stateOf.bind(this); } resolve(target) { if (!this.cache.has(target)) { const resolver = computed(() => { const targetPathNode = FieldPathNode.unwrapFieldPath(target); let field = this.node; let stepsRemaining = getBoundPathDepth(); while (stepsRemaining > 0 || !field.structure.logic.hasLogic(targetPathNode.root.builder)) { stepsRemaining--; field = field.structure.parent; if (field === undefined) { throw new _RuntimeError(1900, ngDevMode && 'Path is not part of this field tree.'); } } for (let key of targetPathNode.keys) { field = field.structure.getChild(key); if (field === undefined) { throw new _RuntimeError(1901, ngDevMode && `Cannot resolve path .${targetPathNode.keys.join('.')} relative to field ${['<root>', ...this.node.structure.pathKeys()].join('.')}.`); } } return field.fieldTree; }, ...(ngDevMode ? [{ debugName: "resolver" }] : [])); this.cache.set(target, resolver); } return this.cache.get(target)(); } get fieldTree() { return this.node.fieldProxy; } get state() { return this.node; } get value() { return this.node.structure.value; } get key() { return this.node.structure.keyInParent; } get pathKeys() { return this.node.structure.pathKeys; } index = computed(() => { const key = this.key(); if (!isArray(untracked(this.node.structure.parent.value))) { throw new _RuntimeError(1906, ngDevMode && 'Cannot access index, parent field is not an array.'); } return Number(key); }, ...(ngDevMode ? [{ debugName: "index" }] : [])); fieldTreeOf(p) { return this.resolve(p); } stateOf(p) { return this.resolve(p)(); } valueOf = p => { const result = this.resolve(p)().value(); if (result instanceof AbstractControl) { throw new _RuntimeError(1907, ngDevMode && `Tried to read an 'AbstractControl' value from a 'form()'. Did you mean to use 'compatForm()' instead?`); } return result; }; } class FieldMetadataState { node; metadata = new Map(); constructor(node) { this.node = node; } runMetadataCreateLifecycle() { if (!this.node.logicNode.logic.hasMetadataKeys()) { return; } untracked(() => runInInjectionContext(this.node.structure.injector, () => { for (const key of this.node.logicNode.logic.getMetadataKeys()) { if (key.create) { const logic = this.node.logicNode.logic.getMetadata(key); const result = key.create(this.node, computed(() => logic.compute(this.node.context))); this.metadata.set(key, result); } } })); } get(key) { if (this.has(key)) { if (!this.metadata.has(key)) { if (key.create) { throw new _RuntimeError(1912, ngDevMode && 'Managed metadata cannot be created lazily'); } const logic = this.node.logicNode.logic.getMetadata(key); this.metadata.set(key, computed(() => logic.compute(this.node.context))); } } return this.metadata.get(key); } has(key) { return this.node.logicNode.logic.hasMetadata(key); } } const FIELD_PROXY_HANDLER = { get(getTgt, p, receiver) { const tgt = getTgt(); const child = tgt.structure.getChild(p); if (child !== undefined) { return child.fieldTree; } const value = untracked(tgt.value); if (isArray(value)) { if (p === 'length') { return tgt.value().length; } if (p === Symbol.iterator) { return () => { tgt.value(); return Array.prototype[Symbol.iterator].apply(tgt.fieldTree); }; } } if (isObject(value)) { if (p === Symbol.iterator) { return function* () { for (const key in receiver) { yield [key, receiver[key]]; } }; } } return undefined; }, getOwnPropertyDescriptor(getTgt, prop) { const value = untracked(getTgt().value); const desc = Reflect.getOwnPropertyDescriptor(value, prop); if (desc && !desc.configurable) { desc.configurable = true; } return desc; }, ownKeys(getTgt) { const value = untracked(getTgt().value); return typeof value === 'object' && value !== null ? Reflect.ownKeys(value) : []; } }; function deepSignal(source, prop) { const read = computed(() => source()[prop()]); read[SIGNAL] = source[SIGNAL]; read.set = value => { if (Object.is(untracked(read), value)) { return; } source.update(current => valueForWrite(current, value, prop())); }; read.update = fn => { read.set(fn(untracked(read))); }; read.asReadonly = () => read; return read; } function valueForWrite(sourceValue, newPropValue, prop) { if (isArray(sourceValue)) { const newValue = [...sourceValue]; newValue[prop] = newPropValue; return newValue; } else { return { ...sourceValue, [prop]: newPropValue }; } } const ORPHAN_TOKEN = Symbol(typeof ngDevMode !== 'undefined' && ngDevMode ? 'ORPHAN_TOKEN' : ''); const FALSE_SIGNAL = computed(() => false, ...(ngDevMode ? [{ debugName: "FALSE_SIGNAL" }] : [])); class FieldNodeStructure { logic; node; createChildNode; identitySymbol = Symbol(); _injector = undefined; _anyChildHasLogic; get injector() { this._injector ??= Injector.create({ providers: [], parent: this.fieldManager.injector }); return this._injector; } constructor(logic, node, createChildNode) { this.logic = logic; this.node = node; this.createChildNode = createChildNode; } children() { this.ensureChildrenMap(); const map = this.childrenMap(); if (map === undefined) { return []; } return Array.from(map.byPropertyKey.values()).map(child => untracked(child.reader)); } materializedChildren() { const map = this.childrenMap(); if (map === undefined) { return []; } return Array.from(map.byPropertyKey.values()).map(child => child.node); } _areChildrenMaterialized() { return untracked(this.childrenMap) !== undefined; } ensureChildrenMap() { if (this._areChildrenMaterialized()) { return; } untracked(() => { this.childrenMap.update(current => this.computeChildrenMap(this.value(), current, true)); }); } getChild(key) { this.ensureChildrenMap(); const strKey = key.toString(); let reader = untracked(this.childrenMap)?.byPropertyKey.get(strKey)?.reader; if (!reader) { reader = this.createReader(strKey); } return reader(); } reduceChildren(initialValue, fn, shortCircuit) { const map = this.childrenMap(); if (!map) { return initialValue; } let value = initialValue; for (const child of map.byPropertyKey.values()) { if (shortCircuit?.(value)) { break; } value = fn(untracked(child.reader), value); } return value; } destroy() { this.injector.destroy(); } createKeyOrOrphanSignals(kind, identityInParent, initialKeyInParent) { if (kind === 'root') { return { keyInParent: ROOT_KEY_IN_PARENT, isOrphaned: FALSE_SIGNAL }; } const parent = this.parent; let lastKnownKey = initialKeyInParent; const keyOrOrphan = computed(() => { if (parent.structure.isOrphaned()) { return ORPHAN_TOKEN; } const map = parent.structure.childrenMap(); if (!map) { return ORPHAN_TOKEN; } const lastKnownChild = map.byPropertyKey.get(lastKnownKey); if (lastKnownChild && lastKnownChild.node === this.node) { return lastKnownKey; } if (identityInParent === undefined) { return ORPHAN_TOKEN; } else { for (const [key, child] of map.byPropertyKey) { if (child.node === this.node) { return lastKnownKey = key; } } return ORPHAN_TOKEN; } }, ...(ngDevMode ? [{ debugName: "keyOrOrphan" }] : [])); const isOrphaned = computed(() => keyOrOrphan() === ORPHAN_TOKEN, ...(ngDevMode ? [{ debugName: "isOrphaned" }] : [])); const keyInParent = computed(() => { const key = keyOrOrphan(); if (key === ORPHAN_TOKEN) { if (identityInParent === undefined) { throw new _RuntimeError(-1902, ngDevMode && `Orphan field, looking for property '${initialKeyInParent}' of ${getDebugName(parent)}`); } else { throw new _RuntimeError(1904, ngDevMode && `Orphan field, can't find element in array ${getDebugName(parent)}`); } } return key; }, ...(ngDevMode ? [{ debugName: "keyInParent" }] : [])); return { keyInParent, isOrphaned }; } createChildrenMap() { return linkedSignal({ source: this.value, computation: (value, previous) => this.computeChildrenMap(value, previous?.value, false) }); } computeChildrenMap(value, prevData, forceMaterialize) { if (!isObject(value)) { return undefined; } if (!forceMaterialize && prevData === undefined) { if (!(this._anyChildHasLogic ??= this.logic.anyChildHasLogic())) { return undefined; } } prevData ??= { byPropertyKey: new Map() }; let materializedChildren; const parentIsArray = isArray(value); if (prevData !== undefined) { if (parentIsArray) { materializedChildren = maybeRemoveStaleArrayFields(prevData, value, this.identitySymbol); } else { materializedChildren = maybeRemoveStaleObjectFields(prevData, value); } } for (const key of Object.keys(value)) { let trackingKey = undefined; const childValue = value[key]; if (childValue === undefined) { if (prevData.byPropertyKey.has(key)) { materializedChildren ??= { ...prevData }; materializedChildren.byPropertyKey.delete(key); } continue; } if (parentIsArray && isObject(childValue) && !isArray(childValue)) { trackingKey = childValue[this.identitySymbol] ??= Symbol(ngDevMode ? `id:${globalId++}` : ''); } let childNode; if (trackingKey) { if (!prevData.byTrackingKey?.has(trackingKey)) { materializedChildren ??= { ...prevData }; materializedChildren.byTrackingKey ??= new Map(); materializedChildren.byTrackingKey.set(trackingKey, this.createChildNode(key, trackingKey, parentIsArray)); } childNode = (materializedChildren ?? prevData).byTrackingKey.get(trackingKey); } const child = prevData.byPropertyKey.get(key); if (child === undefined) { materializedChildren ??= { ...prevData }; materializedChildren.byPropertyKey.set(key, { reader: this.createReader(key), node: childNode ?? this.createChildNode(key, trackingKey, parentIsArray) }); } else if (childNode && childNode !== child.node) { materializedChildren ??= { ...prevData }; child.node = childNode; } } return materializedChildren ?? prevData; } createReader(key) { return computed(() => this.childrenMap()?.byPropertyKey.get(key)?.node); } } class RootFieldNodeStructure extends FieldNodeStructure { fieldManager; value; get parent() { return undefined; } get root() { return this.node; } get pathKeys() { return ROOT_PATH_KEYS; } get keyInParent() { return ROOT_KEY_IN_PARENT; } isOrphaned = FALSE_SIGNAL; childrenMap; constructor(node, logic, fieldManager, value, createChildNode) { super(logic, node, createChildNode); this.fieldManager = fieldManager; this.value = value; this.childrenMap = this.createChildrenMap(); } } class ChildFieldNodeStructure extends FieldNodeStructure { logic; parent; root; pathKeys; keyInParent; value; childrenMap; isOrphaned; get fieldManager() { return this.root.structure.fieldManager; } constructor(node, logic, parent, identityInParent, initialKeyInParent, createChildNode) { super(logic, node, createChildNode); this.logic = logic; this.parent = parent; this.root = this.parent.structure.root; const signals = this.createKeyOrOrphanSignals('child', identityInParent, initialKeyInParent); this.isOrphaned = signals.isOrphaned; this.keyInParent = signals.keyInParent; this.pathKeys = computed(() => [...parent.structure.pathKeys(), this.keyInParent()], ...(ngDevMode ? [{ debugName: "pathKeys" }] : [])); this.value = deepSignal(this.parent.structure.value, this.keyInParent); this.childrenMap = this.createChildrenMap(); this.fieldManager.structures.add(this); } } let globalId = 0; const ROOT_PATH_KEYS = computed(() => [], ...(ngDevMode ? [{ debugName: "ROOT_PATH_KEYS" }] : [])); const ROOT_KEY_IN_PARENT = computed(() => { throw new _RuntimeError(1905, ngDevMode && 'The top-level field in the form has no parent.'); }, ...(ngDevMode ? [{ debugName: "ROOT_KEY_IN_PARENT" }] : [])); function getDebugName(node) { return `<root>.${node.structure.pathKeys().join('.')}`; } function maybeRemoveStaleArrayFields(prevData, value, identitySymbol) { let data; const oldKeys = new Set(prevData.byPropertyKey.keys()); const oldTracking = new Set(prevData.byTrackingKey?.keys()); for (let i = 0; i < value.length; i++) { const childValue = value[i]; oldKeys.delete(i.toString()); if (isObject(childValue) && childValue.hasOwnProperty(identitySymbol)) { oldTracking.delete(childValue[identitySymbol]); } } if (oldKeys.size > 0) { data ??= { ...prevData }; for (const key of oldKeys) { data.byPropertyKey.delete(key); } } if (oldTracking.size > 0) { data ??= { ...prevData }; for (const id of oldTracking) { data.byTrackingKey?.delete(id); } } return data; } function maybeRemoveStaleObjectFields(prevData, value) { let data; for (const key of prevData.byPropertyKey.keys()) { if (!value.hasOwnProperty(key)) { data ??= { ...prevData }; data.byPropertyKey.delete(key); } } return data; } class FieldSubmitState { node; selfSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "selfSubmitting" }] : [])); submissionErrors; constructor(node) { this.node = node; this.submissionErrors = linkedSignal({ ...(ngDevMode ? { debugName: "submissionErrors" } : {}), source: this.node.structure.value, computation: () => [] }); } submitting = computed(() => { return this.selfSubmitting() || (this.node.structure.parent?.submitting() ?? false); }, ...(ngDevMode ? [{ debugName: "submitting" }] : [])); } class FieldNode { structure; validationState; metadataState; nodeState; submitState; fieldAdapter; controlValue; _context = undefined; get context() { return this._context ??= new FieldNodeContext(this); } fieldProxy = new Proxy(() => this, FIELD_PROXY_HANDLER); pathNode; constructor(options) { this.pathNode = options.pathNode; this.fieldAdapter = options.fieldAdapter; this.structure = this.fieldAdapter.createStructure(this, options); this.validationState = this.fieldAdapter.createValidationState(this, options); this.nodeState = this.fieldAdapter.createNodeState(this, options); this.metadataState = new FieldMetadataState(this); this.submitState = new FieldSubmitState(this); this.controlValue = this.controlValueSignal(); this.metadataState.runMetadataCreateLifecycle(); } focusBoundControl(options) { this.getBindingForFocus()?.focus(options); } getBindingForFocus() { const own = this.formFieldBindings().filter(b => b.focus !== undefined).reduce(firstInDom, undefined); if (own) return own; return this.structure.children().map(child => child.getBindingForFocus()).reduce(firstInDom, undefined); } pendingSync = linkedSignal({ ...(ngDevMode ? { debugName: "pendingSync" } : {}), source: () => this.value(), computation: (_source, previous) => { previous?.value?.abort(); return undefined; } }); get fieldTree() { return this.fieldProxy; } get logicNode() { return this.structure.logic; } get value() { return this.structure.value; } get keyInParent() { return this.structure.keyInParent; } get errors() { return this.validationState.errors; } get parseErrors() { return this.validationState.parseErrors; } get errorSummary() { return this.validationState.errorSummary; } get pending() { return this.validationState.pending; } get valid() { return this.validationState.valid; } get invalid() { return this.validationState.invalid; } get dirty() { return this.nodeState.dirty; } get touched() { return this.nodeState.touched; } get disabled() { return this.nodeState.disabled; } get disabledReasons() { return this.nodeState.disabledReasons; } get hidden() { return this.nodeState.hidden; } get readonly() { return this.nodeState.readonly; } get formFieldBindings() { return this.nodeState.formFieldBindings; } get submitting() { return this.submitState.submitting; } get name() { return this.nodeState.name; } get max() { const maxKey = this.metadata(MAX)?.(); return maxKey ? this.metadata(maxKey) : undefined; } get maxLength() { return this.metadata(MAX_LENGTH); } get min() { const minKey = this.metadata(MIN)?.(); return minKey ? this.metadata(minKey) : undefined; } get minLength() { return this.metadata(MIN_LENGTH); } get pattern() { return this.metadata(PATTERN) ?? EMPTY; } get required() { return this.metadata(REQUIRED) ?? FALSE; } metadata(key) { return this.metadataState.get(key); } getError(kind) { return this.errors().find(e => e.kind === kind); } hasMetadata(key) { return this.metadataState.has(key); } markAsTouched(options) { if (this.structure.isOrphaned()) { return; } untracked(() => { this.markAsTouchedInternal(options); this.flushSync(); }); } markAsTouchedInternal(options) { if (this.structure.isOrphaned()) { return; } if (this.validationState.shouldSkipValidation()) { return; } this.nodeState.markAsTouched(); if (options?.skipDescendants) { return; } for (const child of this.structure.children()) { child.markAsTouchedInternal(); } } markAsDirty() { this.nodeState.markAsDirty(); } markAsPristine() { this.nodeState.markAsPristine(); } markAsUntouched() { this.nodeState.markAsUntouched(); } reset(value) { untracked(() => this._reset(value)); } _reset(value) { this.pendingSync()?.abort(); if (value !== undefined) { this.value.set(value); } this.controlValue.rawSet(this.value()); this.nodeState.markAsUntouched(); this.nodeState.markAsPristine(); for (const binding of this.formFieldBindings()) { binding.reset(); } for (const child of this.structure.materializedChildren()) { child._reset(); } } reloadValidation() { untracked(() => this._reloadValidation()); } _reloadValidation() { const keys = this.logicNode.logic.getMetadataKeys(); for (const key of keys) { if (key[IS_ASYNC_VALIDATION_RESOURCE]) { const resource = this.metadata(key); resource.reload?.(); } } for (const child of this.structure.children()) { child._reloadValidation(); } } controlValueSignal() { const controlValue = linkedSignal(this.value); controlValue.rawSet = controlValue.set; controlValue.set = newValue => { controlValue.rawSet(newValue); this.markAsDirty(); this.debounceSync(); }; const rawUpdate = controlValue.update; controlValue.update = updateFn => { rawUpdate(updateFn); this.markAsDirty(); this.debounceSync(); }; return controlValue; } sync() { this.value.set(this.controlValue()); } flushSync() { const pending = this.pendingSync(); if (pending && !pending.signal.aborted) { pending.abort(); this.sync(); } } async debounceSync() { const debouncer = untracked(() => { this.pendingSync()?.abort(); return this.nodeState.debouncer(); }); if (debouncer) { const controller = new AbortController(); const promise = debouncer(controller.signal); if (promise) { this.pendingSync.set(controller); await promise; if (controller.signal.aborted) { return; } } } if (this.structure.isOrphaned()) { return; } this.sync(); } static newRoot(fieldManager, value, pathNode, adapter) { return adapter.newRoot(fieldManager, value, pathNode, adapter); } createStructure(options) { return options.kind === 'root' ? new RootFieldNodeStructure(this, options.logic, options.fieldManager, options.value, this.newChild.bind(this)) : new ChildFieldNodeStructure(this, options.logic, options.parent, options.identityInParent, options.initialKeyInParent, this.newChild.bind(this)); } newChild(key, trackingId, isArray) { let childPath; let childLogic; if (isArray) { childPath = this.pathNode.getChild(DYNAMIC); childLogic = this.structure.logic.getChild(DYNAMIC); } else { childPath = this.pathNode.getChild(key); childLogic = this.structure.logic.getChild(key); } return this.fieldAdapter.newChild({ kind: 'child', parent: this, pathNode: childPath, logic: childLogic, initialKeyInParent: key, identityInParent: trackingId, fieldAdapter: this.fieldAdapter }); } } const EMPTY = computed(() => [], ...(ngDevMode ? [{ debugName: "EMPTY" }] : [])); const FALSE = computed(() => false, ...(ngDevMode ? [{ debugName: "FALSE" }] : [])); function firstInDom(a, b) { if (!a) return b; if (!b) return a; const position = a.element.compareDocumentPosition(b.element); return position & Node.DOCUMENT_POSITION_PRECEDING ? b : a; } class FieldNodeState { node; selfTouched = signal(false, ...(ngDevMode ? [{ debugName: "selfTouched" }] : [])); selfDirty = signal(false, ...(ngDevMode ? [{ debugName: "selfDirty" }] : [])); markAsTouched() { this.selfTouched.set(true); } markAsDirty() { this.selfDirty.set(true); } markAsPristine() { this.selfDirty.set(false); } markAsUntouched() { this.selfTouched.set(false); } formFieldBindings = signal([], ...(ngDevMode ? [{ debugName: "formFieldBindings" }] : [])); constructor(node) { this.node = node; } dirty = computed(() => { const selfDirtyValue = this.selfDirty() && !this.isNonInteractive(); return this.node.structure.reduceChildren(selfDirtyValue, (child, value) => value || child.nodeState.dirty(), shortCircuitTrue); }, ...(ngDevMode ? [{ debugName: "dirty" }] : [])); touched = computed(() => { const selfTouchedValue = this.selfTouched() && !this.isNonInteractive(); return this.node.structure.reduceChildren(selfTouchedValue, (child, value) => value || child.nodeState.touched(), shortCircuitTrue); }, ...(ngDevMode ? [{ debugName: "touched" }] : [])); disabledReasons = computed(() => [...(this.node.structure.parent?.nodeState.disabledReasons() ?? []), ...this.node.logicNode.logic.disabledReasons.compute(this.node.context)], { ...(ngDevMode ? { debugName: "disabledReasons" } : {}), equal: shallowArrayEquals }); disabled = computed(() => !!this.disabledReasons().length, ...(ngDevMode ? [{ debugName: "disabled" }] : [])); readonly = computed(() => (this.node.structure.parent?.nodeState.readonly() || this.node.logicNode.logic.readonly.compute(this.node.context)) ?? false, ...(ngDevMode ? [{ debugName: "readonly" }] : [])); hidden = computed(() => (this.node.structure.parent?.nodeState.hidden() || this.node.logicNode.logic.hidden.compute(this.node.context)) ?? false, ...(ngDevMode ? [{ debugName: "hidden" }] : [])); name = computed(() => { const parent = this.node.structure.parent; if (!parent) { return this.node.structure.fieldManager.rootName; } return `${parent.name()}.${this.node.structure.keyInParent()}`; }, ...(ngDevMode ? [{ debugName: "name" }] : [])); debouncer = computed(() => { if (this.node.logicNode.logic.hasMetadata(DEBOUNCER)) { const debouncerLogic = this.node.logicNode.logic.getMetadata(DEBOUNCER); const debouncer = debouncerLogic.compute(this.node.context); if (debouncer) { return signal => debouncer(this.node.context, signal); } } return this.node.structure.parent?.nodeState.debouncer?.(); }, ...(ngDevMode ? [{ debugName: "debouncer" }] : [])); isNonInteractive = computed(() => this.hidden() || this.disabled() || this.readonly(), ...(ngDevMode ? [{ debugName: "isNonInteractive" }] : [])); } class BasicFieldAdapter { newRoot(fieldManager, value, pathNode, adapter) { return new FieldNode({ kind: 'root', fieldManager, value, pathNode, logic: pathNode.builder.build(), fieldAdapter: adapter }); } newChild(options) { return new FieldNode(options); } createNodeState(node) { return new FieldNodeState(node); } createValidationState(node) { return new FieldValidationState(node); } createStructure(node, options) { return node.createStructure(options); } } class FormFieldManager { injector; rootName; submitOptions; constructor(injector, rootName, submitOptions) { this.injector = injector; this.rootName = rootName ?? `${this.injector.get(APP_ID)}.form${nextFormId++}`; this.submitOptions = submitOptions; } structures = new Set(); createFieldManagementEffect(root) { effect(() => { const liveStructures = new Set(); this.markStructuresLive(root, liveStructures); for (const structure of this.structures) { if (!liveStructures.has(structure)) { this.structures.delete(structure); untracked(() => structure.destroy()); } } }, { injector: this.injector }); } markStructuresLive(structure, liveStructures) { liveStructures.add(structu