/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/core/src/render3/instructions/control.ts
393 строки
13 KB
Alex Rickabaugh
fix(core): ensure custom controls resolve transitive host directives
25 мар 2026, 23:00
25 мар 2026, 23:00
16adbbf
Код
Авторство
О чём код?
/** * @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 {performanceMarkFeature} from '../../util/performance'; import {assertFirstCreatePass} from '../assert'; import type {ControlDirectiveHost} from '../interfaces/control'; import type {DirectiveDef} from '../interfaces/definition'; import {type TNode, TNodeFlags} from '../interfaces/node'; import {isComponentHost} from '../interfaces/type_checks'; import {type LView, RENDERER, type TView} from '../interfaces/view'; import { getCurrentTNode, getLView, getSelectedTNode, getTView, isInCheckNoChangesMode, } from '../state'; import {getNativeByTNode} from '../util/view_utils'; import {debugStringifyTypeForError} from '../util/stringify_utils'; import {listenToDirectiveOutput} from '../view/directive_outputs'; import {listenToDomEvent, wrapListener} from '../view/listeners'; import {setDirectiveInput} from './shared'; import {writeToDirectiveInput} from './write_to_directive_input'; /** * Possibly sets up a {@link ɵFormFieldDirective} to manage a native or custom form control. * * Setup occurs if a `field` input is bound to a {@link ɵFormFieldDirective} on the current node, * but not to a component. If a `field` input is bound to a component, we assume the component * will manage the field in its own template. * * @codeGenApi */ export function ɵɵcontrolCreate(): void { controlCreateInternal(); } export function controlCreateInternal(): void { const lView = getLView(); const tView = getTView(); const tNode = getCurrentTNode()!; if (tView.firstCreatePass) { initializeControlFirstCreatePass(tView, tNode, lView); } if (tNode.controlDirectiveIndex === -1) { return; } performanceMarkFeature('NgSignalForms'); const instance = lView[tNode.controlDirectiveIndex]; const controlDef = (tView.data[tNode.controlDirectiveIndex] as DirectiveDef<unknown>).controlDef!; controlDef.create(instance, new ControlDirectiveHostImpl(lView, tView, tNode)); } /** * Calls the update function of the control directive at the given index. * * @codeGenApi */ export function ɵɵcontrol(): void { controlUpdateInternal(); } export function controlUpdateInternal(): void { if (ngDevMode && isInCheckNoChangesMode()) { return; } const lView = getLView(); const tView = getTView(); const tNode = getSelectedTNode(); if (tNode.controlDirectiveIndex === -1) { return; } const controlDef = (tView.data[tNode.controlDirectiveIndex] as DirectiveDef<unknown>).controlDef!; const instance = lView[tNode.controlDirectiveIndex]; controlDef.update(instance, new ControlDirectiveHostImpl(lView, tView, tNode)); } class ControlDirectiveHostImpl implements ControlDirectiveHost { private readonly lView: LView; private readonly tView: TView; private readonly tNode: TNode; readonly hasPassThrough: boolean; constructor(lView: LView, tView: TView, tNode: TNode) { this.lView = lView; this.tView = tView; this.tNode = tNode; this.hasPassThrough = !!(tNode.flags & TNodeFlags.isPassThroughControl); } get customControl(): unknown { return this.tNode.customControlIndex !== -1 ? this.lView[this.tNode.customControlIndex] : undefined; } get nativeElement(): HTMLElement { return getNativeByTNode(this.tNode, this.lView) as HTMLElement; } get descriptor(): string { if (ngDevMode && isComponentHost(this.tNode)) { const componentIndex = this.tNode.directiveStart + this.tNode.componentOffset; const componentDef = this.tView.data[componentIndex] as DirectiveDef<unknown>; return `Component ${debugStringifyTypeForError(componentDef.type)}`; } return `<${this.tNode.value}>`; } listenToCustomControlOutput(outputName: string, callback: (event: Event) => void): void { const directiveDef = this.tView.data[this.tNode.customControlIndex] as DirectiveDef<unknown>; listenToDirectiveOutput( this.tNode, this.lView, directiveDef, outputName, wrapListener(this.tNode, this.lView, callback), ); } listenToCustomControlModel(listener: (value: unknown) => void): void { const modelName = this.tNode.flags & TNodeFlags.isFormValueControl ? 'valueChange' : 'checkedChange'; const directiveDef = this.tView.data[this.tNode.customControlIndex] as DirectiveDef<unknown>; listenToDirectiveOutput( this.tNode, this.lView, directiveDef, modelName, wrapListener(this.tNode, this.lView, listener), ); } listenToDom(eventName: string, listener: (event: Event) => void): void { listenToDomEvent( this.tNode, this.tView, this.lView as LView<{} | null>, undefined, this.lView[RENDERER], eventName, listener, wrapListener(this.tNode, this.lView, listener), ); } setInputOnDirectives(inputName: string, value: unknown): boolean { const directiveIndices = this.tNode.inputs?.[inputName]; const hostDirectiveInputs = this.tNode.hostDirectiveInputs?.[inputName]; if (!directiveIndices && !hostDirectiveInputs) { return false; } let wasSet = false; if (directiveIndices) { for (const index of directiveIndices) { // Skip the control directive itself to avoid triggering its property setters if (index === this.tNode.controlDirectiveIndex) { continue; } const directiveDef = this.tView.data[index] as DirectiveDef<unknown>; const directive = this.lView[index]; writeToDirectiveInput(directiveDef, directive, inputName, value); wasSet = true; } } if (hostDirectiveInputs) { for (let i = 0; i < hostDirectiveInputs.length; i += 2) { const index = hostDirectiveInputs[i] as number; // Skip the control directive itself to avoid triggering its property setters if (index === this.tNode.controlDirectiveIndex) { continue; } const internalName = hostDirectiveInputs[i + 1] as string; const directiveDef = this.tView.data[index] as DirectiveDef<unknown>; const directive = this.lView[index]; writeToDirectiveInput(directiveDef, directive, internalName, value); wasSet = true; } } return wasSet; } setCustomControlModelInput(value: unknown): void { const directiveDef = this.tView.data[this.tNode.customControlIndex] as DirectiveDef<{}>; const modelName = this.tNode.flags & TNodeFlags.isFormValueControl ? 'value' : 'checked'; setDirectiveInput(this.tNode, this.tView, this.lView, directiveDef, modelName, value); } customControlHasInput(inputName: string): boolean { if (this.tNode.customControlIndex === -1) { return false; } const directiveDef = this.tView.data[this.tNode.customControlIndex] as DirectiveDef<unknown>; const presence = (directiveDef.signalFormsInputPresence ??= this._buildCustomControlInputCache(directiveDef)); return presence[inputName] === true; } private _buildCustomControlInputCache(directiveDef: DirectiveDef<unknown>): { [key: string]: boolean; } { const cache: {[key: string]: boolean} = {}; // First, add all inputs defined directly on the custom control directive. for (const key in directiveDef.inputs) { cache[key] = true; } // Next, gather inputs exposed by host directives recursively. if (directiveDef.hostDirectives !== null) { const queue = [...directiveDef.hostDirectives]; while (queue.length > 0) { const hostDir = queue.shift()!; if (typeof hostDir !== 'function') { // HostDirectiveDef object for (const key in hostDir.inputs) { cache[hostDir.inputs[key]] = true; } const hostDirectives = getHostDirectives(hostDir.directive); if (hostDirectives !== null) { queue.push(...hostDirectives); } continue; } // Factory function returning HostDirectiveConfig[] for (const config of hostDir()) { if (typeof config === 'function') { continue; } if (config.inputs) { for (let i = 0; i < config.inputs.length; i += 2) { const exposedName = config.inputs[i + 1] || config.inputs[i]; cache[exposedName] = true; } } const hostDirectives = getHostDirectives(config.directive); if (hostDirectives !== null) { queue.push(...hostDirectives); } } } } return cache; } } function getHostDirectives(directiveType: any): readonly any[] | null { if (typeof directiveType === 'function' && 'ɵdir' in directiveType) { return (directiveType as any).ɵdir.hostDirectives ?? null; } return null; } function initializeControlFirstCreatePass(tView: TView, tNode: TNode, lView: LView): void { ngDevMode && assertFirstCreatePass(tView); for (let i = tNode.directiveStart; i < tNode.directiveEnd; i++) { const directiveDef = tView.data[i] as DirectiveDef<unknown>; if (directiveDef.controlDef) { tNode.controlDirectiveIndex = i; break; } } if (tNode.controlDirectiveIndex === -1) { // No control directive found on this element. return; } const controlDef = (tView.data[tNode.controlDirectiveIndex] as DirectiveDef<unknown>).controlDef!; if (controlDef.passThroughInput) { // This control directive has a `passThroughInput` property binding. if ((tNode.inputs?.[controlDef.passThroughInput]?.length ?? 0) > 1) { // Another directive has bound `controlDef.passThroughInput`. tNode.flags |= TNodeFlags.isPassThroughControl; return; } } initializeCustomControlStatus(tView, tNode); } /** * Determines whether a custom form control (with a `value` or `checked` model input) is present * on the current `TNode` during the first creation pass. * * If a custom control is found, the function sets the appropriate `TNodeFlags` and stores its * index. * * @param tView The `TView` of the current view. * @param tNode The `TNode` to inspect for a custom control. * @returns `true` if a custom control is found, `false` otherwise. */ function initializeCustomControlStatus(tView: TView, tNode: TNode): void { for (let i = tNode.directiveStart; i < tNode.directiveEnd; i++) { const directiveDef = tView.data[i] as DirectiveDef<unknown>; // Host directives shouldn't be matched directly since their types are not in // `directiveToIndex`. We match them through their host component's `hostDirectiveInputs` instead. if (tNode.directiveToIndex && !tNode.directiveToIndex.has(directiveDef.type)) { continue; } if (hasModelInput(directiveDef, 'value')) { tNode.flags |= TNodeFlags.isFormValueControl; tNode.customControlIndex = i; return; } if (hasModelInput(directiveDef, 'checked')) { tNode.flags |= TNodeFlags.isFormCheckboxControl; tNode.customControlIndex = i; return; } } if ( tNode.hostDirectiveInputs !== null && tNode.hostDirectiveOutputs !== null && tNode.directiveToIndex !== null ) { const checkModel = (modelName: string, flag: TNodeFlags) => { const inputs = tNode.hostDirectiveInputs![modelName]; const outputs = tNode.hostDirectiveOutputs![modelName + 'Change']; if (!inputs || !outputs) { return false; } for (let i = 0; i < inputs.length; i += 2) { const inputIndex = inputs[i] as number; for (let j = 0; j < outputs.length; j += 2) { const outputIndex = outputs[j] as number; // TODO: invert control flow logic here. if (inputIndex !== outputIndex) { continue; } for (const data of tNode.directiveToIndex!.values()) { if (!Array.isArray(data)) { continue; } const [hostIndex, start, end] = data; if (inputIndex >= start && inputIndex <= end) { tNode.flags |= flag; tNode.customControlIndex = hostIndex; return true; } } } } return false; }; if (checkModel('value', TNodeFlags.isFormValueControl)) { return; } if (checkModel('checked', TNodeFlags.isFormCheckboxControl)) { return; } } } /** * Returns whether the specified `directiveDef` has a model-like input named `name`. * * A model-like input is an input-output pair where the input is named `name` and the output is * named `name + 'Change'`. */ function hasModelInput(directiveDef: DirectiveDef<unknown>, name: string): boolean { return hasInput(directiveDef, name) && hasOutput(directiveDef, name + 'Change'); } /** Returns whether the specified `directiveDef` has an input named `name`.*/ function hasInput(directiveDef: DirectiveDef<unknown>, name: string): boolean { return name in directiveDef.inputs; } /** Returns whether the specified `directiveDef` has an output named `name`. */ function hasOutput(directiveDef: DirectiveDef<unknown>, name: string): boolean { return name in directiveDef.outputs; }