mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Paul Gschwendtner
parent
a0af416ca1
commit
34f002db7e
@@ -158,6 +158,7 @@ export {
|
||||
ɵɵpropertyInterpolate7,
|
||||
ɵɵpropertyInterpolate8,
|
||||
ɵɵpropertyInterpolateV,
|
||||
ɵɵstringifyInterpolation,
|
||||
ɵɵProvidersFeature,
|
||||
ɵɵHostDirectivesFeature,
|
||||
ɵɵpureFunction0,
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user