diff --git a/packages/forms/signals/src/controls/control.ts b/packages/forms/signals/src/controls/control.ts index 58d61f74dfbc..16d0cbfdbbe9 100644 --- a/packages/forms/signals/src/controls/control.ts +++ b/packages/forms/signals/src/controls/control.ts @@ -22,6 +22,7 @@ import { OutputRef, OutputRefSubscription, reflectComponentType, + Renderer2, signal, Type, untracked, @@ -37,7 +38,7 @@ import { PATTERN, REQUIRED, } from '../api/property'; -import {Field} from '../api/types'; +import type {Field} from '../api/types'; import type {FieldNode} from '../field/node'; import { illegallyGetComponentInstance, @@ -75,6 +76,7 @@ import {InteropNgControl} from './interop_ng_control'; export class Control { /** The injector for this component. */ private readonly injector = inject(Injector); + private readonly renderer = inject(Renderer2); /** Whether state synchronization with the field has been setup yet. */ private initialized = false; @@ -202,16 +204,25 @@ export class Control { }); input.addEventListener('blur', () => this.state().markAsTouched()); - this.maybeSynchronize(() => this.state().readonly(), withBooleanAttribute(input, 'readonly')); + this.maybeSynchronize( + () => this.state().readonly(), + this.withBooleanAttribute(input, 'readonly'), + ); // TODO: consider making a global configuration option for using aria-disabled instead. - this.maybeSynchronize(() => this.state().disabled(), withBooleanAttribute(input, 'disabled')); - this.maybeSynchronize(() => this.state().name(), withAttribute(input, 'name')); + this.maybeSynchronize( + () => this.state().disabled(), + this.withBooleanAttribute(input, 'disabled'), + ); + this.maybeSynchronize(() => this.state().name(), this.withAttribute(input, 'name')); - this.maybeSynchronize(this.propertySource(REQUIRED), withBooleanAttribute(input, 'required')); - this.maybeSynchronize(this.propertySource(MIN), withAttribute(input, 'min')); - this.maybeSynchronize(this.propertySource(MIN_LENGTH), withAttribute(input, 'minLength')); - this.maybeSynchronize(this.propertySource(MAX), withAttribute(input, 'max')); - this.maybeSynchronize(this.propertySource(MAX_LENGTH), withAttribute(input, 'maxLength')); + this.maybeSynchronize( + this.propertySource(REQUIRED), + this.withBooleanAttribute(input, 'required'), + ); + this.maybeSynchronize(this.propertySource(MIN), this.withAttribute(input, 'min')); + this.maybeSynchronize(this.propertySource(MIN_LENGTH), this.withAttribute(input, 'minLength')); + this.maybeSynchronize(this.propertySource(MAX), this.withAttribute(input, 'max')); + this.maybeSynchronize(this.propertySource(MAX_LENGTH), this.withAttribute(input, 'maxLength')); switch (inputType) { case 'checkbox': @@ -360,6 +371,31 @@ export class Control { ); return () => metaSource()?.(); } + + /** Creates a (non-boolean) value sync that writes the given attribute of the given element. */ + private withAttribute( + element: HTMLElement, + attribute: string, + ): (value: {toString(): string} | undefined) => void { + return (value) => { + if (value !== undefined) { + this.renderer.setAttribute(element, attribute, value.toString()); + } else { + this.renderer.removeAttribute(element, attribute); + } + }; + } + + /** Creates a boolean value sync that writes the given attribute of the given element. */ + private withBooleanAttribute(element: HTMLElement, attribute: string): (value: boolean) => void { + return (value) => { + if (value) { + this.renderer.setAttribute(element, attribute, ''); + } else { + this.renderer.removeAttribute(element, attribute); + } + }; + } } /** Creates a value sync from an input signal. */ @@ -367,31 +403,6 @@ function withInput(input: InputSignal | undefined): ((value: T) => void) | return input ? (value: T) => illegallySetInputSignal(input, value) : undefined; } -/** Creates a boolean value sync that writes the given attribute of the given element. */ -function withBooleanAttribute(element: HTMLElement, attribute: string): (value: boolean) => void { - return (value) => { - if (value) { - element.setAttribute(attribute, ''); - } else { - element.removeAttribute(attribute); - } - }; -} - -/** Creates a (non-boolean) value sync that writes the given attribute of the given element. */ -function withAttribute( - element: HTMLElement, - attribute: string, -): (value: {toString(): string} | undefined) => void { - return (value) => { - if (value !== undefined) { - element.setAttribute(attribute, value.toString()); - } else { - element.removeAttribute(attribute); - } - }; -} - /** * Checks whether the given component matches the contract for either FormValueControl or * FormCheckboxControl. diff --git a/packages/forms/signals/src/field/context.ts b/packages/forms/signals/src/field/context.ts index f14334bc15f6..bb28793ee06b 100644 --- a/packages/forms/signals/src/field/context.ts +++ b/packages/forms/signals/src/field/context.ts @@ -11,7 +11,7 @@ import {Field, FieldContext, FieldPath, FieldState} from '../api/types'; import {FieldPathNode} from '../schema/path_node'; import {isArray} from '../util/type_guards'; import type {FieldNode} from './node'; -import {boundPathDepth} from './resolution'; +import {getBoundPathDepth} from './resolution'; /** * `FieldContext` implementation, backed by a `FieldNode`. @@ -49,7 +49,7 @@ export class FieldNodeContext implements FieldContext { // This ensures that we do not accidentally match on the wrong application of a recursively // applied schema. let field: FieldNode | undefined = this.node; - let stepsRemaining = boundPathDepth; + let stepsRemaining = getBoundPathDepth(); while (stepsRemaining > 0 || !field.structure.logic.hasLogic(targetPathNode.root.logic)) { stepsRemaining--; field = field.structure.parent; diff --git a/packages/forms/signals/src/field/resolution.ts b/packages/forms/signals/src/field/resolution.ts index 76fc3fdce259..efec7ea0ed40 100644 --- a/packages/forms/signals/src/field/resolution.ts +++ b/packages/forms/signals/src/field/resolution.ts @@ -6,11 +6,15 @@ * found in the LICENSE file at https://angular.dev/license */ +let boundPathDepth = 0; + /** * The depth of the current path when evaluating a logic function. * Do not set this directly, it is a context variable managed by `setBoundPathDepthForResolution`. */ -export let boundPathDepth = 0; +export function getBoundPathDepth() { + return boundPathDepth; +} /** * Sets the bound path depth for the duration of the given logic function.