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:
Pawel Kozlowski 2018-05-04 15:58:42 +02:00 committed by Victor Berchet
parent b87d650da2
commit 90bf5d8961
9 changed files with 349 additions and 74 deletions

View File

@ -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 {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList} from './interfaces/definition';
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 {Renderer3} from './interfaces/renderer';
import {LView, TView} from './interfaces/view';
@ -251,7 +251,7 @@ export function injectChangeDetectorRef(): viewEngine_ChangeDetectorRef {
*
* @experimental
*/
export function injectAttribute(attrName: string): string|undefined {
export function injectAttribute(attrNameToInject: string): string|undefined {
ngDevMode && assertPreviousIsParent();
const lElement = getPreviousOrParentNode() as LElementNode;
ngDevMode && assertNodeType(lElement, TNodeType.Element);
@ -260,8 +260,10 @@ export function injectAttribute(attrName: string): string|undefined {
const attrs = tElement.attrs;
if (attrs) {
for (let i = 0; i < attrs.length; i = i + 2) {
if (attrs[i] == attrName) {
return attrs[i + 1];
const attrName = attrs[i];
if (attrName === AttributeMarker.SELECT_ONLY) break;
if (attrName == attrNameToInject) {
return attrs[i + 1] as string;
}
}
}

View File

@ -73,6 +73,10 @@ export {
tick,
} from './instructions';
export {
AttributeMarker
} from './interfaces/node';
export {
pipe as Pp,
pipeBind1 as pb1,

View File

@ -15,7 +15,7 @@ import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interface
import {LQueries} from './interfaces/query';
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 {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@ -366,19 +366,19 @@ export function createLNodeObject(
*/
export function createLNode(
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(
index: number | null, type: TNodeType.View, native: null, name: null, attrs: null,
lView: LView): LViewNode;
export function createLNode(
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(
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;
export function createLNode(
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&LTextNode&LViewNode&LContainerNode&LProjectionNode {
const parent = isParent ? previousOrParentNode :
previousOrParentNode && getParentLNode(previousOrParentNode) !as LNode;
@ -586,7 +586,8 @@ function getRenderFlags(view: LView): RenderFlags {
* ['id', 'warning5', 'class', 'alert']
*/
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 &&
assertEqual(
currentView.bindingStartIndex, -1, 'elements should be created before any bindings');
@ -600,23 +601,18 @@ export function elementStart(
if (attrs) setUpAttributes(native, attrs);
appendChild(getParentLNode(node), native, currentView);
createDirectivesAndLocals(index, name, attrs, localRefs, false);
createDirectivesAndLocals(localRefs);
return native;
}
/**
* 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 inlineViews Whether or not this node will create inline views
*/
function createDirectivesAndLocals(
index: number, name: string | null, attrs: string[] | null | undefined,
localRefs: string[] | null | undefined, inlineViews: boolean) {
function createDirectivesAndLocals(localRefs?: string[] | null) {
const node = previousOrParentNode;
if (firstTemplatePass) {
ngDevMode && ngDevMode.firstTemplatePass++;
cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null);
@ -822,17 +818,18 @@ export function createTView(
};
}
function setUpAttributes(native: RElement, attrs: string[]): void {
ngDevMode && assertEqual(attrs.length % 2, 0, 'each attribute should have a key and a value');
function setUpAttributes(native: RElement, attrs: TAttributes): void {
const isProc = isProceduralRenderer(renderer);
for (let i = 0; i < attrs.length; i += 2) {
const attrName = attrs[i];
if (attrName === AttributeMarker.SELECT_ONLY) break;
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
const attrVal = attrs[i + 1];
ngDevMode && ngDevMode.rendererSetAttribute++;
isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) :
native.setAttribute(attrName, attrVal);
isProc ?
(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
*/
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 {
ngDevMode && ngDevMode.tNode++;
return {
@ -1442,10 +1439,13 @@ function generateInitialInputs(
for (let i = 0; i < attrs.length; i += 2) {
const attrName = attrs[i];
const minifiedInputName = inputs[attrName];
const attrValue = attrs[i + 1];
if (attrName === AttributeMarker.SELECT_ONLY) break;
if (minifiedInputName !== undefined) {
const inputsToStore: InitialInputs =
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
inputsToStore.push(minifiedInputName, attrs[i + 1]);
inputsToStore.push(minifiedInputName, attrValue as string);
}
}
return initialInputData;
@ -1484,7 +1484,7 @@ export function createLContainer(
* @param localRefs A set of local reference bindings on the element.
*/
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 {
ngDevMode && assertEqual(
currentView.bindingStartIndex, -1,
@ -1501,7 +1501,7 @@ export function container(
// Containers are added to the current view tree instead of their embedded views
// because views can be removed and re-inserted.
addToViewTree(currentView, node.data);
createDirectivesAndLocals(index, tagName || null, attrs, localRefs, template == null);
createDirectivesAndLocals(localRefs);
isParent = false;
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container);

View File

@ -158,6 +158,29 @@ export interface LProjectionNode extends LNode {
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
* of a specific type.
@ -198,18 +221,20 @@ export interface TNode {
tagName: string|null;
/**
* Static attributes associated with an element. We need to store
* static attributes to support content projection with selectors.
* Attributes are stored statically because reading them from the DOM
* would be way too slow for content projection and queries.
* Attributes associated with an element. We need to store attributes to support various use-cases
* (attribute injection, content projection with selectors, directives matching).
* Attributes are stored statically because reading them from the DOM would be way too slow for
* content projection and queries.
*
* Since attrs will always be calculated first, they will never need
* to be marked undefined by other instructions.
* Since attrs will always be calculated first, they will never need to be marked undefined by
* 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']
* 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

View File

@ -9,7 +9,7 @@
import './ng_dev_mode';
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';
const unusedValueToPlacateAjd = unused1 + unused2;
@ -40,6 +40,7 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo
let mode: SelectorFlags = SelectorFlags.ELEMENT;
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
// 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];
if (selectorAttrValue !== '') {
const nodeAttrValue = nodeAttrs[attrIndexInNode + 1];
const nodeAttrValue = selectOnlyMarkerIdx > -1 && attrIndexInNode > selectOnlyMarkerIdx ?
'' :
nodeAttrs[attrIndexInNode + 1];
if (mode & SelectorFlags.CLASS &&
!isCssClassMatching(nodeAttrValue, selectorAttrValue as string) ||
!isCssClassMatching(nodeAttrValue as string, selectorAttrValue as string) ||
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
if (isPositive(mode)) return false;
skipToNextSelector = true;
@ -98,10 +101,15 @@ function isPositive(mode: SelectorFlags): boolean {
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;
for (let i = 0; i < attrs.length; i += 2) {
if (attrs[i] === name) return i;
for (let i = 0; i < attrs.length; i += step) {
const attrName = attrs[i];
if (attrName === name) return i;
if (attrName === AttributeMarker.SELECT_ONLY) {
step = 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
// (attribute names are stored at even indexes)
if ((ngProjectAsAttrIdx & 1) === 0) {
return nodeAttrs[ngProjectAsAttrIdx + 1];
return nodeAttrs[ngProjectAsAttrIdx + 1] as string;
}
}
return null;

View File

@ -8,10 +8,11 @@
import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection';
import {detectChanges} from '../../src/render3/index';
import {container, containerRefreshEnd, containerRefreshStart, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, load, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions';
import {AttributeMarker, detectChanges} from '../../src/render3/index';
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 {createComponent, renderComponent, toHtml} from './render_util';
import {ComponentFixture, createComponent, renderComponent, toHtml} from './render_util';
describe('content projection', () => {
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>');
});
// 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', () => {
/**
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>

View File

@ -12,9 +12,9 @@ import {RenderFlags} from '@angular/core/src/render3/interfaces/definition';
import {defineComponent} from '../../src/render3/definition';
import {bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector, injectAttribute} from '../../src/render3/di';
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 {TNodeType} from '../../src/render3/interfaces/node';
import {AttributeMarker, TNodeType} from '../../src/render3/interfaces/node';
import {LViewFlags} from '../../src/render3/interfaces/view';
import {ViewRef} from '../../src/render3/view_ref';
@ -1199,7 +1199,9 @@ describe('di', () => {
});
});
it('should injectAttribute', () => {
describe('@Attribute', () => {
it('should inject attribute', () => {
let exist: string|undefined = 'wrong';
let nonExist: string|undefined = 'wrong';
@ -1211,11 +1213,49 @@ describe('di', () => {
}
});
const app = renderComponent(MyApp);
const fixture = new ComponentFixture(MyApp);
expect(exist).toEqual('existValue');
expect(nonExist).toEqual(undefined);
});
// https://stackblitz.com/edit/angular-8ytqkp?file=src%2Fapp%2Fapp.component.ts
it('should not 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, '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('bloom filter', () => {
let di: LInjector;

View File

@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {defineDirective} from '../../src/render3/index';
import {bind, elementEnd, elementProperty, elementStart, loadDirective} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {renderToHtml} from './render_util';
import {EventEmitter} from '@angular/core';
import {AttributeMarker, defineDirective} from '../../src/render3/index';
import {bind, elementEnd, elementProperty, elementStart, listener, loadDirective} from '../../src/render3/instructions';
import {TemplateFixture} from './render_util';
describe('directive', () => {
@ -31,17 +33,151 @@ describe('directive', () => {
});
}
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span', ['dir', '']);
function Template() {
elementStart(0, 'span', [AttributeMarker.SELECT_ONLY, 'dir']);
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>');
directiveInstance !.klass = 'bar';
expect(renderToHtml(Template, {}, defs)).toEqual('<span class="bar" dir=""></span>');
/**
* <span [test]="false" [other]="true"></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();
});
});

View File

@ -6,12 +6,12 @@
* 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 {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 {
type: TNodeType.Element,
index: 0,
@ -29,7 +29,7 @@ function testLStaticData(tagName: string, attrs: string[] | null): TNode {
}
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);
}
@ -177,6 +177,28 @@ describe('css selector matching', () => {
'', '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', () => {