feat(core): introduce property interpolation and make DOM property bindings reactive

DOM bindings are wrapped into computed now so their values
are memoized. As the result expression in DOM bindings are
re-evaluated when a component is change-detected.
This commit is contained in:
Pawel Kozlowski
2023-06-27 12:59:08 +02:00
committed by Paul Gschwendtner
parent a0af416ca1
commit 34f002db7e
4 changed files with 93 additions and 9 deletions
@@ -158,6 +158,7 @@ export {
ɵɵpropertyInterpolate7,
ɵɵpropertyInterpolate8,
ɵɵpropertyInterpolateV,
ɵɵstringifyInterpolation,
ɵɵProvidersFeature,
ɵɵHostDirectivesFeature,
ɵɵpureFunction0,
+1
View File
@@ -94,6 +94,7 @@ export {
ɵɵpropertyInterpolate7,
ɵɵpropertyInterpolate8,
ɵɵpropertyInterpolateV,
ɵɵstringifyInterpolation,
ɵɵreference,
ɵɵshallowReference,
@@ -22,6 +22,7 @@ import {HEADER_OFFSET, RENDERER} from '../interfaces/view';
import {computed} from '../reactivity/computed';
import {InputSignalNode} from '../reactivity/input_signal';
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
import {renderStringify} from '../util/stringify_utils';
import {getNativeByTNode} from '../util/view_utils';
import {handleUnknownPropertyError, isPropertyValid} from './element_validation';
@@ -70,13 +71,9 @@ export function ɵɵpropertyCreate<T>(
zoneTargets ??= EMPTY_ARRAY;
signalInputs ??= EMPTY_ARRAY;
// If there are multiple signal targets, or any zone targets, then wrap `expr` in a computed. This
// ensures that the expression is only evaluated once, even if it has multiple consumers. Zone
// targets always use a computed as this memoizes all object/literal creation (which would
// otherwise have used pure functions).
if (zoneTargets.length > 0 || signalInputs.length > 1) {
expr = computed(expr);
}
// PERF(pk): I could avoid wrapping into computed for the case of a single binding to a signal
// based component
expr = computed(expr);
lView[expressionSlot] = expr;
for (const inputSignal of signalInputs) {
@@ -95,7 +92,7 @@ export function ɵɵpropertyCreate<T>(
instruction: () =>
propertyUpdateDom(tNode.index, propName, expressionSlot, sanitizer ?? null),
});
} else if (zoneTargets?.length ?? 0 > 0) {
} else if (zoneTargets.length) {
// Some binding targets were zone-based, so we need an update instruction to process them.
(tView.virtualUpdate ??= []).push({
slot: expressionSlot,
@@ -174,3 +171,15 @@ export function propertyUpdateInput(
setNgReflectProperties(lView, element, tNode.type, targets, value);
}
}
export function ɵɵstringifyInterpolation(
staticStrings: TemplateStringsArray, ...expressionValues: any[]): string {
// Build the updated content
let content = staticStrings[0];
for (let i = 1; i < staticStrings.length; i++) {
content += expressionValues[i - 1] + renderStringify(staticStrings[i]);
}
return content;
}
@@ -7,7 +7,7 @@
*/
import {USE_TEMPLATE_PIPELINE} from '@angular/compiler/src/template/pipeline/switch';
import {Component, signal} from '@angular/core';
import {Component, signal, ɵɵdefineComponent, ɵɵelement, ɵɵpropertyCreate, ɵɵStandaloneFeature, ɵɵstringifyInterpolation} from '@angular/core';
import {TestBed} from '@angular/core/testing';
if (!USE_TEMPLATE_PIPELINE) {
@@ -34,6 +34,34 @@ describe('dom property bindings in signal based components', () => {
expect(div.tabIndex).toBe(5);
});
it('should not update DOM binding if a binding is not reactive', () => {
@Component({
signals: true,
template: `<div [tabindex]="nonReactive()">{{unrelated()}}</div>`,
standalone: true,
})
class App {
_idx = 0;
nonReactive() {
return this._idx++;
}
unrelated = signal('foo');
}
const fixture = TestBed.createComponent(App);
const cmpInstance = fixture.componentInstance;
const div = fixture.nativeElement.firstChild;
fixture.detectChanges();
expect(div.tabIndex).toBe(0);
cmpInstance.unrelated.set('bar');
fixture.detectChanges();
expect(div.tabIndex).toBe(0);
});
});
describe('dom property interpolation in signal based components', () => {
xit('should support property interpolation in DOM bindings', () => {
@Component({
signals: true,
@@ -48,6 +76,51 @@ describe('dom property bindings in signal based components', () => {
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!');
});
});
});