mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(forms): move control logic into FormField directive
Refactors the `ɵɵcontrolCreate` and `ɵɵcontrol` instructions to delegate control logic to the forms package via new `ɵngControlCreate` and `ɵngControlUpdate` lifecycle hooks. Previously, the logic for binding form state to native elements and custom controls was hardcoded within `@angular/core`.
**Compiler Changes:**
- Introduces a new compilation phase `specializeControlProperties` (in `control_directives.ts`).
- This phase detects properties named `formField` and specializes them into `ControlCreate` and `Control` IR opcodes.
- These opcodes emit `ɵɵcontrolCreate` and `ɵɵcontrol` instructions, respectively.
**Runtime Changes:**
- `ɵɵcontrolCreate` acts as the creation phase. It locates the control directive and invokes its `ɵngControlCreate` method.
- `ɵɵcontrol` acts as the update phase, and invokes the control directive's `ɵngControlUpdate` method (if present).
- Introduces a `passThroughInput` configuration in `ControlFeature`. This specifies the input name (e.g., `formField`) that triggers the control. If the runtime detects that this input is bound to multiple targets (e.g., the `FormField` directive *and* the host component), the control is flagged as "pass-through". In this state, `ɵngControlCreate` returns a no-op update function, deferring responsibility to the other consumer (e.g., the component managing the field itself).
**Forms Changes:**
- `FormField` directive implements `ɵngControlCreate` and `ɵngControlUpdate`.
- Inside this hook, `FormField` determines the type of control it is attached to (Native, CVA, or Custom Signal Control) and delegates to the appropriate handler (`nativeControlCreate`, `cvaControlCreate`, or `customControlCreate`).
- Consolidates all form binding logic within `@angular/forms/signals`, enabling support for new `FormValueControl` and `FormCheckboxControl` interfaces.
- Reorganizes the codebase by moving `FormField` from `api/` to `directive/` and splitting the binding logic into semantic pieces:
- `control_native.ts`, `control_cva.ts`, and `control_custom.ts` contain the specific handlers for each control type.
- `native.ts` and `select.ts` provide helpers for native element discovery and select-specific synchronization.
- `bindings.ts` manages the tracking and application of property/attribute bindings.
This commit is contained in:
committed by
Leon Senft
parent
88e6ebec01
commit
a67e00741c
@@ -318,6 +318,8 @@ export function extractDirectiveMetadata(
|
||||
!member.isStatic && member.kind === ClassMemberKind.Method && member.name === 'ngOnChanges',
|
||||
);
|
||||
|
||||
const controlCreate = extractControlDirectiveDefinition(members);
|
||||
|
||||
// Parse exportAs.
|
||||
let exportAs: string[] | null = null;
|
||||
if (directive.has('exportAs')) {
|
||||
@@ -442,6 +444,7 @@ export function extractDirectiveMetadata(
|
||||
typeArgumentCount: reflector.getGenericArityOfClass(clazz) || 0,
|
||||
typeSourceSpan: createSourceSpan(clazz.name),
|
||||
usesInheritance,
|
||||
controlCreate,
|
||||
exportAs,
|
||||
providers,
|
||||
isStandalone,
|
||||
@@ -1576,6 +1579,64 @@ function assertEmittableInputType(
|
||||
})(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the `controlCreate` definition for the private control directive contract from the
|
||||
* directive class.
|
||||
*
|
||||
* This looks for a lifecycle method called `ɵngControlCreate`. If present, a control directive
|
||||
* definition will be extracted, and `ɵɵControlFeature` will be applied to the directive.
|
||||
*
|
||||
* A control directive may declare a pass-through input name, by including a generic type on the
|
||||
* type of the `ControlDirectiveHost` parameter of `ɵngControlCreate`:
|
||||
*
|
||||
* ```ts
|
||||
* class MyControlDirective {
|
||||
* ɵngControlCreate<T>(host: ControlDirectiveHost<'formField'>): void {}
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If present, this will be extracted as the `passThroughInput` property of the control directive
|
||||
* definition.
|
||||
*/
|
||||
function extractControlDirectiveDefinition(
|
||||
members: ClassMember[],
|
||||
): R3DirectiveMetadata['controlCreate'] {
|
||||
const controlCreateMember = members.find(
|
||||
(member) =>
|
||||
!member.isStatic &&
|
||||
member.kind === ClassMemberKind.Method &&
|
||||
member.name === 'ɵngControlCreate',
|
||||
);
|
||||
|
||||
if (
|
||||
controlCreateMember === undefined ||
|
||||
controlCreateMember.node === null ||
|
||||
!ts.isMethodDeclaration(controlCreateMember.node)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {node} = controlCreateMember;
|
||||
if (
|
||||
node.parameters.length === 0 ||
|
||||
node.parameters[0].type === undefined ||
|
||||
!ts.isTypeReferenceNode(node.parameters[0].type)
|
||||
) {
|
||||
return {passThroughInput: null};
|
||||
}
|
||||
|
||||
const type = node.parameters[0].type;
|
||||
if (
|
||||
type.typeArguments?.length !== 1 ||
|
||||
!ts.isLiteralTypeNode(type.typeArguments[0]) ||
|
||||
!ts.isStringLiteral(type.typeArguments[0].literal)
|
||||
) {
|
||||
return {passThroughInput: null};
|
||||
}
|
||||
|
||||
return {passThroughInput: type.typeArguments[0].literal.text};
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through all specified class members and attempts to detect
|
||||
* view and content queries defined.
|
||||
|
||||
@@ -299,7 +299,7 @@ export function isFieldDirective(meta: TypeCheckableDirectiveMeta): boolean {
|
||||
ts.isPropertyDeclaration(member) &&
|
||||
ts.isComputedPropertyName(member.name) &&
|
||||
ts.isIdentifier(member.name.expression) &&
|
||||
member.name.expression.text === 'ɵCONTROL',
|
||||
member.name.expression.text === 'ɵNgFieldDirective',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user