feat(ivy): separate attributes for directive matching purposes (#23991)
In ngIvy directives matching (determining which directives are active based on a CSS seletor) happens at runtime. This means that runtime needs to have enough context to match directives. This PR takes care of cases where a directive's selector should match bindings (ex. [foo]="exp") and event handlers (ex. (out)="do()"). In the mentioned cases we need to have binding / output "attributes" for directive's CSS selector matching purposes. At the same time those are not regular attributes and as such should not be reflected in the DOM. Closes #23706 PR Close #23991
This commit is contained in:
parent
b87d650da2
commit
90bf5d8961
@ -22,7 +22,7 @@ import {assertGreaterThan, assertLessThan, assertNotNull} from './assert';
|
|||||||
import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, createTNode, createTView, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions';
|
import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, createTNode, createTView, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions';
|
||||||
import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList} from './interfaces/definition';
|
import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList} from './interfaces/definition';
|
||||||
import {LInjector} from './interfaces/injector';
|
import {LInjector} from './interfaces/injector';
|
||||||
import {LContainerNode, LElementNode, LNode, LViewNode, TNodeFlags, TNodeType} from './interfaces/node';
|
import {AttributeMarker, LContainerNode, LElementNode, LNode, LViewNode, TNodeFlags, TNodeType} from './interfaces/node';
|
||||||
import {QueryReadType} from './interfaces/query';
|
import {QueryReadType} from './interfaces/query';
|
||||||
import {Renderer3} from './interfaces/renderer';
|
import {Renderer3} from './interfaces/renderer';
|
||||||
import {LView, TView} from './interfaces/view';
|
import {LView, TView} from './interfaces/view';
|
||||||
@ -251,7 +251,7 @@ export function injectChangeDetectorRef(): viewEngine_ChangeDetectorRef {
|
|||||||
*
|
*
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
export function injectAttribute(attrName: string): string|undefined {
|
export function injectAttribute(attrNameToInject: string): string|undefined {
|
||||||
ngDevMode && assertPreviousIsParent();
|
ngDevMode && assertPreviousIsParent();
|
||||||
const lElement = getPreviousOrParentNode() as LElementNode;
|
const lElement = getPreviousOrParentNode() as LElementNode;
|
||||||
ngDevMode && assertNodeType(lElement, TNodeType.Element);
|
ngDevMode && assertNodeType(lElement, TNodeType.Element);
|
||||||
@ -260,8 +260,10 @@ export function injectAttribute(attrName: string): string|undefined {
|
|||||||
const attrs = tElement.attrs;
|
const attrs = tElement.attrs;
|
||||||
if (attrs) {
|
if (attrs) {
|
||||||
for (let i = 0; i < attrs.length; i = i + 2) {
|
for (let i = 0; i < attrs.length; i = i + 2) {
|
||||||
if (attrs[i] == attrName) {
|
const attrName = attrs[i];
|
||||||
return attrs[i + 1];
|
if (attrName === AttributeMarker.SELECT_ONLY) break;
|
||||||
|
if (attrName == attrNameToInject) {
|
||||||
|
return attrs[i + 1] as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,10 @@ export {
|
|||||||
tick,
|
tick,
|
||||||
} from './instructions';
|
} from './instructions';
|
||||||
|
|
||||||
|
export {
|
||||||
|
AttributeMarker
|
||||||
|
} from './interfaces/node';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
pipe as Pp,
|
pipe as Pp,
|
||||||
pipeBind1 as pb1,
|
pipeBind1 as pb1,
|
||||||
|
@ -15,7 +15,7 @@ import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interface
|
|||||||
import {LQueries} from './interfaces/query';
|
import {LQueries} from './interfaces/query';
|
||||||
import {CurrentMatchesList, LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view';
|
import {CurrentMatchesList, LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view';
|
||||||
|
|
||||||
import {LContainerNode, LElementNode, LNode, TNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue, TElementNode,} from './interfaces/node';
|
import {AttributeMarker, TAttributes, LContainerNode, LElementNode, LNode, TNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue, TElementNode,} from './interfaces/node';
|
||||||
import {assertNodeType} from './node_assert';
|
import {assertNodeType} from './node_assert';
|
||||||
import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode} from './node_manipulation';
|
import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode} from './node_manipulation';
|
||||||
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
|
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
|
||||||
@ -366,19 +366,19 @@ export function createLNodeObject(
|
|||||||
*/
|
*/
|
||||||
export function createLNode(
|
export function createLNode(
|
||||||
index: number | null, type: TNodeType.Element, native: RElement | RText | null,
|
index: number | null, type: TNodeType.Element, native: RElement | RText | null,
|
||||||
name: string | null, attrs: string[] | null, lView?: LView | null): LElementNode;
|
name: string | null, attrs: TAttributes | null, lView?: LView | null): LElementNode;
|
||||||
export function createLNode(
|
export function createLNode(
|
||||||
index: number | null, type: TNodeType.View, native: null, name: null, attrs: null,
|
index: number | null, type: TNodeType.View, native: null, name: null, attrs: null,
|
||||||
lView: LView): LViewNode;
|
lView: LView): LViewNode;
|
||||||
export function createLNode(
|
export function createLNode(
|
||||||
index: number, type: TNodeType.Container, native: undefined, name: string | null,
|
index: number, type: TNodeType.Container, native: undefined, name: string | null,
|
||||||
attrs: string[] | null, lContainer: LContainer): LContainerNode;
|
attrs: TAttributes | null, lContainer: LContainer): LContainerNode;
|
||||||
export function createLNode(
|
export function createLNode(
|
||||||
index: number, type: TNodeType.Projection, native: null, name: null, attrs: string[] | null,
|
index: number, type: TNodeType.Projection, native: null, name: null, attrs: TAttributes | null,
|
||||||
lProjection: LProjection): LProjectionNode;
|
lProjection: LProjection): LProjectionNode;
|
||||||
export function createLNode(
|
export function createLNode(
|
||||||
index: number | null, type: TNodeType, native: RText | RElement | null | undefined,
|
index: number | null, type: TNodeType, native: RText | RElement | null | undefined,
|
||||||
name: string | null, attrs: string[] | null, state?: null | LView | LContainer |
|
name: string | null, attrs: TAttributes | null, state?: null | LView | LContainer |
|
||||||
LProjection): LElementNode<extNode&LViewNode&LContainerNode&LProjectionNode {
|
LProjection): LElementNode<extNode&LViewNode&LContainerNode&LProjectionNode {
|
||||||
const parent = isParent ? previousOrParentNode :
|
const parent = isParent ? previousOrParentNode :
|
||||||
previousOrParentNode && getParentLNode(previousOrParentNode) !as LNode;
|
previousOrParentNode && getParentLNode(previousOrParentNode) !as LNode;
|
||||||
@ -586,7 +586,8 @@ function getRenderFlags(view: LView): RenderFlags {
|
|||||||
* ['id', 'warning5', 'class', 'alert']
|
* ['id', 'warning5', 'class', 'alert']
|
||||||
*/
|
*/
|
||||||
export function elementStart(
|
export function elementStart(
|
||||||
index: number, name: string, attrs?: string[] | null, localRefs?: string[] | null): RElement {
|
index: number, name: string, attrs?: TAttributes | null,
|
||||||
|
localRefs?: string[] | null): RElement {
|
||||||
ngDevMode &&
|
ngDevMode &&
|
||||||
assertEqual(
|
assertEqual(
|
||||||
currentView.bindingStartIndex, -1, 'elements should be created before any bindings');
|
currentView.bindingStartIndex, -1, 'elements should be created before any bindings');
|
||||||
@ -600,23 +601,18 @@ export function elementStart(
|
|||||||
|
|
||||||
if (attrs) setUpAttributes(native, attrs);
|
if (attrs) setUpAttributes(native, attrs);
|
||||||
appendChild(getParentLNode(node), native, currentView);
|
appendChild(getParentLNode(node), native, currentView);
|
||||||
createDirectivesAndLocals(index, name, attrs, localRefs, false);
|
createDirectivesAndLocals(localRefs);
|
||||||
return native;
|
return native;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates directive instances and populates local refs.
|
* Creates directive instances and populates local refs.
|
||||||
*
|
*
|
||||||
* @param index Index of the current node (to create TNode)
|
|
||||||
* @param name Tag name of the current node
|
|
||||||
* @param attrs Attrs of the current node
|
|
||||||
* @param localRefs Local refs of the current node
|
* @param localRefs Local refs of the current node
|
||||||
* @param inlineViews Whether or not this node will create inline views
|
|
||||||
*/
|
*/
|
||||||
function createDirectivesAndLocals(
|
function createDirectivesAndLocals(localRefs?: string[] | null) {
|
||||||
index: number, name: string | null, attrs: string[] | null | undefined,
|
|
||||||
localRefs: string[] | null | undefined, inlineViews: boolean) {
|
|
||||||
const node = previousOrParentNode;
|
const node = previousOrParentNode;
|
||||||
|
|
||||||
if (firstTemplatePass) {
|
if (firstTemplatePass) {
|
||||||
ngDevMode && ngDevMode.firstTemplatePass++;
|
ngDevMode && ngDevMode.firstTemplatePass++;
|
||||||
cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null);
|
cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null);
|
||||||
@ -822,17 +818,18 @@ export function createTView(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUpAttributes(native: RElement, attrs: string[]): void {
|
function setUpAttributes(native: RElement, attrs: TAttributes): void {
|
||||||
ngDevMode && assertEqual(attrs.length % 2, 0, 'each attribute should have a key and a value');
|
|
||||||
|
|
||||||
const isProc = isProceduralRenderer(renderer);
|
const isProc = isProceduralRenderer(renderer);
|
||||||
for (let i = 0; i < attrs.length; i += 2) {
|
for (let i = 0; i < attrs.length; i += 2) {
|
||||||
const attrName = attrs[i];
|
const attrName = attrs[i];
|
||||||
|
if (attrName === AttributeMarker.SELECT_ONLY) break;
|
||||||
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
|
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
|
||||||
const attrVal = attrs[i + 1];
|
const attrVal = attrs[i + 1];
|
||||||
ngDevMode && ngDevMode.rendererSetAttribute++;
|
ngDevMode && ngDevMode.rendererSetAttribute++;
|
||||||
isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) :
|
isProc ?
|
||||||
native.setAttribute(attrName, attrVal);
|
(renderer as ProceduralRenderer3)
|
||||||
|
.setAttribute(native, attrName as string, attrVal as string) :
|
||||||
|
native.setAttribute(attrName as string, attrVal as string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1046,7 +1043,7 @@ export function elementProperty<T>(
|
|||||||
* @returns the TNode object
|
* @returns the TNode object
|
||||||
*/
|
*/
|
||||||
export function createTNode(
|
export function createTNode(
|
||||||
type: TNodeType, index: number | null, tagName: string | null, attrs: string[] | null,
|
type: TNodeType, index: number | null, tagName: string | null, attrs: TAttributes | null,
|
||||||
parent: TElementNode | TContainerNode | null, tViews: TView[] | null): TNode {
|
parent: TElementNode | TContainerNode | null, tViews: TView[] | null): TNode {
|
||||||
ngDevMode && ngDevMode.tNode++;
|
ngDevMode && ngDevMode.tNode++;
|
||||||
return {
|
return {
|
||||||
@ -1442,10 +1439,13 @@ function generateInitialInputs(
|
|||||||
for (let i = 0; i < attrs.length; i += 2) {
|
for (let i = 0; i < attrs.length; i += 2) {
|
||||||
const attrName = attrs[i];
|
const attrName = attrs[i];
|
||||||
const minifiedInputName = inputs[attrName];
|
const minifiedInputName = inputs[attrName];
|
||||||
|
const attrValue = attrs[i + 1];
|
||||||
|
|
||||||
|
if (attrName === AttributeMarker.SELECT_ONLY) break;
|
||||||
if (minifiedInputName !== undefined) {
|
if (minifiedInputName !== undefined) {
|
||||||
const inputsToStore: InitialInputs =
|
const inputsToStore: InitialInputs =
|
||||||
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
|
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
|
||||||
inputsToStore.push(minifiedInputName, attrs[i + 1]);
|
inputsToStore.push(minifiedInputName, attrValue as string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return initialInputData;
|
return initialInputData;
|
||||||
@ -1484,7 +1484,7 @@ export function createLContainer(
|
|||||||
* @param localRefs A set of local reference bindings on the element.
|
* @param localRefs A set of local reference bindings on the element.
|
||||||
*/
|
*/
|
||||||
export function container(
|
export function container(
|
||||||
index: number, template?: ComponentTemplate<any>, tagName?: string | null, attrs?: string[],
|
index: number, template?: ComponentTemplate<any>, tagName?: string | null, attrs?: TAttributes,
|
||||||
localRefs?: string[] | null): void {
|
localRefs?: string[] | null): void {
|
||||||
ngDevMode && assertEqual(
|
ngDevMode && assertEqual(
|
||||||
currentView.bindingStartIndex, -1,
|
currentView.bindingStartIndex, -1,
|
||||||
@ -1501,7 +1501,7 @@ export function container(
|
|||||||
// Containers are added to the current view tree instead of their embedded views
|
// Containers are added to the current view tree instead of their embedded views
|
||||||
// because views can be removed and re-inserted.
|
// because views can be removed and re-inserted.
|
||||||
addToViewTree(currentView, node.data);
|
addToViewTree(currentView, node.data);
|
||||||
createDirectivesAndLocals(index, tagName || null, attrs, localRefs, template == null);
|
createDirectivesAndLocals(localRefs);
|
||||||
|
|
||||||
isParent = false;
|
isParent = false;
|
||||||
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container);
|
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container);
|
||||||
|
@ -158,6 +158,29 @@ export interface LProjectionNode extends LNode {
|
|||||||
dynamicLContainerNode: null;
|
dynamicLContainerNode: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of marker values to be used in the attributes arrays. Those markers indicate that some
|
||||||
|
* items are not regular attributes and the processing should be adapted accordingly.
|
||||||
|
*/
|
||||||
|
export const enum AttributeMarker {
|
||||||
|
NS = 0, // namespace. Has to be repeated.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This marker indicates that the following attribute names were extracted from bindings (ex.:
|
||||||
|
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()").
|
||||||
|
* Taking the above bindings and outputs as an example an attributes array could look as follows:
|
||||||
|
* ['class', 'fade in', AttributeMarker.SELECT_ONLY, 'foo', 'bar']
|
||||||
|
*/
|
||||||
|
SELECT_ONLY = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A combination of:
|
||||||
|
* - attribute names and values
|
||||||
|
* - special markers acting as flags to alter attributes processing.
|
||||||
|
*/
|
||||||
|
export type TAttributes = (string | AttributeMarker)[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LNode binding data (flyweight) for a particular node that is shared between all templates
|
* LNode binding data (flyweight) for a particular node that is shared between all templates
|
||||||
* of a specific type.
|
* of a specific type.
|
||||||
@ -198,18 +221,20 @@ export interface TNode {
|
|||||||
tagName: string|null;
|
tagName: string|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static attributes associated with an element. We need to store
|
* Attributes associated with an element. We need to store attributes to support various use-cases
|
||||||
* static attributes to support content projection with selectors.
|
* (attribute injection, content projection with selectors, directives matching).
|
||||||
* Attributes are stored statically because reading them from the DOM
|
* Attributes are stored statically because reading them from the DOM would be way too slow for
|
||||||
* would be way too slow for content projection and queries.
|
* content projection and queries.
|
||||||
*
|
*
|
||||||
* Since attrs will always be calculated first, they will never need
|
* Since attrs will always be calculated first, they will never need to be marked undefined by
|
||||||
* to be marked undefined by other instructions.
|
* other instructions.
|
||||||
*
|
*
|
||||||
* The name of the attribute and its value alternate in the array.
|
* For regular attributes a name of an attribute and its value alternate in the array.
|
||||||
* e.g. ['role', 'checkbox']
|
* e.g. ['role', 'checkbox']
|
||||||
|
* This array can contain flags that will indicate "special attributes" (attributes with
|
||||||
|
* namespaces, attributes extracted from bindings and outputs).
|
||||||
*/
|
*/
|
||||||
attrs: string[]|null;
|
attrs: TAttributes|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of local names under which a given element is exported in a template and
|
* A set of local names under which a given element is exported in a template and
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import './ng_dev_mode';
|
import './ng_dev_mode';
|
||||||
|
|
||||||
import {assertNotNull} from './assert';
|
import {assertNotNull} from './assert';
|
||||||
import {TNode, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
|
import {AttributeMarker, TAttributes, TNode, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
|
||||||
import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
|
import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
|
||||||
|
|
||||||
const unusedValueToPlacateAjd = unused1 + unused2;
|
const unusedValueToPlacateAjd = unused1 + unused2;
|
||||||
@ -40,6 +40,7 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo
|
|||||||
|
|
||||||
let mode: SelectorFlags = SelectorFlags.ELEMENT;
|
let mode: SelectorFlags = SelectorFlags.ELEMENT;
|
||||||
const nodeAttrs = tNode.attrs !;
|
const nodeAttrs = tNode.attrs !;
|
||||||
|
const selectOnlyMarkerIdx = nodeAttrs ? nodeAttrs.indexOf(AttributeMarker.SELECT_ONLY) : -1;
|
||||||
|
|
||||||
// When processing ":not" selectors, we skip to the next ":not" if the
|
// When processing ":not" selectors, we skip to the next ":not" if the
|
||||||
// current one doesn't match
|
// current one doesn't match
|
||||||
@ -80,9 +81,11 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo
|
|||||||
|
|
||||||
const selectorAttrValue = mode & SelectorFlags.CLASS ? current : selector[++i];
|
const selectorAttrValue = mode & SelectorFlags.CLASS ? current : selector[++i];
|
||||||
if (selectorAttrValue !== '') {
|
if (selectorAttrValue !== '') {
|
||||||
const nodeAttrValue = nodeAttrs[attrIndexInNode + 1];
|
const nodeAttrValue = selectOnlyMarkerIdx > -1 && attrIndexInNode > selectOnlyMarkerIdx ?
|
||||||
|
'' :
|
||||||
|
nodeAttrs[attrIndexInNode + 1];
|
||||||
if (mode & SelectorFlags.CLASS &&
|
if (mode & SelectorFlags.CLASS &&
|
||||||
!isCssClassMatching(nodeAttrValue, selectorAttrValue as string) ||
|
!isCssClassMatching(nodeAttrValue as string, selectorAttrValue as string) ||
|
||||||
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
|
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
|
||||||
if (isPositive(mode)) return false;
|
if (isPositive(mode)) return false;
|
||||||
skipToNextSelector = true;
|
skipToNextSelector = true;
|
||||||
@ -98,10 +101,15 @@ function isPositive(mode: SelectorFlags): boolean {
|
|||||||
return (mode & SelectorFlags.NOT) === 0;
|
return (mode & SelectorFlags.NOT) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findAttrIndexInNode(name: string, attrs: string[] | null): number {
|
function findAttrIndexInNode(name: string, attrs: TAttributes | null): number {
|
||||||
|
let step = 2;
|
||||||
if (attrs === null) return -1;
|
if (attrs === null) return -1;
|
||||||
for (let i = 0; i < attrs.length; i += 2) {
|
for (let i = 0; i < attrs.length; i += step) {
|
||||||
if (attrs[i] === name) return i;
|
const attrName = attrs[i];
|
||||||
|
if (attrName === name) return i;
|
||||||
|
if (attrName === AttributeMarker.SELECT_ONLY) {
|
||||||
|
step = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -123,7 +131,7 @@ export function getProjectAsAttrValue(tNode: TNode): string|null {
|
|||||||
// only check for ngProjectAs in attribute names, don't accidentally match attribute's value
|
// only check for ngProjectAs in attribute names, don't accidentally match attribute's value
|
||||||
// (attribute names are stored at even indexes)
|
// (attribute names are stored at even indexes)
|
||||||
if ((ngProjectAsAttrIdx & 1) === 0) {
|
if ((ngProjectAsAttrIdx & 1) === 0) {
|
||||||
return nodeAttrs[ngProjectAsAttrIdx + 1];
|
return nodeAttrs[ngProjectAsAttrIdx + 1] as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -8,10 +8,11 @@
|
|||||||
|
|
||||||
import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection';
|
import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection';
|
||||||
|
|
||||||
import {detectChanges} from '../../src/render3/index';
|
import {AttributeMarker, detectChanges} from '../../src/render3/index';
|
||||||
import {container, containerRefreshEnd, containerRefreshStart, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, load, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions';
|
import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions';
|
||||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||||
import {createComponent, renderComponent, toHtml} from './render_util';
|
|
||||||
|
import {ComponentFixture, createComponent, renderComponent, toHtml} from './render_util';
|
||||||
|
|
||||||
describe('content projection', () => {
|
describe('content projection', () => {
|
||||||
it('should project content', () => {
|
it('should project content', () => {
|
||||||
@ -583,6 +584,43 @@ describe('content projection', () => {
|
|||||||
'<child><div id="first"><span title="toFirst">1</span></div><div id="second"><span title="toSecond">2</span></div></child>');
|
'<child><div id="first"><span title="toFirst">1</span></div><div id="second"><span title="toSecond">2</span></div></child>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://stackblitz.com/edit/angular-psokum?file=src%2Fapp%2Fapp.module.ts
|
||||||
|
it('should project nodes where attribute selector matches a binding', () => {
|
||||||
|
/**
|
||||||
|
* <ng-content select="[title]"></ng-content>
|
||||||
|
*/
|
||||||
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
projectionDef(0, [[['', 'title', '']]], ['[title]']);
|
||||||
|
{ projection(1, 0, 1); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <child>
|
||||||
|
* <span [title]="'Some title'">Has title</span>
|
||||||
|
* </child>
|
||||||
|
*/
|
||||||
|
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementStart(0, 'child');
|
||||||
|
{
|
||||||
|
elementStart(1, 'span', [AttributeMarker.SELECT_ONLY, 'title']);
|
||||||
|
{ text(2, 'Has title'); }
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
elementProperty(1, 'title', bind('Some title'));
|
||||||
|
}
|
||||||
|
}, [Child]);
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(Parent);
|
||||||
|
expect(fixture.html).toEqual('<child><span title="Some title">Has title</span></child>');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
it('should project nodes using class selectors', () => {
|
it('should project nodes using class selectors', () => {
|
||||||
/**
|
/**
|
||||||
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>
|
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>
|
||||||
|
@ -12,9 +12,9 @@ import {RenderFlags} from '@angular/core/src/render3/interfaces/definition';
|
|||||||
import {defineComponent} from '../../src/render3/definition';
|
import {defineComponent} from '../../src/render3/definition';
|
||||||
import {bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector, injectAttribute} from '../../src/render3/di';
|
import {bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector, injectAttribute} from '../../src/render3/di';
|
||||||
import {NgOnChangesFeature, PublicFeature, defineDirective, directiveInject, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
import {NgOnChangesFeature, PublicFeature, defineDirective, directiveInject, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
||||||
import {bind, container, containerRefreshEnd, containerRefreshStart, createLNode, createLView, createTView, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, load, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
|
import {bind, container, containerRefreshEnd, containerRefreshStart, createLNode, createLView, createTView, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, load, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
|
||||||
import {LInjector} from '../../src/render3/interfaces/injector';
|
import {LInjector} from '../../src/render3/interfaces/injector';
|
||||||
import {TNodeType} from '../../src/render3/interfaces/node';
|
import {AttributeMarker, TNodeType} from '../../src/render3/interfaces/node';
|
||||||
import {LViewFlags} from '../../src/render3/interfaces/view';
|
import {LViewFlags} from '../../src/render3/interfaces/view';
|
||||||
import {ViewRef} from '../../src/render3/view_ref';
|
import {ViewRef} from '../../src/render3/view_ref';
|
||||||
|
|
||||||
@ -1199,21 +1199,61 @@ describe('di', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should injectAttribute', () => {
|
describe('@Attribute', () => {
|
||||||
let exist: string|undefined = 'wrong';
|
|
||||||
let nonExist: string|undefined = 'wrong';
|
|
||||||
|
|
||||||
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
it('should inject attribute', () => {
|
||||||
if (rf & RenderFlags.Create) {
|
let exist: string|undefined = 'wrong';
|
||||||
elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']);
|
let nonExist: string|undefined = 'wrong';
|
||||||
exist = injectAttribute('exist');
|
|
||||||
nonExist = injectAttribute('nonExist');
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
||||||
}
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']);
|
||||||
|
exist = injectAttribute('exist');
|
||||||
|
nonExist = injectAttribute('nonExist');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(MyApp);
|
||||||
|
expect(exist).toEqual('existValue');
|
||||||
|
expect(nonExist).toEqual(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = renderComponent(MyApp);
|
// https://stackblitz.com/edit/angular-8ytqkp?file=src%2Fapp%2Fapp.component.ts
|
||||||
expect(exist).toEqual('existValue');
|
it('should not inject attributes representing bindings and outputs', () => {
|
||||||
expect(nonExist).toEqual(undefined);
|
let exist: string|undefined = 'wrong';
|
||||||
|
let nonExist: string|undefined = 'wrong';
|
||||||
|
|
||||||
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementStart(0, 'div', ['exist', 'existValue', AttributeMarker.SELECT_ONLY, 'nonExist']);
|
||||||
|
exist = injectAttribute('exist');
|
||||||
|
nonExist = injectAttribute('nonExist');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(MyApp);
|
||||||
|
expect(exist).toEqual('existValue');
|
||||||
|
expect(nonExist).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accidentally inject attributes representing bindings and outputs', () => {
|
||||||
|
let exist: string|undefined = 'wrong';
|
||||||
|
let nonExist: string|undefined = 'wrong';
|
||||||
|
|
||||||
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementStart(0, 'div', [
|
||||||
|
'exist', 'existValue', AttributeMarker.SELECT_ONLY, 'binding1', 'nonExist', 'binding2'
|
||||||
|
]);
|
||||||
|
exist = injectAttribute('exist');
|
||||||
|
nonExist = injectAttribute('nonExist');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(MyApp);
|
||||||
|
expect(exist).toEqual('existValue');
|
||||||
|
expect(nonExist).toEqual(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('inject', () => {
|
describe('inject', () => {
|
||||||
|
@ -6,10 +6,12 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {defineDirective} from '../../src/render3/index';
|
import {EventEmitter} from '@angular/core';
|
||||||
import {bind, elementEnd, elementProperty, elementStart, loadDirective} from '../../src/render3/instructions';
|
|
||||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
import {AttributeMarker, defineDirective} from '../../src/render3/index';
|
||||||
import {renderToHtml} from './render_util';
|
import {bind, elementEnd, elementProperty, elementStart, listener, loadDirective} from '../../src/render3/instructions';
|
||||||
|
|
||||||
|
import {TemplateFixture} from './render_util';
|
||||||
|
|
||||||
describe('directive', () => {
|
describe('directive', () => {
|
||||||
|
|
||||||
@ -31,17 +33,151 @@ describe('directive', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function Template(rf: RenderFlags, ctx: any) {
|
function Template() {
|
||||||
if (rf & RenderFlags.Create) {
|
elementStart(0, 'span', [AttributeMarker.SELECT_ONLY, 'dir']);
|
||||||
elementStart(0, 'span', ['dir', '']);
|
elementEnd();
|
||||||
elementEnd();
|
}
|
||||||
|
|
||||||
|
const fixture = new TemplateFixture(Template, () => {}, [Directive]);
|
||||||
|
expect(fixture.html).toEqual('<span class="foo"></span>');
|
||||||
|
|
||||||
|
directiveInstance !.klass = 'bar';
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<span class="bar"></span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectors', () => {
|
||||||
|
|
||||||
|
it('should match directives with attribute selectors on bindings', () => {
|
||||||
|
let directiveInstance: Directive;
|
||||||
|
|
||||||
|
class Directive {
|
||||||
|
static ngDirectiveDef = defineDirective({
|
||||||
|
type: Directive,
|
||||||
|
selectors: [['', 'test', '']],
|
||||||
|
factory: () => directiveInstance = new Directive,
|
||||||
|
inputs: {test: 'test', other: 'other'}
|
||||||
|
});
|
||||||
|
|
||||||
|
testValue: boolean;
|
||||||
|
other: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A setter to assert that a binding is not invoked with stringified attribute value
|
||||||
|
*/
|
||||||
|
set test(value: any) {
|
||||||
|
// if a binding is processed correctly we should only be invoked with a false Boolean
|
||||||
|
// and never with the "false" string literal
|
||||||
|
this.testValue = value;
|
||||||
|
if (value !== false) {
|
||||||
|
fail('Should only be called with a false Boolean value, got a non-falsy value');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defs = [Directive];
|
/**
|
||||||
expect(renderToHtml(Template, {}, defs)).toEqual('<span class="foo" dir=""></span>');
|
* <span [test]="false" [other]="true"></span>
|
||||||
directiveInstance !.klass = 'bar';
|
*/
|
||||||
expect(renderToHtml(Template, {}, defs)).toEqual('<span class="bar" dir=""></span>');
|
function createTemplate() {
|
||||||
|
// using 2 bindings to show example shape of attributes array
|
||||||
|
elementStart(0, 'span', ['class', 'fade', AttributeMarker.SELECT_ONLY, 'test', 'other']);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTemplate() { elementProperty(0, 'test', bind(false)); }
|
||||||
|
|
||||||
|
const fixture = new TemplateFixture(createTemplate, updateTemplate, [Directive]);
|
||||||
|
|
||||||
|
// the "test" attribute should not be reflected in the DOM as it is here only for directive
|
||||||
|
// matching purposes
|
||||||
|
expect(fixture.html).toEqual('<span class="fade"></span>');
|
||||||
|
expect(directiveInstance !.testValue).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accidentally set inputs from attributes extracted from bindings / outputs',
|
||||||
|
() => {
|
||||||
|
let directiveInstance: Directive;
|
||||||
|
|
||||||
|
class Directive {
|
||||||
|
static ngDirectiveDef = defineDirective({
|
||||||
|
type: Directive,
|
||||||
|
selectors: [['', 'test', '']],
|
||||||
|
factory: () => directiveInstance = new Directive,
|
||||||
|
inputs: {test: 'test', prop1: 'prop1', prop2: 'prop2'}
|
||||||
|
});
|
||||||
|
|
||||||
|
prop1: boolean;
|
||||||
|
prop2: boolean;
|
||||||
|
testValue: boolean;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A setter to assert that a binding is not invoked with stringified attribute value
|
||||||
|
*/
|
||||||
|
set test(value: any) {
|
||||||
|
// if a binding is processed correctly we should only be invoked with a false Boolean
|
||||||
|
// and never with the "false" string literal
|
||||||
|
this.testValue = value;
|
||||||
|
if (value !== false) {
|
||||||
|
fail('Should only be called with a false Boolean value, got a non-falsy value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <span [prop1]="true" [test]="false" [prop2]="true"></span>
|
||||||
|
*/
|
||||||
|
function createTemplate() {
|
||||||
|
// putting name (test) in the "usual" value position
|
||||||
|
elementStart(
|
||||||
|
0, 'span', ['class', 'fade', AttributeMarker.SELECT_ONLY, 'prop1', 'test', 'prop2']);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTemplate() {
|
||||||
|
elementProperty(0, 'prop1', bind(true));
|
||||||
|
elementProperty(0, 'test', bind(false));
|
||||||
|
elementProperty(0, 'prop2', bind(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture = new TemplateFixture(createTemplate, updateTemplate, [Directive]);
|
||||||
|
|
||||||
|
// the "test" attribute should not be reflected in the DOM as it is here only for directive
|
||||||
|
// matching purposes
|
||||||
|
expect(fixture.html).toEqual('<span class="fade"></span>');
|
||||||
|
expect(directiveInstance !.testValue).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match directives with attribute selectors on outputs', () => {
|
||||||
|
let directiveInstance: Directive;
|
||||||
|
|
||||||
|
class Directive {
|
||||||
|
static ngDirectiveDef = defineDirective({
|
||||||
|
type: Directive,
|
||||||
|
selectors: [['', 'out', '']],
|
||||||
|
factory: () => directiveInstance = new Directive,
|
||||||
|
outputs: {out: 'out'}
|
||||||
|
});
|
||||||
|
|
||||||
|
out = new EventEmitter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <span (out)="someVar = true"></span>
|
||||||
|
*/
|
||||||
|
function createTemplate() {
|
||||||
|
elementStart(0, 'span', [AttributeMarker.SELECT_ONLY, 'out']);
|
||||||
|
{ listener('out', () => {}); }
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture = new TemplateFixture(createTemplate, () => {}, [Directive]);
|
||||||
|
|
||||||
|
// "out" should not be part of reflected attributes
|
||||||
|
expect(fixture.html).toEqual('<span></span>');
|
||||||
|
expect(directiveInstance !).not.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TNode, TNodeType} from '../../src/render3/interfaces/node';
|
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 {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags,} from '../../src/render3/interfaces/projection';
|
||||||
import {getProjectAsAttrValue, isNodeMatchingSelectorList, isNodeMatchingSelector} from '../../src/render3/node_selector_matcher';
|
import {getProjectAsAttrValue, isNodeMatchingSelectorList, isNodeMatchingSelector} from '../../src/render3/node_selector_matcher';
|
||||||
|
|
||||||
function testLStaticData(tagName: string, attrs: string[] | null): TNode {
|
function testLStaticData(tagName: string, attrs: TAttributes | null): TNode {
|
||||||
return {
|
return {
|
||||||
type: TNodeType.Element,
|
type: TNodeType.Element,
|
||||||
index: 0,
|
index: 0,
|
||||||
@ -29,7 +29,7 @@ function testLStaticData(tagName: string, attrs: string[] | null): TNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('css selector matching', () => {
|
describe('css selector matching', () => {
|
||||||
function isMatching(tagName: string, attrs: string[] | null, selector: CssSelector): boolean {
|
function isMatching(tagName: string, attrs: TAttributes | null, selector: CssSelector): boolean {
|
||||||
return isNodeMatchingSelector(testLStaticData(tagName, attrs), selector);
|
return isNodeMatchingSelector(testLStaticData(tagName, attrs), selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,6 +177,28 @@ describe('css selector matching', () => {
|
|||||||
'', 'class', 'foo'
|
'', 'class', 'foo'
|
||||||
])).toBeTruthy(`Selector '[class="foo"]' should match <span class="foo">`);
|
])).toBeTruthy(`Selector '[class="foo"]' should match <span class="foo">`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should take optional binding attribute names into account', () => {
|
||||||
|
expect(isMatching('span', [AttributeMarker.SELECT_ONLY, 'directive'], [
|
||||||
|
'', 'directive', ''
|
||||||
|
])).toBeTruthy(`Selector '[directive]' should match <span [directive]="exp">`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match optional binding attribute names if attribute selector has value',
|
||||||
|
() => {
|
||||||
|
expect(isMatching('span', [AttributeMarker.SELECT_ONLY, 'directive'], [
|
||||||
|
'', 'directive', 'value'
|
||||||
|
])).toBeFalsy(`Selector '[directive=value]' should not match <span [directive]="exp">`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match optional binding attribute names if attribute selector has value and next name equals to value',
|
||||||
|
() => {
|
||||||
|
expect(isMatching(
|
||||||
|
'span', [AttributeMarker.SELECT_ONLY, 'directive', 'value'],
|
||||||
|
['', 'directive', 'value']))
|
||||||
|
.toBeFalsy(
|
||||||
|
`Selector '[directive=value]' should not match <span [directive]="exp" [value]="otherExp">`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('class matching', () => {
|
describe('class matching', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user