feat(ivy): add ΔpropertyInterpolate instructions (#29576)
- Adds the instructions - Adds tests for all instructions - Adds TODO to remove all tests when we are able to test this with TestBed after the compiler is updated PR Close #29576
This commit is contained in:
@ -8,9 +8,11 @@
|
||||
import {Injector} from '../../di';
|
||||
import {ErrorHandler} from '../../error_handler';
|
||||
import {Type} from '../../interface/type';
|
||||
import {SchemaMetadata} from '../../metadata/schema';
|
||||
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../metadata/schema';
|
||||
import {validateAgainstEventProperties} from '../../sanitization/sanitization';
|
||||
import {Sanitizer} from '../../sanitization/security';
|
||||
import {assertDataInRange, assertDefined, assertDomNode, assertEqual, assertLessThan, assertNotEqual} from '../../util/assert';
|
||||
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
|
||||
import {assertLView, assertPreviousIsParent} from '../assert';
|
||||
import {attachPatchData, getComponentViewByInstance} from '../context_discovery';
|
||||
import {attachLContainerDebug, attachLViewDebug} from '../debug';
|
||||
@ -23,17 +25,19 @@ import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/inj
|
||||
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node';
|
||||
import {LQueries} from '../interfaces/query';
|
||||
import {RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from '../interfaces/renderer';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {StylingContext} from '../interfaces/styling';
|
||||
import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TVIEW, TView, T_HOST} from '../interfaces/view';
|
||||
import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, T_HOST} from '../interfaces/view';
|
||||
import {assertNodeOfPossibleTypes, assertNodeType} from '../node_assert';
|
||||
import {isNodeMatchingSelectorList} from '../node_selector_matcher';
|
||||
import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, incrementActiveDirectiveId, isCreationMode, leaveView, resetComponentState, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex, ΔnamespaceHTML} from '../state';
|
||||
import {initializeStaticContext as initializeStaticStylingContext} from '../styling/class_and_style_bindings';
|
||||
import {ANIMATION_PROP_PREFIX, isAnimationProp} from '../styling/util';
|
||||
import {NO_CHANGE} from '../tokens';
|
||||
import {attrsStylingIndexOf} from '../util/attrs_utils';
|
||||
import {INTERPOLATION_DELIMITER, renderStringify} from '../util/misc_utils';
|
||||
import {getLViewParent, getRootContext} from '../util/view_traversal_utils';
|
||||
import {getComponentViewByIndex, getNativeByTNode, isComponentDef, isContentQueryHost, isRootView, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils';
|
||||
import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isComponent, isComponentDef, isContentQueryHost, isRootView, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils';
|
||||
|
||||
|
||||
|
||||
@ -430,7 +434,7 @@ export function renderEmbeddedTemplate<T>(viewToRender: LView, tView: TView, con
|
||||
ΔnamespaceHTML();
|
||||
|
||||
// Reset the selected index so we can assert that `select` was called later
|
||||
ngDevMode && setSelectedIndex(-1);
|
||||
setSelectedIndex(-1);
|
||||
|
||||
tView.template !(getRenderFlags(viewToRender), context);
|
||||
// This must be set to false immediately after the first creation run because in an
|
||||
@ -465,7 +469,7 @@ function renderComponentOrTemplate<T>(
|
||||
ΔnamespaceHTML();
|
||||
|
||||
// Reset the selected index so we can assert that `select` was called later
|
||||
ngDevMode && setSelectedIndex(-1);
|
||||
setSelectedIndex(-1);
|
||||
|
||||
templateFn(RenderFlags.Create, context);
|
||||
}
|
||||
@ -810,16 +814,169 @@ export function generatePropertyAliases(tNode: TNode, direction: BindingDirectio
|
||||
return propStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping between attributes names that don't correspond to their element property names.
|
||||
*/
|
||||
const ATTR_TO_PROP: {[name: string]: string} = {
|
||||
'class': 'className',
|
||||
'for': 'htmlFor',
|
||||
'formaction': 'formAction',
|
||||
'innerHtml': 'innerHTML',
|
||||
'readonly': 'readOnly',
|
||||
'tabindex': 'tabIndex',
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
//// Text
|
||||
//////////////////////////
|
||||
export function elementPropertyInternal<T>(
|
||||
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
|
||||
nativeOnly?: boolean,
|
||||
loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void {
|
||||
if (value === NO_CHANGE) return;
|
||||
const lView = getLView();
|
||||
const element = getNativeByIndex(index, lView) as RElement | RComment;
|
||||
const tNode = getTNode(index, lView);
|
||||
let inputData: PropertyAliases|null|undefined;
|
||||
let dataValue: PropertyAliasValue|undefined;
|
||||
if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) &&
|
||||
(dataValue = inputData[propName])) {
|
||||
setInputsForProperty(lView, dataValue, value);
|
||||
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
|
||||
if (ngDevMode) {
|
||||
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) {
|
||||
setNgReflectProperties(lView, element, tNode.type, dataValue, value);
|
||||
}
|
||||
}
|
||||
} else if (tNode.type === TNodeType.Element) {
|
||||
propName = ATTR_TO_PROP[propName] || propName;
|
||||
|
||||
if (ngDevMode) {
|
||||
validateAgainstEventProperties(propName);
|
||||
validateAgainstUnknownProperties(lView, element, propName, tNode);
|
||||
ngDevMode.rendererSetProperty++;
|
||||
}
|
||||
|
||||
savePropertyDebugData(tNode, lView, propName, lView[TVIEW].data, nativeOnly);
|
||||
|
||||
//////////////////////////
|
||||
//// Directive
|
||||
//////////////////////////
|
||||
const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER];
|
||||
// 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.tagName || '', propName) as any) : value;
|
||||
if (isProceduralRenderer(renderer)) {
|
||||
renderer.setProperty(element as RElement, propName, value);
|
||||
} else if (!isAnimationProp(propName)) {
|
||||
(element as RElement).setProperty ? (element as any).setProperty(propName, value) :
|
||||
(element as any)[propName] = value;
|
||||
}
|
||||
} else if (tNode.type === TNodeType.Container) {
|
||||
// If the node is a container and the property didn't
|
||||
// match any of the inputs or schemas we should throw.
|
||||
if (ngDevMode && !matchingSchemas(lView, tNode.tagName)) {
|
||||
throw createUnknownPropertyError(propName, tNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** If node is an OnPush component, marks its LView dirty. */
|
||||
function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
|
||||
ngDevMode && assertLView(lView);
|
||||
const childComponentLView = getComponentViewByIndex(viewIndex, lView);
|
||||
if (!(childComponentLView[FLAGS] & LViewFlags.CheckAlways)) {
|
||||
childComponentLView[FLAGS] |= LViewFlags.Dirty;
|
||||
}
|
||||
}
|
||||
|
||||
function setNgReflectProperties(
|
||||
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue,
|
||||
value: any) {
|
||||
for (let i = 0; i < inputs.length; i += 3) {
|
||||
const renderer = lView[RENDERER];
|
||||
const attrName = normalizeDebugBindingName(inputs[i + 2] as string);
|
||||
const debugValue = normalizeDebugBindingValue(value);
|
||||
if (type === TNodeType.Element) {
|
||||
isProceduralRenderer(renderer) ?
|
||||
renderer.setAttribute((element as RElement), attrName, debugValue) :
|
||||
(element as RElement).setAttribute(attrName, debugValue);
|
||||
} else if (value !== undefined) {
|
||||
const value = `bindings=${JSON.stringify({[attrName]: debugValue}, null, 2)}`;
|
||||
if (isProceduralRenderer(renderer)) {
|
||||
renderer.setValue((element as RComment), value);
|
||||
} else {
|
||||
(element as RComment).textContent = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateAgainstUnknownProperties(
|
||||
hostView: LView, element: RElement | RComment, propName: string, tNode: TNode) {
|
||||
// If the tag matches any of the schemas we shouldn't throw.
|
||||
if (matchingSchemas(hostView, tNode.tagName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If prop is not a known property of the HTML element...
|
||||
if (!(propName in element) &&
|
||||
// and we are in a browser context... (web worker nodes should be skipped)
|
||||
typeof Node === 'function' && element instanceof Node &&
|
||||
// and isn't a synthetic animation property...
|
||||
propName[0] !== ANIMATION_PROP_PREFIX) {
|
||||
// ... it is probably a user error and we should throw.
|
||||
throw createUnknownPropertyError(propName, tNode);
|
||||
}
|
||||
}
|
||||
|
||||
function matchingSchemas(hostView: LView, tagName: string | null): boolean {
|
||||
const schemas = hostView[TVIEW].schemas;
|
||||
|
||||
if (schemas !== null) {
|
||||
for (let i = 0; i < schemas.length; i++) {
|
||||
const schema = schemas[i];
|
||||
if (schema === NO_ERRORS_SCHEMA ||
|
||||
schema === CUSTOM_ELEMENTS_SCHEMA && tagName && tagName.indexOf('-') > -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores debugging data for this property binding on first template pass.
|
||||
* This enables features like DebugElement.properties.
|
||||
*/
|
||||
function savePropertyDebugData(
|
||||
tNode: TNode, lView: LView, propName: string, tData: TData,
|
||||
nativeOnly: boolean | undefined): void {
|
||||
const lastBindingIndex = lView[BINDING_INDEX] - 1;
|
||||
|
||||
// Bind/interpolation functions save binding metadata in the last binding index,
|
||||
// but leave the property name blank. If the interpolation delimiter is at the 0
|
||||
// index, we know that this is our first pass and the property name still needs to
|
||||
// be set.
|
||||
const bindingMetadata = tData[lastBindingIndex] as string;
|
||||
if (bindingMetadata[0] == INTERPOLATION_DELIMITER) {
|
||||
tData[lastBindingIndex] = propName + bindingMetadata;
|
||||
|
||||
// We don't want to store indices for host bindings because they are stored in a
|
||||
// different part of LView (the expando section).
|
||||
if (!nativeOnly) {
|
||||
if (tNode.propertyMetadataStartIndex == -1) {
|
||||
tNode.propertyMetadataStartIndex = lastBindingIndex;
|
||||
}
|
||||
tNode.propertyMetadataEndIndex = lastBindingIndex + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error that should be thrown when encountering an unknown property on an element.
|
||||
* @param propName Name of the invalid property.
|
||||
* @param tNode Node on which we encountered the error.
|
||||
*/
|
||||
function createUnknownPropertyError(propName: string, tNode: TNode): Error {
|
||||
return new Error(
|
||||
`Template error: Can't bind to '${propName}' since it isn't a known property of '${tNode.tagName}'.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a root component.
|
||||
@ -1515,7 +1672,7 @@ export function checkView<T>(hostView: LView, component: T) {
|
||||
creationMode && executeViewQueryFn(RenderFlags.Create, hostTView, component);
|
||||
|
||||
// Reset the selected index so we can assert that `select` was called later
|
||||
ngDevMode && setSelectedIndex(-1);
|
||||
setSelectedIndex(-1);
|
||||
|
||||
templateFn(getRenderFlags(hostView), component);
|
||||
|
||||
|
Reference in New Issue
Block a user