perf(ivy): avoid storing raw selectors in projectionDef (#29578)
Currently in Ivy we pass both the raw and parsed selectors to the projectionDef instruction, because the parsed selectors are used to match most nodes, whereas the raw ones are used to match against nodes with the ngProjectAs attribute. The raw selectors add a fair bit of code that won't be used in most cases, because ngProjectAs is somewhat rare. These changes rework the compiler not to output the raw selectors in the projectionDef, but to parse the selector in ngProjectAs and to store it on the TAttributes. The logic for matching has also been changed so that it matches the pre-parsed ngProjectAs selector against the list of projection selectors. PR Close #29578
This commit is contained in:
parent
f98093a30d
commit
def73a6728
|
@ -1169,7 +1169,6 @@ describe('compiler compliance', () => {
|
|||
const $c3$ = ["id","first"];
|
||||
const $c4$ = ["id","second"];
|
||||
const $c1$ = [[["span", "title", "tofirst"]], [["span", "title", "tosecond"]]];
|
||||
const $c2$ = ["span[title=toFirst]", "span[title=toSecond]"];
|
||||
…
|
||||
ComplexComponent.ngComponentDef = $r3$.ΔdefineComponent({
|
||||
type: ComplexComponent,
|
||||
|
@ -1180,7 +1179,7 @@ describe('compiler compliance', () => {
|
|||
vars: 0,
|
||||
template: function ComplexComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ΔprojectionDef($c1$, $c2$);
|
||||
$r3$.ΔprojectionDef($c1$);
|
||||
$r3$.ΔelementStart(0, "div", $c3$);
|
||||
$r3$.Δprojection(1, 1);
|
||||
$r3$.ΔelementEnd();
|
||||
|
@ -1252,11 +1251,10 @@ describe('compiler compliance', () => {
|
|||
}
|
||||
}
|
||||
const $_c4$ = [[["span", "title", "tofirst"]]];
|
||||
const $_c5$ = ["span[title=toFirst]"];
|
||||
…
|
||||
template: function Cmp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ΔprojectionDef($_c4$, $_c5$);
|
||||
$r3$.ΔprojectionDef($_c4$);
|
||||
$r3$.Δtemplate(0, Cmp_div_0_Template, 2, 0, "div", $_c0$);
|
||||
$r3$.Δtemplate(1, Cmp_div_1_Template, 2, 0, "div", $_c1$);
|
||||
$r3$.Δtemplate(2, Cmp_ng_template_2_Template, 2, 0, "ng-template");
|
||||
|
@ -1326,7 +1324,7 @@ describe('compiler compliance', () => {
|
|||
…
|
||||
template: function Cmp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ΔprojectionDef($_c2$, $_c3$);
|
||||
$r3$.ΔprojectionDef($_c2$);
|
||||
$r3$.Δprojection(0, 1);
|
||||
$r3$.Δtemplate(1, Cmp_ng_template_1_Template, 2, 0, "ng-template");
|
||||
$r3$.Δtemplate(2, Cmp_ng_template_2_Template, 2, 0, "ng-template");
|
||||
|
@ -1338,6 +1336,117 @@ describe('compiler compliance', () => {
|
|||
const {source} = compile(files, angularFiles);
|
||||
expectEmit(source, output, 'Invalid content projection instructions generated');
|
||||
});
|
||||
|
||||
it('should parse the selector that is passed into ngProjectAs', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'simple',
|
||||
template: '<div><ng-content select="[title]"></ng-content></div>'
|
||||
})
|
||||
export class SimpleComponent {}
|
||||
|
||||
@NgModule({declarations: [SimpleComponent]})
|
||||
export class MyModule {}
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: '<simple><h1 ngProjectAs="[title]"></h1></simple>'
|
||||
})
|
||||
export class MyApp {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
// Note that the c0 and c1 constants aren't being used in this particular test,
|
||||
// but they are used in some of the logic that is folded under the ellipsis.
|
||||
const SimpleComponentDefinition = `
|
||||
const $_c0$ = [[["", "title", ""]]];
|
||||
const $_c1$ = ["[title]"];
|
||||
const $_c2$ = [5, ["", "title", ""]];
|
||||
…
|
||||
MyApp.ngComponentDef = $r3$.ΔdefineComponent({
|
||||
type: MyApp,
|
||||
selectors: [["my-app"]],
|
||||
factory: function MyApp_Factory(t) {
|
||||
return new(t || MyApp)();
|
||||
},
|
||||
consts: 2,
|
||||
vars: 0,
|
||||
template: function MyApp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ΔelementStart(0, "simple");
|
||||
$r3$.Δelement(1, "h1", $_c2$);
|
||||
$r3$.ΔelementEnd();
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
})`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
|
||||
expectEmit(
|
||||
result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition');
|
||||
});
|
||||
|
||||
it('should take the first selector if multiple values are passed into ngProjectAs', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'simple',
|
||||
template: '<div><ng-content select="[title]"></ng-content></div>'
|
||||
})
|
||||
export class SimpleComponent {}
|
||||
|
||||
@NgModule({declarations: [SimpleComponent]})
|
||||
export class MyModule {}
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: '<simple><h1 ngProjectAs="[title],[header]"></h1></simple>'
|
||||
})
|
||||
export class MyApp {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
// Note that the c0 and c1 constants aren't being used in this particular test,
|
||||
// but they are used in some of the logic that is folded under the ellipsis.
|
||||
const SimpleComponentDefinition = `
|
||||
const $_c0$ = [[["", "title", ""]]];
|
||||
const $_c1$ = ["[title]"];
|
||||
const $_c2$ = [5, ["", "title", ""]];
|
||||
…
|
||||
MyApp.ngComponentDef = $r3$.ΔdefineComponent({
|
||||
type: MyApp,
|
||||
selectors: [["my-app"]],
|
||||
factory: function MyApp_Factory(t) {
|
||||
return new(t || MyApp)();
|
||||
},
|
||||
consts: 2,
|
||||
vars: 0,
|
||||
template: function MyApp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ΔelementStart(0, "simple");
|
||||
$r3$.Δelement(1, "h1", $_c2$);
|
||||
$r3$.ΔelementEnd();
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
})`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
|
||||
expectEmit(
|
||||
result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('queries', () => {
|
||||
|
|
|
@ -475,4 +475,21 @@ export const enum AttributeMarker {
|
|||
* ```
|
||||
*/
|
||||
Template = 4,
|
||||
|
||||
/**
|
||||
* Signals that the following attribute is `ngProjectAs` and its value is a parsed `CssSelector`.
|
||||
*
|
||||
* For example, given the following HTML:
|
||||
*
|
||||
* ```
|
||||
* <h1 attr="value" ngProjectAs="[title]">
|
||||
* ```
|
||||
*
|
||||
* the generated code for the `element()` instruction would include:
|
||||
*
|
||||
* ```
|
||||
* ['attr', 'value', AttributeMarker.ProjectAs, ['', 'title', '']]
|
||||
* ```
|
||||
*/
|
||||
ProjectAs = 5
|
||||
}
|
||||
|
|
|
@ -45,6 +45,9 @@ const DEFAULT_NG_CONTENT_SELECTOR = '*';
|
|||
// Selector attribute name of `<ng-content>`
|
||||
const NG_CONTENT_SELECT_ATTR = 'select';
|
||||
|
||||
// Attribute name of `ngProjectAs`.
|
||||
const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs';
|
||||
|
||||
// List of supported global targets for event listeners
|
||||
const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>(
|
||||
[['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]);
|
||||
|
@ -264,11 +267,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
// Only selectors with a non-default value are generated
|
||||
if (this._ngContentSelectors.length) {
|
||||
const r3Selectors = this._ngContentSelectors.map(s => core.parseSelectorToR3Selector(s));
|
||||
// `projectionDef` needs both the parsed and raw value of the selectors
|
||||
const parsed = this.constantPool.getConstLiteral(asLiteral(r3Selectors), true);
|
||||
const unParsed =
|
||||
this.constantPool.getConstLiteral(asLiteral(this._ngContentSelectors), true);
|
||||
parameters.push(parsed, unParsed);
|
||||
parameters.push(this.constantPool.getConstLiteral(asLiteral(r3Selectors), true));
|
||||
}
|
||||
|
||||
// Since we accumulate ngContent selectors while processing template elements,
|
||||
|
@ -475,18 +474,19 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
0 :
|
||||
this._ngContentSelectors.push(ngContent.selector) + this._ngContentSelectorsOffset;
|
||||
const parameters: o.Expression[] = [o.literal(slot)];
|
||||
|
||||
const attributeAsList: string[] = [];
|
||||
const attributes: o.Expression[] = [];
|
||||
|
||||
ngContent.attributes.forEach((attribute) => {
|
||||
const {name, value} = attribute;
|
||||
if (name.toLowerCase() !== NG_CONTENT_SELECT_ATTR) {
|
||||
attributeAsList.push(name, value);
|
||||
if (name === NG_PROJECT_AS_ATTR_NAME) {
|
||||
attributes.push(...getNgProjectAsLiteral(attribute));
|
||||
} else if (name.toLowerCase() !== NG_CONTENT_SELECT_ATTR) {
|
||||
attributes.push(o.literal(name), o.literal(value));
|
||||
}
|
||||
});
|
||||
|
||||
if (attributeAsList.length > 0) {
|
||||
parameters.push(o.literal(selectorIndex), asLiteral(attributeAsList));
|
||||
if (attributes.length > 0) {
|
||||
parameters.push(o.literal(selectorIndex), o.literalArr(attributes));
|
||||
} else if (selectorIndex !== 0) {
|
||||
parameters.push(o.literal(selectorIndex));
|
||||
}
|
||||
|
@ -574,7 +574,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
});
|
||||
|
||||
outputAttrs.forEach(attr => {
|
||||
attributes.push(...getAttributeNameLiterals(attr.name), o.literal(attr.value));
|
||||
if (attr.name === NG_PROJECT_AS_ATTR_NAME) {
|
||||
attributes.push(...getNgProjectAsLiteral(attr));
|
||||
} else {
|
||||
attributes.push(...getAttributeNameLiterals(attr.name), o.literal(attr.value));
|
||||
}
|
||||
});
|
||||
|
||||
// add attributes for directive and projection matching purposes
|
||||
|
@ -1571,6 +1575,17 @@ function createCssSelector(tag: string, attributes: {[name: string]: string}): C
|
|||
return cssSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of expressions out of an `ngProjectAs` attributes
|
||||
* which can be added to the instruction parameters.
|
||||
*/
|
||||
function getNgProjectAsLiteral(attribute: t.TextAttribute): o.Expression[] {
|
||||
// Parse the attribute value into a CssSelectorList. Note that we only take the
|
||||
// first selector, because we don't support multiple selectors in ngProjectAs.
|
||||
const parsedR3Selector = core.parseSelectorToR3Selector(attribute.value)[0];
|
||||
return [o.literal(core.AttributeMarker.ProjectAs), asLiteral(parsedR3Selector)];
|
||||
}
|
||||
|
||||
function interpolate(args: o.Expression[]): o.Expression {
|
||||
args = args.slice(1); // Ignore the length prefix added for render2
|
||||
switch (args.length) {
|
||||
|
|
|
@ -5,15 +5,17 @@
|
|||
* 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 {TElementNode, TNode, TNodeType} from '../interfaces/node';
|
||||
import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node';
|
||||
import {CssSelectorList} from '../interfaces/projection';
|
||||
import {T_HOST} from '../interfaces/view';
|
||||
import {appendProjectedNodes} from '../node_manipulation';
|
||||
import {matchingProjectionSelectorIndex} from '../node_selector_matcher';
|
||||
import {getLView, setIsParent} from '../state';
|
||||
import {findComponentView} from '../util/view_traversal_utils';
|
||||
|
||||
import {createNodeAtIndex} from './shared';
|
||||
|
||||
|
||||
/**
|
||||
* Instruction to distribute projectable nodes among <ng-content> occurrences in a given template.
|
||||
* It takes all the selectors from the entire component's template and decides where
|
||||
|
@ -37,7 +39,7 @@ import {createNodeAtIndex} from './shared';
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function ΔprojectionDef(selectors?: CssSelectorList[], textSelectors?: string[]): void {
|
||||
export function ΔprojectionDef(selectors?: CssSelectorList[]): void {
|
||||
const componentNode = findComponentView(getLView())[T_HOST] as TElementNode;
|
||||
|
||||
if (!componentNode.projection) {
|
||||
|
@ -49,9 +51,8 @@ export function ΔprojectionDef(selectors?: CssSelectorList[], textSelectors?: s
|
|||
let componentChild: TNode|null = componentNode.child;
|
||||
|
||||
while (componentChild !== null) {
|
||||
const bucketIndex = selectors ?
|
||||
matchingProjectionSelectorIndex(componentChild, selectors, textSelectors !) :
|
||||
0;
|
||||
const bucketIndex =
|
||||
selectors ? matchingProjectionSelectorIndex(componentChild, selectors) : 0;
|
||||
|
||||
if (tails[bucketIndex]) {
|
||||
tails[bucketIndex] !.projectionNext = componentChild;
|
||||
|
@ -77,7 +78,8 @@ export function ΔprojectionDef(selectors?: CssSelectorList[], textSelectors?: s
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function Δprojection(nodeIndex: number, selectorIndex: number = 0, attrs?: string[]): void {
|
||||
export function Δprojection(
|
||||
nodeIndex: number, selectorIndex: number = 0, attrs?: TAttributes): void {
|
||||
const lView = getLView();
|
||||
const tProjectionNode =
|
||||
createNodeAtIndex(nodeIndex, TNodeType.Projection, null, null, attrs || null);
|
||||
|
|
|
@ -1360,18 +1360,22 @@ function generateInitialInputs(
|
|||
// We do not allow inputs on namespaced attributes.
|
||||
i += 4;
|
||||
continue;
|
||||
} else if (attrName === AttributeMarker.ProjectAs) {
|
||||
// Skip over the `ngProjectAs` value.
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we hit any other attribute markers, we're done anyway. None of those are valid inputs.
|
||||
if (typeof attrName === 'number') break;
|
||||
|
||||
const minifiedInputName = inputs[attrName];
|
||||
const minifiedInputName = inputs[attrName as string];
|
||||
const attrValue = attrs[i + 1];
|
||||
|
||||
if (minifiedInputName !== undefined) {
|
||||
const inputsToStore: InitialInputs =
|
||||
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
|
||||
inputsToStore.push(attrName, minifiedInputName, attrValue as string);
|
||||
inputsToStore.push(attrName as string, minifiedInputName, attrValue as string);
|
||||
}
|
||||
|
||||
i += 2;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CssSelector} from './projection';
|
||||
import {RNode} from './renderer';
|
||||
import {StylingContext} from './styling';
|
||||
import {LView, TView} from './view';
|
||||
|
@ -163,14 +164,32 @@ export const enum AttributeMarker {
|
|||
* ```
|
||||
*/
|
||||
Template = 4,
|
||||
|
||||
/**
|
||||
* Signals that the following attribute is `ngProjectAs` and its value is a parsed `CssSelector`.
|
||||
*
|
||||
* For example, given the following HTML:
|
||||
*
|
||||
* ```
|
||||
* <h1 attr="value" ngProjectAs="[title]">
|
||||
* ```
|
||||
*
|
||||
* the generated code for the `element()` instruction would include:
|
||||
*
|
||||
* ```
|
||||
* ['attr', 'value', AttributeMarker.ProjectAs, ['', 'title', '']]
|
||||
* ```
|
||||
*/
|
||||
ProjectAs = 5
|
||||
}
|
||||
|
||||
/**
|
||||
* A combination of:
|
||||
* - attribute names and values
|
||||
* - special markers acting as flags to alter attributes processing.
|
||||
* - Attribute names and values.
|
||||
* - Special markers acting as flags to alter attributes processing.
|
||||
* - Parsed ngProjectAs selectors.
|
||||
*/
|
||||
export type TAttributes = (string | AttributeMarker)[];
|
||||
export type TAttributes = (string | AttributeMarker | CssSelector)[];
|
||||
|
||||
/**
|
||||
* Binding data (flyweight) for a particular node that is shared between all templates
|
||||
|
|
|
@ -65,8 +65,6 @@ export const enum SelectorFlags {
|
|||
CLASS = 0b1000,
|
||||
}
|
||||
|
||||
export const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs';
|
||||
|
||||
// Note: This hack is necessary so we don't erroneously get a circular dependency
|
||||
// failure based on types.
|
||||
export const unusedValueExportToPlacateAjd = 1;
|
||||
|
|
|
@ -11,7 +11,7 @@ import '../util/ng_dev_mode';
|
|||
import {assertDefined, assertNotEqual} from '../util/assert';
|
||||
|
||||
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 {CssSelector, CssSelectorList, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
|
||||
import {getInitialClassNameValue} from './styling/class_and_style_bindings';
|
||||
import {isNameOnlyAttributeMarker} from './util/attrs_utils';
|
||||
|
||||
|
@ -234,14 +234,14 @@ export function isNodeMatchingSelectorList(
|
|||
return false;
|
||||
}
|
||||
|
||||
export function getProjectAsAttrValue(tNode: TNode): string|null {
|
||||
export function getProjectAsAttrValue(tNode: TNode): CssSelector|null {
|
||||
const nodeAttrs = tNode.attrs;
|
||||
if (nodeAttrs != null) {
|
||||
const ngProjectAsAttrIdx = nodeAttrs.indexOf(NG_PROJECT_AS_ATTR_NAME);
|
||||
const ngProjectAsAttrIdx = nodeAttrs.indexOf(AttributeMarker.ProjectAs);
|
||||
// only check for ngProjectAs in attribute names, don't accidentally match attribute's value
|
||||
// (attribute names are stored at even indexes)
|
||||
if ((ngProjectAsAttrIdx & 1) === 0) {
|
||||
return nodeAttrs[ngProjectAsAttrIdx + 1] as string;
|
||||
return nodeAttrs[ngProjectAsAttrIdx + 1] as CssSelector;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -251,18 +251,19 @@ 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 parsed ngProjectAs selector from the node's attributes.
|
||||
* If present, it will check whether the ngProjectAs selector matches any of the projection
|
||||
* selectors.
|
||||
*/
|
||||
export function matchingProjectionSelectorIndex(
|
||||
tNode: TNode, selectors: CssSelectorList[], textSelectors: string[]): number {
|
||||
tNode: TNode, selectors: CssSelectorList[]): number {
|
||||
const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
|
||||
for (let i = 0; i < selectors.length; i++) {
|
||||
// if a node has the ngProjectAs attribute match it against unparsed selector
|
||||
// match a node against a parsed selector only if ngProjectAs attribute is not present
|
||||
if (ngProjectAsAttrVal === textSelectors[i] ||
|
||||
ngProjectAsAttrVal === null &&
|
||||
isNodeMatchingSelectorList(tNode, selectors[i], /* isProjectionMode */ true)) {
|
||||
// If we ran into an `ngProjectAs` attribute, we should match its parsed selector
|
||||
// to the list of selectors, otherwise we fall back to matching against the node.
|
||||
if (ngProjectAsAttrVal === null ?
|
||||
isNodeMatchingSelectorList(tNode, selectors[i], /* isProjectionMode */ true) :
|
||||
isSelectorInSelectorList(ngProjectAsAttrVal, selectors[i])) {
|
||||
return i + 1; // first matching selector "captures" a given node
|
||||
}
|
||||
}
|
||||
|
@ -290,3 +291,24 @@ function matchTemplateAttribute(attrs: TAttributes, name: string): number {
|
|||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a selector is inside a CssSelectorList
|
||||
* @param selector Selector to be checked.
|
||||
* @param list List in which to look for the selector.
|
||||
*/
|
||||
function isSelectorInSelectorList(selector: CssSelector, list: CssSelectorList): boolean {
|
||||
selectorListLoop: for (let i = 0; i < list.length; i++) {
|
||||
const currentSelectorInList = list[i];
|
||||
if (selector.length !== currentSelectorInList.length) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < selector.length; j++) {
|
||||
if (selector[j] !== currentSelectorInList[j]) {
|
||||
continue selectorListLoop;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -75,10 +75,10 @@ export function patchContextWithStaticAttrs(
|
|||
mode = attr;
|
||||
} else if (mode == AttributeMarker.Classes) {
|
||||
initialClasses = initialClasses || context[StylingIndex.InitialClassValuesPosition];
|
||||
patchInitialStylingValue(initialClasses, attr, true, directiveIndex);
|
||||
patchInitialStylingValue(initialClasses, attr as string, true, directiveIndex);
|
||||
} else if (mode == AttributeMarker.Styles) {
|
||||
initialStyles = initialStyles || context[StylingIndex.InitialStyleValuesPosition];
|
||||
patchInitialStylingValue(initialStyles, attr, attrs[++i], directiveIndex);
|
||||
patchInitialStylingValue(initialStyles, attr as string, attrs[++i], directiveIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AttributeMarker, TAttributes} from '../interfaces/node';
|
||||
import {NG_PROJECT_AS_ATTR_NAME} from '../interfaces/projection';
|
||||
import {CssSelector} from '../interfaces/projection';
|
||||
import {ProceduralRenderer3, RElement, isProceduralRenderer} from '../interfaces/renderer';
|
||||
import {RENDERER} from '../interfaces/view';
|
||||
import {getLView} from '../state';
|
||||
|
@ -70,19 +70,17 @@ export function setUpAttributes(native: RElement, attrs: TAttributes): number {
|
|||
/// attrName is string;
|
||||
const attrName = value as string;
|
||||
const attrVal = attrs[++i];
|
||||
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
|
||||
// Standard attributes
|
||||
ngDevMode && ngDevMode.rendererSetAttribute++;
|
||||
if (isAnimationProp(attrName)) {
|
||||
if (isProc) {
|
||||
(renderer as ProceduralRenderer3).setProperty(native, attrName, attrVal);
|
||||
}
|
||||
} else {
|
||||
isProc ?
|
||||
(renderer as ProceduralRenderer3)
|
||||
.setAttribute(native, attrName as string, attrVal as string) :
|
||||
native.setAttribute(attrName as string, attrVal as string);
|
||||
// Standard attributes
|
||||
ngDevMode && ngDevMode.rendererSetAttribute++;
|
||||
if (isAnimationProp(attrName)) {
|
||||
if (isProc) {
|
||||
(renderer as ProceduralRenderer3).setProperty(native, attrName, attrVal);
|
||||
}
|
||||
} else {
|
||||
isProc ?
|
||||
(renderer as ProceduralRenderer3)
|
||||
.setAttribute(native, attrName as string, attrVal as string) :
|
||||
native.setAttribute(attrName as string, attrVal as string);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
@ -113,6 +111,6 @@ export function attrsStylingIndexOf(attrs: TAttributes, startIndex: number): num
|
|||
* @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) {
|
||||
export function isNameOnlyAttributeMarker(marker: string | AttributeMarker | CssSelector) {
|
||||
return marker === AttributeMarker.Bindings || marker === AttributeMarker.Template;
|
||||
}
|
||||
|
|
|
@ -81,6 +81,76 @@ describe('projection', () => {
|
|||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
});
|
||||
|
||||
it('should support ngProjectAs on elements (including <ng-content>)', () => {
|
||||
@Component({
|
||||
selector: 'card',
|
||||
template: `
|
||||
<ng-content select="[card-title]"></ng-content>
|
||||
---
|
||||
<ng-content select="[card-content]"></ng-content>
|
||||
`
|
||||
})
|
||||
class Card {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'card-with-title',
|
||||
template: `
|
||||
<card>
|
||||
<h1 ngProjectAs="[card-title]">Title</h1>
|
||||
<ng-content ngProjectAs="[card-content]"></ng-content>
|
||||
</card>
|
||||
`
|
||||
})
|
||||
class CardWithTitle {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
template: `
|
||||
<card-with-title>content</card-with-title>
|
||||
`
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Card, CardWithTitle, App]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Compare the text output, because Ivy and ViewEngine produce slightly different HTML.
|
||||
expect(fixture.nativeElement.textContent).toContain('Title --- content');
|
||||
});
|
||||
|
||||
it('should not match multiple selectors in ngProjectAs', () => {
|
||||
@Component({
|
||||
selector: 'card',
|
||||
template: `
|
||||
<ng-content select="[card-title]"></ng-content>
|
||||
content
|
||||
`
|
||||
})
|
||||
class Card {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<card>
|
||||
<h1 ngProjectAs="[non-existing-title-slot],[card-title]">Title</h1>
|
||||
</card>
|
||||
`
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Card, App]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Compare the text output, because Ivy and ViewEngine produce slightly different HTML.
|
||||
expect(fixture.nativeElement.textContent).not.toContain('Title content');
|
||||
});
|
||||
|
||||
describe('on inline templates (e.g. *ngIf)', () => {
|
||||
it('should work when matching the element name', () => {
|
||||
let divDirectives = 0;
|
||||
|
|
|
@ -86,9 +86,6 @@
|
|||
{
|
||||
"name": "NG_PIPE_DEF"
|
||||
},
|
||||
{
|
||||
"name": "NG_PROJECT_AS_ATTR_NAME"
|
||||
},
|
||||
{
|
||||
"name": "NG_TEMPLATE_SELECTOR"
|
||||
},
|
||||
|
|
|
@ -131,9 +131,6 @@
|
|||
{
|
||||
"name": "NG_PIPE_DEF"
|
||||
},
|
||||
{
|
||||
"name": "NG_PROJECT_AS_ATTR_NAME"
|
||||
},
|
||||
{
|
||||
"name": "NG_TEMPLATE_SELECTOR"
|
||||
},
|
||||
|
|
|
@ -941,7 +941,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['div']]], ['div']);
|
||||
ΔprojectionDef([[['div']]]);
|
||||
Δprojection(0);
|
||||
Δtext(1, 'Before-');
|
||||
Δtemplate(2, IfTemplate, 1, 0, 'ng-template', [AttributeMarker.Bindings, 'ngIf']);
|
||||
|
@ -1531,9 +1531,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef(
|
||||
[[['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]],
|
||||
['span[title=toFirst]', 'span[title=toSecond]']);
|
||||
ΔprojectionDef([[['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]]);
|
||||
ΔelementStart(0, 'div', ['id', 'first']);
|
||||
{ Δprojection(1, 1); }
|
||||
ΔelementEnd();
|
||||
|
@ -1577,7 +1575,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['', 'title', '']]], ['[title]']);
|
||||
ΔprojectionDef([[['', 'title', '']]]);
|
||||
{ Δprojection(0, 1); }
|
||||
}
|
||||
}, 1);
|
||||
|
@ -1614,12 +1612,10 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef(
|
||||
[
|
||||
[['span', SelectorFlags.CLASS, 'toFirst']],
|
||||
[['span', SelectorFlags.CLASS, 'toSecond']]
|
||||
],
|
||||
['span.toFirst', 'span.toSecond']);
|
||||
ΔprojectionDef([
|
||||
[['span', SelectorFlags.CLASS, 'toFirst']],
|
||||
[['span', SelectorFlags.CLASS, 'toSecond']]
|
||||
]);
|
||||
ΔelementStart(0, 'div', ['id', 'first']);
|
||||
{ Δprojection(1, 1); }
|
||||
ΔelementEnd();
|
||||
|
@ -1663,12 +1659,10 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef(
|
||||
[
|
||||
[['span', SelectorFlags.CLASS, 'toFirst']],
|
||||
[['span', SelectorFlags.CLASS, 'toSecond']]
|
||||
],
|
||||
['span.toFirst', 'span.toSecond']);
|
||||
ΔprojectionDef([
|
||||
[['span', SelectorFlags.CLASS, 'toFirst']],
|
||||
[['span', SelectorFlags.CLASS, 'toSecond']]
|
||||
]);
|
||||
ΔelementStart(0, 'div', ['id', 'first']);
|
||||
{ Δprojection(1, 1); }
|
||||
ΔelementEnd();
|
||||
|
@ -1712,8 +1706,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef(
|
||||
[[['span']], [['span', SelectorFlags.CLASS, 'toSecond']]], ['span', 'span.toSecond']);
|
||||
ΔprojectionDef([[['span']], [['span', SelectorFlags.CLASS, 'toSecond']]]);
|
||||
ΔelementStart(0, 'div', ['id', 'first']);
|
||||
{ Δprojection(1, 1); }
|
||||
ΔelementEnd();
|
||||
|
@ -1757,7 +1750,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['span', SelectorFlags.CLASS, 'toFirst']]], ['span.toFirst']);
|
||||
ΔprojectionDef([[['span', SelectorFlags.CLASS, 'toFirst']]]);
|
||||
ΔelementStart(0, 'div', ['id', 'first']);
|
||||
{ Δprojection(1, 1); }
|
||||
ΔelementEnd();
|
||||
|
@ -1802,7 +1795,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['span', SelectorFlags.CLASS, 'toSecond']]], ['span.toSecond']);
|
||||
ΔprojectionDef([[['span', SelectorFlags.CLASS, 'toSecond']]]);
|
||||
ΔelementStart(0, 'div', ['id', 'first']);
|
||||
{ Δprojection(1); }
|
||||
ΔelementEnd();
|
||||
|
@ -1854,7 +1847,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['span']]], ['span']);
|
||||
ΔprojectionDef([[['span']]]);
|
||||
Δprojection(0, 1);
|
||||
Δelement(1, 'hr');
|
||||
Δprojection(2);
|
||||
|
@ -1915,9 +1908,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Card = createComponent('card', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef(
|
||||
[[['', 'card-title', '']], [['', 'card-content', '']]],
|
||||
['[card-title]', '[card-content]']);
|
||||
ΔprojectionDef([[['', 'card-title', '']], [['', 'card-content', '']]]);
|
||||
Δprojection(0, 1);
|
||||
Δelement(1, 'hr');
|
||||
Δprojection(2, 2);
|
||||
|
@ -1963,64 +1954,6 @@ describe('content projection', () => {
|
|||
'<card-with-title><card><h1 card-title="">Title</h1><hr>content</card></card-with-title>');
|
||||
});
|
||||
|
||||
|
||||
it('should support ngProjectAs on elements (including <ng-content>)', () => {
|
||||
|
||||
/**
|
||||
* <ng-content select="[card-title]"></ng-content>
|
||||
* <hr>
|
||||
* <ng-content select="[card-content]"></ng-content>
|
||||
*/
|
||||
const Card = createComponent('card', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef(
|
||||
[[['', 'card-title', '']], [['', 'card-content', '']]],
|
||||
['[card-title]', '[card-content]']);
|
||||
Δprojection(0, 1);
|
||||
Δelement(1, 'hr');
|
||||
Δprojection(2, 2);
|
||||
}
|
||||
}, 3);
|
||||
|
||||
/**
|
||||
* <card>
|
||||
* <h1 ngProjectAs="[card-title]>Title</h1>
|
||||
* <ng-content ngProjectAs="[card-content]"></ng-content>
|
||||
* </card>
|
||||
*/
|
||||
const CardWithTitle = createComponent('card-with-title', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef();
|
||||
ΔelementStart(0, 'card');
|
||||
{
|
||||
ΔelementStart(1, 'h1', ['ngProjectAs', '[card-title]']);
|
||||
{ Δtext(2, 'Title'); }
|
||||
ΔelementEnd();
|
||||
Δprojection(3, 0, ['ngProjectAs', '[card-content]']);
|
||||
}
|
||||
ΔelementEnd();
|
||||
}
|
||||
}, 4, 0, [Card]);
|
||||
|
||||
/**
|
||||
* <card-with-title>
|
||||
* content
|
||||
* </card-with-title>
|
||||
*/
|
||||
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔelementStart(0, 'card-with-title');
|
||||
{ Δtext(1, 'content'); }
|
||||
ΔelementEnd();
|
||||
}
|
||||
}, 2, 0, [CardWithTitle]);
|
||||
|
||||
const app = renderComponent(App);
|
||||
expect(toHtml(app))
|
||||
.toEqual('<card-with-title><card><h1>Title</h1><hr>content</card></card-with-title>');
|
||||
|
||||
});
|
||||
|
||||
it('should not match selectors against node having ngProjectAs attribute', function() {
|
||||
|
||||
/**
|
||||
|
@ -2028,7 +1961,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['div']]], ['div']);
|
||||
ΔprojectionDef([[['div']]]);
|
||||
Δprojection(0, 1);
|
||||
}
|
||||
}, 1);
|
||||
|
@ -2043,7 +1976,7 @@ describe('content projection', () => {
|
|||
if (rf & RenderFlags.Create) {
|
||||
ΔelementStart(0, 'child');
|
||||
{
|
||||
ΔelementStart(1, 'div', ['ngProjectAs', 'span']);
|
||||
ΔelementStart(1, 'div', [AttributeMarker.ProjectAs, ['span']]);
|
||||
{ Δtext(2, 'should not project'); }
|
||||
ΔelementEnd();
|
||||
ΔelementStart(3, 'div');
|
||||
|
@ -2067,7 +2000,7 @@ describe('content projection', () => {
|
|||
*/
|
||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['div']]], ['div']);
|
||||
ΔprojectionDef([[['div']]]);
|
||||
ΔelementStart(0, 'span');
|
||||
{ Δprojection(1, 1); }
|
||||
ΔelementEnd();
|
||||
|
|
|
@ -2058,7 +2058,7 @@ describe('Runtime i18n', () => {
|
|||
vars: 0,
|
||||
template: (rf: RenderFlags, cmp: Child) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['span']]], ['span']);
|
||||
ΔprojectionDef([[['span']]]);
|
||||
Δprojection(0, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AttributeMarker, TAttributes, TNode, TNodeType} from '../../src/render3/interfaces/node';
|
||||
|
||||
import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags,} from '../../src/render3/interfaces/projection';
|
||||
import {getProjectAsAttrValue, isNodeMatchingSelectorList, isNodeMatchingSelector} from '../../src/render3/node_selector_matcher';
|
||||
import {initializeStaticContext} from '../../src/render3/styling/class_and_style_bindings';
|
||||
import {createTNode} from '@angular/core/src/render3/instructions/shared';
|
||||
|
||||
import {AttributeMarker, TAttributes, TNode, TNodeType} from '../../src/render3/interfaces/node';
|
||||
import {CssSelector, CssSelectorList, SelectorFlags} from '../../src/render3/interfaces/projection';
|
||||
import {getProjectAsAttrValue, isNodeMatchingSelector, isNodeMatchingSelectorList} from '../../src/render3/node_selector_matcher';
|
||||
import {initializeStaticContext} from '../../src/render3/styling/class_and_style_bindings';
|
||||
|
||||
function testLStaticData(tagName: string, attrs: TAttributes | null): TNode {
|
||||
return createTNode(null, TNodeType.Element, 0, tagName, attrs);
|
||||
}
|
||||
|
@ -466,11 +466,11 @@ describe('css selector matching', () => {
|
|||
|
||||
describe('reading the ngProjectAs attribute value', function() {
|
||||
|
||||
function testTNode(attrs: string[] | null) { return testLStaticData('tag', attrs); }
|
||||
function testTNode(attrs: TAttributes | null) { return testLStaticData('tag', attrs); }
|
||||
|
||||
it('should get ngProjectAs value if present', function() {
|
||||
expect(getProjectAsAttrValue(testTNode([NG_PROJECT_AS_ATTR_NAME, 'tag[foo=bar]'])))
|
||||
.toBe('tag[foo=bar]');
|
||||
expect(getProjectAsAttrValue(testTNode([AttributeMarker.ProjectAs, ['tag', 'foo', 'bar']])))
|
||||
.toEqual(['tag', 'foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should return null if there are no attributes',
|
||||
|
@ -481,7 +481,7 @@ describe('css selector matching', () => {
|
|||
});
|
||||
|
||||
it('should not accidentally identify ngProjectAs in attribute values', function() {
|
||||
expect(getProjectAsAttrValue(testTNode(['foo', NG_PROJECT_AS_ATTR_NAME]))).toBe(null);
|
||||
expect(getProjectAsAttrValue(testTNode(['foo', AttributeMarker.ProjectAs]))).toBe(null);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1475,7 +1475,7 @@ describe('ViewContainerRef', () => {
|
|||
vars: 0,
|
||||
template: (rf: RenderFlags, cmp: ChildWithSelector) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
ΔprojectionDef([[['header']]], ['header']);
|
||||
ΔprojectionDef([[['header']]]);
|
||||
ΔelementStart(0, 'first');
|
||||
{ Δprojection(1, 1); }
|
||||
ΔelementEnd();
|
||||
|
|
|
@ -1313,9 +1313,9 @@ export declare function ΔpipeBindV(index: number, slotOffset: number, values: a
|
|||
|
||||
export declare type ΔPipeDefWithMeta<T, Name extends string> = PipeDef<T>;
|
||||
|
||||
export declare function Δprojection(nodeIndex: number, selectorIndex?: number, attrs?: string[]): void;
|
||||
export declare function Δprojection(nodeIndex: number, selectorIndex?: number, attrs?: TAttributes): void;
|
||||
|
||||
export declare function ΔprojectionDef(selectors?: CssSelectorList[], textSelectors?: string[]): void;
|
||||
export declare function ΔprojectionDef(selectors?: CssSelectorList[]): void;
|
||||
|
||||
export declare function ΔProvidersFeature<T>(providers: Provider[], viewProviders?: Provider[]): (definition: DirectiveDef<T>) => void;
|
||||
|
||||
|
|
Loading…
Reference in New Issue