fix(DomSchemaRegistry): detect invalid elements

This commit is contained in:
Victor Berchet 2016-08-23 10:52:40 -07:00
parent 2b20db6c5a
commit 1df69cb4d2
22 changed files with 638 additions and 395 deletions

View File

@ -19,7 +19,7 @@ let thisArg: any;
export function main() { export function main() {
describe('ngFor', () => { describe('ngFor', () => {
const TEMPLATE = const TEMPLATE =
'<div><copy-me template="ngFor let item of items">{{item.toString()}};</copy-me></div>'; '<div><span template="ngFor let item of items">{{item.toString()}};</span></div>';
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule( TestBed.configureTestingModule(
@ -218,7 +218,7 @@ export function main() {
it('should display indices correctly', async(() => { it('should display indices correctly', async(() => {
const template = const template =
'<div><copy-me template="ngFor: let item of items; let i=index">{{i.toString()}}</copy-me></div>'; '<div><span template="ngFor: let item of items; let i=index">{{i.toString()}}</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
@ -233,7 +233,7 @@ export function main() {
it('should display first item correctly', async(() => { it('should display first item correctly', async(() => {
const template = const template =
'<div><copy-me template="ngFor: let item of items; let isFirst=first">{{isFirst.toString()}}</copy-me></div>'; '<div><span template="ngFor: let item of items; let isFirst=first">{{isFirst.toString()}}</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
@ -248,7 +248,7 @@ export function main() {
it('should display last item correctly', async(() => { it('should display last item correctly', async(() => {
const template = const template =
'<div><copy-me template="ngFor: let item of items; let isLast=last">{{isLast.toString()}}</copy-me></div>'; '<div><span template="ngFor: let item of items; let isLast=last">{{isLast.toString()}}</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
@ -263,7 +263,7 @@ export function main() {
it('should display even items correctly', async(() => { it('should display even items correctly', async(() => {
const template = const template =
'<div><copy-me template="ngFor: let item of items; let isEven=even">{{isEven.toString()}}</copy-me></div>'; '<div><span template="ngFor: let item of items; let isEven=even">{{isEven.toString()}}</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
@ -278,7 +278,7 @@ export function main() {
it('should display odd items correctly', async(() => { it('should display odd items correctly', async(() => {
const template = const template =
'<div><copy-me template="ngFor: let item of items; let isOdd=odd">{{isOdd.toString()}}</copy-me></div>'; '<div><span template="ngFor: let item of items; let isOdd=odd">{{isOdd.toString()}}</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);

View File

@ -20,149 +20,149 @@ export function main() {
}); });
it('should work in a template attribute', async(() => { it('should work in a template attribute', async(() => {
const template = '<div><copy-me template="ngIf booleanCondition">hello</copy-me></div>'; const template = '<div><span template="ngIf booleanCondition">hello</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('hello'); expect(fixture.debugElement.nativeElement).toHaveText('hello');
})); }));
it('should work in a template element', async(() => { it('should work in a template element', async(() => {
const template = const template =
'<div><template [ngIf]="booleanCondition"><copy-me>hello2</copy-me></template></div>'; '<div><template [ngIf]="booleanCondition"><span>hello2</span></template></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('hello2'); expect(fixture.debugElement.nativeElement).toHaveText('hello2');
})); }));
it('should toggle node when condition changes', async(() => { it('should toggle node when condition changes', async(() => {
const template = '<div><copy-me template="ngIf booleanCondition">hello</copy-me></div>'; const template = '<div><span template="ngIf booleanCondition">hello</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.debugElement.componentInstance.booleanCondition = false; fixture.debugElement.componentInstance.booleanCondition = false;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(0); .toEqual(0);
expect(fixture.debugElement.nativeElement).toHaveText(''); expect(fixture.debugElement.nativeElement).toHaveText('');
fixture.debugElement.componentInstance.booleanCondition = true; fixture.debugElement.componentInstance.booleanCondition = true;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('hello'); expect(fixture.debugElement.nativeElement).toHaveText('hello');
fixture.debugElement.componentInstance.booleanCondition = false; fixture.debugElement.componentInstance.booleanCondition = false;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(0); .toEqual(0);
expect(fixture.debugElement.nativeElement).toHaveText(''); expect(fixture.debugElement.nativeElement).toHaveText('');
})); }));
it('should handle nested if correctly', async(() => { it('should handle nested if correctly', async(() => {
const template = const template =
'<div><template [ngIf]="booleanCondition"><copy-me *ngIf="nestedBooleanCondition">hello</copy-me></template></div>'; '<div><template [ngIf]="booleanCondition"><span *ngIf="nestedBooleanCondition">hello</span></template></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.debugElement.componentInstance.booleanCondition = false; fixture.debugElement.componentInstance.booleanCondition = false;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(0); .toEqual(0);
expect(fixture.debugElement.nativeElement).toHaveText(''); expect(fixture.debugElement.nativeElement).toHaveText('');
fixture.debugElement.componentInstance.booleanCondition = true; fixture.debugElement.componentInstance.booleanCondition = true;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('hello'); expect(fixture.debugElement.nativeElement).toHaveText('hello');
fixture.debugElement.componentInstance.nestedBooleanCondition = false; fixture.debugElement.componentInstance.nestedBooleanCondition = false;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(0); .toEqual(0);
expect(fixture.debugElement.nativeElement).toHaveText(''); expect(fixture.debugElement.nativeElement).toHaveText('');
fixture.debugElement.componentInstance.nestedBooleanCondition = true; fixture.debugElement.componentInstance.nestedBooleanCondition = true;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('hello'); expect(fixture.debugElement.nativeElement).toHaveText('hello');
fixture.debugElement.componentInstance.booleanCondition = false; fixture.debugElement.componentInstance.booleanCondition = false;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(0); .toEqual(0);
expect(fixture.debugElement.nativeElement).toHaveText(''); expect(fixture.debugElement.nativeElement).toHaveText('');
})); }));
it('should update several nodes with if', async(() => { it('should update several nodes with if', async(() => {
const template = '<div>' + const template = '<div>' +
'<copy-me template="ngIf numberCondition + 1 >= 2">helloNumber</copy-me>' + '<span template="ngIf numberCondition + 1 >= 2">helloNumber</span>' +
'<copy-me template="ngIf stringCondition == \'foo\'">helloString</copy-me>' + '<span template="ngIf stringCondition == \'foo\'">helloString</span>' +
'<copy-me template="ngIf functionCondition(stringCondition, numberCondition)">helloFunction</copy-me>' + '<span template="ngIf functionCondition(stringCondition, numberCondition)">helloFunction</span>' +
'</div>'; '</div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(3); .toEqual(3);
expect(getDOM().getText(fixture.debugElement.nativeElement)) expect(getDOM().getText(fixture.debugElement.nativeElement))
.toEqual('helloNumberhelloStringhelloFunction'); .toEqual('helloNumberhelloStringhelloFunction');
fixture.debugElement.componentInstance.numberCondition = 0; fixture.debugElement.componentInstance.numberCondition = 0;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('helloString'); expect(fixture.debugElement.nativeElement).toHaveText('helloString');
fixture.debugElement.componentInstance.numberCondition = 1; fixture.debugElement.componentInstance.numberCondition = 1;
fixture.debugElement.componentInstance.stringCondition = 'bar'; fixture.debugElement.componentInstance.stringCondition = 'bar';
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('helloNumber'); expect(fixture.debugElement.nativeElement).toHaveText('helloNumber');
})); }));
it('should not add the element twice if the condition goes from true to true (JS)', it('should not add the element twice if the condition goes from true to true (JS)',
async(() => { async(() => {
const template = '<div><copy-me template="ngIf numberCondition">hello</copy-me></div>'; const template = '<div><span template="ngIf numberCondition">hello</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('hello'); expect(fixture.debugElement.nativeElement).toHaveText('hello');
fixture.debugElement.componentInstance.numberCondition = 2; fixture.debugElement.componentInstance.numberCondition = 2;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length) expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
.toEqual(1); .toEqual(1);
expect(fixture.debugElement.nativeElement).toHaveText('hello'); expect(fixture.debugElement.nativeElement).toHaveText('hello');
})); }));
it('should not recreate the element if the condition goes from true to true (JS)', async(() => { it('should not recreate the element if the condition goes from true to true (JS)', async(() => {
const template = '<div><copy-me template="ngIf numberCondition">hello</copy-me></div>'; const template = '<div><span template="ngIf numberCondition">hello</span></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
getDOM().addClass( getDOM().addClass(
getDOM().querySelector(fixture.debugElement.nativeElement, 'copy-me'), 'foo'); getDOM().querySelector(fixture.debugElement.nativeElement, 'span'), 'foo');
fixture.debugElement.componentInstance.numberCondition = 2; fixture.debugElement.componentInstance.numberCondition = 2;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().hasClass( expect(getDOM().hasClass(
getDOM().querySelector(fixture.debugElement.nativeElement, 'copy-me'), 'foo')) getDOM().querySelector(fixture.debugElement.nativeElement, 'span'), 'foo'))
.toBe(true); .toBe(true);
})); }));
}); });

View File

@ -7,7 +7,7 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, ContentChildren, Directive, QueryList, TemplateRef} from '@angular/core'; import {Component, ContentChildren, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef} from '@angular/core';
import {TestBed, async} from '@angular/core/testing'; import {TestBed, async} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers'; import {expect} from '@angular/platform-browser/testing/matchers';
@ -20,7 +20,7 @@ export function main() {
}); });
it('should do nothing if templateRef is null', async(() => { it('should do nothing if templateRef is null', async(() => {
var template = `<template [ngTemplateOutlet]="null"></template>`; const template = `<template [ngTemplateOutlet]="null"></template>`;
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
@ -29,9 +29,11 @@ export function main() {
})); }));
it('should insert content specified by TemplateRef', async(() => { it('should insert content specified by TemplateRef', async(() => {
var template = const template =
`<tpl-refs #refs="tplRefs"><template>foo</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`; `<tpl-refs #refs="tplRefs"><template>foo</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`;
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
@ -45,9 +47,10 @@ export function main() {
})); }));
it('should clear content if TemplateRef becomes null', async(() => { it('should clear content if TemplateRef becomes null', async(() => {
var template = const template =
`<tpl-refs #refs="tplRefs"><template>foo</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`; `<tpl-refs #refs="tplRefs"><template>foo</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`;
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
@ -63,9 +66,10 @@ export function main() {
})); }));
it('should swap content if TemplateRef changes', async(() => { it('should swap content if TemplateRef changes', async(() => {
var template = const template =
`<tpl-refs #refs="tplRefs"><template>foo</template><template>bar</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`; `<tpl-refs #refs="tplRefs"><template>foo</template><template>bar</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`;
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
@ -81,9 +85,10 @@ export function main() {
})); }));
it('should display template if context is null', async(() => { it('should display template if context is null', async(() => {
var template = const template =
`<tpl-refs #refs="tplRefs"><template>foo</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef" [ngOutletContext]="null"></template>`; `<tpl-refs #refs="tplRefs"><template>foo</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef" [ngOutletContext]="null"></template>`;
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
@ -97,9 +102,10 @@ export function main() {
})); }));
it('should reflect initial context and changes', async(() => { it('should reflect initial context and changes', async(() => {
var template = const template =
`<tpl-refs #refs="tplRefs"><template let-foo="foo"><span>{{foo}}</span></template></tpl-refs><template [ngTemplateOutlet]="currentTplRef" [ngOutletContext]="context"></template>`; `<tpl-refs #refs="tplRefs"><template let-foo="foo"><span>{{foo}}</span></template></tpl-refs><template [ngTemplateOutlet]="currentTplRef" [ngOutletContext]="context"></template>`;
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
@ -116,9 +122,10 @@ export function main() {
})); }));
it('should reflect user defined $implicit property in the context', async(() => { it('should reflect user defined $implicit property in the context', async(() => {
var template = const template =
`<tpl-refs #refs="tplRefs"><template let-ctx><span>{{ctx.foo}}</span></template></tpl-refs><template [ngTemplateOutlet]="currentTplRef" [ngOutletContext]="context"></template>`; `<tpl-refs #refs="tplRefs"><template let-ctx><span>{{ctx.foo}}</span></template></tpl-refs><template [ngTemplateOutlet]="currentTplRef" [ngOutletContext]="context"></template>`;
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();
@ -131,9 +138,11 @@ export function main() {
})); }));
it('should reflect context re-binding', async(() => { it('should reflect context re-binding', async(() => {
var template = const template =
`<tpl-refs #refs="tplRefs"><template let-shawshank="shawshank"><span>{{shawshank}}</span></template></tpl-refs><template [ngTemplateOutlet]="currentTplRef" [ngOutletContext]="context"></template>`; `<tpl-refs #refs="tplRefs"><template let-shawshank="shawshank"><span>{{shawshank}}</span></template></tpl-refs><template [ngTemplateOutlet]="currentTplRef" [ngOutletContext]="context"></template>`;
TestBed.overrideComponent(TestComponent, {set: {template: template}}); TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
let fixture = TestBed.createComponent(TestComponent); let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges(); fixture.detectChanges();

View File

@ -8,13 +8,9 @@
import {CUSTOM_ELEMENTS_SCHEMA, Injectable, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '@angular/core'; import {CUSTOM_ELEMENTS_SCHEMA, Injectable, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '@angular/core';
import {StringMapWrapper} from '../facade/collection';
import {isPresent} from '../facade/lang';
import {SECURITY_SCHEMA} from './dom_security_schema'; import {SECURITY_SCHEMA} from './dom_security_schema';
import {ElementSchemaRegistry} from './element_schema_registry'; import {ElementSchemaRegistry} from './element_schema_registry';
const EVENT = 'event';
const BOOLEAN = 'boolean'; const BOOLEAN = 'boolean';
const NUMBER = 'number'; const NUMBER = 'number';
const STRING = 'string'; const STRING = 'string';
@ -26,7 +22,7 @@ const OBJECT = 'object';
* ## Overview * ## Overview
* *
* Each line represents one kind of element. The `element_inheritance` and properties are joined * Each line represents one kind of element. The `element_inheritance` and properties are joined
* using `element_inheritance|preperties` syntax. * using `element_inheritance|properties` syntax.
* *
* ## Element Inheritance * ## Element Inheritance
* *
@ -54,7 +50,7 @@ const OBJECT = 'object';
* *
* ## Query * ## Query
* *
* The class creates an internal squas representaino which allows to easily answer the query of * The class creates an internal squas representation which allows to easily answer the query of
* if a given property exist on a given element. * if a given property exist on a given element.
* *
* NOTE: We don't yet support querying for types or events. * NOTE: We don't yet support querying for types or events.
@ -77,9 +73,9 @@ const OBJECT = 'object';
const SCHEMA: string[] = ([ const SCHEMA: string[] = ([
'*|textContent,%classList,className,id,innerHTML,*beforecopy,*beforecut,*beforepaste,*copy,*cut,*paste,*search,*selectstart,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerHTML,#scrollLeft,#scrollTop', '*|textContent,%classList,className,id,innerHTML,*beforecopy,*beforecut,*beforepaste,*copy,*cut,*paste,*search,*selectstart,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerHTML,#scrollLeft,#scrollTop',
'^*|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*autocomplete,*autocompleteerror,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate', 'abbr,address,article,aside,b,bdi,bdo,cite,code,dd,dfn,dt,em,figcaption,figure,footer,header,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr^*|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate',
'media|!autoplay,!controls,%crossOrigin,#currentTime,!defaultMuted,#defaultPlaybackRate,!disableRemotePlayback,!loop,!muted,*encrypted,#playbackRate,preload,src,#volume', 'media^abbr|!autoplay,!controls,%crossOrigin,#currentTime,!defaultMuted,#defaultPlaybackRate,!disableRemotePlayback,!loop,!muted,*encrypted,#playbackRate,preload,src,%srcObject,#volume',
':svg:^*|*abort,*autocomplete,*autocompleteerror,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*cuechange,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*seeked,*seeking,*select,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,%style,#tabIndex', ':svg:^abbr|*abort,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*cuechange,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*seeked,*seeking,*select,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,%style,#tabIndex',
':svg:graphics^:svg:|', ':svg:graphics^:svg:|',
':svg:animation^:svg:|*begin,*end,*repeat', ':svg:animation^:svg:|*begin,*end,*repeat',
':svg:geometry^:svg:|', ':svg:geometry^:svg:|',
@ -87,74 +83,75 @@ const SCHEMA: string[] = ([
':svg:gradient^:svg:|', ':svg:gradient^:svg:|',
':svg:textContent^:svg:graphics|', ':svg:textContent^:svg:graphics|',
':svg:textPositioning^:svg:textContent|', ':svg:textPositioning^:svg:textContent|',
'a|charset,coords,download,hash,host,hostname,href,hreflang,name,password,pathname,ping,port,protocol,referrerpolicy,rel,rev,search,shape,target,text,type,username', 'abbr^*|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate',
'area|alt,coords,hash,host,hostname,href,!noHref,password,pathname,ping,port,protocol,referrerpolicy,search,shape,target,username', 'a^abbr|charset,coords,download,hash,host,hostname,href,hreflang,name,password,pathname,ping,port,protocol,referrerPolicy,rel,rev,search,shape,target,text,type,username',
'area^abbr|alt,coords,hash,host,hostname,href,!noHref,password,pathname,ping,port,protocol,referrerPolicy,search,shape,target,username',
'audio^media|', 'audio^media|',
'br|clear', 'br^abbr|clear',
'base|href,target', 'base^abbr|href,target',
'body|aLink,background,bgColor,link,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,text,vLink', 'body^abbr|aLink,background,bgColor,link,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,text,vLink',
'button|!autofocus,!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,type,value', 'button^abbr|!autofocus,!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,type,value',
'canvas|#height,#width', 'canvas^abbr|#height,#width',
'content|select', 'content^abbr|select',
'dl|!compact', 'dl^abbr|!compact',
'datalist|', 'datalist^abbr|',
'details|!open', 'details^abbr|!open',
'dialog|!open,returnValue', 'dialog^abbr|!open,returnValue',
'dir|!compact', 'dir^abbr|!compact',
'div|align', 'div^abbr|align',
'embed|align,height,name,src,type,width', 'embed^abbr|align,height,name,src,type,width',
'fieldset|!disabled,name', 'fieldset^abbr|!disabled,name',
'font|color,face,size', 'font^abbr|color,face,size',
'form|acceptCharset,action,autocomplete,encoding,enctype,method,name,!noValidate,target', 'form^abbr|acceptCharset,action,autocomplete,encoding,enctype,method,name,!noValidate,target',
'frame|frameBorder,longDesc,marginHeight,marginWidth,name,!noResize,scrolling,src', 'frame^abbr|frameBorder,longDesc,marginHeight,marginWidth,name,!noResize,scrolling,src',
'frameset|cols,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,rows', 'frameset^abbr|cols,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,rows',
'hr|align,color,!noShade,size,width', 'hr^abbr|align,color,!noShade,size,width',
'head|', 'head^abbr|',
'h1,h2,h3,h4,h5,h6|align', 'h1,h2,h3,h4,h5,h6^abbr|align',
'html|version', 'html^abbr|version',
'iframe|align,!allowFullscreen,frameBorder,height,longDesc,marginHeight,marginWidth,name,referrerpolicy,%sandbox,scrolling,src,srcdoc,width', 'iframe^abbr|align,!allowFullscreen,frameBorder,height,longDesc,marginHeight,marginWidth,name,referrerPolicy,%sandbox,scrolling,src,srcdoc,width',
'img|align,alt,border,%crossOrigin,#height,#hspace,!isMap,longDesc,lowsrc,name,referrerpolicy,sizes,src,srcset,useMap,#vspace,#width', 'img^abbr|align,alt,border,%crossOrigin,#height,#hspace,!isMap,longDesc,lowsrc,name,referrerPolicy,sizes,src,srcset,useMap,#vspace,#width',
'input|accept,align,alt,autocapitalize,autocomplete,!autofocus,!checked,!defaultChecked,defaultValue,dirName,!disabled,%files,formAction,formEnctype,formMethod,!formNoValidate,formTarget,#height,!incremental,!indeterminate,max,#maxLength,min,#minLength,!multiple,name,pattern,placeholder,!readOnly,!required,selectionDirection,#selectionEnd,#selectionStart,#size,src,step,type,useMap,value,%valueAsDate,#valueAsNumber,#width', 'input^abbr|accept,align,alt,autocapitalize,autocomplete,!autofocus,!checked,!defaultChecked,defaultValue,dirName,!disabled,%files,formAction,formEnctype,formMethod,!formNoValidate,formTarget,#height,!incremental,!indeterminate,max,#maxLength,min,#minLength,!multiple,name,pattern,placeholder,!readOnly,!required,selectionDirection,#selectionEnd,#selectionStart,#size,src,step,type,useMap,value,%valueAsDate,#valueAsNumber,#width',
'keygen|!autofocus,challenge,!disabled,keytype,name', 'keygen^abbr|!autofocus,challenge,!disabled,keytype,name',
'li|type,#value', 'li^abbr|type,#value',
'label|htmlFor', 'label^abbr|htmlFor',
'legend|align', 'legend^abbr|align',
'link|as,charset,%crossOrigin,!disabled,href,hreflang,integrity,media,rel,%relList,rev,%sizes,target,type', 'link^abbr|as,charset,%crossOrigin,!disabled,href,hreflang,integrity,media,rel,%relList,rev,%sizes,target,type',
'map|name', 'map^abbr|name',
'marquee|behavior,bgColor,direction,height,#hspace,#loop,#scrollAmount,#scrollDelay,!trueSpeed,#vspace,width', 'marquee^abbr|behavior,bgColor,direction,height,#hspace,#loop,#scrollAmount,#scrollDelay,!trueSpeed,#vspace,width',
'menu|!compact', 'menu^abbr|!compact',
'meta|content,httpEquiv,name,scheme', 'meta^abbr|content,httpEquiv,name,scheme',
'meter|#high,#low,#max,#min,#optimum,#value', 'meter^abbr|#high,#low,#max,#min,#optimum,#value',
'ins,del|cite,dateTime', 'ins,del^abbr|cite,dateTime',
'ol|!compact,!reversed,#start,type', 'ol^abbr|!compact,!reversed,#start,type',
'object|align,archive,border,code,codeBase,codeType,data,!declare,height,#hspace,name,standby,type,useMap,#vspace,width', 'object^abbr|align,archive,border,code,codeBase,codeType,data,!declare,height,#hspace,name,standby,type,useMap,#vspace,width',
'optgroup|!disabled,label', 'optgroup^abbr|!disabled,label',
'option|!defaultSelected,!disabled,label,!selected,text,value', 'option^abbr|!defaultSelected,!disabled,label,!selected,text,value',
'output|defaultValue,%htmlFor,name,value', 'output^abbr|defaultValue,%htmlFor,name,value',
'p|align', 'p^abbr|align',
'param|name,type,value,valueType', 'param^abbr|name,type,value,valueType',
'picture|', 'picture^abbr|',
'pre|#width', 'pre^abbr|#width',
'progress|#max,#value', 'progress^abbr|#max,#value',
'q,blockquote,cite|', 'q,blockquote,cite^abbr|',
'script|!async,charset,%crossOrigin,!defer,event,htmlFor,integrity,src,text,type', 'script^abbr|!async,charset,%crossOrigin,!defer,event,htmlFor,integrity,src,text,type',
'select|!autofocus,!disabled,#length,!multiple,name,!required,#selectedIndex,#size,value', 'select^abbr|!autofocus,!disabled,#length,!multiple,name,!required,#selectedIndex,#size,value',
'shadow|', 'shadow^abbr|',
'source|media,sizes,src,srcset,type', 'source^abbr|media,sizes,src,srcset,type',
'span|', 'span^abbr|',
'style|!disabled,media,type', 'style^abbr|!disabled,media,type',
'caption|align', 'caption^abbr|align',
'th,td|abbr,align,axis,bgColor,ch,chOff,#colSpan,headers,height,!noWrap,#rowSpan,scope,vAlign,width', 'th,td^abbr|abbr,align,axis,bgColor,ch,chOff,#colSpan,headers,height,!noWrap,#rowSpan,scope,vAlign,width',
'col,colgroup|align,ch,chOff,#span,vAlign,width', 'col,colgroup^abbr|align,ch,chOff,#span,vAlign,width',
'table|align,bgColor,border,%caption,cellPadding,cellSpacing,frame,rules,summary,%tFoot,%tHead,width', 'table^abbr|align,bgColor,border,%caption,cellPadding,cellSpacing,frame,rules,summary,%tFoot,%tHead,width',
'tr|align,bgColor,ch,chOff,vAlign', 'tr^abbr|align,bgColor,ch,chOff,vAlign',
'tfoot,thead,tbody|align,ch,chOff,vAlign', 'tfoot,thead,tbody^abbr|align,ch,chOff,vAlign',
'template|', 'template^abbr|',
'textarea|autocapitalize,!autofocus,#cols,defaultValue,dirName,!disabled,#maxLength,#minLength,name,placeholder,!readOnly,!required,#rows,selectionDirection,#selectionEnd,#selectionStart,value,wrap', 'textarea^abbr|autocapitalize,!autofocus,#cols,defaultValue,dirName,!disabled,#maxLength,#minLength,name,placeholder,!readOnly,!required,#rows,selectionDirection,#selectionEnd,#selectionStart,value,wrap',
'title|text', 'title^abbr|text',
'track|!default,kind,label,src,srclang', 'track^abbr|!default,kind,label,src,srclang',
'ul|!compact,type', 'ul^abbr|!compact,type',
'unknown|', 'unknown^abbr|',
'video^media|#height,poster,#width', 'video^media|#height,poster,#width',
':svg:a^:svg:graphics|', ':svg:a^:svg:graphics|',
':svg:animate^:svg:animation|', ':svg:animate^:svg:animation|',
@ -223,7 +220,7 @@ const SCHEMA: string[] = ([
':svg:view^:svg:|#zoomAndPan', ':svg:view^:svg:|#zoomAndPan',
]); ]);
var attrToPropMap: {[name: string]: string} = <any>{ const _ATTR_TO_PROP: {[name: string]: string} = {
'class': 'className', 'class': 'className',
'formaction': 'formAction', 'formaction': 'formAction',
'innerHtml': 'innerHTML', 'innerHtml': 'innerHTML',
@ -233,37 +230,42 @@ var attrToPropMap: {[name: string]: string} = <any>{
@Injectable() @Injectable()
export class DomElementSchemaRegistry extends ElementSchemaRegistry { export class DomElementSchemaRegistry extends ElementSchemaRegistry {
schema = <{[element: string]: {[property: string]: string}}>{}; private _schema: {[element: string]: {[property: string]: string}} = {};
constructor() { constructor() {
super(); super();
SCHEMA.forEach(encodedType => { SCHEMA.forEach(encodedType => {
var parts = encodedType.split('|'); const [strType, strProperties] = encodedType.split('|');
var properties = parts[1].split(','); const properties = strProperties.split(',');
var typeParts = (parts[0] + '^').split('^'); const [typeNames, superName] = strType.split('^');
var typeName = typeParts[0]; const type: {[property: string]: string} = {};
var type = <{[property: string]: string}>{}; typeNames.split(',').forEach(tag => this._schema[tag.toLowerCase()] = type);
typeName.split(',').forEach(tag => this.schema[tag] = type); const superType = this._schema[superName];
var superType = this.schema[typeParts[1]]; if (superType) {
if (isPresent(superType)) { Object.keys(superType).forEach((prop: string) => { type[prop] = superType[prop]; });
StringMapWrapper.forEach(
superType, (v: any /** TODO #9100 */, k: any /** TODO #9100 */) => type[k] = v);
} }
properties.forEach((property: string) => { properties.forEach((property: string) => {
if (property == '') { if (property.length > 0) {
} else if (property.startsWith('*')) { switch (property[0]) {
// We don't yet support events. case '*':
// If ever allowing to bind to events, GO THROUGH A SECURITY REVIEW, allowing events will // We don't yet support events.
// almost certainly introduce bad XSS vulnerabilities. // If ever allowing to bind to events, GO THROUGH A SECURITY REVIEW, allowing events
// type[property.substring(1)] = EVENT; // will
} else if (property.startsWith('!')) { // almost certainly introduce bad XSS vulnerabilities.
type[property.substring(1)] = BOOLEAN; // type[property.substring(1)] = EVENT;
} else if (property.startsWith('#')) { break;
type[property.substring(1)] = NUMBER; case '!':
} else if (property.startsWith('%')) { type[property.substring(1)] = BOOLEAN;
type[property.substring(1)] = OBJECT; break;
} else { case '#':
type[property] = STRING; type[property.substring(1)] = NUMBER;
break;
case '%':
type[property.substring(1)] = OBJECT;
break;
default:
type[property] = STRING;
}
} }
}); });
}); });
@ -274,10 +276,11 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
return true; return true;
} }
if (tagName.indexOf('-') !== -1) { if (tagName.indexOf('-') > -1) {
if (tagName === 'ng-container' || tagName === 'ng-content') { if (tagName === 'ng-container' || tagName === 'ng-content') {
return false; return false;
} }
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) { if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) {
// Can't tell now as we don't know which properties a custom element will get // Can't tell now as we don't know which properties a custom element will get
// once it is instantiated // once it is instantiated
@ -285,11 +288,27 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
} }
} }
var elementProperties = this.schema[tagName.toLowerCase()]; const elementProperties = this._schema[tagName.toLowerCase()] || this._schema['unknown'];
if (!isPresent(elementProperties)) { return !!elementProperties[propName];
elementProperties = this.schema['unknown']; }
hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean {
if (schemaMetas.some((schema) => schema.name === NO_ERRORS_SCHEMA.name)) {
return true;
} }
return isPresent(elementProperties[propName]);
if (tagName.indexOf('-') > -1) {
if (tagName === 'ng-container' || tagName === 'ng-content') {
return true;
}
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) {
// Allow any custom elements
return true;
}
}
return !!this._schema[tagName.toLowerCase()];
} }
/** /**
@ -308,15 +327,14 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
tagName = tagName.toLowerCase(); tagName = tagName.toLowerCase();
propName = propName.toLowerCase(); propName = propName.toLowerCase();
let ctx = SECURITY_SCHEMA[tagName + '|' + propName]; let ctx = SECURITY_SCHEMA[tagName + '|' + propName];
if (ctx !== undefined) return ctx; if (ctx) {
return ctx;
}
ctx = SECURITY_SCHEMA['*|' + propName]; ctx = SECURITY_SCHEMA['*|' + propName];
return ctx !== undefined ? ctx : SecurityContext.NONE; return ctx ? ctx : SecurityContext.NONE;
} }
getMappedPropName(propName: string): string { getMappedPropName(propName: string): string { return _ATTR_TO_PROP[propName] || propName; }
var mappedPropName = StringMapWrapper.get(attrToPropMap, propName);
return isPresent(mappedPropName) ? mappedPropName : propName;
}
getDefaultComponentElementName(): string { return 'ng-component'; } getDefaultComponentElementName(): string { return 'ng-component'; }
} }

View File

@ -10,6 +10,7 @@ import {SchemaMetadata} from '@angular/core';
export abstract class ElementSchemaRegistry { export abstract class ElementSchemaRegistry {
abstract hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean; abstract hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean;
abstract hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean;
abstract securityContext(tagName: string, propName: string): any; abstract securityContext(tagName: string, propName: string): any;
abstract getMappedPropName(propName: string): string; abstract getMappedPropName(propName: string): string;
abstract getDefaultComponentElementName(): string; abstract getDefaultComponentElementName(): string;

View File

@ -81,10 +81,12 @@ export class CssSelector {
} }
isElementSelector(): boolean { isElementSelector(): boolean {
return isPresent(this.element) && ListWrapper.isEmpty(this.classNames) && return this.hasElementSelector() && this.classNames.length == 0 && this.attrs.length == 0 &&
ListWrapper.isEmpty(this.attrs) && this.notSelectors.length === 0; this.notSelectors.length === 0;
} }
hasElementSelector(): boolean { return !!this.element; }
setElement(element: string = null) { this.element = element; } setElement(element: string = null) { this.element = element; }
/** Gets a template string for an element that matches the selector. */ /** Gets a template string for an element that matches the selector. */

View File

@ -11,7 +11,7 @@ import {Inject, Injectable, OpaqueToken, Optional, SchemaMetadata, SecurityConte
import {CompileDirectiveMetadata, CompilePipeMetadata, CompileTokenMetadata, removeIdentifierDuplicates} from '../compile_metadata'; import {CompileDirectiveMetadata, CompilePipeMetadata, CompileTokenMetadata, removeIdentifierDuplicates} from '../compile_metadata';
import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast'; import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser'; import {Parser} from '../expression_parser/parser';
import {ListWrapper, SetWrapper, StringMapWrapper} from '../facade/collection'; import {StringMapWrapper} from '../facade/collection';
import {isBlank, isPresent, isString} from '../facade/lang'; import {isBlank, isPresent, isString} from '../facade/lang';
import {I18NHtmlParser} from '../i18n/i18n_html_parser'; import {I18NHtmlParser} from '../i18n/i18n_html_parser';
import {Identifiers, identifierToken, resolveIdentifierToken} from '../identifiers'; import {Identifiers, identifierToken, resolveIdentifierToken} from '../identifiers';
@ -32,7 +32,6 @@ import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventA
import {PreparsedElementType, preparseElement} from './template_preparser'; import {PreparsedElementType, preparseElement} from './template_preparser';
// Group 1 = "bind-" // Group 1 = "bind-"
// Group 2 = "let-" // Group 2 = "let-"
// Group 3 = "ref-/#" // Group 3 = "ref-/#"
@ -102,9 +101,11 @@ export class TemplateParser {
const result = this.tryParse(component, template, directives, pipes, schemas, templateUrl); const result = this.tryParse(component, template, directives, pipes, schemas, templateUrl);
const warnings = result.errors.filter(error => error.level === ParseErrorLevel.WARNING); const warnings = result.errors.filter(error => error.level === ParseErrorLevel.WARNING);
const errors = result.errors.filter(error => error.level === ParseErrorLevel.FATAL); const errors = result.errors.filter(error => error.level === ParseErrorLevel.FATAL);
if (warnings.length > 0) { if (warnings.length > 0) {
this._console.warn(`Template parse warnings:\n${warnings.join('\n')}`); this._console.warn(`Template parse warnings:\n${warnings.join('\n')}`);
} }
if (errors.length > 0) { if (errors.length > 0) {
const errorString = errors.join('\n'); const errorString = errors.join('\n');
throw new Error(`Template parse errors:\n${errorString}`); throw new Error(`Template parse errors:\n${errorString}`);
@ -183,36 +184,33 @@ export class TemplateParser {
} }
class TemplateParseVisitor implements html.Visitor { class TemplateParseVisitor implements html.Visitor {
selectorMatcher: SelectorMatcher; selectorMatcher = new SelectorMatcher();
errors: TemplateParseError[] = []; errors: TemplateParseError[] = [];
directivesIndex = new Map<CompileDirectiveMetadata, number>(); directivesIndex = new Map<CompileDirectiveMetadata, number>();
ngContentCount: number = 0; ngContentCount: number = 0;
pipesByName: Map<string, CompilePipeMetadata>; pipesByName: Map<string, CompilePipeMetadata> = new Map();
private _interpolationConfig: InterpolationConfig; private _interpolationConfig: InterpolationConfig;
constructor( constructor(
public providerViewContext: ProviderViewContext, directives: CompileDirectiveMetadata[], public providerViewContext: ProviderViewContext, directives: CompileDirectiveMetadata[],
pipes: CompilePipeMetadata[], private _schemas: SchemaMetadata[], private _exprParser: Parser, pipes: CompilePipeMetadata[], private _schemas: SchemaMetadata[], private _exprParser: Parser,
private _schemaRegistry: ElementSchemaRegistry) { private _schemaRegistry: ElementSchemaRegistry) {
this.selectorMatcher = new SelectorMatcher();
const tempMeta = providerViewContext.component.template; const tempMeta = providerViewContext.component.template;
if (isPresent(tempMeta) && isPresent(tempMeta.interpolation)) { if (tempMeta && tempMeta.interpolation) {
this._interpolationConfig = { this._interpolationConfig = {
start: tempMeta.interpolation[0], start: tempMeta.interpolation[0],
end: tempMeta.interpolation[1] end: tempMeta.interpolation[1]
}; };
} }
ListWrapper.forEachWithIndex( directives.forEach((directive: CompileDirectiveMetadata, index: number) => {
directives, (directive: CompileDirectiveMetadata, index: number) => { const selector = CssSelector.parse(directive.selector);
const selector = CssSelector.parse(directive.selector); this.selectorMatcher.addSelectables(selector, directive);
this.selectorMatcher.addSelectables(selector, directive); this.directivesIndex.set(directive, index);
this.directivesIndex.set(directive, index); });
});
this.pipesByName = new Map<string, CompilePipeMetadata>();
pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe)); pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
} }
@ -222,7 +220,7 @@ class TemplateParseVisitor implements html.Visitor {
this.errors.push(new TemplateParseError(message, sourceSpan, level)); this.errors.push(new TemplateParseError(message, sourceSpan, level));
} }
private _reportParserErors(errors: ParserError[], sourceSpan: ParseSourceSpan) { private _reportParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
for (const error of errors) { for (const error of errors) {
this._reportError(error.message, sourceSpan); this._reportError(error.message, sourceSpan);
} }
@ -230,9 +228,10 @@ class TemplateParseVisitor implements html.Visitor {
private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource { private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
const sourceInfo = sourceSpan.start.toString(); const sourceInfo = sourceSpan.start.toString();
try { try {
const ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig); const ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
if (ast) this._reportParserErors(ast.errors, sourceSpan); if (ast) this._reportParserErrors(ast.errors, sourceSpan);
this._checkPipes(ast, sourceSpan); this._checkPipes(ast, sourceSpan);
if (isPresent(ast) && if (isPresent(ast) &&
(<Interpolation>ast.ast).expressions.length > MAX_INTERPOLATION_VALUES) { (<Interpolation>ast.ast).expressions.length > MAX_INTERPOLATION_VALUES) {
@ -251,7 +250,7 @@ class TemplateParseVisitor implements html.Visitor {
try { try {
const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig); const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
if (ast) { if (ast) {
this._reportParserErors(ast.errors, sourceSpan); this._reportParserErrors(ast.errors, sourceSpan);
} }
if (!ast || ast.ast instanceof EmptyExpr) { if (!ast || ast.ast instanceof EmptyExpr) {
this._reportError(`Empty expressions are not allowed`, sourceSpan); this._reportError(`Empty expressions are not allowed`, sourceSpan);
@ -267,9 +266,10 @@ class TemplateParseVisitor implements html.Visitor {
private _parseBinding(value: string, sourceSpan: ParseSourceSpan): ASTWithSource { private _parseBinding(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
const sourceInfo = sourceSpan.start.toString(); const sourceInfo = sourceSpan.start.toString();
try { try {
const ast = this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig); const ast = this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
if (ast) this._reportParserErors(ast.errors, sourceSpan); if (ast) this._reportParserErrors(ast.errors, sourceSpan);
this._checkPipes(ast, sourceSpan); this._checkPipes(ast, sourceSpan);
return ast; return ast;
} catch (e) { } catch (e) {
@ -280,9 +280,10 @@ class TemplateParseVisitor implements html.Visitor {
private _parseTemplateBindings(value: string, sourceSpan: ParseSourceSpan): TemplateBinding[] { private _parseTemplateBindings(value: string, sourceSpan: ParseSourceSpan): TemplateBinding[] {
const sourceInfo = sourceSpan.start.toString(); const sourceInfo = sourceSpan.start.toString();
try { try {
const bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo); const bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo);
this._reportParserErors(bindingsResult.errors, sourceSpan); this._reportParserErrors(bindingsResult.errors, sourceSpan);
bindingsResult.templateBindings.forEach((binding) => { bindingsResult.templateBindings.forEach((binding) => {
if (isPresent(binding.expression)) { if (isPresent(binding.expression)) {
this._checkPipes(binding.expression, sourceSpan); this._checkPipes(binding.expression, sourceSpan);
@ -323,7 +324,7 @@ class TemplateParseVisitor implements html.Visitor {
} }
} }
visitAttribute(attribute: html.Attribute, contex: any): any { visitAttribute(attribute: html.Attribute, context: any): any {
return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan); return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan);
} }
@ -366,6 +367,7 @@ class TemplateParseVisitor implements html.Visitor {
const hasBinding = this._parseAttr( const hasBinding = this._parseAttr(
isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, animationProps, events, isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, animationProps, events,
elementOrDirectiveRefs, elementVars); elementOrDirectiveRefs, elementVars);
const hasTemplateBinding = this._parseInlineTemplateBinding( const hasTemplateBinding = this._parseInlineTemplateBinding(
attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateElementVars); attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateElementVars);
@ -380,13 +382,15 @@ class TemplateParseVisitor implements html.Visitor {
attrs.push(this.visitAttribute(attr, null)); attrs.push(this.visitAttribute(attr, null));
matchableAttrs.push([attr.name, attr.value]); matchableAttrs.push([attr.name, attr.value]);
} }
if (hasTemplateBinding) { if (hasTemplateBinding) {
hasInlineTemplates = true; hasInlineTemplates = true;
} }
}); });
const elementCssSelector = createElementCssSelector(nodeName, matchableAttrs); const elementCssSelector = createElementCssSelector(nodeName, matchableAttrs);
const directiveMetas = this._parseDirectives(this.selectorMatcher, elementCssSelector); const {directives: directiveMetas, matchElement} =
this._parseDirectives(this.selectorMatcher, elementCssSelector);
const references: ReferenceAst[] = []; const references: ReferenceAst[] = [];
const directiveAsts = this._createDirectiveAsts( const directiveAsts = this._createDirectiveAsts(
isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps, isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps,
@ -430,7 +434,9 @@ class TemplateParseVisitor implements html.Visitor {
providerContext.transformProviders, providerContext.transformedHasViewContainer, children, providerContext.transformProviders, providerContext.transformedHasViewContainer, children,
hasInlineTemplates ? null : ngContentIndex, element.sourceSpan); hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
} else { } else {
this._assertElementExists(matchElement, element);
this._assertOnlyOneComponent(directiveAsts, element.sourceSpan); this._assertOnlyOneComponent(directiveAsts, element.sourceSpan);
const ngContentIndex = const ngContentIndex =
hasInlineTemplates ? null : parent.findNgContentIndex(projectionSelector); hasInlineTemplates ? null : parent.findNgContentIndex(projectionSelector);
parsedElement = new ElementAst( parsedElement = new ElementAst(
@ -439,10 +445,11 @@ class TemplateParseVisitor implements html.Visitor {
providerContext.transformedHasViewContainer, children, providerContext.transformedHasViewContainer, children,
hasInlineTemplates ? null : ngContentIndex, element.sourceSpan); hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
} }
if (hasInlineTemplates) { if (hasInlineTemplates) {
const templateCssSelector = const templateCssSelector =
createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs); createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
const templateDirectiveMetas = const {directives: templateDirectiveMetas} =
this._parseDirectives(this.selectorMatcher, templateCssSelector); this._parseDirectives(this.selectorMatcher, templateCssSelector);
const templateDirectiveAsts = this._createDirectiveAsts( const templateDirectiveAsts = this._createDirectiveAsts(
true, element.name, templateDirectiveMetas, templateElementOrDirectiveProps, [], true, element.name, templateDirectiveMetas, templateElementOrDirectiveProps, [],
@ -681,15 +688,23 @@ class TemplateParseVisitor implements html.Visitor {
} }
private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector): private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector):
CompileDirectiveMetadata[] { {directives: CompileDirectiveMetadata[], matchElement: boolean} {
// Need to sort the directives so that we get consistent results throughout, // Need to sort the directives so that we get consistent results throughout,
// as selectorMatcher uses Maps inside. // as selectorMatcher uses Maps inside.
// Also dedupe directives as they might match more than one time! // Also deduplicate directives as they might match more than one time!
const directives = ListWrapper.createFixedSize(this.directivesIndex.size); const directives = new Array(this.directivesIndex.size);
// Whether any directive selector matches on the element name
let matchElement = false;
selectorMatcher.match(elementCssSelector, (selector, directive) => { selectorMatcher.match(elementCssSelector, (selector, directive) => {
directives[this.directivesIndex.get(directive)] = directive; directives[this.directivesIndex.get(directive)] = directive;
matchElement = matchElement || selector.hasElementSelector();
}); });
return directives.filter(dir => isPresent(dir));
return {
directives: directives.filter(dir => !!dir),
matchElement,
};
} }
private _createDirectiveAsts( private _createDirectiveAsts(
@ -724,12 +739,12 @@ class TemplateParseVisitor implements html.Visitor {
}); });
elementOrDirectiveRefs.forEach((elOrDirRef) => { elementOrDirectiveRefs.forEach((elOrDirRef) => {
if (elOrDirRef.value.length > 0) { if (elOrDirRef.value.length > 0) {
if (!SetWrapper.has(matchedReferences, elOrDirRef.name)) { if (!matchedReferences.has(elOrDirRef.name)) {
this._reportError( this._reportError(
`There is no directive with "exportAs" set to "${elOrDirRef.value}"`, `There is no directive with "exportAs" set to "${elOrDirRef.value}"`,
elOrDirRef.sourceSpan); elOrDirRef.sourceSpan);
} }
} else if (isBlank(component)) { } else if (!component) {
let refToken: CompileTokenMetadata = null; let refToken: CompileTokenMetadata = null;
if (isTemplateElement) { if (isTemplateElement) {
refToken = resolveIdentifierToken(Identifiers.TemplateRef); refToken = resolveIdentifierToken(Identifiers.TemplateRef);
@ -743,7 +758,7 @@ class TemplateParseVisitor implements html.Visitor {
private _createDirectiveHostPropertyAsts( private _createDirectiveHostPropertyAsts(
elementName: string, hostProps: {[key: string]: string}, sourceSpan: ParseSourceSpan, elementName: string, hostProps: {[key: string]: string}, sourceSpan: ParseSourceSpan,
targetPropertyAsts: BoundElementPropertyAst[]) { targetPropertyAsts: BoundElementPropertyAst[]) {
if (isPresent(hostProps)) { if (hostProps) {
StringMapWrapper.forEach(hostProps, (expression: string, propName: string) => { StringMapWrapper.forEach(hostProps, (expression: string, propName: string) => {
if (isString(expression)) { if (isString(expression)) {
const exprAst = this._parseBinding(expression, sourceSpan); const exprAst = this._parseBinding(expression, sourceSpan);
@ -761,7 +776,7 @@ class TemplateParseVisitor implements html.Visitor {
private _createDirectiveHostEventAsts( private _createDirectiveHostEventAsts(
hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan, hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan,
targetEventAsts: BoundEventAst[]) { targetEventAsts: BoundEventAst[]) {
if (isPresent(hostListeners)) { if (hostListeners) {
StringMapWrapper.forEach(hostListeners, (expression: string, propName: string) => { StringMapWrapper.forEach(hostListeners, (expression: string, propName: string) => {
if (isString(expression)) { if (isString(expression)) {
this._parseEvent(propName, expression, sourceSpan, [], targetEventAsts); this._parseEvent(propName, expression, sourceSpan, [], targetEventAsts);
@ -777,7 +792,7 @@ class TemplateParseVisitor implements html.Visitor {
private _createDirectivePropertyAsts( private _createDirectivePropertyAsts(
directiveProperties: {[key: string]: string}, boundProps: BoundElementOrDirectiveProperty[], directiveProperties: {[key: string]: string}, boundProps: BoundElementOrDirectiveProperty[],
targetBoundDirectiveProps: BoundDirectivePropertyAst[]) { targetBoundDirectiveProps: BoundDirectivePropertyAst[]) {
if (isPresent(directiveProperties)) { if (directiveProperties) {
const boundPropsByName = new Map<string, BoundElementOrDirectiveProperty>(); const boundPropsByName = new Map<string, BoundElementOrDirectiveProperty>();
boundProps.forEach(boundProp => { boundProps.forEach(boundProp => {
const prevValue = boundPropsByName.get(boundProp.name); const prevValue = boundPropsByName.get(boundProp.name);
@ -791,7 +806,7 @@ class TemplateParseVisitor implements html.Visitor {
const boundProp = boundPropsByName.get(elProp); const boundProp = boundPropsByName.get(elProp);
// Bindings are optional, so this binding only needs to be set up if an expression is given. // Bindings are optional, so this binding only needs to be set up if an expression is given.
if (isPresent(boundProp)) { if (boundProp) {
targetBoundDirectiveProps.push(new BoundDirectivePropertyAst( targetBoundDirectiveProps.push(new BoundDirectivePropertyAst(
dirProp, boundProp.name, boundProp.expression, boundProp.sourceSpan)); dirProp, boundProp.name, boundProp.expression, boundProp.sourceSpan));
} }
@ -804,11 +819,13 @@ class TemplateParseVisitor implements html.Visitor {
directives: DirectiveAst[]): BoundElementPropertyAst[] { directives: DirectiveAst[]): BoundElementPropertyAst[] {
const boundElementProps: BoundElementPropertyAst[] = []; const boundElementProps: BoundElementPropertyAst[] = [];
const boundDirectivePropsIndex = new Map<string, BoundDirectivePropertyAst>(); const boundDirectivePropsIndex = new Map<string, BoundDirectivePropertyAst>();
directives.forEach((directive: DirectiveAst) => { directives.forEach((directive: DirectiveAst) => {
directive.inputs.forEach((prop: BoundDirectivePropertyAst) => { directive.inputs.forEach((prop: BoundDirectivePropertyAst) => {
boundDirectivePropsIndex.set(prop.templateName, prop); boundDirectivePropsIndex.set(prop.templateName, prop);
}); });
}); });
props.forEach((prop: BoundElementOrDirectiveProperty) => { props.forEach((prop: BoundElementOrDirectiveProperty) => {
if (!prop.isLiteral && isBlank(boundDirectivePropsIndex.get(prop.name))) { if (!prop.isLiteral && isBlank(boundDirectivePropsIndex.get(prop.name))) {
boundElementProps.push(this._createElementPropertyAst( boundElementProps.push(this._createElementPropertyAst(
@ -826,6 +843,7 @@ class TemplateParseVisitor implements html.Visitor {
let boundPropertyName: string; let boundPropertyName: string;
const parts = name.split(PROPERTY_PARTS_SEPARATOR); const parts = name.split(PROPERTY_PARTS_SEPARATOR);
let securityContext: SecurityContext; let securityContext: SecurityContext;
if (parts.length === 1) { if (parts.length === 1) {
var partValue = parts[0]; var partValue = parts[0];
if (partValue[0] == '@') { if (partValue[0] == '@') {
@ -839,7 +857,7 @@ class TemplateParseVisitor implements html.Visitor {
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) { if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) {
let errorMsg = let errorMsg =
`Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`; `Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`;
if (elementName.indexOf('-') !== -1) { if (elementName.indexOf('-') > -1) {
errorMsg += errorMsg +=
`\n1. If '${elementName}' is an Angular component and it has '${boundPropertyName}' input, then verify that it is part of this module.` + `\n1. If '${elementName}' is an Angular component and it has '${boundPropertyName}' input, then verify that it is part of this module.` +
`\n2. If '${elementName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message.\n`; `\n2. If '${elementName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message.\n`;
@ -857,12 +875,13 @@ class TemplateParseVisitor implements html.Visitor {
sourceSpan); sourceSpan);
} }
// NB: For security purposes, use the mapped property name, not the attribute name. // NB: For security purposes, use the mapped property name, not the attribute name.
securityContext = this._schemaRegistry.securityContext( const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName);
elementName, this._schemaRegistry.getMappedPropName(boundPropertyName)); securityContext = this._schemaRegistry.securityContext(elementName, mapPropName);
let nsSeparatorIdx = boundPropertyName.indexOf(':');
const nsSeparatorIdx = boundPropertyName.indexOf(':');
if (nsSeparatorIdx > -1) { if (nsSeparatorIdx > -1) {
let ns = boundPropertyName.substring(0, nsSeparatorIdx); const ns = boundPropertyName.substring(0, nsSeparatorIdx);
let name = boundPropertyName.substring(nsSeparatorIdx + 1); const name = boundPropertyName.substring(nsSeparatorIdx + 1);
boundPropertyName = mergeNsAndName(ns, name); boundPropertyName = mergeNsAndName(ns, name);
} }
@ -906,6 +925,26 @@ class TemplateParseVisitor implements html.Visitor {
} }
} }
/**
* Make sure that non-angular tags conform to the schemas.
*
* Note: An element is considered an angular tag when at least one directive selector matches the
* tag name.
*
* @param matchElement Whether any directive has matched on the tag name
* @param element the html element
*/
private _assertElementExists(matchElement: boolean, element: html.Element) {
const elName = element.name.replace(/^:xhtml:/, '');
if (!matchElement && !this._schemaRegistry.hasElement(elName, this._schemas)) {
const errorMsg = `'${elName}' is not a known element:\n` +
`1. If '${elName}' is an Angular component, then verify that it is part of this module.\n` +
`2. If '${elName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message.`;
this._reportError(errorMsg, element.sourceSpan);
}
}
private _assertNoComponentsNorElementBindingsOnTemplate( private _assertNoComponentsNorElementBindingsOnTemplate(
directives: DirectiveAst[], elementProps: BoundElementPropertyAst[], directives: DirectiveAst[], elementProps: BoundElementPropertyAst[],
sourceSpan: ParseSourceSpan) { sourceSpan: ParseSourceSpan) {
@ -924,13 +963,15 @@ class TemplateParseVisitor implements html.Visitor {
private _assertAllEventsPublishedByDirectives( private _assertAllEventsPublishedByDirectives(
directives: DirectiveAst[], events: BoundEventAst[]) { directives: DirectiveAst[], events: BoundEventAst[]) {
const allDirectiveEvents = new Set<string>(); const allDirectiveEvents = new Set<string>();
directives.forEach(directive => { directives.forEach(directive => {
StringMapWrapper.forEach(directive.directive.outputs, (eventName: string) => { StringMapWrapper.forEach(directive.directive.outputs, (eventName: string) => {
allDirectiveEvents.add(eventName); allDirectiveEvents.add(eventName);
}); });
}); });
events.forEach(event => { events.forEach(event => {
if (isPresent(event.target) || !SetWrapper.has(allDirectiveEvents, event.name)) { if (isPresent(event.target) || !allDirectiveEvents.has(event.name)) {
this._reportError( this._reportError(
`Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "directives" section.`, `Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "directives" section.`,
event.sourceSpan); event.sourceSpan);
@ -996,7 +1037,7 @@ class ElementContext {
const matcher = new SelectorMatcher(); const matcher = new SelectorMatcher();
let wildcardNgContentIndex: number = null; let wildcardNgContentIndex: number = null;
const component = directives.find(directive => directive.directive.isComponent); const component = directives.find(directive => directive.directive.isComponent);
if (isPresent(component)) { if (component) {
const ngContentSelectors = component.directive.template.ngContentSelectors; const ngContentSelectors = component.directive.template.ngContentSelectors;
for (let i = 0; i < ngContentSelectors.length; i++) { for (let i = 0; i < ngContentSelectors.length; i++) {
const selector = ngContentSelectors[i]; const selector = ngContentSelectors[i];
@ -1017,7 +1058,7 @@ class ElementContext {
const ngContentIndices: number[] = []; const ngContentIndices: number[] = [];
this._ngContentIndexMatcher.match( this._ngContentIndexMatcher.match(
selector, (selector, ngContentIndex) => { ngContentIndices.push(ngContentIndex); }); selector, (selector, ngContentIndex) => { ngContentIndices.push(ngContentIndex); });
ListWrapper.sort(ngContentIndices); ngContentIndices.sort();
if (isPresent(this._wildcardNgContentIndex)) { if (isPresent(this._wildcardNgContentIndex)) {
ngContentIndices.push(this._wildcardNgContentIndex); ngContentIndices.push(this._wildcardNgContentIndex);
} }
@ -1027,7 +1068,7 @@ class ElementContext {
function createElementCssSelector(elementName: string, matchableAttrs: string[][]): CssSelector { function createElementCssSelector(elementName: string, matchableAttrs: string[][]): CssSelector {
const cssSelector = new CssSelector(); const cssSelector = new CssSelector();
let elNameNoNs = splitNsName(elementName)[1]; const elNameNoNs = splitNsName(elementName)[1];
cssSelector.setElement(elNameNoNs); cssSelector.setElement(elNameNoNs);

View File

@ -21,6 +21,16 @@ export function main() {
let registry: DomElementSchemaRegistry; let registry: DomElementSchemaRegistry;
beforeEach(() => { registry = new DomElementSchemaRegistry(); }); beforeEach(() => { registry = new DomElementSchemaRegistry(); });
it('should detect elements', () => {
expect(registry.hasElement('div', [])).toBeTruthy();
expect(registry.hasElement('b', [])).toBeTruthy();
expect(registry.hasElement('ng-container', [])).toBeTruthy();
expect(registry.hasElement('ng-content', [])).toBeTruthy();
expect(registry.hasElement('my-cmp', [])).toBeFalsy();
expect(registry.hasElement('abc', [])).toBeFalsy();
});
it('should detect properties on regular elements', () => { it('should detect properties on regular elements', () => {
expect(registry.hasProperty('div', 'id', [])).toBeTruthy(); expect(registry.hasProperty('div', 'id', [])).toBeTruthy();
expect(registry.hasProperty('div', 'title', [])).toBeTruthy(); expect(registry.hasProperty('div', 'title', [])).toBeTruthy();
@ -37,7 +47,7 @@ export function main() {
}); });
it('should detect different kinds of types', () => { it('should detect different kinds of types', () => {
// inheritance: video => media => * // inheritance: video => media => HTMLElement
expect(registry.hasProperty('video', 'className', [])).toBeTruthy(); // from * expect(registry.hasProperty('video', 'className', [])).toBeTruthy(); // from *
expect(registry.hasProperty('video', 'id', [])).toBeTruthy(); // string expect(registry.hasProperty('video', 'id', [])).toBeTruthy(); // string
expect(registry.hasProperty('video', 'scrollLeft', [])).toBeTruthy(); // number expect(registry.hasProperty('video', 'scrollLeft', [])).toBeTruthy(); // number
@ -57,11 +67,16 @@ export function main() {
it('should return true for custom-like elements if the CUSTOM_ELEMENTS_SCHEMA was used', () => { it('should return true for custom-like elements if the CUSTOM_ELEMENTS_SCHEMA was used', () => {
expect(registry.hasProperty('custom-like', 'unknown', [CUSTOM_ELEMENTS_SCHEMA])).toBeTruthy(); expect(registry.hasProperty('custom-like', 'unknown', [CUSTOM_ELEMENTS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('custom-like', [CUSTOM_ELEMENTS_SCHEMA])).toBeTruthy();
}); });
it('should return true for all elements if the NO_ERRORS_SCHEMA was used', () => { it('should return true for all elements if the NO_ERRORS_SCHEMA was used', () => {
expect(registry.hasProperty('custom-like', 'unknown', [NO_ERRORS_SCHEMA])).toBeTruthy(); expect(registry.hasProperty('custom-like', 'unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasProperty('a', 'unknown', [NO_ERRORS_SCHEMA])).toBeTruthy(); expect(registry.hasProperty('a', 'unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('custom-like', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
}); });
it('should re-map property names that are specified in DOM facade', it('should re-map property names that are specified in DOM facade',

View File

@ -6,38 +6,43 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {isPresent, isString} from '../../src/facade/lang';
const SVG_PREFIX = ':svg:'; const SVG_PREFIX = ':svg:';
const HTMLELEMENT_NAMES =
'abbr,address,article,aside,b,bdi,bdo,cite,code,dd,dfn,dt,em,figcaption,figure,footer,header,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr';
const HTMLELEMENT_NAME = 'abbr';
var document = typeof(global as any /** TODO #???? */)['document'] == 'object' ? const _G: any = global;
(global as any /** TODO #???? */)['document'] : const document: any = typeof _G['document'] == 'object' ? _G['document'] : null;
null;
export function extractSchema(): Map<string, string[]> { export function extractSchema(): Map<string, string[]> {
var SVGGraphicsElement = (global as any /** TODO #???? */)['SVGGraphicsElement']; if (!document) return null;
var SVGAnimationElement = (global as any /** TODO #???? */)['SVGAnimationElement']; const SVGGraphicsElement = _G['SVGGraphicsElement'];
var SVGGeometryElement = (global as any /** TODO #???? */)['SVGGeometryElement']; if (!SVGGraphicsElement) return null;
var SVGComponentTransferFunctionElement =
(global as any /** TODO #???? */)['SVGComponentTransferFunctionElement'];
var SVGGradientElement = (global as any /** TODO #???? */)['SVGGradientElement'];
var SVGTextContentElement = (global as any /** TODO #???? */)['SVGTextContentElement'];
var SVGTextPositioningElement = (global as any /** TODO #???? */)['SVGTextPositioningElement'];
if (!document || !SVGGraphicsElement) return null;
var descMap: Map<string, string[]> = new Map();
var visited: {[name: string]: boolean} = {};
var element = document.createElement('video');
var svgAnimation = document.createElementNS('http://www.w3.org/2000/svg', 'set');
var svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
var svgFeFuncA = document.createElementNS('http://www.w3.org/2000/svg', 'feFuncA');
var svgGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
var svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const SVGAnimationElement = _G['SVGAnimationElement'];
const SVGGeometryElement = _G['SVGGeometryElement'];
const SVGComponentTransferFunctionElement = _G['SVGComponentTransferFunctionElement'];
const SVGGradientElement = _G['SVGGradientElement'];
const SVGTextContentElement = _G['SVGTextContentElement'];
const SVGTextPositioningElement = _G['SVGTextPositioningElement'];
const element = document.createElement('video');
const svgAnimation = document.createElementNS('http://www.w3.org/2000/svg', 'set');
const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const svgFeFuncA = document.createElementNS('http://www.w3.org/2000/svg', 'feFuncA');
const svgGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
const svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const descMap: Map<string, string[]> = new Map();
let visited: {[name: string]: boolean} = {};
// HTML top level
extractProperties(Node, element, visited, descMap, '*', ''); extractProperties(Node, element, visited, descMap, '*', '');
extractProperties(Element, element, visited, descMap, '*', ''); extractProperties(Element, element, visited, descMap, '*', '');
extractProperties(HTMLElement, element, visited, descMap, '', '*'); extractProperties(HTMLElement, element, visited, descMap, HTMLELEMENT_NAMES, '*');
extractProperties(HTMLMediaElement, element, visited, descMap, 'media', ''); extractProperties(HTMLMediaElement, element, visited, descMap, 'media', HTMLELEMENT_NAME);
extractProperties(SVGElement, svgText, visited, descMap, SVG_PREFIX, '*');
// SVG top level
extractProperties(SVGElement, svgText, visited, descMap, SVG_PREFIX, HTMLELEMENT_NAME);
extractProperties( extractProperties(
SVGGraphicsElement, svgText, visited, descMap, SVG_PREFIX + 'graphics', SVG_PREFIX); SVGGraphicsElement, svgText, visited, descMap, SVG_PREFIX + 'graphics', SVG_PREFIX);
extractProperties( extractProperties(
@ -55,37 +60,64 @@ export function extractSchema(): Map<string, string[]> {
extractProperties( extractProperties(
SVGTextPositioningElement, svgText, visited, descMap, SVG_PREFIX + 'textPositioning', SVGTextPositioningElement, svgText, visited, descMap, SVG_PREFIX + 'textPositioning',
SVG_PREFIX + 'textContent'); SVG_PREFIX + 'textContent');
var keys = Object.keys(window).filter(
k => k.endsWith('Element') && (k.startsWith('HTML') || k.startsWith('SVG'))); // Get all element types
keys.sort(); const types = Object.getOwnPropertyNames(window).filter(k => /^(HTML|SVG).*?Element$/.test(k));
keys.forEach(
name => types.sort();
extractRecursiveProperties(visited, descMap, (window as any /** TODO #???? */)[name]));
types.forEach(type => { extractRecursiveProperties(visited, descMap, (window as any)[type]); });
return descMap; return descMap;
} }
function extractRecursiveProperties( function extractRecursiveProperties(
visited: {[name: string]: boolean}, descMap: Map<string, string[]>, type: Function): string { visited: {[name: string]: boolean}, descMap: Map<string, string[]>, type: Function): string {
var name = extractName(type); const name = extractName(type);
if (visited[name]) return name; // already been here
var superName = ''; if (visited[name]) {
if (name != '*') { return name;
superName = extractRecursiveProperties(visited, descMap, type.prototype.__proto__.constructor);
} }
var instance: HTMLElement = null; let superName: string;
switch (name) {
case '*':
superName = '';
break;
case HTMLELEMENT_NAME:
superName = '*';
break;
default:
superName =
extractRecursiveProperties(visited, descMap, type.prototype.__proto__.constructor);
}
// If the ancestor is an HTMLElement, use one of the multiple implememtation
superName = superName.split(',')[0];
let instance: HTMLElement = null;
name.split(',').forEach(tagName => { name.split(',').forEach(tagName => {
instance = isSVG(type) ? instance = isSVG(type) ?
document.createElementNS('http://www.w3.org/2000/svg', tagName.replace(SVG_PREFIX, '')) : document.createElementNS('http://www.w3.org/2000/svg', tagName.replace(SVG_PREFIX, '')) :
document.createElement(tagName); document.createElement(tagName);
var htmlType = type;
if (tagName == 'cite') htmlType = HTMLElement; let htmlType: Function;
switch (tagName) {
case 'cite':
htmlType = HTMLElement;
break;
default:
htmlType = type;
}
if (!(instance instanceof htmlType)) { if (!(instance instanceof htmlType)) {
throw new Error(`Tag <${tagName}> is not an instance of ${htmlType['name']}`); throw new Error(`Tag <${tagName}> is not an instance of ${htmlType['name']}`);
} }
}); });
extractProperties(type, instance, visited, descMap, name, superName); extractProperties(type, instance, visited, descMap, name, superName);
return name; return name;
} }
@ -93,21 +125,26 @@ function extractProperties(
type: Function, instance: any, visited: {[name: string]: boolean}, type: Function, instance: any, visited: {[name: string]: boolean},
descMap: Map<string, string[]>, name: string, superName: string) { descMap: Map<string, string[]>, name: string, superName: string) {
if (!type) return; if (!type) return;
visited[name] = true; visited[name] = true;
const fullName = name + (superName ? '^' + superName : ''); const fullName = name + (superName ? '^' + superName : '');
let props: string[] = descMap.has(fullName) ? descMap.get(fullName) : [];
var prototype = type.prototype; const props: string[] = descMap.has(fullName) ? descMap.get(fullName) : [];
var keys = Object.keys(prototype);
const prototype = type.prototype;
let keys = Object.getOwnPropertyNames(prototype);
keys.sort(); keys.sort();
keys.forEach((n) => { keys.forEach((name) => {
if (n.startsWith('on')) { if (name.startsWith('on')) {
props.push('*' + n.substr(2)); props.push('*' + name.substr(2));
} else { } else {
var typeCh = typeMap[typeof instance[n]]; const typeCh = _TYPE_MNEMONICS[typeof instance[name]];
var descriptor = Object.getOwnPropertyDescriptor(prototype, n); const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
var isSetter = descriptor && isPresent(descriptor.set); const isSetter = descriptor && descriptor.set;
if (isString(typeCh) && !n.startsWith('webkit') && isSetter) { if (typeCh !== void 0 && !name.startsWith('webkit') && isSetter) {
props.push(typeCh + n); props.push(typeCh + name);
} }
} }
}); });
@ -117,44 +154,76 @@ function extractProperties(
} }
function extractName(type: Function): string { function extractName(type: Function): string {
var name = type['name']; let name = type['name'];
if (name == 'Element') return '*';
if (name == 'HTMLImageElement') return 'img'; switch (name) {
if (name == 'HTMLAnchorElement') return 'a'; // see https://www.w3.org/TR/html5/index.html
if (name == 'HTMLDListElement') return 'dl'; // TODO(vicb): generate this map from all the element types
if (name == 'HTMLDirectoryElement') return 'dir'; case 'Element':
if (name == 'HTMLHeadingElement') return 'h1,h2,h3,h4,h5,h6'; return '*';
if (name == 'HTMLModElement') return 'ins,del'; case 'HTMLElement':
if (name == 'HTMLOListElement') return 'ol'; return HTMLELEMENT_NAME;
if (name == 'HTMLParagraphElement') return 'p'; case 'HTMLImageElement':
if (name == 'HTMLQuoteElement') return 'q,blockquote,cite'; return 'img';
if (name == 'HTMLTableCaptionElement') return 'caption'; case 'HTMLAnchorElement':
if (name == 'HTMLTableCellElement') return 'th,td'; return 'a';
if (name == 'HTMLTableColElement') return 'col,colgroup'; case 'HTMLDListElement':
if (name == 'HTMLTableRowElement') return 'tr'; return 'dl';
if (name == 'HTMLTableSectionElement') return 'tfoot,thead,tbody'; case 'HTMLDirectoryElement':
if (name == 'HTMLUListElement') return 'ul'; return 'dir';
if (name == 'SVGGraphicsElement') return SVG_PREFIX + 'graphics'; case 'HTMLHeadingElement':
if (name == 'SVGMPathElement') return SVG_PREFIX + 'mpath'; return 'h1,h2,h3,h4,h5,h6';
if (name == 'SVGSVGElement') return SVG_PREFIX + 'svg'; case 'HTMLModElement':
if (name == 'SVGTSpanElement') return SVG_PREFIX + 'tspan'; return 'ins,del';
var isSVG = name.startsWith('SVG'); case 'HTMLOListElement':
if (name.startsWith('HTML') || isSVG) { return 'ol';
name = name.replace('HTML', '').replace('SVG', '').replace('Element', ''); case 'HTMLParagraphElement':
if (isSVG && name.startsWith('FE')) { return 'p';
name = 'fe' + name.substring(2); case 'HTMLQuoteElement':
} else if (name) { return 'q,blockquote,cite';
name = name.charAt(0).toLowerCase() + name.substring(1); case 'HTMLTableCaptionElement':
} return 'caption';
return isSVG ? SVG_PREFIX + name : name.toLowerCase(); case 'HTMLTableCellElement':
} else { return 'th,td';
return null; case 'HTMLTableColElement':
return 'col,colgroup';
case 'HTMLTableRowElement':
return 'tr';
case 'HTMLTableSectionElement':
return 'tfoot,thead,tbody';
case 'HTMLUListElement':
return 'ul';
case 'SVGGraphicsElement':
return SVG_PREFIX + 'graphics';
case 'SVGMPathElement':
return SVG_PREFIX + 'mpath';
case 'SVGSVGElement':
return SVG_PREFIX + 'svg';
case 'SVGTSpanElement':
return SVG_PREFIX + 'tspan';
default:
const isSVG = name.startsWith('SVG');
if (name.startsWith('HTML') || isSVG) {
name = name.replace('HTML', '').replace('SVG', '').replace('Element', '');
if (isSVG && name.startsWith('FE')) {
name = 'fe' + name.substring(2);
} else if (name) {
name = name.charAt(0).toLowerCase() + name.substring(1);
}
return isSVG ? SVG_PREFIX + name : name.toLowerCase();
}
} }
return null;
} }
function isSVG(type: Function): boolean { function isSVG(type: Function): boolean {
return type['name'].startsWith('SVG'); return type['name'].startsWith('SVG');
} }
const typeMap = const _TYPE_MNEMONICS: {[type: string]: string} = {
<{[type: string]: string}>{'string': '', 'number': '#', 'boolean': '!', 'object': '%'}; 'string': '',
'number': '#',
'boolean': '!',
'object': '%',
};

View File

@ -22,11 +22,12 @@ import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_pa
import {MockSchemaRegistry} from '../../testing/index'; import {MockSchemaRegistry} from '../../testing/index';
import {unparse} from '../expression_parser/unparser'; import {unparse} from '../expression_parser/unparser';
var someModuleUrl = 'package:someModule'; const someModuleUrl = 'package:someModule';
var MOCK_SCHEMA_REGISTRY = [{ const MOCK_SCHEMA_REGISTRY = [{
provide: ElementSchemaRegistry, provide: ElementSchemaRegistry,
useValue: new MockSchemaRegistry({'invalidProp': false}, {'mappedAttr': 'mappedProp'}) useValue: new MockSchemaRegistry(
{'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false}),
}]; }];
export function main() { export function main() {
@ -256,7 +257,7 @@ export function main() {
}); });
describe('errors', () => { describe('errors', () => {
it('should throw error when binding to an unkonown property', () => { it('should throw error when binding to an unknown property', () => {
expect(() => parse('<my-component [invalidProp]="bar"></my-component>', [])) expect(() => parse('<my-component [invalidProp]="bar"></my-component>', []))
.toThrowError(`Template parse errors: .toThrowError(`Template parse errors:
Can't bind to 'invalidProp' since it isn't a known property of 'my-component'. Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
@ -264,6 +265,20 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
2. If 'my-component' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message. 2. If 'my-component' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message.
("<my-component [ERROR ->][invalidProp]="bar"></my-component>"): TestComp@0:14`); ("<my-component [ERROR ->][invalidProp]="bar"></my-component>"): TestComp@0:14`);
}); });
it('should throw error when binding to an unknown element w/o bindings', () => {
expect(() => parse('<unknown></unknown>', [])).toThrowError(`Template parse errors:
'unknown' is not a known element:
1. If 'unknown' is an Angular component, then verify that it is part of this module.
2. If 'unknown' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message. ("[ERROR ->]<unknown></unknown>"): TestComp@0:0`);
});
it('should throw error when binding to an unknown custom element w/o bindings', () => {
expect(() => parse('<un-known></un-known>', [])).toThrowError(`Template parse errors:
'un-known' is not a known element:
1. If 'un-known' is an Angular component, then verify that it is part of this module.
2. If 'un-known' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message. ("[ERROR ->]<un-known></un-known>"): TestComp@0:0`);
});
}); });
it('should parse bound properties via [...] and not report them as attributes', () => { it('should parse bound properties via [...] and not report them as attributes', () => {

View File

@ -8,27 +8,29 @@
import {ElementSchemaRegistry} from '@angular/compiler'; import {ElementSchemaRegistry} from '@angular/compiler';
import {SchemaMetadata, SecurityContext} from '@angular/core'; import {SchemaMetadata, SecurityContext} from '@angular/core';
import {ElementSchemaRegistry} from '../index';
import {isPresent} from './facade/lang';
export class MockSchemaRegistry implements ElementSchemaRegistry { export class MockSchemaRegistry implements ElementSchemaRegistry {
constructor( constructor(
public existingProperties: {[key: string]: boolean}, public existingProperties: {[key: string]: boolean},
public attrPropMapping: {[key: string]: string}) {} public attrPropMapping: {[key: string]: string},
public existingElements: {[key: string]: boolean}) {}
hasProperty(tagName: string, property: string, schemas: SchemaMetadata[]): boolean { hasProperty(tagName: string, property: string, schemas: SchemaMetadata[]): boolean {
var result = this.existingProperties[property]; const value = this.existingProperties[property];
return isPresent(result) ? result : true; return value === void 0 ? true : value;
}
hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean {
const value = this.existingElements[tagName.toLowerCase()];
return value === void 0 ? true : value;
} }
securityContext(tagName: string, property: string): SecurityContext { securityContext(tagName: string, property: string): SecurityContext {
return SecurityContext.NONE; return SecurityContext.NONE;
} }
getMappedPropName(attrName: string): string { getMappedPropName(attrName: string): string { return this.attrPropMapping[attrName] || attrName; }
var result = this.attrPropMapping[attrName];
return isPresent(result) ? result : attrName;
}
getDefaultComponentElementName(): string { return 'ng-component'; } getDefaultComponentElementName(): string { return 'ng-component'; }
} }

View File

@ -19,7 +19,7 @@ export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
// internal test packages. // internal test packages.
// TODO: get rid of it or move to a separate @angular/internal_testing package // TODO: get rid of it or move to a separate @angular/internal_testing package
export var TEST_COMPILER_PROVIDERS: Provider[] = [ export var TEST_COMPILER_PROVIDERS: Provider[] = [
{provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {})}, {provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {})},
{provide: ResourceLoader, useClass: MockResourceLoader}, {provide: ResourceLoader, useClass: MockResourceLoader},
{provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix} {provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix}
]; ];

View File

@ -27,8 +27,10 @@ export interface ModuleWithProviders {
export interface SchemaMetadata { name: string; } export interface SchemaMetadata { name: string; }
/** /**
* Defines a schema that will allow any property on elements with a `-` in their name, * Defines a schema that will allow:
* which is the common rule for custom elements. * - any non-angular elements with a `-` in their name,
* - any properties on elements with a `-` in their name which is the common rule for custom
* elements.
* *
* @stable * @stable
*/ */
@ -161,6 +163,18 @@ export class NgModuleMetadata extends InjectableMetadata implements NgModuleMeta
*/ */
bootstrap: Array<Type<any>|any[]>; bootstrap: Array<Type<any>|any[]>;
/**
* Elements and properties that are not angular Components nor Directives have to be declared in
* the schema.
*
* Available schemas:
* - `NO_ERRORS_SCHEMA`: any elements and properties are allowed,
* - `CUSTOM_ELEMENTS_SCHEMA`: any custom elements (tag name has "-") with any properties are
* allowed.
*
* @security When using one of `NO_ERRORS_SCHEMA` or `CUSTOM_ELEMENTS_SCHEMA` we're trusting that
* allowed elements (and its properties) securely escape inputs.
*/
schemas: Array<SchemaMetadata|any[]>; schemas: Array<SchemaMetadata|any[]>;
constructor(options: NgModuleMetadataType = {}) { constructor(options: NgModuleMetadataType = {}) {

View File

@ -7,7 +7,7 @@
*/ */
import {NgFor, NgIf} from '@angular/common'; import {NgFor, NgIf} from '@angular/common';
import {Injectable} from '@angular/core'; import {Injectable, NO_ERRORS_SCHEMA} from '@angular/core';
import {Component, Directive, Input} from '@angular/core/src/metadata'; import {Component, Directive, Input} from '@angular/core/src/metadata';
import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
@ -185,9 +185,8 @@ export function main() {
TestApp, TestApp,
UsingFor, UsingFor,
], ],
providers: [ providers: [Logger],
Logger, schemas: [NO_ERRORS_SCHEMA],
]
}); });
})); }));

View File

@ -7,7 +7,7 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, ContentChildren, Directive, Inject, NgModule, QueryList, asNativeElements, forwardRef} from '@angular/core'; import {Component, ContentChildren, Directive, Inject, NO_ERRORS_SCHEMA, NgModule, QueryList, asNativeElements, forwardRef} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers'; import {expect} from '@angular/platform-browser/testing/matchers';
@ -16,7 +16,7 @@ export function main() {
beforeEach(() => { TestBed.configureTestingModule({imports: [Module], declarations: [App]}); }); beforeEach(() => { TestBed.configureTestingModule({imports: [Module], declarations: [App]}); });
it('should instantiate components which are declared using forwardRef', () => { it('should instantiate components which are declared using forwardRef', () => {
const a = TestBed.createComponent(App); const a = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}).createComponent(App);
a.detectChanges(); a.detectChanges();
expect(asNativeElements(a.debugElement.children)).toHaveText('frame(lock)'); expect(asNativeElements(a.debugElement.children)).toHaveText('frame(lock)');
expect(TestBed.get(ModuleFrame)).toBeDefined(); expect(TestBed.get(ModuleFrame)).toBeDefined();

View File

@ -7,7 +7,7 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {ComponentFactory, Host, Inject, Injectable, Injector, NgModule, OnDestroy, OpaqueToken, ReflectiveInjector, SkipSelf, SkipSelfMetadata, forwardRef} from '@angular/core'; import {ComponentFactory, Host, Inject, Injectable, Injector, NO_ERRORS_SCHEMA, NgModule, OnDestroy, OpaqueToken, ReflectiveInjector, SkipSelf, SkipSelfMetadata} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection'; import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection';
import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver'; import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver';
import {ElementRef} from '@angular/core/src/linker/element_ref'; import {ElementRef} from '@angular/core/src/linker/element_ref';
@ -304,7 +304,7 @@ function declareTests({useJit}: {useJit: boolean}) {
it('should support template directives via `<template>` elements.', () => { it('should support template directives via `<template>` elements.', () => {
TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]}); TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]});
const template = const template =
'<template some-viewport let-greeting="someTmpl"><copy-me>{{greeting}}</copy-me></template>'; '<template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></template>';
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
@ -364,7 +364,7 @@ function declareTests({useJit}: {useJit: boolean}) {
it('should support template directives via `template` attribute.', () => { it('should support template directives via `template` attribute.', () => {
TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]}); TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]});
const template = const template =
'<copy-me template="some-viewport: let greeting=someTmpl">{{greeting}}</copy-me>'; '<span template="some-viewport: let greeting=someTmpl">{{greeting}}</span>';
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
@ -382,7 +382,8 @@ function declareTests({useJit}: {useJit: boolean}) {
declarations: [ declarations: [
MyComp, SomeDirective, CompWithHost, ToolbarComponent, ToolbarViewContainer, ToolbarPart MyComp, SomeDirective, CompWithHost, ToolbarComponent, ToolbarViewContainer, ToolbarPart
], ],
imports: [CommonModule] imports: [CommonModule],
schemas: [NO_ERRORS_SCHEMA],
}); });
const template = const template =
'<some-directive><toolbar><template toolbarpart let-toolbarProp="toolbarProp">{{ctxProp}},{{toolbarProp}},<cmp-with-host></cmp-with-host></template></toolbar></some-directive>'; '<some-directive><toolbar><template toolbarpart let-toolbarProp="toolbarProp">{{ctxProp}},{{toolbarProp}},<cmp-with-host></cmp-with-host></template></toolbar></some-directive>';
@ -640,7 +641,10 @@ function declareTests({useJit}: {useJit: boolean}) {
}); });
it('should create a component that injects an @Host', () => { it('should create a component that injects an @Host', () => {
TestBed.configureTestingModule({declarations: [MyComp, SomeDirective, CompWithHost]}); TestBed.configureTestingModule({
declarations: [MyComp, SomeDirective, CompWithHost],
schemas: [NO_ERRORS_SCHEMA],
});
const template = ` const template = `
<some-directive> <some-directive>
<p> <p>
@ -656,7 +660,10 @@ function declareTests({useJit}: {useJit: boolean}) {
}); });
it('should create a component that injects an @Host through viewcontainer directive', () => { it('should create a component that injects an @Host through viewcontainer directive', () => {
TestBed.configureTestingModule({declarations: [MyComp, SomeDirective, CompWithHost]}); TestBed.configureTestingModule({
declarations: [MyComp, SomeDirective, CompWithHost],
schemas: [NO_ERRORS_SCHEMA],
});
const template = ` const template = `
<some-directive> <some-directive>
<p *ngIf="true"> <p *ngIf="true">
@ -879,25 +886,24 @@ function declareTests({useJit}: {useJit: boolean}) {
describe('dynamic ViewContainers', () => { describe('dynamic ViewContainers', () => {
beforeEach(() => { beforeEach(() => {
// we need a module to declarate ChildCompUsingService as an entryComponent otherwise the // we need a module to declarate ChildCompUsingService as an entryComponent otherwise the
// factory doesn't get created // factory doesn't get created
@NgModule({ @NgModule({
declarations: [MyComp, DynamicViewport, ChildCompUsingService], declarations: [MyComp, DynamicViewport, ChildCompUsingService],
entryComponents: [ChildCompUsingService] entryComponents: [ChildCompUsingService],
schemas: [NO_ERRORS_SCHEMA],
}) })
class MyModule { class MyModule {
} }
TestBed.configureTestingModule({imports: [MyModule]}); TestBed.configureTestingModule({imports: [MyModule]});
TestBed.overrideComponent( TestBed.overrideComponent(
MyComp, {add: {template: '<div><dynamic-vp #dynamic></dynamic-vp></div>'}}); MyComp, {add: {template: '<div><dynamic-vp #dynamic></dynamic-vp></div>'}});
}); });
it('should allow to create a ViewContainerRef at any bound location', async(() => { it('should allow to create a ViewContainerRef at any bound location', async(() => {
var fixture = TestBed.createComponent(MyComp); var fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]})
.createComponent(MyComp);
var tc = fixture.debugElement.children[0].children[0]; var tc = fixture.debugElement.children[0].children[0];
var dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); var dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport);
dynamicVp.done.then((_) => { dynamicVp.done.then((_) => {
@ -945,8 +951,10 @@ function declareTests({useJit}: {useJit: boolean}) {
describe('dependency injection', () => { describe('dependency injection', () => {
it('should support bindings', () => { it('should support bindings', () => {
TestBed.configureTestingModule( TestBed.configureTestingModule({
{declarations: [MyComp, DirectiveProvidingInjectable, DirectiveConsumingInjectable]}); declarations: [MyComp, DirectiveProvidingInjectable, DirectiveConsumingInjectable],
schemas: [NO_ERRORS_SCHEMA],
});
const template = ` const template = `
<directive-providing-injectable > <directive-providing-injectable >
<directive-consuming-injectable #consuming> <directive-consuming-injectable #consuming>
@ -962,8 +970,8 @@ function declareTests({useJit}: {useJit: boolean}) {
it('should support viewProviders', () => { it('should support viewProviders', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: declarations: [MyComp, DirectiveProvidingInjectableInView, DirectiveConsumingInjectable],
[MyComp, DirectiveProvidingInjectableInView, DirectiveConsumingInjectable] schemas: [NO_ERRORS_SCHEMA],
}); });
const template = ` const template = `
<directive-consuming-injectable #consuming> <directive-consuming-injectable #consuming>
@ -981,7 +989,8 @@ function declareTests({useJit}: {useJit: boolean}) {
declarations: [ declarations: [
MyComp, DirectiveProvidingInjectable, DirectiveContainingDirectiveConsumingAnInjectable, MyComp, DirectiveProvidingInjectable, DirectiveContainingDirectiveConsumingAnInjectable,
DirectiveConsumingInjectableUnbounded DirectiveConsumingInjectableUnbounded
] ],
schemas: [NO_ERRORS_SCHEMA],
}); });
const template = ` const template = `
<directive-providing-injectable> <directive-providing-injectable>
@ -1007,7 +1016,8 @@ function declareTests({useJit}: {useJit: boolean}) {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ declarations: [
MyComp, GrandParentProvidingEventBus, ParentProvidingEventBus, ChildConsumingEventBus MyComp, GrandParentProvidingEventBus, ParentProvidingEventBus, ChildConsumingEventBus
] ],
schemas: [NO_ERRORS_SCHEMA],
}); });
const template = ` const template = `
<grand-parent-providing-event-bus> <grand-parent-providing-event-bus>
@ -1036,8 +1046,8 @@ function declareTests({useJit}: {useJit: boolean}) {
it('should instantiate bindings lazily', () => { it('should instantiate bindings lazily', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: declarations: [MyComp, DirectiveConsumingInjectable, ComponentProvidingLoggingInjectable],
[MyComp, DirectiveConsumingInjectable, ComponentProvidingLoggingInjectable] schemas: [NO_ERRORS_SCHEMA],
}); });
const template = ` const template = `
<component-providing-logging-injectable #providing> <component-providing-logging-injectable #providing>
@ -1138,7 +1148,10 @@ function declareTests({useJit}: {useJit: boolean}) {
}); });
it('should provide an error context when an error happens in DI', () => { it('should provide an error context when an error happens in DI', () => {
TestBed.configureTestingModule({declarations: [MyComp, DirectiveThrowingAnError]}); TestBed.configureTestingModule({
declarations: [MyComp, DirectiveThrowingAnError],
schemas: [NO_ERRORS_SCHEMA],
});
const template = `<directive-throwing-error></directive-throwing-error>`; const template = `<directive-throwing-error></directive-throwing-error>`;
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
@ -1190,8 +1203,10 @@ function declareTests({useJit}: {useJit: boolean}) {
if (getDOM().supportsDOMEvents()) { // this is required to use fakeAsync if (getDOM().supportsDOMEvents()) { // this is required to use fakeAsync
it('should provide an error context when an error happens in an event handler', it('should provide an error context when an error happens in an event handler',
fakeAsync(() => { fakeAsync(() => {
TestBed.configureTestingModule( TestBed.configureTestingModule({
{declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent]}); declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent],
schemas: [NO_ERRORS_SCHEMA],
});
const template = `<span emitter listener (event)="throwError()" #local></span>`; const template = `<span emitter listener (event)="throwError()" #local></span>`;
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
@ -1366,7 +1381,10 @@ function declareTests({useJit}: {useJit: boolean}) {
describe('property decorators', () => { describe('property decorators', () => {
it('should support property decorators', () => { it('should support property decorators', () => {
TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithPropDecorators]}); TestBed.configureTestingModule({
declarations: [MyComp, DirectiveWithPropDecorators],
schemas: [NO_ERRORS_SCHEMA],
});
const template = '<with-prop-decorators elProp="aaa"></with-prop-decorators>'; const template = '<with-prop-decorators elProp="aaa"></with-prop-decorators>';
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
@ -1377,7 +1395,10 @@ function declareTests({useJit}: {useJit: boolean}) {
}); });
it('should support host binding decorators', () => { it('should support host binding decorators', () => {
TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithPropDecorators]}); TestBed.configureTestingModule({
declarations: [MyComp, DirectiveWithPropDecorators],
schemas: [NO_ERRORS_SCHEMA],
});
const template = '<with-prop-decorators></with-prop-decorators>'; const template = '<with-prop-decorators></with-prop-decorators>';
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
@ -1393,7 +1414,10 @@ function declareTests({useJit}: {useJit: boolean}) {
if (getDOM().supportsDOMEvents()) { if (getDOM().supportsDOMEvents()) {
it('should support event decorators', fakeAsync(() => { it('should support event decorators', fakeAsync(() => {
TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithPropDecorators]}); TestBed.configureTestingModule({
declarations: [MyComp, DirectiveWithPropDecorators],
schemas: [NO_ERRORS_SCHEMA],
});
const template = `<with-prop-decorators (elEvent)="ctxProp='called'">`; const template = `<with-prop-decorators (elEvent)="ctxProp='called'">`;
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
@ -1411,7 +1435,10 @@ function declareTests({useJit}: {useJit: boolean}) {
it('should support host listener decorators', () => { it('should support host listener decorators', () => {
TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithPropDecorators]}); TestBed.configureTestingModule({
declarations: [MyComp, DirectiveWithPropDecorators],
schemas: [NO_ERRORS_SCHEMA],
});
const template = '<with-prop-decorators></with-prop-decorators>'; const template = '<with-prop-decorators></with-prop-decorators>';
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
@ -1426,8 +1453,11 @@ function declareTests({useJit}: {useJit: boolean}) {
} }
it('should support defining views in the component decorator', () => { it('should support defining views in the component decorator', () => {
TestBed.configureTestingModule( TestBed.configureTestingModule({
{declarations: [MyComp, ComponentWithTemplate], imports: [CommonModule]}); declarations: [MyComp, ComponentWithTemplate],
imports: [CommonModule],
schemas: [NO_ERRORS_SCHEMA],
});
const template = '<component-with-template></component-with-template>'; const template = '<component-with-template></component-with-template>';
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);

View File

@ -6,10 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, Directive, ElementRef, TemplateRef, ViewContainerRef, ViewEncapsulation, forwardRef} from '@angular/core'; import {Component, Directive, ElementRef, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {getAllDebugNodes} from '@angular/core/src/debug/debug_node'; import {getAllDebugNodes} from '@angular/core/src/debug/debug_node';
import {ViewMetadata} from '@angular/core/src/metadata/view'; import {TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {beforeEach, beforeEachProviders, ddescribe, describe, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; import {beforeEach, beforeEachProviders, ddescribe, describe, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -457,8 +456,8 @@ export function main() {
main.detectChanges(); main.detectChanges();
expect(getDOM().getInnerHTML(main.debugElement.nativeElement)) expect(getDOM().getInnerHTML(main.debugElement.nativeElement))
.toEqual( .toEqual(
'<cmp-a><cmp-b><cmp-d><d>cmp-d</d></cmp-d></cmp-b>' + '<cmp-a><cmp-b><cmp-d><i>cmp-d</i></cmp-d></cmp-b>' +
'<cmp-c><c>cmp-c</c></cmp-c></cmp-a>'); '<cmp-c><b>cmp-c</b></cmp-c></cmp-a>');
}); });
it('should create nested components in the right order', () => { it('should create nested components in the right order', () => {
@ -652,7 +651,7 @@ class Tree {
} }
@Component({selector: 'cmp-d', template: `<d>{{tagName}}</d>`}) @Component({selector: 'cmp-d', template: `<i>{{tagName}}</i>`})
class CmpD { class CmpD {
tagName: string; tagName: string;
constructor(elementRef: ElementRef) { constructor(elementRef: ElementRef) {
@ -661,7 +660,7 @@ class CmpD {
} }
@Component({selector: 'cmp-c', template: `<c>{{tagName}}</c>`}) @Component({selector: 'cmp-c', template: `<b>{{tagName}}</b>`})
class CmpC { class CmpC {
tagName: string; tagName: string;
constructor(elementRef: ElementRef) { constructor(elementRef: ElementRef) {

View File

@ -6,14 +6,15 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, Directive, ElementRef, Input, QueryList, ViewChild, ViewChildren} from '@angular/core'; import {Component, Directive, ElementRef, Input, NO_ERRORS_SCHEMA, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
export function main() { export function main() {
describe('ViewChild', () => { describe('ViewChild', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ViewChildTypeSelectorComponent, ViewChildStringSelectorComponent, Simple] declarations: [ViewChildTypeSelectorComponent, ViewChildStringSelectorComponent, Simple],
schemas: [NO_ERRORS_SCHEMA],
}); });
}); });
@ -42,7 +43,8 @@ export function main() {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: declarations:
[ViewChildrenTypeSelectorComponent, ViewChildrenStringSelectorComponent, Simple] [ViewChildrenTypeSelectorComponent, ViewChildrenStringSelectorComponent, Simple],
schemas: [NO_ERRORS_SCHEMA],
}); });
}); });
@ -61,7 +63,8 @@ export function main() {
TestBed.overrideComponent( TestBed.overrideComponent(
ViewChildrenStringSelectorComponent, ViewChildrenStringSelectorComponent,
{set: {template: `<simple #child1></simple><simple #child2></simple>`}}); {set: {template: `<simple #child1></simple><simple #child2></simple>`}});
const view = TestBed.createComponent(ViewChildrenStringSelectorComponent); const view = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]})
.createComponent(ViewChildrenStringSelectorComponent);
view.detectChanges(); view.detectChanges();
expect(view.componentInstance.children).toBeDefined(); expect(view.componentInstance.children).toBeDefined();
expect(view.componentInstance.children.length).toBe(2); expect(view.componentInstance.children.length).toBe(2);

View File

@ -1456,7 +1456,7 @@ describe('Integration', () => {
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/22/link;exact=true'); expect(location.path()).toEqual('/team/22/link;exact=true');
const native = fixture.debugElement.nativeElement.querySelector('link-parent'); const native = fixture.debugElement.nativeElement.querySelector('#link-parent');
expect(native.className).toEqual('active'); expect(native.className).toEqual('active');
router.navigateByUrl('/team/22/link/simple'); router.navigateByUrl('/team/22/link/simple');
@ -1845,9 +1845,9 @@ class LinkInNgIf {
@Component({ @Component({
selector: 'link-cmp', selector: 'link-cmp',
template: `<router-outlet></router-outlet> template: `<router-outlet></router-outlet>
<link-parent routerLinkActive="active" [routerLinkActiveOptions]="{exact: exact}"> <div id="link-parent" routerLinkActive="active" [routerLinkActiveOptions]="{exact: exact}">
<div ngClass="{one: 'true'}"><a [routerLink]="['./']">link</a></div> <div ngClass="{one: 'true'}"><a [routerLink]="['./']">link</a></div>
</link-parent>` </div>`
}) })
class DummyLinkWithParentCmp { class DummyLinkWithParentCmp {
private exact: boolean; private exact: boolean;

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Class, Component, EventEmitter, Inject, NgModule, OpaqueToken, Testability, destroyPlatform, forwardRef} from '@angular/core'; import {Class, Component, EventEmitter, Inject, NO_ERRORS_SCHEMA, NgModule, OpaqueToken, Testability, destroyPlatform, forwardRef} from '@angular/core';
import {async} from '@angular/core/testing'; import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {UpgradeAdapter} from '@angular/upgrade'; import {UpgradeAdapter} from '@angular/upgrade';
@ -51,7 +51,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function Ng2Module() {}}); }).Class({constructor: function Ng2Module() {}});
ng1Module.directive('ng1', () => { ng1Module.directive('ng1', () => {
@ -93,7 +94,8 @@ export function main() {
declarations: [ declarations: [
adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b'), Ng2 adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b'), Ng2
], ],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -196,9 +198,11 @@ export function main() {
}); });
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var Ng2Module = NgModule({declarations: [Ng2], imports: [BrowserModule]}).Class({ var Ng2Module = NgModule({
constructor: function() {} declarations: [Ng2],
}); imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}});
var element = html(`<div> var element = html(`<div>
<ng2 literal="Text" interpolate="Hello {{'world'}}" <ng2 literal="Text" interpolate="Hello {{'world'}}"
@ -239,9 +243,11 @@ export function main() {
ngOnDestroy: function() { onDestroyed.emit('destroyed'); } ngOnDestroy: function() { onDestroyed.emit('destroyed'); }
}); });
var Ng2Module = NgModule({declarations: [Ng2], imports: [BrowserModule]}).Class({ var Ng2Module = NgModule({
constructor: function() {} declarations: [Ng2],
}); imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html('<ng1></ng1>'); var element = html('<ng1></ng1>');
@ -273,9 +279,11 @@ export function main() {
var Ng2 = var Ng2 =
Component({selector: 'ng2', template: 'test'}).Class({constructor: function() {}}); Component({selector: 'ng2', template: 'test'}).Class({constructor: function() {}});
var Ng2Module = NgModule({declarations: [Ng2], imports: [BrowserModule]}).Class({ var Ng2Module = NgModule({
constructor: function() {} declarations: [Ng2],
}); imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
var element = html('<ng1></ng1>'); var element = html('<ng1></ng1>');
@ -327,7 +335,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -376,7 +385,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -423,7 +433,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -455,7 +466,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -483,7 +495,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -507,7 +520,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -531,7 +545,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -557,7 +572,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -601,7 +617,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -633,7 +650,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -665,7 +683,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -706,7 +725,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -752,7 +772,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -782,7 +803,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -812,7 +834,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@ -844,7 +867,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2a, Ng2b], declarations: [adapter.upgradeNg1Component('ng1'), Ng2a, Ng2b],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
var element = html(`<div><ng2a></ng2a></div>`); var element = html(`<div><ng2a></ng2a></div>`);
@ -860,7 +884,8 @@ export function main() {
it('should export ng2 instance to ng1', async(() => { it('should export ng2 instance to ng1', async(() => {
var MyNg2Module = NgModule({ var MyNg2Module = NgModule({
providers: [{provide: SomeToken, useValue: 'correct_value'}], providers: [{provide: SomeToken, useValue: 'correct_value'}],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module); const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module);
@ -897,7 +922,7 @@ export function main() {
NgModule({imports: [BrowserModule]}).Class({constructor: function() {}}); NgModule({imports: [BrowserModule]}).Class({constructor: function() {}});
const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module); const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module);
var ng1Module = angular.module('ng1', []); angular.module('ng1', []);
var bootstrapResumed: boolean = false; var bootstrapResumed: boolean = false;
var element = html('<div></div>'); var element = html('<div></div>');
@ -919,7 +944,7 @@ export function main() {
NgModule({imports: [BrowserModule]}).Class({constructor: function() {}}); NgModule({imports: [BrowserModule]}).Class({constructor: function() {}});
const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module); const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module);
var ng1Module = angular.module('ng1', []); angular.module('ng1', []);
var element = html('<div></div>'); var element = html('<div></div>');
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
var ng2Testability: Testability = ref.ng2Injector.get(Testability); var ng2Testability: Testability = ref.ng2Injector.get(Testability);
@ -961,7 +986,8 @@ export function main() {
var Ng2Module = NgModule({ var Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule] imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}}); }).Class({constructor: function() {}});
module.directive('ng2', adapter.downgradeNg2Component(Ng2)); module.directive('ng2', adapter.downgradeNg2Component(Ng2));

View File

@ -1,5 +1,5 @@
<inbox-side-menu class="inbox-aside"> <div class="inbox-aside inbox-side-menu">
<a [routerLink]="['/inbox']" class="link" routerLinkActive="active">Inbox</a> <a [routerLink]="['/inbox']" class="link" routerLinkActive="active">Inbox</a>
<a [routerLink]="['/drafts']" class="link" routerLinkActive="active">Drafts</a> <a [routerLink]="['/drafts']" class="link" routerLinkActive="active">Drafts</a>
</inbox-side-menu> </div>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -15,25 +15,25 @@ body {
display:block; display:block;
} }
inbox, drafts, inbox-side-menu { inbox, drafts, .inbox-side-menu {
display:block; display:block;
} }
inbox-side-menu .link { .inbox-side-menu .link {
display:block; display:block;
text-align:center; text-align:center;
padding:1em; padding:1em;
} }
inbox-side-menu .link.active { .inbox-side-menu .link.active {
background:white; background:white;
} }
inbox-side-menu .link:hover { .inbox-side-menu .link:hover {
background:#eee; background:#eee;
} }
inbox-side-menu { .inbox-side-menu {
position:fixed; position:fixed;
left:0; left:0;
top:0; top:0;
@ -42,7 +42,7 @@ inbox-side-menu {
background:#ddd; background:#ddd;
} }
inbox-side-menu a { .inbox-side-menu a {
display: block; display: block;
} }