fix(ivy): match attribute selectors for content projection with inline-templates (#29041)
The content projection mechanism is static, in that it only looks at the static template nodes before directives are matched and change detection is run. When you have a selector-based content projection the selection is based on nodes that are available in the template. For example: ``` <ng-content selector="[some-attr]"></ng-content> ``` would match ``` <div some-attr="..."></div> ``` If you have an inline-template in your projected nodes. For example: ``` <div *ngIf="..." some-attr="..."></div> ``` This gets pre-parsed and converted to a canonical form. For example: ``` <ng-template [ngIf]="..."> <div some-attr=".."></div> </ng-template> ``` Note that only structural attributes (e.g. `*ngIf`) stay with the `<ng-template>` node. The other attributes move to the contained element inside the template. When this happens in ivy, the ng-template content is removed from the component template function and is compiled into its own template function. But this means that the information about the attributes that were on the content are lost and the projection selection mechanism is unable to match the original `<div *ngIf="..." some-attr>`. This commit adds support for this in ivy. Attributes are separated into three groups (Bindings, Templates and "other"). For inline-templates the Bindings and "other" types are hoisted back from the contained node to the `template()` instruction, so that they can be used in content projection matching. PR Close #29041
This commit is contained in:

committed by
Kara Erickson

parent
e3a401d20c
commit
f535f31d78
@ -17,10 +17,11 @@ import {getComponentDef, getDirectiveDef, getPipeDef} from './definition';
|
||||
import {NG_ELEMENT_ID} from './fields';
|
||||
import {DirectiveDef} from './interfaces/definition';
|
||||
import {NO_PARENT_INJECTOR, NodeInjectorFactory, PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags, TNODE, isFactory} from './interfaces/injector';
|
||||
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, isNameOnlyAttributeMarker} from './interfaces/node';
|
||||
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node';
|
||||
import {DECLARATION_VIEW, INJECTOR, LView, TData, TVIEW, TView, T_HOST} from './interfaces/view';
|
||||
import {assertNodeOfPossibleTypes} from './node_assert';
|
||||
import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state';
|
||||
import {isNameOnlyAttributeMarker} from './util/attrs_utils';
|
||||
import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './util/injector_utils';
|
||||
import {renderStringify} from './util/misc_utils';
|
||||
import {findComponentView} from './util/view_traversal_utils';
|
||||
|
@ -149,10 +149,6 @@ export const enum AttributeMarker {
|
||||
Template = 4,
|
||||
}
|
||||
|
||||
export function isNameOnlyAttributeMarker(marker: string | AttributeMarker) {
|
||||
return marker === AttributeMarker.Bindings || marker === AttributeMarker.Template;
|
||||
}
|
||||
|
||||
/**
|
||||
* A combination of:
|
||||
* - attribute names and values
|
||||
|
@ -10,9 +10,10 @@ import '../util/ng_dev_mode';
|
||||
|
||||
import {assertDefined, assertNotEqual} from '../util/assert';
|
||||
|
||||
import {AttributeMarker, TAttributes, TNode, TNodeType, isNameOnlyAttributeMarker, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
|
||||
import {AttributeMarker, TAttributes, TNode, TNodeType, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
|
||||
import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
|
||||
import {getInitialClassNameValue} from './styling/class_and_style_bindings';
|
||||
import {isNameOnlyAttributeMarker} from './util/attrs_utils';
|
||||
|
||||
const unusedValueToPlacateAjd = unused1 + unused2;
|
||||
|
||||
@ -35,7 +36,7 @@ function isCssClassMatching(nodeClassAttrVal: string, cssClassToMatch: string):
|
||||
/**
|
||||
* Function that checks whether a given tNode matches tag-based selector and has a valid type.
|
||||
*
|
||||
* Matching can be perfomed in 2 modes: projection mode (when we project nodes) and regular
|
||||
* Matching can be performed in 2 modes: projection mode (when we project nodes) and regular
|
||||
* directive matching mode. In "projection" mode, we do not need to check types, so if tag name
|
||||
* matches selector, we declare a match. In "directive matching" mode, we also check whether tNode
|
||||
* is of expected type:
|
||||
@ -53,8 +54,10 @@ function hasTagAndTypeMatch(
|
||||
/**
|
||||
* A utility function to match an Ivy node static data against a simple CSS selector
|
||||
*
|
||||
* @param node static data to match
|
||||
* @param selector
|
||||
* @param node static data of the node to match
|
||||
* @param selector The selector to try matching against the node.
|
||||
* @param isProjectionMode if `true` we are matching for content projection, otherwise we are doing
|
||||
* directive matching.
|
||||
* @returns true if node matches the selector.
|
||||
*/
|
||||
export function isNodeMatchingSelector(
|
||||
@ -64,14 +67,7 @@ export function isNodeMatchingSelector(
|
||||
const nodeAttrs = tNode.attrs || [];
|
||||
|
||||
// Find the index of first attribute that has no value, only a name.
|
||||
let nameOnlyMarkerIdx = nodeAttrs && nodeAttrs.length;
|
||||
for (let i = 0; i < nodeAttrs.length; i++) {
|
||||
const nodeAttr = nodeAttrs[i];
|
||||
if (isNameOnlyAttributeMarker(nodeAttr)) {
|
||||
nameOnlyMarkerIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const nameOnlyMarkerIdx = getNameOnlyMarkerIndex(nodeAttrs);
|
||||
|
||||
// When processing ":not" selectors, we skip to the next ":not" if the
|
||||
// current one doesn't match
|
||||
@ -114,8 +110,11 @@ export function isNodeMatchingSelector(
|
||||
continue;
|
||||
}
|
||||
|
||||
const isInlineTemplate =
|
||||
tNode.type == TNodeType.Container && tNode.tagName !== NG_TEMPLATE_SELECTOR;
|
||||
const attrName = (mode & SelectorFlags.CLASS) ? 'class' : current;
|
||||
const attrIndexInNode = findAttrIndexInNode(attrName, nodeAttrs);
|
||||
const attrIndexInNode =
|
||||
findAttrIndexInNode(attrName, nodeAttrs, isInlineTemplate, isProjectionMode);
|
||||
|
||||
if (attrIndexInNode === -1) {
|
||||
if (isPositive(mode)) return false;
|
||||
@ -125,12 +124,11 @@ export function isNodeMatchingSelector(
|
||||
|
||||
if (selectorAttrValue !== '') {
|
||||
let nodeAttrValue: string;
|
||||
const maybeAttrName = nodeAttrs[attrIndexInNode];
|
||||
if (attrIndexInNode > nameOnlyMarkerIdx) {
|
||||
nodeAttrValue = '';
|
||||
} else {
|
||||
ngDevMode && assertNotEqual(
|
||||
maybeAttrName, AttributeMarker.NamespaceURI,
|
||||
nodeAttrs[attrIndexInNode], AttributeMarker.NamespaceURI,
|
||||
'We do not match directives on namespaced attributes');
|
||||
nodeAttrValue = nodeAttrs[attrIndexInNode + 1] as string;
|
||||
}
|
||||
@ -164,34 +162,64 @@ function readClassValueFromTNode(tNode: TNode): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines an attribute's definition array from a node to find the index of the
|
||||
* attribute with the specified name.
|
||||
* Examines the attribute's definition array for a node to find the index of the
|
||||
* attribute that matches the given `name`.
|
||||
*
|
||||
* NOTE: Will not find namespaced attributes.
|
||||
* NOTE: This will not match namespaced attributes.
|
||||
*
|
||||
* Attribute matching depends upon `isInlineTemplate` and `isProjectionMode`.
|
||||
* The following table summarizes which types of attributes we attempt to match:
|
||||
*
|
||||
* =========================================================================================
|
||||
* Modes | Normal Attributes | Bindings Attributes | Template Attributes
|
||||
* =========================================================================================
|
||||
* Inline + Projection | YES | YES | NO
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* Inline + Directive | NO | NO | YES
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* Non-inline + Projection | YES | YES | NO
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* Non-inline + Directive | YES | YES | NO
|
||||
* =========================================================================================
|
||||
*
|
||||
* @param name the name of the attribute to find
|
||||
* @param attrs the attribute array to examine
|
||||
* @param isInlineTemplate true if the node being matched is an inline template (e.g. `*ngFor`)
|
||||
* rather than a manually expanded template node (e.g `<ng-template>`).
|
||||
* @param isProjectionMode true if we are matching against content projection otherwise we are
|
||||
* matching against directives.
|
||||
*/
|
||||
function findAttrIndexInNode(name: string, attrs: TAttributes | null): number {
|
||||
function findAttrIndexInNode(
|
||||
name: string, attrs: TAttributes | null, isInlineTemplate: boolean,
|
||||
isProjectionMode: boolean): number {
|
||||
if (attrs === null) return -1;
|
||||
let nameOnlyMode = false;
|
||||
let i = 0;
|
||||
while (i < attrs.length) {
|
||||
const maybeAttrName = attrs[i];
|
||||
if (maybeAttrName === name) {
|
||||
return i;
|
||||
} else if (maybeAttrName === AttributeMarker.NamespaceURI) {
|
||||
// NOTE(benlesh): will not find namespaced attributes. This is by design.
|
||||
i += 4;
|
||||
} else {
|
||||
if (isNameOnlyAttributeMarker(maybeAttrName)) {
|
||||
nameOnlyMode = true;
|
||||
}
|
||||
i += nameOnlyMode ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
let i = 0;
|
||||
|
||||
if (isProjectionMode || !isInlineTemplate) {
|
||||
let bindingsMode = false;
|
||||
while (i < attrs.length) {
|
||||
const maybeAttrName = attrs[i];
|
||||
if (maybeAttrName === name) {
|
||||
return i;
|
||||
} else if (maybeAttrName === AttributeMarker.Bindings) {
|
||||
bindingsMode = true;
|
||||
} else if (maybeAttrName === AttributeMarker.Template) {
|
||||
// We do not care about Template attributes in this scenario.
|
||||
break;
|
||||
} else if (maybeAttrName === AttributeMarker.NamespaceURI) {
|
||||
// Skip the whole namespaced attribute and value. This is by design.
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
// In binding mode there are only names, rather than name-value pairs.
|
||||
i += bindingsMode ? 1 : 2;
|
||||
}
|
||||
// We did not match the attribute
|
||||
return -1;
|
||||
} else {
|
||||
return matchTemplateAttribute(attrs, name);
|
||||
}
|
||||
}
|
||||
|
||||
export function isNodeMatchingSelectorList(
|
||||
@ -222,8 +250,8 @@ export function getProjectAsAttrValue(tNode: TNode): string|null {
|
||||
* Checks a given node against matching projection selectors and returns
|
||||
* selector index (or 0 if none matched).
|
||||
*
|
||||
* This function takes into account the ngProjectAs attribute: if present its value will be compared
|
||||
* to the raw (un-parsed) CSS selector instead of using standard selector matching logic.
|
||||
* This function takes into account the ngProjectAs attribute: if present its value will be
|
||||
* compared to the raw (un-parsed) CSS selector instead of using standard selector matching logic.
|
||||
*/
|
||||
export function matchingProjectionSelectorIndex(
|
||||
tNode: TNode, selectors: CssSelectorList[], textSelectors: string[]): number {
|
||||
@ -239,3 +267,25 @@ export function matchingProjectionSelectorIndex(
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getNameOnlyMarkerIndex(nodeAttrs: TAttributes) {
|
||||
for (let i = 0; i < nodeAttrs.length; i++) {
|
||||
const nodeAttr = nodeAttrs[i];
|
||||
if (isNameOnlyAttributeMarker(nodeAttr)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return nodeAttrs.length;
|
||||
}
|
||||
|
||||
function matchTemplateAttribute(attrs: TAttributes, name: string): number {
|
||||
let i = attrs.indexOf(AttributeMarker.Template);
|
||||
if (i > -1) {
|
||||
i++;
|
||||
while (i < attrs.length) {
|
||||
if (attrs[i] === name) return i;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
@ -105,3 +105,14 @@ export function attrsStylingIndexOf(attrs: TAttributes, startIndex: number): num
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the given value is a marker that indicates that the following
|
||||
* attribute values in a `TAttributes` array are only the names of attributes,
|
||||
* and not name-value pairs.
|
||||
* @param marker The attribute marker to test.
|
||||
* @returns true if the marker is a "name-only" marker (e.g. `Bindings` or `Template`).
|
||||
*/
|
||||
export function isNameOnlyAttributeMarker(marker: string | AttributeMarker) {
|
||||
return marker === AttributeMarker.Bindings || marker === AttributeMarker.Template;
|
||||
}
|
||||
|
Reference in New Issue
Block a user