Skip to content

Commit 42e1062

Browse files
leonsenftmmalerba
authored andcommitted
refactor(core): add specialized instruction for [control] bindings (angular#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 angular#63773
1 parent 548ea02 commit 42e1062

File tree

20 files changed

+255
-2
lines changed

20 files changed

+255
-2
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/****************************************************************************************************
2+
* PARTIAL FILE: control_bindings.js
3+
****************************************************************************************************/
4+
import { Component, Directive, input } from '@angular/core';
5+
import * as i0 from "@angular/core";
6+
export class Control {
7+
constructor() {
8+
this.control = input(...(ngDevMode ? [undefined, { debugName: "control" }] : []));
9+
}
10+
}
11+
Control.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive });
12+
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 });
13+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Control, decorators: [{
14+
type: Directive,
15+
args: [{ selector: '[control]' }]
16+
}] });
17+
export class MyComponent {
18+
constructor() {
19+
this.value = 'Hello, world!';
20+
}
21+
}
22+
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
23+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
24+
<input [control]="value">
25+
`, isInline: true, dependencies: [{ kind: "directive", type: Control, selector: "[control]", inputs: ["control"] }] });
26+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
27+
type: Component,
28+
args: [{
29+
template: `
30+
<input [control]="value">
31+
`,
32+
imports: [Control],
33+
}]
34+
}] });
35+
36+
/****************************************************************************************************
37+
* PARTIAL FILE: control_bindings.d.ts
38+
****************************************************************************************************/
39+
import * as i0 from "@angular/core";
40+
export declare class Control {
41+
readonly control: import("@angular/core").InputSignal<string | undefined>;
42+
static ɵfac: i0.ɵɵFactoryDeclaration<Control, never>;
43+
static ɵdir: i0.ɵɵDirectiveDeclaration<Control, "[control]", never, { "control": { "alias": "control"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
44+
}
45+
export declare class MyComponent {
46+
value: string;
47+
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
48+
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "ng-component", never, {}, {}, never, never, true, never>;
49+
}
50+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "../../test_case_schema.json",
3+
"cases": [
4+
{
5+
"description": "should generate control instruction for `control` property bindings",
6+
"inputFiles": [
7+
"control_bindings.ts"
8+
],
9+
"expectations": [
10+
{
11+
"files": [
12+
"control_bindings.js"
13+
]
14+
}
15+
]
16+
}
17+
]
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
MyComponent.ɵcmp = /* @__PURE__ */i0.ɵɵdefineComponent({
2+
type: MyComponent,
3+
selectors: [["ng-component"]],
4+
decls: 1,
5+
vars: 1,
6+
consts: [[3, "control"]],
7+
template: function MyComponent_Template(rf, ctx) {
8+
if (rf & 1) {
9+
i0.ɵɵelement(0, "input", 0);
10+
}
11+
if (rf & 2) {
12+
i0.ɵɵcontrol(ctx.value);
13+
}
14+
},
15+
dependencies: [Control],
16+
encapsulation: 2
17+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {Component, Directive, input} from '@angular/core';
2+
3+
@Directive({selector: '[control]'})
4+
export class Control {
5+
readonly control = input<string>();
6+
}
7+
8+
@Component({
9+
template: `
10+
<input [control]="value">
11+
`,
12+
imports: [Control],
13+
})
14+
export class MyComponent {
15+
value = 'Hello, world!';
16+
}

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ export class Identifiers {
254254

255255
static ariaProperty: o.ExternalReference = {name: 'ɵɵariaProperty', moduleName: CORE};
256256
static property: o.ExternalReference = {name: 'ɵɵproperty', moduleName: CORE};
257+
static control: o.ExternalReference = {name: 'ɵɵcontrol', moduleName: CORE};
257258

258259
static animationEnterListener: o.ExternalReference = {
259260
name: 'ɵɵanimateEnterListener',

packages/compiler/src/render3/view/compiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export function compileComponentFromMetadata(
248248
getTemplateSourceLocationsEnabled(),
249249
);
250250

251-
// Then the IR is transformed to prepare it for cod egeneration.
251+
// Then the IR is transformed to prepare it for code generation.
252252
transform(tpl, CompilationJobKind.Tmpl);
253253

254254
// Finally we emit the template function:

packages/compiler/src/template/pipeline/ir/src/enums.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ export enum OpKind {
299299
* An operation to bind animation events to an element.
300300
*/
301301
AnimationListener,
302+
303+
/**
304+
* An operation to bind a signal form field to a control directive.
305+
*/
306+
Control,
302307
}
303308

304309
/**

packages/compiler/src/template/pipeline/ir/src/expression.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,7 @@ export function transformExpressionsInOp(
11031103
case OpKind.Property:
11041104
case OpKind.DomProperty:
11051105
case OpKind.Attribute:
1106+
case OpKind.Control:
11061107
if (op.expression instanceof Interpolation) {
11071108
transformExpressionsInInterpolation(op.expression, transform, flags);
11081109
} else {

packages/compiler/src/template/pipeline/ir/src/ops/update.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ export type UpdateOp =
5656
| RepeaterOp
5757
| DeferWhenOp
5858
| AnimationBindingOp
59-
| StoreLetOp;
59+
| StoreLetOp
60+
| ControlOp;
6061

6162
/**
6263
* A logical operation to perform string interpolation on a text node.
@@ -1062,3 +1063,30 @@ export function createStoreLetOp(
10621063
...NEW_OP,
10631064
};
10641065
}
1066+
1067+
/**
1068+
* A specialized {@link PropertyOp} that may bind a form field to a control.
1069+
*/
1070+
export interface ControlOp extends Omit<PropertyOp, 'kind' | 'name'> {
1071+
kind: OpKind.Control;
1072+
}
1073+
1074+
/** Creates a {@link ControlOp}. */
1075+
export function createControlOp(op: BindingOp): ControlOp {
1076+
return {
1077+
kind: OpKind.Control,
1078+
target: op.target,
1079+
expression: op.expression,
1080+
bindingKind: op.bindingKind,
1081+
securityContext: op.securityContext,
1082+
sanitizer: null,
1083+
isStructuralTemplateAttribute: op.isStructuralTemplateAttribute,
1084+
templateKind: op.templateKind,
1085+
i18nContext: op.i18nContext,
1086+
i18nMessage: op.i18nMessage,
1087+
sourceSpan: op.sourceSpan,
1088+
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
1089+
...TRAIT_CONSUMES_VARS,
1090+
...NEW_OP,
1091+
};
1092+
}

packages/compiler/src/template/pipeline/src/instruction.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,23 @@ export function property(
599599
return propertyBase(Identifiers.property, name, expression, sanitizer, sourceSpan);
600600
}
601601

602+
export function control(
603+
expression: o.Expression | ir.Interpolation,
604+
sanitizer: o.Expression | null,
605+
sourceSpan: ParseSourceSpan,
606+
): ir.UpdateOp {
607+
const args = [];
608+
if (expression instanceof ir.Interpolation) {
609+
args.push(interpolationToExpression(expression, sourceSpan));
610+
} else {
611+
args.push(expression);
612+
}
613+
if (sanitizer !== null) {
614+
args.push(sanitizer);
615+
}
616+
return call(Identifiers.control, args, sourceSpan);
617+
}
618+
602619
export function twoWayProperty(
603620
name: string,
604621
expression: o.Expression,

0 commit comments

Comments
 (0)