mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(core): add specialized instruction for [control] bindings (#63773)
* Emit a `ɵɵcontrol` instruction in place of `ɵɵproperty` for property bindings named "control". This instruction cannot be chained, but is otherwise functionally equivalent. * Upcoming changes will use the `ɵɵcontrol` instruction to bind a signal form field to a UI control (be it a native element or custom directive). PR Close #63773
This commit is contained in:
committed by
Miles Malerba
parent
548ea027f6
commit
42e1062bc5
+50
@@ -0,0 +1,50 @@
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: control_bindings.js
|
||||
****************************************************************************************************/
|
||||
import { Component, Directive, input } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class Control {
|
||||
constructor() {
|
||||
this.control = input(...(ngDevMode ? [undefined, { debugName: "control" }] : []));
|
||||
}
|
||||
}
|
||||
Control.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
||||
Control.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: Control, isStandalone: true, selector: "[control]", inputs: { control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Control, decorators: [{
|
||||
type: Directive,
|
||||
args: [{ selector: '[control]' }]
|
||||
}] });
|
||||
export class MyComponent {
|
||||
constructor() {
|
||||
this.value = 'Hello, world!';
|
||||
}
|
||||
}
|
||||
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
|
||||
<input [control]="value">
|
||||
`, isInline: true, dependencies: [{ kind: "directive", type: Control, selector: "[control]", inputs: ["control"] }] });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
template: `
|
||||
<input [control]="value">
|
||||
`,
|
||||
imports: [Control],
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: control_bindings.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class Control {
|
||||
readonly control: import("@angular/core").InputSignal<string | undefined>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<Control, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<Control, "[control]", never, { "control": { "alias": "control"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class MyComponent {
|
||||
value: string;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "ng-component", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "../../test_case_schema.json",
|
||||
"cases": [
|
||||
{
|
||||
"description": "should generate control instruction for `control` property bindings",
|
||||
"inputFiles": [
|
||||
"control_bindings.ts"
|
||||
],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [
|
||||
"control_bindings.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
MyComponent.ɵcmp = /* @__PURE__ */i0.ɵɵdefineComponent({
|
||||
type: MyComponent,
|
||||
selectors: [["ng-component"]],
|
||||
decls: 1,
|
||||
vars: 1,
|
||||
consts: [[3, "control"]],
|
||||
template: function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelement(0, "input", 0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
i0.ɵɵcontrol(ctx.value);
|
||||
}
|
||||
},
|
||||
dependencies: [Control],
|
||||
encapsulation: 2
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import {Component, Directive, input} from '@angular/core';
|
||||
|
||||
@Directive({selector: '[control]'})
|
||||
export class Control {
|
||||
readonly control = input<string>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<input [control]="value">
|
||||
`,
|
||||
imports: [Control],
|
||||
})
|
||||
export class MyComponent {
|
||||
value = 'Hello, world!';
|
||||
}
|
||||
@@ -254,6 +254,7 @@ export class Identifiers {
|
||||
|
||||
static ariaProperty: o.ExternalReference = {name: 'ɵɵariaProperty', moduleName: CORE};
|
||||
static property: o.ExternalReference = {name: 'ɵɵproperty', moduleName: CORE};
|
||||
static control: o.ExternalReference = {name: 'ɵɵcontrol', moduleName: CORE};
|
||||
|
||||
static animationEnterListener: o.ExternalReference = {
|
||||
name: 'ɵɵanimateEnterListener',
|
||||
|
||||
@@ -248,7 +248,7 @@ export function compileComponentFromMetadata(
|
||||
getTemplateSourceLocationsEnabled(),
|
||||
);
|
||||
|
||||
// Then the IR is transformed to prepare it for cod egeneration.
|
||||
// Then the IR is transformed to prepare it for code generation.
|
||||
transform(tpl, CompilationJobKind.Tmpl);
|
||||
|
||||
// Finally we emit the template function:
|
||||
|
||||
@@ -299,6 +299,11 @@ export enum OpKind {
|
||||
* An operation to bind animation events to an element.
|
||||
*/
|
||||
AnimationListener,
|
||||
|
||||
/**
|
||||
* An operation to bind a signal form field to a control directive.
|
||||
*/
|
||||
Control,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1103,6 +1103,7 @@ export function transformExpressionsInOp(
|
||||
case OpKind.Property:
|
||||
case OpKind.DomProperty:
|
||||
case OpKind.Attribute:
|
||||
case OpKind.Control:
|
||||
if (op.expression instanceof Interpolation) {
|
||||
transformExpressionsInInterpolation(op.expression, transform, flags);
|
||||
} else {
|
||||
|
||||
@@ -56,7 +56,8 @@ export type UpdateOp =
|
||||
| RepeaterOp
|
||||
| DeferWhenOp
|
||||
| AnimationBindingOp
|
||||
| StoreLetOp;
|
||||
| StoreLetOp
|
||||
| ControlOp;
|
||||
|
||||
/**
|
||||
* A logical operation to perform string interpolation on a text node.
|
||||
@@ -1062,3 +1063,30 @@ export function createStoreLetOp(
|
||||
...NEW_OP,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A specialized {@link PropertyOp} that may bind a form field to a control.
|
||||
*/
|
||||
export interface ControlOp extends Omit<PropertyOp, 'kind' | 'name'> {
|
||||
kind: OpKind.Control;
|
||||
}
|
||||
|
||||
/** Creates a {@link ControlOp}. */
|
||||
export function createControlOp(op: BindingOp): ControlOp {
|
||||
return {
|
||||
kind: OpKind.Control,
|
||||
target: op.target,
|
||||
expression: op.expression,
|
||||
bindingKind: op.bindingKind,
|
||||
securityContext: op.securityContext,
|
||||
sanitizer: null,
|
||||
isStructuralTemplateAttribute: op.isStructuralTemplateAttribute,
|
||||
templateKind: op.templateKind,
|
||||
i18nContext: op.i18nContext,
|
||||
i18nMessage: op.i18nMessage,
|
||||
sourceSpan: op.sourceSpan,
|
||||
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
||||
...TRAIT_CONSUMES_VARS,
|
||||
...NEW_OP,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -599,6 +599,23 @@ export function property(
|
||||
return propertyBase(Identifiers.property, name, expression, sanitizer, sourceSpan);
|
||||
}
|
||||
|
||||
export function control(
|
||||
expression: o.Expression | ir.Interpolation,
|
||||
sanitizer: o.Expression | null,
|
||||
sourceSpan: ParseSourceSpan,
|
||||
): ir.UpdateOp {
|
||||
const args = [];
|
||||
if (expression instanceof ir.Interpolation) {
|
||||
args.push(interpolationToExpression(expression, sourceSpan));
|
||||
} else {
|
||||
args.push(expression);
|
||||
}
|
||||
if (sanitizer !== null) {
|
||||
args.push(sanitizer);
|
||||
}
|
||||
return call(Identifiers.control, args, sourceSpan);
|
||||
}
|
||||
|
||||
export function twoWayProperty(
|
||||
name: string,
|
||||
expression: o.Expression,
|
||||
|
||||
@@ -55,6 +55,22 @@ export function extractAttributes(job: CompilationJob): void {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ir.OpKind.Control:
|
||||
ir.OpList.insertBefore<ir.CreateOp>(
|
||||
// Deliberately null i18nMessage value
|
||||
ir.createExtractedAttributeOp(
|
||||
op.target,
|
||||
ir.BindingKind.Property,
|
||||
null,
|
||||
'control',
|
||||
/* expression */ null,
|
||||
/* i18nContext */ null,
|
||||
/* i18nMessage */ null,
|
||||
op.securityContext,
|
||||
),
|
||||
lookupElement(elements, op.target),
|
||||
);
|
||||
break;
|
||||
case ir.OpKind.TwoWayProperty:
|
||||
ir.OpList.insertBefore<ir.CreateOp>(
|
||||
ir.createExtractedAttributeOp(
|
||||
|
||||
@@ -127,6 +127,8 @@ export function specializeBindings(job: CompilationJob): void {
|
||||
op.sourceSpan,
|
||||
),
|
||||
);
|
||||
} else if (op.name === 'control') {
|
||||
ir.OpList.replace<ir.UpdateOp>(op, ir.createControlOp(op));
|
||||
} else {
|
||||
ir.OpList.replace<ir.UpdateOp>(
|
||||
op,
|
||||
|
||||
@@ -600,6 +600,9 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
|
||||
: reifyProperty(op),
|
||||
);
|
||||
break;
|
||||
case ir.OpKind.Control:
|
||||
ir.OpList.replace(op, reifyControl(op));
|
||||
break;
|
||||
case ir.OpKind.TwoWayProperty:
|
||||
ir.OpList.replace(
|
||||
op,
|
||||
@@ -719,6 +722,10 @@ function reifyProperty(op: ir.PropertyOp): ir.UpdateOp {
|
||||
: ng.property(op.name, op.expression, op.sanitizer, op.sourceSpan);
|
||||
}
|
||||
|
||||
function reifyControl(op: ir.ControlOp): ir.UpdateOp {
|
||||
return ng.control(op.expression, op.sanitizer, op.sourceSpan);
|
||||
}
|
||||
|
||||
function reifyIrExpression(expr: o.Expression): o.Expression {
|
||||
if (!ir.isIrExpression(expr)) {
|
||||
return expr;
|
||||
|
||||
@@ -119,6 +119,7 @@ function varsUsedByOp(op: (ir.CreateOp | ir.UpdateOp) & ir.ConsumesVarsTrait): n
|
||||
return slots;
|
||||
case ir.OpKind.Property:
|
||||
case ir.OpKind.DomProperty:
|
||||
case ir.OpKind.Control:
|
||||
slots = 1;
|
||||
|
||||
// We need to assign a slot even for singleton interpolations, because the
|
||||
|
||||
@@ -93,6 +93,7 @@ export {
|
||||
ɵɵconditionalBranchCreate,
|
||||
ɵɵcontentQuery,
|
||||
ɵɵcontentQuerySignal,
|
||||
ɵɵcontrol,
|
||||
ɵɵcomponentInstance,
|
||||
ɵɵCopyDefinitionFeature,
|
||||
ɵɵdefineComponent,
|
||||
|
||||
@@ -95,6 +95,7 @@ export {
|
||||
ɵɵprojection,
|
||||
ɵɵprojectionDef,
|
||||
ɵɵproperty,
|
||||
ɵɵcontrol,
|
||||
ɵɵcontentQuery,
|
||||
ɵɵcontentQuerySignal,
|
||||
ɵɵloadQuery,
|
||||
|
||||
@@ -32,6 +32,7 @@ export * from './attribute';
|
||||
export * from './animation';
|
||||
export * from './change_detection';
|
||||
export * from './component_instance';
|
||||
export * from './control';
|
||||
export * from './control_flow';
|
||||
export * from './di';
|
||||
export * from './di_attr';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @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 {bindingUpdated} from '../bindings';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {RENDERER} from '../interfaces/view';
|
||||
import {getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
|
||||
|
||||
import {setPropertyAndInputs, storePropertyBindingMetadata} from './shared';
|
||||
|
||||
/**
|
||||
* Update a `control` property on a selected element.
|
||||
*
|
||||
* Operates on the element selected by index via the {@link select} instruction.
|
||||
*
|
||||
* If the property name also exists as an input property on one of the element's directives,
|
||||
* the component property will be set instead of the element property. This check must
|
||||
* be conducted at runtime so child components that add new `@Inputs` don't have to be re-compiled
|
||||
*
|
||||
* @param propName Name of property. Because it is going to DOM, this is not subject to
|
||||
* renaming as part of minification.
|
||||
* @param value New value to write.
|
||||
* @param sanitizer An optional function used to sanitize the value.
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵcontrol<T>(value: T, sanitizer?: SanitizerFn | null): void {
|
||||
const lView = getLView();
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (bindingUpdated(lView, bindingIndex, value)) {
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
setPropertyAndInputs(tNode, lView, 'control', value, lView[RENDERER], sanitizer);
|
||||
ngDevMode && storePropertyBindingMetadata(tView.data, tNode, 'control', bindingIndex);
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,7 @@ export const angularCoreEnv: {[name: string]: unknown} = (() => ({
|
||||
'ɵɵdomProperty': r3.ɵɵdomProperty,
|
||||
'ɵɵariaProperty': r3.ɵɵariaProperty,
|
||||
'ɵɵproperty': r3.ɵɵproperty,
|
||||
'ɵɵcontrol': r3.ɵɵcontrol,
|
||||
'ɵɵpipe': r3.ɵɵpipe,
|
||||
'ɵɵqueryRefresh': r3.ɵɵqueryRefresh,
|
||||
'ɵɵqueryAdvance': r3.ɵɵqueryAdvance,
|
||||
|
||||
@@ -610,6 +610,36 @@ describe('property bindings', () => {
|
||||
expect(idDir.idNumber).toBe('four');
|
||||
expect(otherDir.id).toBe(3);
|
||||
});
|
||||
|
||||
it('should support input bindings named "control"', () => {
|
||||
// Angular has specialized support for binding to form controls (e.g. `[control]="field"`).
|
||||
// This test ensures that `[control]` property bindings can still target other inputs bearing
|
||||
// the same name.
|
||||
|
||||
@Directive({selector: '[control]'})
|
||||
class Control {
|
||||
@Input() control = 'Default control value';
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div [control]="value"></div>
|
||||
`,
|
||||
imports: [Control],
|
||||
})
|
||||
class App {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const control = fixture.debugElement.query(By.directive(Control)).injector.get(Control);
|
||||
expect(control.control).toBe('Default control value');
|
||||
|
||||
fixture.componentInstance.value = 'Bound control value';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.control).toBe('Bound control value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('attributes and input properties', () => {
|
||||
|
||||
Reference in New Issue
Block a user