feat(ivy): namespaced attributes added to output instructions (#24386)
NOTE: This does NOT add parsing of namespaced attributes - Adds AttributeMarker for namespaced attributes - Adds test for namespaced attributes - Updates AttributeMarker enum to use CamelCase, and not UPPER_CASE names PR Close #24386
This commit is contained in:
parent
c8e865ac8e
commit
82c5313740
@ -163,6 +163,53 @@ describe('compiler compliance', () => {
|
|||||||
expectEmit(result.source, template, 'Incorrect template');
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO(https://github.com/angular/angular/issues/24426): We need to support the parser actually
|
||||||
|
// building the proper attributes based off of xmlns atttribuates.
|
||||||
|
xit('should support namspaced attributes', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-component',
|
||||||
|
template: \`<div xmlns:foo="http://someuri/foo" class="my-app" foo:bar="baz" title="Hello" foo:qux="quacks">Hello <b>World</b>!</div>\`
|
||||||
|
})
|
||||||
|
export class MyComponent {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The factory should look like this:
|
||||||
|
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
|
||||||
|
|
||||||
|
// The template should look like this (where IDENT is a wild card for an identifier):
|
||||||
|
const template = `
|
||||||
|
const $c1$ = ['class', 'my-app', 0, 'http://someuri/foo', 'foo:bar', 'baz', 'title', 'Hello', 0, 'http://someuri/foo', 'foo:qux', 'quacks'];
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵE(0, 'div', $e0_attrs$);
|
||||||
|
$r3$.ɵT(1, 'Hello ');
|
||||||
|
$r3$.ɵE(2, 'b');
|
||||||
|
$r3$.ɵT(3, 'World');
|
||||||
|
$r3$.ɵe();
|
||||||
|
$r3$.ɵT(4, '!');
|
||||||
|
$r3$.ɵe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
|
||||||
|
expectEmit(result.source, factory, 'Incorrect factory');
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
it('should bind to element properties', () => {
|
it('should bind to element properties', () => {
|
||||||
const files = {
|
const files = {
|
||||||
app: {
|
app: {
|
||||||
|
@ -147,6 +147,7 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S
|
|||||||
| `<div style="literal">` | ✅ | ✅ | ✅ |
|
| `<div style="literal">` | ✅ | ✅ | ✅ |
|
||||||
| `<div [style]="exp">` | ✅ | ✅ | ✅ |
|
| `<div [style]="exp">` | ✅ | ✅ | ✅ |
|
||||||
| `<div [style.foo]="exp">` | ✅ | ✅ | ✅ |
|
| `<div [style.foo]="exp">` | ✅ | ✅ | ✅ |
|
||||||
|
| `<div xmlns:foo="url" foo:bar="baz">` <br/>Compiler still needs to be updated to process templates with namespaced attributes. ([see #24386](https://github.com/angular/angular/pull/24386)) | ✅ | ✅ | ❌ |
|
||||||
| `{{ ['literal', exp ] }}` | ✅ | ✅ | ✅ |
|
| `{{ ['literal', exp ] }}` | ✅ | ✅ | ✅ |
|
||||||
| `{{ { a: 'literal', b: exp } }}` | ✅ | ✅ | ✅ |
|
| `{{ { a: 'literal', b: exp } }}` | ✅ | ✅ | ✅ |
|
||||||
| `{{ exp \| pipe: arg }}` | ✅ | ✅ | ✅ |
|
| `{{ exp \| pipe: arg }}` | ✅ | ✅ | ✅ |
|
||||||
|
@ -262,7 +262,7 @@ export function injectAttribute(attrNameToInject: string): string|undefined {
|
|||||||
if (attrs) {
|
if (attrs) {
|
||||||
for (let i = 0; i < attrs.length; i = i + 2) {
|
for (let i = 0; i < attrs.length; i = i + 2) {
|
||||||
const attrName = attrs[i];
|
const attrName = attrs[i];
|
||||||
if (attrName === AttributeMarker.SELECT_ONLY) break;
|
if (attrName === AttributeMarker.SelectOnly) break;
|
||||||
if (attrName == attrNameToInject) {
|
if (attrName == attrNameToInject) {
|
||||||
return attrs[i + 1] as string;
|
return attrs[i + 1] as string;
|
||||||
}
|
}
|
||||||
|
@ -858,16 +858,34 @@ export function createTView(
|
|||||||
|
|
||||||
function setUpAttributes(native: RElement, attrs: TAttributes): void {
|
function setUpAttributes(native: RElement, attrs: TAttributes): void {
|
||||||
const isProc = isProceduralRenderer(renderer);
|
const isProc = isProceduralRenderer(renderer);
|
||||||
for (let i = 0; i < attrs.length; i += 2) {
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < attrs.length) {
|
||||||
const attrName = attrs[i];
|
const attrName = attrs[i];
|
||||||
if (attrName === AttributeMarker.SELECT_ONLY) break;
|
if (attrName === AttributeMarker.SelectOnly) break;
|
||||||
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
|
if (attrName === NG_PROJECT_AS_ATTR_NAME) {
|
||||||
const attrVal = attrs[i + 1];
|
i += 2;
|
||||||
|
} else {
|
||||||
ngDevMode && ngDevMode.rendererSetAttribute++;
|
ngDevMode && ngDevMode.rendererSetAttribute++;
|
||||||
|
if (attrName === AttributeMarker.NamespaceURI) {
|
||||||
|
// Namespaced attributes
|
||||||
|
const namespaceURI = attrs[i + 1] as string;
|
||||||
|
const attrName = attrs[i + 2] as string;
|
||||||
|
const attrVal = attrs[i + 3] as string;
|
||||||
|
isProc ?
|
||||||
|
(renderer as ProceduralRenderer3)
|
||||||
|
.setAttribute(native, attrName, attrVal, namespaceURI) :
|
||||||
|
native.setAttributeNS(namespaceURI, attrName, attrVal);
|
||||||
|
i += 4;
|
||||||
|
} else {
|
||||||
|
// Standard attributes
|
||||||
|
const attrVal = attrs[i + 1];
|
||||||
isProc ?
|
isProc ?
|
||||||
(renderer as ProceduralRenderer3)
|
(renderer as ProceduralRenderer3)
|
||||||
.setAttribute(native, attrName as string, attrVal as string) :
|
.setAttribute(native, attrName as string, attrVal as string) :
|
||||||
native.setAttribute(attrName as string, attrVal as string);
|
native.setAttribute(attrName as string, attrVal as string);
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1509,17 +1527,25 @@ function generateInitialInputs(
|
|||||||
initialInputData[directiveIndex] = null;
|
initialInputData[directiveIndex] = null;
|
||||||
|
|
||||||
const attrs = tNode.attrs !;
|
const attrs = tNode.attrs !;
|
||||||
for (let i = 0; i < attrs.length; i += 2) {
|
let i = 0;
|
||||||
|
while (i < attrs.length) {
|
||||||
const attrName = attrs[i];
|
const attrName = attrs[i];
|
||||||
|
if (attrName === AttributeMarker.SelectOnly) break;
|
||||||
|
if (attrName === AttributeMarker.NamespaceURI) {
|
||||||
|
// We do not allow inputs on namespaced attributes.
|
||||||
|
i += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const minifiedInputName = inputs[attrName];
|
const minifiedInputName = inputs[attrName];
|
||||||
const attrValue = attrs[i + 1];
|
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, attrValue as string);
|
inputsToStore.push(minifiedInputName, attrValue as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
}
|
}
|
||||||
return initialInputData;
|
return initialInputData;
|
||||||
}
|
}
|
||||||
|
@ -163,15 +163,20 @@ export interface LProjectionNode extends LNode {
|
|||||||
* items are not regular attributes and the processing should be adapted accordingly.
|
* items are not regular attributes and the processing should be adapted accordingly.
|
||||||
*/
|
*/
|
||||||
export const enum AttributeMarker {
|
export const enum AttributeMarker {
|
||||||
NS = 0, // namespace. Has to be repeated.
|
/**
|
||||||
|
* Marker indicates that the following 3 values in the attributes array are:
|
||||||
|
* namespaceUri, attributeName, attributeValue
|
||||||
|
* in that order.
|
||||||
|
*/
|
||||||
|
NamespaceURI = 0,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This marker indicates that the following attribute names were extracted from bindings (ex.:
|
* This marker indicates that the following attribute names were extracted from bindings (ex.:
|
||||||
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()").
|
* [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:
|
* Taking the above bindings and outputs as an example an attributes array could look as follows:
|
||||||
* ['class', 'fade in', AttributeMarker.SELECT_ONLY, 'foo', 'bar']
|
* ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar']
|
||||||
*/
|
*/
|
||||||
SELECT_ONLY = 1
|
SelectOnly = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import './ng_dev_mode';
|
import './ng_dev_mode';
|
||||||
|
|
||||||
import {assertDefined} from './assert';
|
import {assertDefined, assertNotEqual} from './assert';
|
||||||
import {AttributeMarker, TAttributes, 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';
|
||||||
|
|
||||||
@ -40,7 +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;
|
const selectOnlyMarkerIdx = nodeAttrs ? nodeAttrs.indexOf(AttributeMarker.SelectOnly) : -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
|
||||||
@ -81,9 +81,16 @@ 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 = selectOnlyMarkerIdx > -1 && attrIndexInNode > selectOnlyMarkerIdx ?
|
let nodeAttrValue: string;
|
||||||
'' :
|
const maybeAttrName = nodeAttrs[attrIndexInNode];
|
||||||
nodeAttrs[attrIndexInNode + 1];
|
if (selectOnlyMarkerIdx > -1 && attrIndexInNode > selectOnlyMarkerIdx) {
|
||||||
|
nodeAttrValue = '';
|
||||||
|
} else {
|
||||||
|
ngDevMode && assertNotEqual(
|
||||||
|
maybeAttrName, AttributeMarker.NamespaceURI,
|
||||||
|
'We do not match directives on namespaced attributes');
|
||||||
|
nodeAttrValue = nodeAttrs[attrIndexInNode + 1] as string;
|
||||||
|
}
|
||||||
if (mode & SelectorFlags.CLASS &&
|
if (mode & SelectorFlags.CLASS &&
|
||||||
!isCssClassMatching(nodeAttrValue as string, selectorAttrValue as string) ||
|
!isCssClassMatching(nodeAttrValue as string, selectorAttrValue as string) ||
|
||||||
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
|
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
|
||||||
@ -101,16 +108,34 @@ function isPositive(mode: SelectorFlags): boolean {
|
|||||||
return (mode & SelectorFlags.NOT) === 0;
|
return (mode & SelectorFlags.NOT) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Examines an attributes definition array from a node to find the index of the
|
||||||
|
* attribute with the specified name.
|
||||||
|
*
|
||||||
|
* NOTE: Will not find namespaced attributes.
|
||||||
|
*
|
||||||
|
* @param name the name of the attribute to find
|
||||||
|
* @param attrs the attribute array to examine
|
||||||
|
*/
|
||||||
function findAttrIndexInNode(name: string, attrs: TAttributes | 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 += step) {
|
let selectOnlyMode = false;
|
||||||
const attrName = attrs[i];
|
let i = 0;
|
||||||
if (attrName === name) return i;
|
while (i < attrs.length) {
|
||||||
if (attrName === AttributeMarker.SELECT_ONLY) {
|
const maybeAttrName = attrs[i];
|
||||||
step = 1;
|
if (maybeAttrName === name) {
|
||||||
|
return i;
|
||||||
|
} else if (maybeAttrName === AttributeMarker.NamespaceURI) {
|
||||||
|
// NOTE(benlesh): will not find namespaced attributes. This is by design.
|
||||||
|
i += 4;
|
||||||
|
} else {
|
||||||
|
if (maybeAttrName === AttributeMarker.SelectOnly) {
|
||||||
|
selectOnlyMode = true;
|
||||||
|
}
|
||||||
|
i += selectOnlyMode ? 1 : 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,10 +10,12 @@ import {browserDetection} from '@angular/platform-browser/testing/src/browser_ut
|
|||||||
|
|
||||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core';
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core';
|
||||||
import * as $r3$ from '../../../src/core_render3_private_export';
|
import * as $r3$ from '../../../src/core_render3_private_export';
|
||||||
|
import {AttributeMarker} from '../../../src/render3';
|
||||||
import {ComponentDef} from '../../../src/render3/interfaces/definition';
|
import {ComponentDef} from '../../../src/render3/interfaces/definition';
|
||||||
import {ComponentFixture, renderComponent, toHtml} from '../render_util';
|
import {ComponentFixture, renderComponent, toHtml} from '../render_util';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// See: `normative.md`
|
/// See: `normative.md`
|
||||||
describe('elements', () => {
|
describe('elements', () => {
|
||||||
// Saving type as $any$, etc to simplify testing for compiler, as types aren't saved
|
// Saving type as $any$, etc to simplify testing for compiler, as types aren't saved
|
||||||
@ -150,6 +152,60 @@ describe('elements', () => {
|
|||||||
expect(toHtml(listenerComp)).toEqual('<button>Click</button>');
|
expect(toHtml(listenerComp)).toEqual('<button>Click</button>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support namespaced attributes', () => {
|
||||||
|
type $MyComponent$ = MyComponent;
|
||||||
|
|
||||||
|
// Important: keep arrays outside of function to not create new instances.
|
||||||
|
const $e0_attrs$ = [
|
||||||
|
// class="my-app"
|
||||||
|
'class',
|
||||||
|
'my-app',
|
||||||
|
// foo:bar="baz"
|
||||||
|
AttributeMarker.NamespaceURI,
|
||||||
|
'http://someuri/foo',
|
||||||
|
'foo:bar',
|
||||||
|
'baz',
|
||||||
|
// title="Hello"
|
||||||
|
'title',
|
||||||
|
'Hello',
|
||||||
|
// foo:qux="quacks"
|
||||||
|
AttributeMarker.NamespaceURI,
|
||||||
|
'http://someuri/foo',
|
||||||
|
'foo:qux',
|
||||||
|
'quacks',
|
||||||
|
];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-component',
|
||||||
|
template:
|
||||||
|
`<div xmlns:foo="http://someuri/foo" class="my-app" foo:bar="baz" title="Hello" foo:qux="quacks">Hello <b>World</b>!</div>`
|
||||||
|
})
|
||||||
|
class MyComponent {
|
||||||
|
// NORMATIVE
|
||||||
|
static ngComponentDef = $r3$.ɵdefineComponent({
|
||||||
|
type: MyComponent,
|
||||||
|
selectors: [['my-component']],
|
||||||
|
factory: () => new MyComponent(),
|
||||||
|
template: function(rf: $RenderFlags$, ctx: $MyComponent$) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵE(0, 'div', $e0_attrs$);
|
||||||
|
$r3$.ɵT(1, 'Hello ');
|
||||||
|
$r3$.ɵE(2, 'b');
|
||||||
|
$r3$.ɵT(3, 'World');
|
||||||
|
$r3$.ɵe();
|
||||||
|
$r3$.ɵT(4, '!');
|
||||||
|
$r3$.ɵe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// /NORMATIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(toHtml(renderComponent(MyComponent)))
|
||||||
|
.toEqual(
|
||||||
|
'<div class="my-app" foo:bar="baz" foo:qux="quacks" title="Hello">Hello <b>World</b>!</div>');
|
||||||
|
});
|
||||||
|
|
||||||
describe('bindings', () => {
|
describe('bindings', () => {
|
||||||
it('should bind to property', () => {
|
it('should bind to property', () => {
|
||||||
type $MyComponent$ = MyComponent;
|
type $MyComponent$ = MyComponent;
|
||||||
|
@ -605,7 +605,7 @@ describe('content projection', () => {
|
|||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
elementStart(0, 'child');
|
elementStart(0, 'child');
|
||||||
{
|
{
|
||||||
elementStart(1, 'span', [AttributeMarker.SELECT_ONLY, 'title']);
|
elementStart(1, 'span', [AttributeMarker.SelectOnly, 'title']);
|
||||||
{ text(2, 'Has title'); }
|
{ text(2, 'Has title'); }
|
||||||
elementEnd();
|
elementEnd();
|
||||||
}
|
}
|
||||||
|
@ -1225,7 +1225,7 @@ describe('di', () => {
|
|||||||
|
|
||||||
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
elementStart(0, 'div', ['exist', 'existValue', AttributeMarker.SELECT_ONLY, 'nonExist']);
|
elementStart(0, 'div', ['exist', 'existValue', AttributeMarker.SelectOnly, 'nonExist']);
|
||||||
exist = injectAttribute('exist');
|
exist = injectAttribute('exist');
|
||||||
nonExist = injectAttribute('nonExist');
|
nonExist = injectAttribute('nonExist');
|
||||||
}
|
}
|
||||||
@ -1243,7 +1243,7 @@ describe('di', () => {
|
|||||||
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
elementStart(0, 'div', [
|
elementStart(0, 'div', [
|
||||||
'exist', 'existValue', AttributeMarker.SELECT_ONLY, 'binding1', 'nonExist', 'binding2'
|
'exist', 'existValue', AttributeMarker.SelectOnly, 'binding1', 'nonExist', 'binding2'
|
||||||
]);
|
]);
|
||||||
exist = injectAttribute('exist');
|
exist = injectAttribute('exist');
|
||||||
nonExist = injectAttribute('nonExist');
|
nonExist = injectAttribute('nonExist');
|
||||||
|
@ -34,7 +34,7 @@ describe('directive', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Template() {
|
function Template() {
|
||||||
elementStart(0, 'span', [AttributeMarker.SELECT_ONLY, 'dir']);
|
elementStart(0, 'span', [AttributeMarker.SelectOnly, 'dir']);
|
||||||
elementEnd();
|
elementEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ describe('directive', () => {
|
|||||||
*/
|
*/
|
||||||
function createTemplate() {
|
function createTemplate() {
|
||||||
// using 2 bindings to show example shape of attributes array
|
// using 2 bindings to show example shape of attributes array
|
||||||
elementStart(0, 'span', ['class', 'fade', AttributeMarker.SELECT_ONLY, 'test', 'other']);
|
elementStart(0, 'span', ['class', 'fade', AttributeMarker.SelectOnly, 'test', 'other']);
|
||||||
elementEnd();
|
elementEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,12 +127,12 @@ describe('directive', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <span [prop1]="true" [test]="false" [prop2]="true"></span>
|
* <span class="fade" [prop1]="true" [test]="false" [prop2]="true"></span>
|
||||||
*/
|
*/
|
||||||
function createTemplate() {
|
function createTemplate() {
|
||||||
// putting name (test) in the "usual" value position
|
// putting name (test) in the "usual" value position
|
||||||
elementStart(
|
elementStart(
|
||||||
0, 'span', ['class', 'fade', AttributeMarker.SELECT_ONLY, 'prop1', 'test', 'prop2']);
|
0, 'span', ['class', 'fade', AttributeMarker.SelectOnly, 'prop1', 'test', 'prop2']);
|
||||||
elementEnd();
|
elementEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ describe('directive', () => {
|
|||||||
* <span (out)="someVar = true"></span>
|
* <span (out)="someVar = true"></span>
|
||||||
*/
|
*/
|
||||||
function createTemplate() {
|
function createTemplate() {
|
||||||
elementStart(0, 'span', [AttributeMarker.SELECT_ONLY, 'out']);
|
elementStart(0, 'span', [AttributeMarker.SelectOnly, 'out']);
|
||||||
{ listener('out', () => {}); }
|
{ listener('out', () => {}); }
|
||||||
elementEnd();
|
elementEnd();
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import {NgForOfContext} from '@angular/common';
|
|||||||
import {RenderFlags, directiveInject} from '../../src/render3';
|
import {RenderFlags, directiveInject} from '../../src/render3';
|
||||||
import {defineComponent} from '../../src/render3/definition';
|
import {defineComponent} from '../../src/render3/definition';
|
||||||
import {bind, container, element, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions';
|
import {bind, container, element, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions';
|
||||||
import {LElementNode, LNode} from '../../src/render3/interfaces/node';
|
import {AttributeMarker, LElementNode, LNode} from '../../src/render3/interfaces/node';
|
||||||
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
||||||
import {TrustedString, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
|
import {TrustedString, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
|
||||||
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
|
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
|
||||||
@ -79,12 +79,53 @@ describe('instructions', () => {
|
|||||||
const div = (t.hostNode.native as HTMLElement).querySelector('div') !;
|
const div = (t.hostNode.native as HTMLElement).querySelector('div') !;
|
||||||
expect(div.id).toEqual('test');
|
expect(div.id).toEqual('test');
|
||||||
expect(div.title).toEqual('Hello');
|
expect(div.title).toEqual('Hello');
|
||||||
|
expect(ngDevMode).toHaveProperties({
|
||||||
|
firstTemplatePass: 1,
|
||||||
|
tNode: 2, // 1 for div, 1 for host element
|
||||||
|
tView: 1,
|
||||||
|
rendererCreateElement: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow setting namespaced attributes', () => {
|
||||||
|
const t = new TemplateFixture(() => {
|
||||||
|
elementStart(0, 'div', [
|
||||||
|
// id="test"
|
||||||
|
'id',
|
||||||
|
'test',
|
||||||
|
// test:foo="bar"
|
||||||
|
AttributeMarker.NamespaceURI,
|
||||||
|
'http://someuri.com/2018/test',
|
||||||
|
'test:foo',
|
||||||
|
'bar',
|
||||||
|
// title="Hello"
|
||||||
|
'title',
|
||||||
|
'Hello',
|
||||||
|
]);
|
||||||
|
elementEnd();
|
||||||
|
});
|
||||||
|
|
||||||
|
const div = (t.hostNode.native as HTMLElement).querySelector('div') !;
|
||||||
|
const attrs: any = div.attributes;
|
||||||
|
|
||||||
|
expect(attrs['id'].name).toEqual('id');
|
||||||
|
expect(attrs['id'].namespaceURI).toEqual(null);
|
||||||
|
expect(attrs['id'].value).toEqual('test');
|
||||||
|
|
||||||
|
expect(attrs['test:foo'].name).toEqual('test:foo');
|
||||||
|
expect(attrs['test:foo'].namespaceURI).toEqual('http://someuri.com/2018/test');
|
||||||
|
expect(attrs['test:foo'].value).toEqual('bar');
|
||||||
|
|
||||||
|
expect(attrs['title'].name).toEqual('title');
|
||||||
|
expect(attrs['title'].namespaceURI).toEqual(null);
|
||||||
|
expect(attrs['title'].value).toEqual('Hello');
|
||||||
|
|
||||||
expect(ngDevMode).toHaveProperties({
|
expect(ngDevMode).toHaveProperties({
|
||||||
firstTemplatePass: 1,
|
firstTemplatePass: 1,
|
||||||
tNode: 2, // 1 for div, 1 for host element
|
tNode: 2, // 1 for div, 1 for host element
|
||||||
tView: 1,
|
tView: 1,
|
||||||
rendererCreateElement: 1,
|
rendererCreateElement: 1,
|
||||||
|
rendererSetAttribute: 3
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -86,6 +86,12 @@ describe('css selector matching', () => {
|
|||||||
])).toBeFalsy(`Selector '[other]' should NOT match <span title="">'`);
|
])).toBeFalsy(`Selector '[other]' should NOT match <span title="">'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should match namespaced attributes', () => {
|
||||||
|
expect(isMatching(
|
||||||
|
'span', [AttributeMarker.NamespaceURI, 'http://some/uri', 'title', 'name'],
|
||||||
|
['', 'title', '']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should match selector with one attribute without value when element has several attributes',
|
it('should match selector with one attribute without value when element has several attributes',
|
||||||
() => {
|
() => {
|
||||||
expect(isMatching('span', ['id', 'my_id', 'title', 'test_title'], [
|
expect(isMatching('span', ['id', 'my_id', 'title', 'test_title'], [
|
||||||
@ -179,14 +185,14 @@ describe('css selector matching', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should take optional binding attribute names into account', () => {
|
it('should take optional binding attribute names into account', () => {
|
||||||
expect(isMatching('span', [AttributeMarker.SELECT_ONLY, 'directive'], [
|
expect(isMatching('span', [AttributeMarker.SelectOnly, 'directive'], [
|
||||||
'', 'directive', ''
|
'', 'directive', ''
|
||||||
])).toBeTruthy(`Selector '[directive]' should match <span [directive]="exp">`);
|
])).toBeTruthy(`Selector '[directive]' should match <span [directive]="exp">`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not match optional binding attribute names if attribute selector has value',
|
it('should not match optional binding attribute names if attribute selector has value',
|
||||||
() => {
|
() => {
|
||||||
expect(isMatching('span', [AttributeMarker.SELECT_ONLY, 'directive'], [
|
expect(isMatching('span', [AttributeMarker.SelectOnly, 'directive'], [
|
||||||
'', 'directive', 'value'
|
'', 'directive', 'value'
|
||||||
])).toBeFalsy(`Selector '[directive=value]' should not match <span [directive]="exp">`);
|
])).toBeFalsy(`Selector '[directive=value]' should not match <span [directive]="exp">`);
|
||||||
});
|
});
|
||||||
@ -194,7 +200,7 @@ describe('css selector matching', () => {
|
|||||||
it('should not match optional binding attribute names if attribute selector has value and next name equals to value',
|
it('should not match optional binding attribute names if attribute selector has value and next name equals to value',
|
||||||
() => {
|
() => {
|
||||||
expect(isMatching(
|
expect(isMatching(
|
||||||
'span', [AttributeMarker.SELECT_ONLY, 'directive', 'value'],
|
'span', [AttributeMarker.SelectOnly, 'directive', 'value'],
|
||||||
['', 'directive', 'value']))
|
['', 'directive', 'value']))
|
||||||
.toBeFalsy(
|
.toBeFalsy(
|
||||||
`Selector '[directive=value]' should not match <span [directive]="exp" [value]="otherExp">`);
|
`Selector '[directive=value]' should not match <span [directive]="exp" [value]="otherExp">`);
|
||||||
|
@ -859,7 +859,7 @@ describe('query', () => {
|
|||||||
}
|
}
|
||||||
}, null, []);
|
}, null, []);
|
||||||
|
|
||||||
container(5, undefined, null, [AttributeMarker.SELECT_ONLY, 'vc']);
|
container(5, undefined, null, [AttributeMarker.SelectOnly, 'vc']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rf & RenderFlags.Update) {
|
if (rf & RenderFlags.Update) {
|
||||||
@ -938,8 +938,8 @@ describe('query', () => {
|
|||||||
}
|
}
|
||||||
}, null, []);
|
}, null, []);
|
||||||
|
|
||||||
container(2, undefined, null, [AttributeMarker.SELECT_ONLY, 'vc']);
|
container(2, undefined, null, [AttributeMarker.SelectOnly, 'vc']);
|
||||||
container(3, undefined, null, [AttributeMarker.SELECT_ONLY, 'vc']);
|
container(3, undefined, null, [AttributeMarker.SelectOnly, 'vc']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rf & RenderFlags.Update) {
|
if (rf & RenderFlags.Update) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user