feat: support property interpolation in signal components

This commit is contained in:
Paul Gschwendtner
2023-06-30 12:58:53 +00:00
parent 7acf163f51
commit 0bf724f4d4
19 changed files with 392 additions and 162 deletions
@@ -1,67 +0,0 @@
/****************************************************************************************************
* PARTIAL FILE: simple_dom_binding.js
****************************************************************************************************/
import { Component, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class LiteralValueBinding {
}
LiteralValueBinding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, deps: [], target: i0.ɵɵFactoryTarget.Component });
LiteralValueBinding.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: LiteralValueBinding, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="true"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button [disabled]="true"></button>`,
}]
}] });
export class FromContextBindingStatic {
constructor() {
this.isDisabled = true;
}
}
FromContextBindingStatic.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, deps: [], target: i0.ɵɵFactoryTarget.Component });
FromContextBindingStatic.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingStatic, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="isDisabled"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button [disabled]="isDisabled"></button>`,
}]
}] });
export class FromContextBindingSignal {
constructor() {
this.isDisabled = signal(true);
}
}
FromContextBindingSignal.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, deps: [], target: i0.ɵɵFactoryTarget.Component });
FromContextBindingSignal.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingSignal, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="isDisabled()"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button [disabled]="isDisabled()"></button>`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: simple_dom_binding.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class LiteralValueBinding {
static ɵfac: i0.ɵɵFactoryDeclaration<LiteralValueBinding, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<LiteralValueBinding, "app", never, {}, {}, never, never, false, never, true>;
}
export declare class FromContextBindingStatic {
isDisabled: boolean;
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingStatic, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingStatic, "app", never, {}, {}, never, never, false, never, true>;
}
export declare class FromContextBindingSignal {
isDisabled: import("@angular/core").WritableSignal<boolean>;
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingSignal, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingSignal, "app", never, {}, {}, never, never, false, never, true>;
}
@@ -1,15 +0,0 @@
{
"$schema": "../../test_case_schema.json",
"cases": [
{
"onlyForTemplatePipeline": true,
"description": "should support binding to DOM properties",
"inputFiles": ["simple_dom_binding.ts"],
"expectations": [
{
"files": [{"generated": "simple_dom_binding.js", "expected": "simple_dom_binding.js"}]
}
]
}
]
}
@@ -0,0 +1,134 @@
/****************************************************************************************************
* PARTIAL FILE: simple_binding.js
****************************************************************************************************/
import { Component, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class LiteralValueBinding {
}
LiteralValueBinding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, deps: [], target: i0.ɵɵFactoryTarget.Component });
LiteralValueBinding.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: LiteralValueBinding, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="true"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button [disabled]="true"></button>`,
}]
}] });
export class FromContextBindingStatic {
constructor() {
this.isDisabled = true;
}
}
FromContextBindingStatic.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, deps: [], target: i0.ɵɵFactoryTarget.Component });
FromContextBindingStatic.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingStatic, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="isDisabled"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button [disabled]="isDisabled"></button>`,
}]
}] });
export class FromContextBindingSignal {
constructor() {
this.isDisabled = signal(true);
}
}
FromContextBindingSignal.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, deps: [], target: i0.ɵɵFactoryTarget.Component });
FromContextBindingSignal.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingSignal, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="isDisabled()"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button [disabled]="isDisabled()"></button>`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: simple_binding.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class LiteralValueBinding {
static ɵfac: i0.ɵɵFactoryDeclaration<LiteralValueBinding, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<LiteralValueBinding, "app", never, {}, {}, never, never, false, never, true>;
}
export declare class FromContextBindingStatic {
isDisabled: boolean;
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingStatic, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingStatic, "app", never, {}, {}, never, never, false, never, true>;
}
export declare class FromContextBindingSignal {
isDisabled: import("@angular/core").WritableSignal<boolean>;
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingSignal, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingSignal, "app", never, {}, {}, never, never, false, never, true>;
}
/****************************************************************************************************
* PARTIAL FILE: with_interpolation.js
****************************************************************************************************/
import { Component, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class LiteralValueBinding {
}
LiteralValueBinding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, deps: [], target: i0.ɵɵFactoryTarget.Component });
LiteralValueBinding.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: LiteralValueBinding, isSignal: true, selector: "app", ngImport: i0, template: `<button title="This is a {{'submit'}} button"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button title="This is a {{'submit'}} button"></button>`,
}]
}] });
export class FromContextBindingStatic {
constructor() {
this.type = 'submit';
}
}
FromContextBindingStatic.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, deps: [], target: i0.ɵɵFactoryTarget.Component });
FromContextBindingStatic.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingStatic, isSignal: true, selector: "app", ngImport: i0, template: `<button title="This is a {{type}} button"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button title="This is a {{type}} button"></button>`,
}]
}] });
export class FromContextBindingSignal {
constructor() {
this.type = signal('submit');
}
}
FromContextBindingSignal.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, deps: [], target: i0.ɵɵFactoryTarget.Component });
FromContextBindingSignal.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingSignal, isSignal: true, selector: "app", ngImport: i0, template: `<button title="This is a {{type()}} button"></button>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, decorators: [{
type: Component,
args: [{
selector: 'app',
signals: true,
template: `<button title="This is a {{type()}} button"></button>`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: with_interpolation.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class LiteralValueBinding {
static ɵfac: i0.ɵɵFactoryDeclaration<LiteralValueBinding, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<LiteralValueBinding, "app", never, {}, {}, never, never, false, never, true>;
}
export declare class FromContextBindingStatic {
type: string;
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingStatic, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingStatic, "app", never, {}, {}, never, never, false, never, true>;
}
export declare class FromContextBindingSignal {
type: import("@angular/core").WritableSignal<string>;
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingSignal, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingSignal, "app", never, {}, {}, never, never, false, never, true>;
}
@@ -0,0 +1,25 @@
{
"$schema": "../../test_case_schema.json",
"cases": [
{
"onlyForTemplatePipeline": true,
"description": "should support binding to DOM properties",
"inputFiles": ["simple_binding.ts"],
"expectations": [
{
"files": [{"generated": "simple_binding.js", "expected": "simple_binding.js"}]
}
]
},
{
"onlyForTemplatePipeline": true,
"description": "should support interpolation binding to DOM properties",
"inputFiles": ["with_interpolation.ts"],
"expectations": [
{
"files": [{"generated": "with_interpolation.js", "expected": "with_interpolation.js"}]
}
]
}
]
}
@@ -0,0 +1,27 @@
template: function LiteralValueBinding_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "button");
i0.ɵɵpropertyCreate(1, "title", function () { return i0.ɵɵstringifyInterpolation`This is a ${"submit"} button`; });
i0.ɵɵelementEnd();
}
}
// ...
template: function FromContextBindingStatic_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "button");
i0.ɵɵpropertyCreate(1, "title", function () { return i0.ɵɵstringifyInterpolation`This is a ${ctx.type} button`; });
i0.ɵɵelementEnd();
}
}
// ...
template: function FromContextBindingSignal_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "button");
i0.ɵɵpropertyCreate(1, "title", function () { return i0.ɵɵstringifyInterpolation`This is a ${ctx.type()} button`; });
i0.ɵɵelementEnd();
}
}
@@ -0,0 +1,27 @@
import {Component, signal} from '@angular/core';
@Component({
selector: 'app',
signals: true,
template: `<button title="This is a {{'submit'}} button"></button>`,
})
export class LiteralValueBinding {
}
@Component({
selector: 'app',
signals: true,
template: `<button title="This is a {{type}} button"></button>`,
})
export class FromContextBindingStatic {
type = 'submit';
}
@Component({
selector: 'app',
signals: true,
template: `<button title="This is a {{type()}} button"></button>`,
})
export class FromContextBindingSignal {
type = signal('submit');
}
@@ -182,6 +182,9 @@ export class Identifiers {
static propertyCreate: o.ExternalReference = {name: 'ɵɵpropertyCreate', moduleName: CORE};
static property: o.ExternalReference = {name: 'ɵɵproperty', moduleName: CORE};
static stringifyInterpolation:
o.ExternalReference = {name: 'ɵɵstringifyInterpolation', moduleName: CORE};
static propertyInterpolate:
o.ExternalReference = {name: 'ɵɵpropertyInterpolate', moduleName: CORE};
static propertyInterpolate1:
@@ -236,7 +236,13 @@ export enum ExpressionKind {
/**
* A reference to a temporary variable.
*/
ReadTemporaryExpr,
/**
* An interpolation template string expression. Such template string expression is
* commonly expected to be part of the `PropertyCreate` operation expression.
*/
InterpolationTemplateExpr,
}
/**
@@ -18,11 +18,11 @@ import {ConsumesVarsTrait, UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, Use
/**
* An `o.Expression` subtype representing a logical expression in the intermediate representation.
*/
export type Expression =
LexicalReadExpr|ReferenceExpr|ShallowReferenceExpr|ContextExpr|NextContextExpr|
GetCurrentViewExpr|RestoreViewExpr|ResetViewExpr|ReadVariableExpr|PureFunctionExpr|
PureFunctionParameterExpr|PipeBindingExpr|PipeBindingVariadicExpr|SafePropertyReadExpr|
SafeKeyedReadExpr|SafeInvokeFunctionExpr|EmptyExpr|AssignTemporaryExpr|ReadTemporaryExpr;
export type Expression = LexicalReadExpr|ReferenceExpr|ShallowReferenceExpr|ContextExpr|
NextContextExpr|GetCurrentViewExpr|RestoreViewExpr|ResetViewExpr|ReadVariableExpr|
PureFunctionExpr|PureFunctionParameterExpr|PipeBindingExpr|PipeBindingVariadicExpr|
SafePropertyReadExpr|SafeKeyedReadExpr|SafeInvokeFunctionExpr|EmptyExpr|AssignTemporaryExpr|
ReadTemporaryExpr|InterpolationTemplateExpr;
/**
* Transformer type which converts expressions into general `o.Expression`s (which may be an
@@ -661,6 +661,47 @@ export class SafeTernaryExpr extends ExpressionBase {
}
}
export class InterpolationTemplateExpr extends ExpressionBase {
override readonly kind = ExpressionKind.InterpolationTemplateExpr;
constructor(public staticParts: string[], public expressionParts: o.Expression[]) {
super();
}
override visitExpression() {
throw new Error('Not implemented.');
}
override isEquivalent(e: o.Expression): boolean {
if (!(e instanceof InterpolationTemplateExpr)) {
return false;
}
if (e.staticParts.length !== this.staticParts.length) {
return false;
}
if (e.expressionParts.length !== this.expressionParts.length) {
return false;
}
return e.staticParts.every((p, i) => p === this.staticParts[i]) &&
e.expressionParts.every((e, i) => e === this.expressionParts[i]);
}
override isConstant(): boolean {
return false;
}
override transformInternalExpressions(transform: ExpressionTransform, flags: VisitorContextFlag):
void {
this.expressionParts =
this.expressionParts.map(p => transformExpressionsInExpression(p, transform, flags));
}
override clone(): InterpolationTemplateExpr {
return new InterpolationTemplateExpr(
[...this.staticParts], this.expressionParts.map(p => p.clone()));
}
}
export class EmptyExpr extends ExpressionBase {
override readonly kind = ExpressionKind.EmptyExpr;
@@ -272,11 +272,26 @@ function ingestPropertyBinding(
}
view.update.push(ir.createInterpolateStyleMapOp(
xref, value.strings, value.expressions.map(expr => convertAst(expr, view.tpl))));
} else {
view.update.push(ir.createInterpolatePropertyOp(
xref, bindingKind, name, value.strings,
value.expressions.map(expr => convertAst(expr, view.tpl))));
break;
}
if (view.tpl.isSignal) {
// Allocating an XRef ID because a property create operation
// consumes a slot to store the expression.
const propertyXrefId = view.tpl.allocateXrefId();
view.create.push(ir.createPropertyCreateOp(
propertyXrefId, xref, name,
new ir.InterpolationTemplateExpr(
value.strings, value.expressions.map(e => convertAst(e, view.tpl)))));
break;
}
view.update.push(ir.createInterpolatePropertyOp(
xref, bindingKind, name, value.strings,
value.expressions.map(expr => convertAst(expr, view.tpl))));
break;
case e.BindingType.Style:
if (bindingKind !== ir.ElementAttributeKind.Binding) {
@@ -265,6 +265,13 @@ export function pureFunction(
);
}
export function stringifyInterpolation(
staticParts: string[], expressions: o.Expression[]): o.Expression {
return o.taggedTemplate(
o.importExpr(Identifiers.stringifyInterpolation),
new o.TemplateLiteral(staticParts.map(p => new o.TemplateLiteralElement(p)), expressions));
}
/**
* Collates the string an expression arguments for an interpolation instruction.
*/
@@ -220,6 +220,8 @@ function reifyIrExpression(expr: o.Expression): o.Expression {
return ng.pipeBind(expr.slot!, expr.varOffset!, expr.args);
case ir.ExpressionKind.PipeBindingVariadic:
return ng.pipeBindV(expr.slot!, expr.varOffset!, expr.args);
case ir.ExpressionKind.InterpolationTemplateExpr:
return ng.stringifyInterpolation(expr.staticParts, expr.expressionParts);
default:
throw new Error(`AssertionError: Unsupported reification of ir.Expression kind: ${
ir.ExpressionKind[(expr as ir.Expression).kind]}`);
@@ -90,6 +90,7 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵhostProperty': r3.ɵɵhostProperty,
'ɵɵproperty': r3.ɵɵproperty,
'ɵɵpropertyCreate': r3.ɵɵpropertyCreate,
'ɵɵstringifyInterpolation': r3.ɵɵstringifyInterpolation,
'ɵɵpropertyInterpolate': r3.ɵɵpropertyInterpolate,
'ɵɵpropertyInterpolate1': r3.ɵɵpropertyInterpolate1,
'ɵɵpropertyInterpolate2': r3.ɵɵpropertyInterpolate2,
@@ -60,67 +60,3 @@ describe('dom property bindings in signal based components', () => {
expect(div.tabIndex).toBe(0);
});
});
describe('dom property interpolation in signal based components', () => {
xit('should support property interpolation in DOM bindings', () => {
@Component({
signals: true,
template: `<div title="Hello, {{name()}}!"></div>`,
standalone: true,
})
class App {
name = signal('Angular');
}
const fixture = TestBed.createComponent(App);
const div = fixture.nativeElement.firstChild;
fixture.detectChanges();
expect(div.title).toBe('Hello, Angular!');
});
describe('hand-generated code - DELETE WHEN COMPILER CAN GENERATE INTERPOLATION', () => {
it('should support property interpolation in DOM bindings - hand generated code', () => {
class TestComponent {
name = signal('Angular');
static ɵfac = () => new TestComponent();
static ɵcmp = ɵɵdefineComponent({
type: TestComponent,
selectors: [['test-cmp']],
standalone: true,
signals: true,
features: [ɵɵStandaloneFeature],
decls: 1,
vars: 2,
consts: [[3, 'title']],
template:
(rf: any, ctx: any) => {
if ((rf & 1)) {
ɵɵelement(0, 'div', 0);
ɵɵpropertyCreate(
1, 'title', () => ɵɵstringifyInterpolation`Hello, ${ctx.name()}!`);
}
},
styles: []
});
}
@Component({
signals: true,
template: `<test-cmp />`,
standalone: true,
imports: [TestComponent],
})
class App {
}
const fixture = TestBed.createComponent(App);
const div = fixture.nativeElement.firstChild.firstChild;
fixture.detectChanges();
expect(div.title).toBe('Hello, Angular!');
});
});
});
@@ -0,0 +1,60 @@
/**
* @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.io/license
*/
import {USE_TEMPLATE_PIPELINE} from '@angular/compiler/src/template/pipeline/switch/index';
import {Component, signal} from '@angular/core';
import {TestBed} from '@angular/core/testing';
if (!USE_TEMPLATE_PIPELINE) {
console.error(
'ERROR: Cannot run this test target without: --//packages/compiler:use_template_pipeline');
process.exit(1);
}
describe('Signal component DOM property interpolations', () => {
it('should bind interpolated values', () => {
@Component({
signals: true,
template: `<div title="Hello, {{name()}}!"></div>`,
standalone: true,
})
class App {
name = signal('Angular');
}
const fixture = TestBed.createComponent(App);
const div = fixture.nativeElement.firstChild;
fixture.detectChanges();
expect(div.title).toBe('Hello, Angular!');
});
it('should support updating values', () => {
@Component({
signals: true,
template: `<div title="Hello, {{name()}}!"></div>`,
standalone: true,
})
class App {
name = signal('Angular');
}
const fixture = TestBed.createComponent(App);
const div = fixture.nativeElement.firstChild;
fixture.detectChanges();
expect(div.title).toBe('Hello, Angular!');
fixture.componentInstance.name.set('New Name');
fixture.detectChanges();
expect(div.title).toBe('Hello, New Name!');
});
});
@@ -12,7 +12,6 @@ import {TestBed} from '@angular/core/testing';
import {Input} from '../../../src/metadata';
if (!USE_TEMPLATE_PIPELINE) {
console.error(
'ERROR: Cannot run this test target without: --//packages/compiler:use_template_pipeline');
@@ -30,11 +29,10 @@ describe('Signal component input interpolations', () => {
@Input() text = input('');
}
// TODO: compiler needs to generate the ɵɵpropertyInterpolationCreate instruction
xit('should bind interpolated values', () => {
it('should bind interpolated values', () => {
@Component({
signals: true,
template: `<print text="Hello, {{name}}!">`,
template: `<print text="Hello, {{name()}}!">`,
imports: [Print],
standalone: true,
})
@@ -47,4 +45,26 @@ describe('Signal component input interpolations', () => {
expect(fixture.nativeElement.textContent).toBe('Hello, Angular!');
});
it('should allow updating interpolated values', () => {
@Component({
signals: true,
template: `<print text="Hello, {{name()}}!">`,
imports: [Print],
standalone: true,
})
class App {
name = signal('Angular');
}
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('Hello, Angular!');
fixture.componentInstance.name.set('New Name');
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('Hello, New Name!');
});
});
+11 -3
View File
@@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, input, Input} from '@angular/core';
import {Component, input, Input, signal} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
@Component({
selector: 'greet',
standalone: true,
signals: true,
template: `{{ counter() }}`,
template: `{{ counter() }} -- {{label()}}`,
})
export class Greet<T> {
counter = input(0);
@@ -24,6 +24,8 @@ export class Greet<T> {
gen = input<T>({required: true});
gen2 = input<T>();
label = input<string>();
works(): T {
return this.gen();
}
@@ -37,16 +39,22 @@ export class Greet<T> {
selector: 'my-app',
template: `
Hello <greet [counter]="3" [bla4Public]="10" #ok
[bla3]="10" [gen]="{yes: true}"
[bla3]="someVar" [gen]="{yes: true}" label="Hello {{name()}}"
/>
<button (click)="ok.works().yes">Click</button>
<button (click)="updateName()">Change name</button>
`,
imports: [Greet],
signals: true,
})
export class MyApp {
name = signal('Angular');
someVar = -10;
protected updateName() {
this.name.update(n => `${n}-`);
}
}
bootstrapApplication(MyApp).catch((e) => console.error(e));