mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Revert "feat(core): propertyInterpolationCreate"
This reverts commit 2e3fe0747990647c5cfbba5429e90ed621b18e6e. Based on the recent design discussion we are going to express property interpolation as a computed function and reuse the propertyCreate infrastructure.
This commit is contained in:
committed by
Paul Gschwendtner
parent
7feda88abb
commit
3bd2fcd044
@@ -7,14 +7,25 @@
|
||||
*/
|
||||
|
||||
|
||||
import {assertDefined} from '../../util/assert';
|
||||
import {EMPTY_ARRAY} from '../../util/empty';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {HEADER_OFFSET} from '../interfaces/view';
|
||||
import {computed} from '../reactivity/computed';
|
||||
import {getCurrentTNode, getLView, getTView} from '../state';
|
||||
import {SIGNAL} from '@angular/core/primitives/signals';
|
||||
|
||||
import {determineInputTargets, propertyUpdateDom, propertyUpdateInput} from './property_create_shared';
|
||||
import {validateAgainstEventProperties} from '../../sanitization/sanitization';
|
||||
import {assertDefined, assertIndexInRange} from '../../util/assert';
|
||||
import {EMPTY_ARRAY} from '../../util/empty';
|
||||
import {bindingUpdated} from '../bindings';
|
||||
import {DirectiveDef} from '../interfaces/definition';
|
||||
import {PropertyAliasValue, TNode} from '../interfaces/node';
|
||||
import {RComment, RElement} from '../interfaces/renderer_dom';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {isComponentHost} from '../interfaces/type_checks';
|
||||
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 {getNativeByTNode} from '../util/view_utils';
|
||||
|
||||
import {handleUnknownPropertyError, isPropertyValid} from './element_validation';
|
||||
import {mapPropName, markDirtyIfOnPush, setNgReflectProperties, writeToDirectiveInput} from './shared';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
@@ -32,7 +43,32 @@ export function ɵɵpropertyCreate<T>(
|
||||
assertDefined(tNode, `propertyCreate() must follow an actual element`);
|
||||
|
||||
const inputData = tNode.inputs?.[propName] ?? EMPTY_ARRAY;
|
||||
const {zoneTargets, signalInputs} = determineInputTargets(inputData, tView, lView);
|
||||
|
||||
let signalInputs: InputSignalNode<unknown, unknown>[]|null = null;
|
||||
|
||||
// PERF: the fact that we need to iterate over all the inputs here isn't great.
|
||||
// We might consider storing more info on TView
|
||||
let zoneTargets: PropertyAliasValue|null = null;
|
||||
for (let i = 0; i < inputData.length;) {
|
||||
const directiveIndex = inputData[i++] as number;
|
||||
const privateName = inputData[i++] as string;
|
||||
const def = tView.data[directiveIndex] as DirectiveDef<any>;
|
||||
if (!def.signals) {
|
||||
// TODO(pk): refactor - code flow with all those firstCreatePass checks becomes hard to follow
|
||||
if (tView.firstCreatePass) {
|
||||
(zoneTargets ??= []).push(directiveIndex, privateName);
|
||||
}
|
||||
} else {
|
||||
ngDevMode && assertIndexInRange(lView, directiveIndex);
|
||||
// PERF: megamorphic read on [privateName] access
|
||||
const inputSignal =
|
||||
lView[directiveIndex][privateName][SIGNAL] as InputSignalNode<unknown, unknown>;
|
||||
(signalInputs ??= []).push(inputSignal);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -72,3 +108,69 @@ export function ɵɵpropertyCreate<T>(
|
||||
|
||||
return ɵɵpropertyCreate;
|
||||
}
|
||||
|
||||
export function propertyUpdateDom(
|
||||
nodeSlot: number, propName: string, expressionSlot: number, sanitizer: SanitizerFn|null): void {
|
||||
const lView = getLView();
|
||||
const expr = lView[expressionSlot];
|
||||
|
||||
let value = expr();
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (!bindingUpdated(lView, bindingIndex, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tView = getTView();
|
||||
const tNode = tView.data[nodeSlot] as TNode;
|
||||
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
|
||||
|
||||
propName = mapPropName(propName);
|
||||
|
||||
if (ngDevMode) {
|
||||
validateAgainstEventProperties(propName);
|
||||
if (!isPropertyValid(element, propName, tNode.value, tView.schemas)) {
|
||||
handleUnknownPropertyError(propName, tNode.value, tNode.type, lView);
|
||||
}
|
||||
ngDevMode.rendererSetProperty++;
|
||||
}
|
||||
|
||||
// It is assumed that the sanitizer is only added when the compiler determines that the
|
||||
// property is risky, so sanitization can be done without further checks.
|
||||
value = sanitizer != null ? (sanitizer(value, tNode.value || '', propName) as any) : value;
|
||||
lView[RENDERER].setProperty(element as RElement, propName, value);
|
||||
}
|
||||
|
||||
export function propertyUpdateInput(
|
||||
propName: string, expressionSlot: number, targets: PropertyAliasValue): void {
|
||||
const lView = getLView();
|
||||
const expr = lView[expressionSlot];
|
||||
const value = expr();
|
||||
|
||||
const tNode = getSelectedTNode();
|
||||
const tView = getTView();
|
||||
|
||||
ngDevMode && assertDefined(tNode.inputs, `Expected tNode to have inputs`);
|
||||
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (!bindingUpdated(lView, bindingIndex, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < targets.length;) {
|
||||
const index = targets[i++] as number;
|
||||
const privateName = targets[i++] as string;
|
||||
ngDevMode && assertIndexInRange(lView, index);
|
||||
const instance = lView[index];
|
||||
const def = tView.data[index] as DirectiveDef<any>;
|
||||
|
||||
writeToDirectiveInput(def, instance, propName, privateName, value);
|
||||
}
|
||||
|
||||
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
|
||||
if (isComponentHost(tNode)) {
|
||||
markDirtyIfOnPush(lView, tNode.index);
|
||||
}
|
||||
if (ngDevMode) {
|
||||
setNgReflectProperties(lView, element, tNode.type, targets, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* @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 {validateAgainstEventProperties} from '../../sanitization/sanitization';
|
||||
import {SIGNAL} from '../../signals';
|
||||
import {assertDefined, assertIndexInRange} from '../../util/assert';
|
||||
import {EMPTY_ARRAY} from '../../util/empty';
|
||||
import {bindingUpdated} from '../bindings';
|
||||
import {DirectiveDef} from '../interfaces/definition';
|
||||
import {PropertyAliasValue, TNode} from '../interfaces/node';
|
||||
import {RComment, RElement} from '../interfaces/renderer_dom';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {isComponentHost} from '../interfaces/type_checks';
|
||||
import {LView, RENDERER, TView} from '../interfaces/view';
|
||||
import {InputSignalNode} from '../reactivity/input_signal';
|
||||
import {getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
|
||||
import {getNativeByTNode} from '../util/view_utils';
|
||||
|
||||
import {handleUnknownPropertyError, isPropertyValid} from './element_validation';
|
||||
import {mapPropName, markDirtyIfOnPush, setNgReflectProperties, writeToDirectiveInput} from './shared';
|
||||
|
||||
export function determineInputTargets(inputData: any[], tView: TView, lView: LView<unknown>) {
|
||||
let signalInputs: InputSignalNode<unknown, unknown>[]|null = null;
|
||||
let zoneTargets: PropertyAliasValue|null = null;
|
||||
|
||||
// PERF: the fact that we need to iterate over all the inputs here isn't great.
|
||||
// We might consider storing more info on TView
|
||||
|
||||
for (let i = 0; i < inputData.length;) {
|
||||
const directiveIndex = inputData[i++] as number;
|
||||
const privateName = inputData[i++] as string;
|
||||
const def = tView.data[directiveIndex] as DirectiveDef<any>;
|
||||
if (!def.signals) {
|
||||
// TODO(pk): refactor - code flow with all those firstCreatePass checks becomes hard to follow
|
||||
if (tView.firstCreatePass) {
|
||||
(zoneTargets ??= []).push(directiveIndex, privateName);
|
||||
}
|
||||
} else {
|
||||
ngDevMode && assertIndexInRange(lView, directiveIndex);
|
||||
// PERF: megamorphic read on [privateName] access
|
||||
const inputSignal =
|
||||
lView[directiveIndex][privateName][SIGNAL] as InputSignalNode<unknown, unknown>;
|
||||
(signalInputs ??= []).push(inputSignal);
|
||||
}
|
||||
}
|
||||
|
||||
zoneTargets ??= EMPTY_ARRAY;
|
||||
signalInputs ??= EMPTY_ARRAY;
|
||||
return {zoneTargets, signalInputs};
|
||||
}
|
||||
|
||||
export function propertyUpdateDom(
|
||||
nodeSlot: number, propName: string, expressionSlot: number, sanitizer: SanitizerFn|null): void {
|
||||
const lView = getLView();
|
||||
const expr = lView[expressionSlot];
|
||||
|
||||
let value = expr();
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (!bindingUpdated(lView, bindingIndex, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tView = getTView();
|
||||
const tNode = tView.data[nodeSlot] as TNode;
|
||||
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
|
||||
|
||||
propName = mapPropName(propName);
|
||||
|
||||
if (ngDevMode) {
|
||||
validateAgainstEventProperties(propName);
|
||||
if (!isPropertyValid(element, propName, tNode.value, tView.schemas)) {
|
||||
handleUnknownPropertyError(propName, tNode.value, tNode.type, lView);
|
||||
}
|
||||
ngDevMode.rendererSetProperty++;
|
||||
}
|
||||
|
||||
// It is assumed that the sanitizer is only added when the compiler determines that the
|
||||
// property is risky, so sanitization can be done without further checks.
|
||||
value = sanitizer != null ? (sanitizer(value, tNode.value || '', propName) as any) : value;
|
||||
lView[RENDERER].setProperty(element as RElement, propName, value);
|
||||
}
|
||||
|
||||
export function propertyUpdateInput(
|
||||
propName: string, expressionSlot: number, targets: PropertyAliasValue): void {
|
||||
const lView = getLView();
|
||||
const expr = lView[expressionSlot];
|
||||
const value = expr();
|
||||
|
||||
const tNode = getSelectedTNode();
|
||||
const tView = getTView();
|
||||
|
||||
ngDevMode && assertDefined(tNode.inputs, `Expected tNode to have inputs`);
|
||||
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (!bindingUpdated(lView, bindingIndex, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < targets.length;) {
|
||||
const index = targets[i++] as number;
|
||||
const privateName = targets[i++] as string;
|
||||
ngDevMode && assertIndexInRange(lView, index);
|
||||
const instance = lView[index];
|
||||
const def = tView.data[index] as DirectiveDef<any>;
|
||||
|
||||
writeToDirectiveInput(def, instance, propName, privateName, value);
|
||||
}
|
||||
|
||||
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
|
||||
if (isComponentHost(tNode)) {
|
||||
markDirtyIfOnPush(lView, tNode.index);
|
||||
}
|
||||
if (ngDevMode) {
|
||||
setNgReflectProperties(lView, element, tNode.type, targets, value);
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* @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 {computed} from '../../render3/reactivity/computed';
|
||||
import {assertDefined, assertIndexInRange} from '../../util/assert';
|
||||
import {EMPTY_ARRAY} from '../../util/empty';
|
||||
import {DirectiveDef} from '../interfaces/definition';
|
||||
import {PropertyAliasValue, TNode} from '../interfaces/node';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {isComponentHost} from '../interfaces/type_checks';
|
||||
import {HEADER_OFFSET, RENDERER, TVIEW} from '../interfaces/view';
|
||||
import {getCurrentTNode, getLView, getSelectedTNode, getTView} from '../state';
|
||||
import {NO_CHANGE} from '../tokens';
|
||||
import {renderStringify} from '../util/stringify_utils';
|
||||
|
||||
import {interpolationV} from './interpolation';
|
||||
import {determineInputTargets} from './property_create_shared';
|
||||
import {elementPropertyInternal, markDirtyIfOnPush, writeToDirectiveInput} from './shared';
|
||||
|
||||
|
||||
function concatenateInterpolatedValue(values: any[]): string {
|
||||
// Build the updated content
|
||||
let content = values[0];
|
||||
for (let i = 1; i < values.length; i += 2) {
|
||||
content += renderStringify(values[i]) + values[i + 1];
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵpropertyInterpolateVCreate(
|
||||
slot: number, propName: string, staticParts: string[], values: any[],
|
||||
sanitizer?: SanitizerFn|null): typeof ɵɵpropertyInterpolateVCreate {
|
||||
const lView = getLView();
|
||||
const expressionsSlot = HEADER_OFFSET + slot;
|
||||
|
||||
const tView = getTView();
|
||||
const tNode = getCurrentTNode();
|
||||
assertDefined(tNode, `propertyCreate() must follow an actual element`);
|
||||
|
||||
const inputData = tNode.inputs?.[propName] ?? EMPTY_ARRAY;
|
||||
const {zoneTargets, signalInputs} = determineInputTargets(inputData, tView, lView);
|
||||
|
||||
// If there are multiple signal targets, or any zone targets, then wrap `expr` in a computed. This
|
||||
// ensures that the interpolated string is concatenated only once.
|
||||
let exprValueComputed: (() => string)|null = null;
|
||||
if (zoneTargets.length > 0 || signalInputs.length > 1) {
|
||||
exprValueComputed = computed(() => concatenateInterpolatedValue(values));
|
||||
}
|
||||
|
||||
lView[expressionsSlot] = values;
|
||||
for (const inputSignal of signalInputs) {
|
||||
inputSignal.bind(inputSignal, {computation: exprValueComputed!});
|
||||
}
|
||||
|
||||
if (tView.firstCreatePass) {
|
||||
if (inputData.length === 0) {
|
||||
// Untargeted input -> DOM binding.
|
||||
(tView.virtualUpdate ??= []).push({
|
||||
slot: expressionsSlot,
|
||||
instruction: () => propertyInterpolationUpdateDom(
|
||||
tNode.index, propName, expressionsSlot, sanitizer ?? null),
|
||||
});
|
||||
} else if (zoneTargets?.length ?? 0 > 0) {
|
||||
// Some binding targets were zone-based, so we need an update instruction to process them.
|
||||
(tView.virtualUpdate ??= []).push({
|
||||
slot: expressionsSlot,
|
||||
instruction: () =>
|
||||
propertyInterpolationUpdateInput(propName, expressionsSlot, zoneTargets!),
|
||||
});
|
||||
} else {
|
||||
// The only target(s) were signal-based, so no update path is needed.
|
||||
}
|
||||
}
|
||||
|
||||
return ɵɵpropertyInterpolateVCreate;
|
||||
}
|
||||
function propertyInterpolationUpdateDom(
|
||||
nodeSlot: number, propName: string, expressionSlot: number, sanitizer: SanitizerFn|null): void {
|
||||
const lView = getLView();
|
||||
const values: any[] = lView[expressionSlot];
|
||||
const interpolatedValue = interpolationV(lView, values);
|
||||
if (interpolatedValue !== NO_CHANGE) {
|
||||
const tView = getTView();
|
||||
const tNode = tView.data[nodeSlot] as TNode;
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
|
||||
// TODO(pk): ngDevMode part of this instruction
|
||||
}
|
||||
}
|
||||
|
||||
export function propertyInterpolationUpdateInput(
|
||||
propName: string, expressionSlot: number, targets: PropertyAliasValue): void {
|
||||
const lView = getLView();
|
||||
const values: any[] = lView[expressionSlot];
|
||||
const interpolatedValue = interpolationV(lView, values);
|
||||
if (interpolatedValue !== NO_CHANGE) {
|
||||
// TODO(pk): inconsistent signature of update instructions
|
||||
const tNode = getSelectedTNode();
|
||||
const tView = lView[TVIEW];
|
||||
ngDevMode && assertDefined(tNode.inputs, `Expected tNode to have inputs`);
|
||||
|
||||
for (let i = 0; i < targets.length;) {
|
||||
const index = targets[i++] as number;
|
||||
const privateName = targets[i++] as string;
|
||||
ngDevMode && assertIndexInRange(lView, index);
|
||||
const instance = lView[index];
|
||||
const def = tView.data[index] as DirectiveDef<any>;
|
||||
|
||||
writeToDirectiveInput(def, instance, propName, privateName, interpolatedValue as string);
|
||||
|
||||
if (isComponentHost(tNode)) {
|
||||
markDirtyIfOnPush(lView, tNode.index);
|
||||
}
|
||||
|
||||
// TODO(pk): ngDevMode part of the inputs update
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ describe('Signal component input interpolations', () => {
|
||||
xit('should bind interpolated values', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<print text="a{{1}}b{{2}}c{{3}}d{{4}}e{{5}}f{{6}}g{{7}}h{{8}}i{{9}}j"></print>`,
|
||||
template: `<print text="Hello, {{name}}!">`,
|
||||
imports: [Print],
|
||||
standalone: true,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user