diff --git a/modules/@angular/compiler/src/template_parser/binding_parser.ts b/modules/@angular/compiler/src/template_parser/binding_parser.ts index d2f4930551..07b47770a4 100644 --- a/modules/@angular/compiler/src/template_parser/binding_parser.ts +++ b/modules/@angular/compiler/src/template_parser/binding_parser.ts @@ -245,18 +245,12 @@ export class BindingParser { let unit: string = null; let bindingType: PropertyBindingType; - let boundPropertyName: string; + let boundPropertyName: string = null; const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR); let securityContexts: SecurityContext[]; - if (parts.length === 1) { - const partValue = parts[0]; - boundPropertyName = this._schemaRegistry.getMappedPropName(partValue); - securityContexts = calcPossibleSecurityContexts( - this._schemaRegistry, elementSelector, boundPropertyName, false); - bindingType = PropertyBindingType.Property; - this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false); - } else { + // Check check for special cases (prefix style, attr, class) + if (parts.length > 1) { if (parts[0] == ATTRIBUTE_PREFIX) { boundPropertyName = parts[1]; this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true); @@ -280,12 +274,18 @@ export class BindingParser { boundPropertyName = parts[1]; bindingType = PropertyBindingType.Style; securityContexts = [SecurityContext.STYLE]; - } else { - this._reportError(`Invalid property name '${boundProp.name}'`, boundProp.sourceSpan); - bindingType = null; - securityContexts = []; } } + + // If not a special case, use the full property name + if (boundPropertyName === null) { + boundPropertyName = this._schemaRegistry.getMappedPropName(boundProp.name); + securityContexts = calcPossibleSecurityContexts( + this._schemaRegistry, elementSelector, boundPropertyName, false); + bindingType = PropertyBindingType.Property; + this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false); + } + return new BoundElementPropertyAst( boundPropertyName, bindingType, securityContexts.length === 1 ? securityContexts[0] : null, securityContexts.length > 1, boundProp.expression, unit, boundProp.sourceSpan); diff --git a/modules/@angular/compiler/test/template_parser/template_parser_spec.ts b/modules/@angular/compiler/test/template_parser/template_parser_spec.ts index b4b0fd4a51..6810318baf 100644 --- a/modules/@angular/compiler/test/template_parser/template_parser_spec.ts +++ b/modules/@angular/compiler/test/template_parser/template_parser_spec.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileQueryMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata'; +import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata'; import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry'; import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAstType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast'; import {TEMPLATE_TRANSFORMS, TemplateParser, splitClasses} from '@angular/compiler/src/template_parser/template_parser'; import {TEST_COMPILER_PROVIDERS} from '@angular/compiler/testing/test_bindings'; -import {SchemaMetadata, SecurityContext, Type} from '@angular/core'; +import {SchemaMetadata, SecurityContext} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {TestBed, inject} from '@angular/core/testing'; @@ -257,1501 +257,1011 @@ export function main() { }); }); - describe( - 'TemplateParser', () => { - beforeEach(() => { - TestBed.configureCompiler({providers: [TEST_COMPILER_PROVIDERS, MOCK_SCHEMA_REGISTRY]}); + describe('TemplateParser', () => { + beforeEach(() => { + TestBed.configureCompiler({providers: [TEST_COMPILER_PROVIDERS, MOCK_SCHEMA_REGISTRY]}); + }); + + commonBeforeEach(); + + describe('parse', () => { + describe('nodes without bindings', () => { + + it('should parse text nodes', () => { + expect(humanizeTplAst(parse('a', []))).toEqual([[TextAst, 'a']]); }); - commonBeforeEach(); + it('should parse elements with attributes', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [AttrAst, 'a', 'b']]); + }); + }); - describe('parse', () => { - describe('nodes without bindings', () => { + it('should parse ngContent', () => { + const parsed = parse('', []); + expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); + }); - it('should parse text nodes', () => { - expect(humanizeTplAst(parse('a', []))).toEqual([[TextAst, 'a']]); - }); + it('should parse ngContent when it contains WS only', () => { + const parsed = parse(' \n ', []); + expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); + }); - it('should parse elements with attributes', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [AttrAst, 'a', 'b']]); - }); - }); + it('should parse ngContent regardless the namespace', () => { + const parsed = parse('', []); + expect(humanizeTplAst(parsed)).toEqual([ + [ElementAst, ':svg:svg'], + [NgContentAst], + ]); + }); - it('should parse ngContent', () => { - const parsed = parse('', []); - expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); - }); + it('should parse bound text nodes', () => { + expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); + }); - it('should parse ngContent when it contains WS only', () => { - const parsed = parse(' \n ', []); - expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); - }); + it('should parse with custom interpolation config', + inject([TemplateParser], (parser: TemplateParser) => { + const component = CompileDirectiveMetadata.create({ + selector: 'test', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Test'}}), + isComponent: true, + template: new CompileTemplateMetadata({interpolation: ['{%', '%}']}) + }); + expect(humanizeTplAst(parser.parse(component, '{%a%}', [], [], [], 'TestComp'), { + start: '{%', + end: '%}' + })).toEqual([[BoundTextAst, '{% a %}']]); + })); - it('should parse ngContent regardless the namespace', () => { - const parsed = parse('', []); - expect(humanizeTplAst(parsed)).toEqual([ - [ElementAst, ':svg:svg'], - [NgContentAst], - ]); - }); + describe('bound properties', () => { - it('should parse bound text nodes', () => { - expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); - }); + it('should parse mixed case bound properties', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'someProp', 'v', null] + ]); + }); - it('should parse with custom interpolation config', - inject([TemplateParser], (parser: TemplateParser) => { - const component = CompileDirectiveMetadata.create({ - selector: 'test', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Test'}}), - isComponent: true, - template: new CompileTemplateMetadata({interpolation: ['{%', '%}']}) - }); - expect(humanizeTplAst(parser.parse(component, '{%a%}', [], [], [], 'TestComp'), { - start: '{%', - end: '%}' - })).toEqual([[BoundTextAst, '{% a %}']]); - })); + it('should parse dash case bound properties', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'some-prop', 'v', null] + ]); + }); - describe('bound properties', () => { + it('should parse dotted name bound properties', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'dot.name', 'v', null] + ]); + }); - it('should parse mixed case bound properties', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'someProp', 'v', null] - ]); - }); + it('should normalize property names via the element schema', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'mappedProp', 'v', null] + ]); + }); - it('should parse dash case bound properties', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'some-prop', 'v', null] - ]); - }); + it('should parse mixed case bound attributes', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr', 'v', null] + ]); + }); - it('should normalize property names via the element schema', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'mappedProp', 'v', null] - ]); - }); + it('should parse and dash case bound classes', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Class, 'some-class', 'v', null] + ]); + }); - it('should parse mixed case bound attributes', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr', 'v', null] - ]); - }); + it('should parse mixed case bound classes', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Class, 'someClass', 'v', null] + ]); + }); - it('should parse and dash case bound classes', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Class, 'some-class', 'v', null] - ]); - }); + it('should parse mixed case bound styles', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Style, 'someStyle', 'v', null] + ]); + }); - it('should parse mixed case bound classes', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Class, 'someClass', 'v', null] - ]); - }); - - it('should parse mixed case bound styles', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Style, 'someStyle', 'v', null] - ]); - }); - - it('should report invalid prefixes', () => { - expect(() => parse('

', [])) - .toThrowError( - `Template parse errors:\nInvalid property name 'atTr.foo' ("

][atTr.foo]>"): TestComp@0:3`); - expect(() => parse('

', [])) - .toThrowError( - `Template parse errors:\nInvalid property name 'sTyle.foo' ("

][sTyle.foo]>"): TestComp@0:3`); - expect(() => parse('

', [])) - .toThrowError( - `Template parse errors:\nInvalid property name 'Class.foo' ("

][Class.foo]>"): TestComp@0:3`); - expect(() => parse('

', [])) - .toThrowError( - `Template parse errors:\nInvalid property name 'bar.foo' ("

][bar.foo]>"): TestComp@0:3`); - }); - - describe('errors', () => { - it('should throw error when binding to an unknown property', () => { - expect(() => parse('', [])) - .toThrowError(`Template parse errors: + describe('errors', () => { + it('should throw error when binding to an unknown property', () => { + expect(() => parse('', [])) + .toThrowError(`Template parse errors: Can't bind to 'invalidProp' since it isn't a known property of 'my-component'. 1. If 'my-component' is an Angular component and it has 'invalidProp' input, then verify that it is part of this module. 2. If 'my-component' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message. ("][invalidProp]="bar">"): TestComp@0:14`); - }); + }); - it('should throw error when binding to an unknown element w/o bindings', () => { - expect(() => parse('', [])).toThrowError(`Template parse errors: + it('should throw error when binding to an unknown element w/o bindings', () => { + expect(() => parse('', [])).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.schemas' of this component to suppress this message. ("[ERROR ->]"): TestComp@0:0`); - }); + }); - it('should throw error when binding to an unknown custom element w/o bindings', - () => { - expect(() => parse('', [])) - .toThrowError(`Template parse errors: + it('should throw error when binding to an unknown custom element w/o bindings', () => { + expect(() => parse('', [])).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.schemas' of this component to suppress this message. ("[ERROR ->]"): TestComp@0:0`); - }); + }); - it('should throw error when binding to an invalid property', () => { - expect(() => parse('', [])) - .toThrowError(`Template parse errors: + it('should throw error when binding to an invalid property', () => { + expect(() => parse('', [])) + .toThrowError(`Template parse errors: Binding to property 'onEvent' is disallowed for security reasons ("][onEvent]="bar">"): TestComp@0:14`); - }); + }); - it('should throw error when binding to an invalid attribute', () => { - expect(() => parse('', [])) - .toThrowError(`Template parse errors: + it('should throw error when binding to an invalid attribute', () => { + expect(() => parse('', [])) + .toThrowError(`Template parse errors: Binding to attribute 'onEvent' is disallowed for security reasons ("][attr.onEvent]="bar">"): TestComp@0:14`); - }); - }); + }); + }); - it('should parse bound properties via [...] and not report them as attributes', () => { - expect(humanizeTplAst(parse('

', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] - ]); - }); + it('should parse bound properties via [...] and not report them as attributes', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] + ]); + }); - it('should parse bound properties via bind- and not report them as attributes', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] - ]); - }); + it('should parse bound properties via bind- and not report them as attributes', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] + ]); + }); - it('should parse bound properties via {{...}} and not report them as attributes', - () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ + it('should parse bound properties via {{...}} and not report them as attributes', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', null] + ]); + }); + + it('should parse bound properties via bind-animate- and not report them as attributes', + () => { + expect(humanizeTplAst(parse('
', [], [], []))) + .toEqual([ [ElementAst, 'div'], [ - BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', null + BoundElementPropertyAst, PropertyBindingType.Animation, 'someAnimation', + 'value2', null ] ]); - }); + }); - it('should parse bound properties via bind-animate- and not report them as attributes', - () => { - expect( - humanizeTplAst(parse('
', [], [], []))) - .toEqual([ - [ElementAst, 'div'], - [ - BoundElementPropertyAst, PropertyBindingType.Animation, 'someAnimation', - 'value2', null - ] - ]); - }); + it('should throw an error when parsing detects non-bound properties via @ that contain a value', + () => { + expect(() => { parse('
', [], [], []); }) + .toThrowError( + /Assigning animation triggers via @prop="exp" attributes with an expression is invalid. Use property bindings \(e.g. \[@prop\]="exp"\) or use an attribute without a value \(e.g. @prop\) instead. \("
\]@someAnimation="value2">"\): TestComp@0:5/); + }); - it('should throw an error when parsing detects non-bound properties via @ that contain a value', - () => { - expect(() => { parse('
', [], [], []); }) - .toThrowError( - /Assigning animation triggers via @prop="exp" attributes with an expression is invalid. Use property bindings \(e.g. \[@prop\]="exp"\) or use an attribute without a value \(e.g. @prop\) instead. \("
\]@someAnimation="value2">"\): TestComp@0:5/); - }); + it('should not issue a warning when host attributes contain a valid property-bound animation trigger', + () => { + const animationEntries = [new CompileAnimationEntryMetadata('prop', [])]; + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'div', + template: new CompileTemplateMetadata({animations: animationEntries}), + type: createTypeMeta({ + reference: {filePath: someModuleUrl, name: 'DirA'}, + }), + host: {'[@prop]': 'expr'} + }) + .toSummary(); - it('should not issue a warning when host attributes contain a valid property-bound animation trigger', - () => { - const animationEntries = [new CompileAnimationEntryMetadata('prop', [])]; - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'div', - template: new CompileTemplateMetadata({animations: animationEntries}), - type: createTypeMeta({ - reference: {filePath: someModuleUrl, name: 'DirA'}, - }), - host: {'[@prop]': 'expr'} - }) - .toSummary(); + humanizeTplAst(parse('
', [dirA])); + expect(console.warnings.length).toEqual(0); + }); - humanizeTplAst(parse('
', [dirA])); - expect(console.warnings.length).toEqual(0); - }); - - it('should throw descriptive error when a host binding is not a string expression', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'broken', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - host: {'[class.foo]': null} - }) - .toSummary(); - - expect(() => { parse('', [dirA]); }) - .toThrowError( - `Template parse errors:\nValue of the host property binding "class.foo" needs to be a string representing an expression but got "null" (object) ("[ERROR ->]"): TestComp@0:0, Directive DirA`); - }); - - it('should throw descriptive error when a host event is not a string expression', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'broken', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - host: {'(click)': null} - }) - .toSummary(); - - expect(() => { parse('', [dirA]); }) - .toThrowError( - `Template parse errors:\nValue of the host listener "click" needs to be a string representing an expression but got "null" (object) ("[ERROR ->]"): TestComp@0:0, Directive DirA`); - }); - - it('should not issue a warning when an animation property is bound without an expression', - () => { - humanizeTplAst(parse('
', [], [], [])); - expect(console.warnings.length).toEqual(0); - }); - - it('should parse bound properties via [@] and not report them as attributes', () => { - expect(humanizeTplAst(parse('
', [], [], []))).toEqual([ - [ElementAst, 'div'], - [ - BoundElementPropertyAst, PropertyBindingType.Animation, 'someAnimation', 'value2', - null - ] - ]); - }); - }); - - describe('events', () => { - - it('should parse bound events with a target', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundEventAst, 'event', 'window', 'v'], - ]); - }); - - it('should report an error on empty expression', () => { - expect(() => parse('
', [])) - .toThrowError(/Empty expressions are not allowed/); - - expect(() => parse('
', [])) - .toThrowError(/Empty expressions are not allowed/); - }); - - it('should parse bound events via (...) and not report them as attributes', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); - }); - - it('should parse event names case sensitive', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'some-event', null, 'v']]); - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'someEvent', null, 'v']]); - }); - - it('should parse bound events via on- and not report them as attributes', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); - }); - - it('should allow events on explicit embedded templates that are emitted by a directive', - () => { - const dirA = CompileDirectiveMetadata - .create({ - selector: 'template', - outputs: ['e'], - type: createTypeMeta( - {reference: {filePath: someModuleUrl, name: 'DirA'}}) - }) - .toSummary(); - expect(humanizeTplAst(parse('', [dirA]))).toEqual([ - [EmbeddedTemplateAst], - [BoundEventAst, 'e', null, 'f'], - [DirectiveAst, dirA], - ]); - }); - }); - - describe('bindon', () => { - it('should parse bound events and properties via [(...)] and not report them as attributes', - () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], - [BoundEventAst, 'propChange', null, 'v = $event'] - ]); - }); - - it('should parse bound events and properties via bindon- and not report them as attributes', - () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], - [BoundEventAst, 'propChange', null, 'v = $event'] - ]); - }); - - }); - - describe('directives', () => { - it('should order directives by the directives array in the View and match them only once', - () => { - const dirA = CompileDirectiveMetadata - .create({ - selector: '[a]', - type: createTypeMeta( - {reference: {filePath: someModuleUrl, name: 'DirA'}}) - }) - .toSummary(); - const dirB = CompileDirectiveMetadata - .create({ - selector: '[b]', - type: createTypeMeta( - {reference: {filePath: someModuleUrl, name: 'DirB'}}) - }) - .toSummary(); - const dirC = CompileDirectiveMetadata - .create({ - selector: '[c]', - type: createTypeMeta( - {reference: {filePath: someModuleUrl, name: 'DirC'}}) - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA, dirB, dirC]))).toEqual([ - [ElementAst, 'div'], [AttrAst, 'a', ''], [AttrAst, 'c', ''], [AttrAst, 'b', ''], - [AttrAst, 'a', ''], [AttrAst, 'b', ''], [DirectiveAst, dirA], - [DirectiveAst, dirB], [DirectiveAst, dirC] - ]); - }); - - it('should locate directives in property bindings', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: '[a=b]', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}) - }) - .toSummary(); - const dirB = - CompileDirectiveMetadata - .create({ - selector: '[b]', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirB'}}) - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA, dirB]))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'a', 'b', null], - [DirectiveAst, dirA] - ]); - }); - - it('should locate directives in event bindings', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: '[a]', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirB'}}) - }) - .toSummary(); - - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], [BoundEventAst, 'a', null, 'b'], [DirectiveAst, dirA] - ]); - }); - - it('should parse directive host properties', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'div', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - host: {'[a]': 'expr'} - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], [DirectiveAst, dirA], - [BoundElementPropertyAst, PropertyBindingType.Property, 'a', 'expr', null] - ]); - }); - - it('should parse directive host listeners', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'div', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - host: {'(a)': 'expr'} - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], [DirectiveAst, dirA], [BoundEventAst, 'a', null, 'expr'] - ]); - }); - - it('should parse directive properties', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'div', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - inputs: ['aProp'] - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], [DirectiveAst, dirA], - [BoundDirectivePropertyAst, 'aProp', 'expr'] - ]); - }); - - it('should parse renamed directive properties', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'div', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - inputs: ['b:a'] - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], [DirectiveAst, dirA], - [BoundDirectivePropertyAst, 'b', 'expr'] - ]); - }); - - it('should parse literal directive properties', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'div', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - inputs: ['a'] - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], [AttrAst, 'a', 'literal'], [DirectiveAst, dirA], - [BoundDirectivePropertyAst, 'a', '"literal"'] - ]); - }); - - it('should favor explicit bound properties over literal properties', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'div', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - inputs: ['a'] - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))) - .toEqual([ - [ElementAst, 'div'], [AttrAst, 'a', 'literal'], [DirectiveAst, dirA], - [BoundDirectivePropertyAst, 'a', '"literal2"'] - ]); - }); - - it('should support optional directive properties', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: 'div', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - inputs: ['a'] - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], [DirectiveAst, dirA] - ]); - }); - - }); - - describe('providers', () => { - let nextProviderId: number; - - function createToken(value: string): CompileTokenMetadata { - let token: CompileTokenMetadata; - if (value.startsWith('type:')) { - const name = value.substring(5); - token = {identifier: createTypeMeta({reference: name})}; - } else { - token = {value: value}; - } - return token; - } - - function createDep(value: string): CompileDiDependencyMetadata { - let isOptional = false; - if (value.startsWith('optional:')) { - isOptional = true; - value = value.substring(9); - } - let isSelf = false; - if (value.startsWith('self:')) { - isSelf = true; - value = value.substring(5); - } - let isHost = false; - if (value.startsWith('host:')) { - isHost = true; - value = value.substring(5); - } - return { - token: createToken(value), - isOptional: isOptional, - isSelf: isSelf, - isHost: isHost - }; - } - - function createProvider( - token: string, {multi = false, deps = []}: {multi?: boolean, deps?: string[]} = {}): - CompileProviderMetadata { - const name = `provider${nextProviderId++}`; - const compileToken = createToken(token); - return { - token: compileToken, - multi: multi, - useClass: createTypeMeta({reference: tokenReference(compileToken)}), - deps: deps.map(createDep), - useExisting: undefined, - useFactory: undefined, - useValue: undefined - }; - } - - function createDir( - selector: string, - {providers = null, viewProviders = null, deps = [], queries = []}: { - providers?: CompileProviderMetadata[], - viewProviders?: CompileProviderMetadata[], - deps?: string[], - queries?: string[] - } = {}): CompileDirectiveSummary { - const isComponent = !selector.startsWith('['); - return CompileDirectiveMetadata + it('should throw descriptive error when a host binding is not a string expression', () => { + const dirA = + CompileDirectiveMetadata .create({ - selector: selector, - type: createTypeMeta({ - reference: selector, - diDeps: deps.map(createDep), - }), - isComponent: isComponent, - template: new CompileTemplateMetadata({ngContentSelectors: []}), - providers: providers, - viewProviders: viewProviders, - queries: queries.map((value) => { - return { - selectors: [createToken(value)], - descendants: false, - first: false, - propertyName: 'test', - read: undefined - }; - }) + selector: 'broken', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + host: {'[class.foo]': null} }) .toSummary(); - } - beforeEach(() => { nextProviderId = 0; }); + expect(() => { parse('', [dirA]); }) + .toThrowError( + `Template parse errors:\nValue of the host property binding "class.foo" needs to be a string representing an expression but got "null" (object) ("[ERROR ->]"): TestComp@0:0, Directive DirA`); + }); - it('should provide a component', () => { - const comp = createDir('my-comp'); - const elAst: ElementAst = parse('', [comp])[0]; - expect(elAst.providers.length).toBe(1); - expect(elAst.providers[0].providerType).toBe(ProviderAstType.Component); - expect(elAst.providers[0].providers[0].useClass).toBe(comp.type); - }); + it('should throw descriptive error when a host event is not a string expression', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'broken', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + host: {'(click)': null} + }) + .toSummary(); - it('should provide a directive', () => { - const dirA = createDir('[dirA]'); - const elAst: ElementAst = parse('
', [dirA])[0]; - expect(elAst.providers.length).toBe(1); - expect(elAst.providers[0].providerType).toBe(ProviderAstType.Directive); - expect(elAst.providers[0].providers[0].useClass).toBe(dirA.type); - }); + expect(() => { parse('', [dirA]); }) + .toThrowError( + `Template parse errors:\nValue of the host listener "click" needs to be a string representing an expression but got "null" (object) ("[ERROR ->]"): TestComp@0:0, Directive DirA`); + }); - it('should use the public providers of a directive', () => { - const provider = createProvider('service'); - const dirA = createDir('[dirA]', {providers: [provider]}); - const elAst: ElementAst = parse('
', [dirA])[0]; - expect(elAst.providers.length).toBe(2); - expect(elAst.providers[1].providerType).toBe(ProviderAstType.PublicService); - expect(elAst.providers[1].providers).toEqual([provider]); - }); + it('should not issue a warning when an animation property is bound without an expression', + () => { + humanizeTplAst(parse('
', [], [], [])); + expect(console.warnings.length).toEqual(0); + }); - it('should use the private providers of a component', () => { - const provider = createProvider('service'); - const comp = createDir('my-comp', {viewProviders: [provider]}); - const elAst: ElementAst = parse('', [comp])[0]; - expect(elAst.providers.length).toBe(2); - expect(elAst.providers[1].providerType).toBe(ProviderAstType.PrivateService); - expect(elAst.providers[1].providers).toEqual([provider]); - }); + it('should parse bound properties via [@] and not report them as attributes', () => { + expect(humanizeTplAst(parse('
', [], [], []))).toEqual([ + [ElementAst, 'div'], + [ + BoundElementPropertyAst, PropertyBindingType.Animation, 'someAnimation', 'value2', + null + ] + ]); + }); + }); - it('should support multi providers', () => { - const provider0 = createProvider('service0', {multi: true}); - const provider1 = createProvider('service1', {multi: true}); - const provider2 = createProvider('service0', {multi: true}); - const dirA = createDir('[dirA]', {providers: [provider0, provider1]}); - const dirB = createDir('[dirB]', {providers: [provider2]}); - const elAst: ElementAst = parse('
', [dirA, dirB])[0]; - expect(elAst.providers.length).toBe(4); - expect(elAst.providers[2].providers).toEqual([provider0, provider2]); - expect(elAst.providers[3].providers).toEqual([provider1]); - }); + describe('events', () => { - it('should overwrite non multi providers', () => { - const provider1 = createProvider('service0'); - const provider2 = createProvider('service1'); - const provider3 = createProvider('service0'); - const dirA = createDir('[dirA]', {providers: [provider1, provider2]}); - const dirB = createDir('[dirB]', {providers: [provider3]}); - const elAst: ElementAst = parse('
', [dirA, dirB])[0]; - expect(elAst.providers.length).toBe(4); - expect(elAst.providers[2].providers).toEqual([provider3]); - expect(elAst.providers[3].providers).toEqual([provider2]); - }); + it('should parse bound events with a target', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundEventAst, 'event', 'window', 'v'], + ]); + }); - it('should overwrite component providers by directive providers', () => { - const compProvider = createProvider('service0'); - const dirProvider = createProvider('service0'); - const comp = createDir('my-comp', {providers: [compProvider]}); - const dirA = createDir('[dirA]', {providers: [dirProvider]}); - const elAst: ElementAst = parse('', [dirA, comp])[0]; - expect(elAst.providers.length).toBe(3); - expect(elAst.providers[2].providers).toEqual([dirProvider]); - }); + it('should report an error on empty expression', () => { + expect(() => parse('
', [])) + .toThrowError(/Empty expressions are not allowed/); - it('should overwrite view providers by directive providers', () => { - const viewProvider = createProvider('service0'); - const dirProvider = createProvider('service0'); - const comp = createDir('my-comp', {viewProviders: [viewProvider]}); - const dirA = createDir('[dirA]', {providers: [dirProvider]}); - const elAst: ElementAst = parse('', [dirA, comp])[0]; - expect(elAst.providers.length).toBe(3); - expect(elAst.providers[2].providers).toEqual([dirProvider]); - }); + expect(() => parse('
', [])) + .toThrowError(/Empty expressions are not allowed/); + }); - it('should overwrite directives by providers', () => { - const dirProvider = createProvider('type:my-comp'); - const comp = createDir('my-comp', {providers: [dirProvider]}); - const elAst: ElementAst = parse('', [comp])[0]; - expect(elAst.providers.length).toBe(1); - expect(elAst.providers[0].providers).toEqual([dirProvider]); - }); + it('should parse bound events via (...) and not report them as attributes', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); + }); - it('if mixing multi and non multi providers', () => { - const provider0 = createProvider('service0'); - const provider1 = createProvider('service0', {multi: true}); - const dirA = createDir('[dirA]', {providers: [provider0]}); - const dirB = createDir('[dirB]', {providers: [provider1]}); - expect(() => parse('
', [dirA, dirB])) - .toThrowError( - `Template parse errors:\n` + - `Mixing multi and non multi provider is not possible for token service0 ("[ERROR ->]
"): TestComp@0:0`); - }); + it('should parse event names case sensitive', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'some-event', null, 'v']]); + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'someEvent', null, 'v']]); + }); - it('should sort providers by their DI order', () => { - const provider0 = createProvider('service0', {deps: ['type:[dir2]']}); - const provider1 = createProvider('service1'); - const dir2 = createDir('[dir2]', {deps: ['service1']}); - const comp = createDir('my-comp', {providers: [provider0, provider1]}); - const elAst: ElementAst = parse('', [comp, dir2])[0]; - expect(elAst.providers.length).toBe(4); - expect(elAst.providers[0].providers[0].useClass).toEqual(comp.type); - expect(elAst.providers[1].providers).toEqual([provider1]); - expect(elAst.providers[2].providers[0].useClass).toEqual(dir2.type); - expect(elAst.providers[3].providers).toEqual([provider0]); - }); + it('should parse bound events via on- and not report them as attributes', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); + }); - it('should sort directives by their DI order', () => { - const dir0 = createDir('[dir0]', {deps: ['type:my-comp']}); - const dir1 = createDir('[dir1]', {deps: ['type:[dir0]']}); - const dir2 = createDir('[dir2]', {deps: ['type:[dir1]']}); - const comp = createDir('my-comp'); - const elAst: ElementAst = - parse('', [comp, dir2, dir0, dir1])[0]; - expect(elAst.providers.length).toBe(4); - expect(elAst.directives[0].directive).toBe(comp); - expect(elAst.directives[1].directive).toBe(dir0); - expect(elAst.directives[2].directive).toBe(dir1); - expect(elAst.directives[3].directive).toBe(dir2); - }); + it('should allow events on explicit embedded templates that are emitted by a directive', + () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'template', + outputs: ['e'], + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}) + }) + .toSummary(); + expect(humanizeTplAst(parse('', [dirA]))).toEqual([ + [EmbeddedTemplateAst], + [BoundEventAst, 'e', null, 'f'], + [DirectiveAst, dirA], + ]); + }); + }); - it('should mark directives and dependencies of directives as eager', () => { - const provider0 = createProvider('service0'); - const provider1 = createProvider('service1'); - const dirA = - createDir('[dirA]', {providers: [provider0, provider1], deps: ['service0']}); - const elAst: ElementAst = parse('
', [dirA])[0]; - expect(elAst.providers.length).toBe(3); - expect(elAst.providers[0].providers).toEqual([provider0]); - expect(elAst.providers[0].eager).toBe(true); - expect(elAst.providers[1].providers[0].useClass).toEqual(dirA.type); - expect(elAst.providers[1].eager).toBe(true); - expect(elAst.providers[2].providers).toEqual([provider1]); - expect(elAst.providers[2].eager).toBe(false); - }); + describe('bindon', () => { + it('should parse bound events and properties via [(...)] and not report them as attributes', + () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], + [BoundEventAst, 'propChange', null, 'v = $event'] + ]); + }); - it('should mark dependencies on parent elements as eager', () => { - const provider0 = createProvider('service0'); - const provider1 = createProvider('service1'); - const dirA = createDir('[dirA]', {providers: [provider0, provider1]}); - const dirB = createDir('[dirB]', {deps: ['service0']}); - const elAst: ElementAst = - parse('
', [dirA, dirB])[0]; - expect(elAst.providers.length).toBe(3); - expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); - expect(elAst.providers[0].eager).toBe(true); - expect(elAst.providers[1].providers).toEqual([provider0]); - expect(elAst.providers[1].eager).toBe(true); - expect(elAst.providers[2].providers).toEqual([provider1]); - expect(elAst.providers[2].eager).toBe(false); - }); + it('should parse bound events and properties via bindon- and not report them as attributes', + () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], + [BoundEventAst, 'propChange', null, 'v = $event'] + ]); + }); - it('should mark queried providers as eager', () => { - const provider0 = createProvider('service0'); - const provider1 = createProvider('service1'); - const dirA = - createDir('[dirA]', {providers: [provider0, provider1], queries: ['service0']}); - const elAst: ElementAst = parse('
', [dirA])[0]; - expect(elAst.providers.length).toBe(3); - expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); - expect(elAst.providers[0].eager).toBe(true); - expect(elAst.providers[1].providers).toEqual([provider0]); - expect(elAst.providers[1].eager).toBe(true); - expect(elAst.providers[2].providers).toEqual([provider1]); - expect(elAst.providers[2].eager).toBe(false); - }); + }); - it('should not mark dependencies accross embedded views as eager', () => { - const provider0 = createProvider('service0'); - const dirA = createDir('[dirA]', {providers: [provider0]}); - const dirB = createDir('[dirB]', {deps: ['service0']}); - const elAst: ElementAst = - parse('
', [dirA, dirB])[0]; - expect(elAst.providers.length).toBe(2); - expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); - expect(elAst.providers[0].eager).toBe(true); - expect(elAst.providers[1].providers).toEqual([provider0]); - expect(elAst.providers[1].eager).toBe(false); - }); + describe('directives', () => { + it('should order directives by the directives array in the View and match them only once', + () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: '[a]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}) + }) + .toSummary(); + const dirB = + CompileDirectiveMetadata + .create({ + selector: '[b]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirB'}}) + }) + .toSummary(); + const dirC = + CompileDirectiveMetadata + .create({ + selector: '[c]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirC'}}) + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA, dirB, dirC]))).toEqual([ + [ElementAst, 'div'], [AttrAst, 'a', ''], [AttrAst, 'c', ''], [AttrAst, 'b', ''], + [AttrAst, 'a', ''], [AttrAst, 'b', ''], [DirectiveAst, dirA], [DirectiveAst, dirB], + [DirectiveAst, dirC] + ]); + }); - it('should report missing @Self() deps as errors', () => { - const dirA = createDir('[dirA]', {deps: ['self:provider0']}); - expect(() => parse('
', [dirA])) - .toThrowError( - 'Template parse errors:\nNo provider for provider0 ("[ERROR ->]
"): TestComp@0:0'); - }); + it('should locate directives in property bindings', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: '[a=b]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}) + }) + .toSummary(); + const dirB = + CompileDirectiveMetadata + .create({ + selector: '[b]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirB'}}) + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA, dirB]))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'a', 'b', null], + [DirectiveAst, dirA] + ]); + }); - it('should change missing @Self() that are optional to nulls', () => { - const dirA = createDir('[dirA]', {deps: ['optional:self:provider0']}); - const elAst: ElementAst = parse('
', [dirA])[0]; - expect(elAst.providers[0].providers[0].deps[0].isValue).toBe(true); - expect(elAst.providers[0].providers[0].deps[0].value).toBe(null); - }); + it('should locate directives in event bindings', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: '[a]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirB'}}) + }) + .toSummary(); - it('should report missing @Host() deps as errors', () => { - const dirA = createDir('[dirA]', {deps: ['host:provider0']}); - expect(() => parse('
', [dirA])) - .toThrowError( - 'Template parse errors:\nNo provider for provider0 ("[ERROR ->]
"): TestComp@0:0'); - }); + expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ + [ElementAst, 'div'], [BoundEventAst, 'a', null, 'b'], [DirectiveAst, dirA] + ]); + }); - it('should change missing @Host() that are optional to nulls', () => { - const dirA = createDir('[dirA]', {deps: ['optional:host:provider0']}); - const elAst: ElementAst = parse('
', [dirA])[0]; - expect(elAst.providers[0].providers[0].deps[0].isValue).toBe(true); - expect(elAst.providers[0].providers[0].deps[0].value).toBe(null); - }); - }); + it('should parse directive host properties', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'div', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + host: {'[a]': 'expr'} + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ + [ElementAst, 'div'], [DirectiveAst, dirA], + [BoundElementPropertyAst, PropertyBindingType.Property, 'a', 'expr', null] + ]); + }); - describe('references', () => { + it('should parse directive host listeners', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'div', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + host: {'(a)': 'expr'} + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ + [ElementAst, 'div'], [DirectiveAst, dirA], [BoundEventAst, 'a', null, 'expr'] + ]); + }); - it('should parse references via #... and not report them as attributes', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); - }); + it('should parse directive properties', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'div', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + inputs: ['aProp'] + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ + [ElementAst, 'div'], [DirectiveAst, dirA], + [BoundDirectivePropertyAst, 'aProp', 'expr'] + ]); + }); - it('should parse references via ref-... and not report them as attributes', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); - }); + it('should parse renamed directive properties', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'div', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + inputs: ['b:a'] + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ + [ElementAst, 'div'], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'b', 'expr'] + ]); + }); - it('should parse camel case references', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'someA', null]]); - }); + it('should parse literal directive properties', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'div', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + inputs: ['a'] + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ + [ElementAst, 'div'], [AttrAst, 'a', 'literal'], [DirectiveAst, dirA], + [BoundDirectivePropertyAst, 'a', '"literal"'] + ]); + }); - it('should assign references with empty value to the element', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); - }); - - it('should assign references to directives via exportAs', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: '[a]', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - exportAs: 'dirA' - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], - [AttrAst, 'a', ''], - [ReferenceAst, 'a', identifierToken(dirA.type)], - [DirectiveAst, dirA], + it('should favor explicit bound properties over literal properties', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'div', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + inputs: ['a'] + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA]))) + .toEqual([ + [ElementAst, 'div'], [AttrAst, 'a', 'literal'], [DirectiveAst, dirA], + [BoundDirectivePropertyAst, 'a', '"literal2"'] ]); - }); + }); - it('should report references with values that dont match a directive as errors', () => { - expect(() => parse('
', [])).toThrowError(`Template parse errors: + it('should support optional directive properties', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: 'div', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + inputs: ['a'] + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ + [ElementAst, 'div'], [DirectiveAst, dirA] + ]); + }); + + }); + + describe('providers', () => { + let nextProviderId: number; + + function createToken(value: string): CompileTokenMetadata { + let token: CompileTokenMetadata; + if (value.startsWith('type:')) { + const name = value.substring(5); + token = {identifier: createTypeMeta({reference: name})}; + } else { + token = {value: value}; + } + return token; + } + + function createDep(value: string): CompileDiDependencyMetadata { + let isOptional = false; + if (value.startsWith('optional:')) { + isOptional = true; + value = value.substring(9); + } + let isSelf = false; + if (value.startsWith('self:')) { + isSelf = true; + value = value.substring(5); + } + let isHost = false; + if (value.startsWith('host:')) { + isHost = true; + value = value.substring(5); + } + return { + token: createToken(value), + isOptional: isOptional, + isSelf: isSelf, + isHost: isHost + }; + } + + function createProvider( + token: string, {multi = false, deps = []}: {multi?: boolean, deps?: string[]} = {}): + CompileProviderMetadata { + const name = `provider${nextProviderId++}`; + const compileToken = createToken(token); + return { + token: compileToken, + multi: multi, + useClass: createTypeMeta({reference: tokenReference(compileToken)}), + deps: deps.map(createDep), + useExisting: undefined, + useFactory: undefined, + useValue: undefined + }; + } + + function createDir( + selector: string, {providers = null, viewProviders = null, deps = [], queries = []}: { + providers?: CompileProviderMetadata[], + viewProviders?: CompileProviderMetadata[], + deps?: string[], + queries?: string[] + } = {}): CompileDirectiveSummary { + const isComponent = !selector.startsWith('['); + return CompileDirectiveMetadata + .create({ + selector: selector, + type: createTypeMeta({ + reference: selector, + diDeps: deps.map(createDep), + }), + isComponent: isComponent, + template: new CompileTemplateMetadata({ngContentSelectors: []}), + providers: providers, + viewProviders: viewProviders, + queries: queries.map((value) => { + return { + selectors: [createToken(value)], + descendants: false, + first: false, + propertyName: 'test', + read: undefined + }; + }) + }) + .toSummary(); + } + + beforeEach(() => { nextProviderId = 0; }); + + it('should provide a component', () => { + const comp = createDir('my-comp'); + const elAst: ElementAst = parse('', [comp])[0]; + expect(elAst.providers.length).toBe(1); + expect(elAst.providers[0].providerType).toBe(ProviderAstType.Component); + expect(elAst.providers[0].providers[0].useClass).toBe(comp.type); + }); + + it('should provide a directive', () => { + const dirA = createDir('[dirA]'); + const elAst: ElementAst = parse('
', [dirA])[0]; + expect(elAst.providers.length).toBe(1); + expect(elAst.providers[0].providerType).toBe(ProviderAstType.Directive); + expect(elAst.providers[0].providers[0].useClass).toBe(dirA.type); + }); + + it('should use the public providers of a directive', () => { + const provider = createProvider('service'); + const dirA = createDir('[dirA]', {providers: [provider]}); + const elAst: ElementAst = parse('
', [dirA])[0]; + expect(elAst.providers.length).toBe(2); + expect(elAst.providers[1].providerType).toBe(ProviderAstType.PublicService); + expect(elAst.providers[1].providers).toEqual([provider]); + }); + + it('should use the private providers of a component', () => { + const provider = createProvider('service'); + const comp = createDir('my-comp', {viewProviders: [provider]}); + const elAst: ElementAst = parse('', [comp])[0]; + expect(elAst.providers.length).toBe(2); + expect(elAst.providers[1].providerType).toBe(ProviderAstType.PrivateService); + expect(elAst.providers[1].providers).toEqual([provider]); + }); + + it('should support multi providers', () => { + const provider0 = createProvider('service0', {multi: true}); + const provider1 = createProvider('service1', {multi: true}); + const provider2 = createProvider('service0', {multi: true}); + const dirA = createDir('[dirA]', {providers: [provider0, provider1]}); + const dirB = createDir('[dirB]', {providers: [provider2]}); + const elAst: ElementAst = parse('
', [dirA, dirB])[0]; + expect(elAst.providers.length).toBe(4); + expect(elAst.providers[2].providers).toEqual([provider0, provider2]); + expect(elAst.providers[3].providers).toEqual([provider1]); + }); + + it('should overwrite non multi providers', () => { + const provider1 = createProvider('service0'); + const provider2 = createProvider('service1'); + const provider3 = createProvider('service0'); + const dirA = createDir('[dirA]', {providers: [provider1, provider2]}); + const dirB = createDir('[dirB]', {providers: [provider3]}); + const elAst: ElementAst = parse('
', [dirA, dirB])[0]; + expect(elAst.providers.length).toBe(4); + expect(elAst.providers[2].providers).toEqual([provider3]); + expect(elAst.providers[3].providers).toEqual([provider2]); + }); + + it('should overwrite component providers by directive providers', () => { + const compProvider = createProvider('service0'); + const dirProvider = createProvider('service0'); + const comp = createDir('my-comp', {providers: [compProvider]}); + const dirA = createDir('[dirA]', {providers: [dirProvider]}); + const elAst: ElementAst = parse('', [dirA, comp])[0]; + expect(elAst.providers.length).toBe(3); + expect(elAst.providers[2].providers).toEqual([dirProvider]); + }); + + it('should overwrite view providers by directive providers', () => { + const viewProvider = createProvider('service0'); + const dirProvider = createProvider('service0'); + const comp = createDir('my-comp', {viewProviders: [viewProvider]}); + const dirA = createDir('[dirA]', {providers: [dirProvider]}); + const elAst: ElementAst = parse('', [dirA, comp])[0]; + expect(elAst.providers.length).toBe(3); + expect(elAst.providers[2].providers).toEqual([dirProvider]); + }); + + it('should overwrite directives by providers', () => { + const dirProvider = createProvider('type:my-comp'); + const comp = createDir('my-comp', {providers: [dirProvider]}); + const elAst: ElementAst = parse('', [comp])[0]; + expect(elAst.providers.length).toBe(1); + expect(elAst.providers[0].providers).toEqual([dirProvider]); + }); + + it('if mixing multi and non multi providers', () => { + const provider0 = createProvider('service0'); + const provider1 = createProvider('service0', {multi: true}); + const dirA = createDir('[dirA]', {providers: [provider0]}); + const dirB = createDir('[dirB]', {providers: [provider1]}); + expect(() => parse('
', [dirA, dirB])) + .toThrowError( + `Template parse errors:\n` + + `Mixing multi and non multi provider is not possible for token service0 ("[ERROR ->]
"): TestComp@0:0`); + }); + + it('should sort providers by their DI order', () => { + const provider0 = createProvider('service0', {deps: ['type:[dir2]']}); + const provider1 = createProvider('service1'); + const dir2 = createDir('[dir2]', {deps: ['service1']}); + const comp = createDir('my-comp', {providers: [provider0, provider1]}); + const elAst: ElementAst = parse('', [comp, dir2])[0]; + expect(elAst.providers.length).toBe(4); + expect(elAst.providers[0].providers[0].useClass).toEqual(comp.type); + expect(elAst.providers[1].providers).toEqual([provider1]); + expect(elAst.providers[2].providers[0].useClass).toEqual(dir2.type); + expect(elAst.providers[3].providers).toEqual([provider0]); + }); + + it('should sort directives by their DI order', () => { + const dir0 = createDir('[dir0]', {deps: ['type:my-comp']}); + const dir1 = createDir('[dir1]', {deps: ['type:[dir0]']}); + const dir2 = createDir('[dir2]', {deps: ['type:[dir1]']}); + const comp = createDir('my-comp'); + const elAst: ElementAst = + parse('', [comp, dir2, dir0, dir1])[0]; + expect(elAst.providers.length).toBe(4); + expect(elAst.directives[0].directive).toBe(comp); + expect(elAst.directives[1].directive).toBe(dir0); + expect(elAst.directives[2].directive).toBe(dir1); + expect(elAst.directives[3].directive).toBe(dir2); + }); + + it('should mark directives and dependencies of directives as eager', () => { + const provider0 = createProvider('service0'); + const provider1 = createProvider('service1'); + const dirA = createDir('[dirA]', {providers: [provider0, provider1], deps: ['service0']}); + const elAst: ElementAst = parse('
', [dirA])[0]; + expect(elAst.providers.length).toBe(3); + expect(elAst.providers[0].providers).toEqual([provider0]); + expect(elAst.providers[0].eager).toBe(true); + expect(elAst.providers[1].providers[0].useClass).toEqual(dirA.type); + expect(elAst.providers[1].eager).toBe(true); + expect(elAst.providers[2].providers).toEqual([provider1]); + expect(elAst.providers[2].eager).toBe(false); + }); + + it('should mark dependencies on parent elements as eager', () => { + const provider0 = createProvider('service0'); + const provider1 = createProvider('service1'); + const dirA = createDir('[dirA]', {providers: [provider0, provider1]}); + const dirB = createDir('[dirB]', {deps: ['service0']}); + const elAst: ElementAst = + parse('
', [dirA, dirB])[0]; + expect(elAst.providers.length).toBe(3); + expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); + expect(elAst.providers[0].eager).toBe(true); + expect(elAst.providers[1].providers).toEqual([provider0]); + expect(elAst.providers[1].eager).toBe(true); + expect(elAst.providers[2].providers).toEqual([provider1]); + expect(elAst.providers[2].eager).toBe(false); + }); + + it('should mark queried providers as eager', () => { + const provider0 = createProvider('service0'); + const provider1 = createProvider('service1'); + const dirA = + createDir('[dirA]', {providers: [provider0, provider1], queries: ['service0']}); + const elAst: ElementAst = parse('
', [dirA])[0]; + expect(elAst.providers.length).toBe(3); + expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); + expect(elAst.providers[0].eager).toBe(true); + expect(elAst.providers[1].providers).toEqual([provider0]); + expect(elAst.providers[1].eager).toBe(true); + expect(elAst.providers[2].providers).toEqual([provider1]); + expect(elAst.providers[2].eager).toBe(false); + }); + + it('should not mark dependencies accross embedded views as eager', () => { + const provider0 = createProvider('service0'); + const dirA = createDir('[dirA]', {providers: [provider0]}); + const dirB = createDir('[dirB]', {deps: ['service0']}); + const elAst: ElementAst = + parse('
', [dirA, dirB])[0]; + expect(elAst.providers.length).toBe(2); + expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); + expect(elAst.providers[0].eager).toBe(true); + expect(elAst.providers[1].providers).toEqual([provider0]); + expect(elAst.providers[1].eager).toBe(false); + }); + + it('should report missing @Self() deps as errors', () => { + const dirA = createDir('[dirA]', {deps: ['self:provider0']}); + expect(() => parse('
', [dirA])) + .toThrowError( + 'Template parse errors:\nNo provider for provider0 ("[ERROR ->]
"): TestComp@0:0'); + }); + + it('should change missing @Self() that are optional to nulls', () => { + const dirA = createDir('[dirA]', {deps: ['optional:self:provider0']}); + const elAst: ElementAst = parse('
', [dirA])[0]; + expect(elAst.providers[0].providers[0].deps[0].isValue).toBe(true); + expect(elAst.providers[0].providers[0].deps[0].value).toBe(null); + }); + + it('should report missing @Host() deps as errors', () => { + const dirA = createDir('[dirA]', {deps: ['host:provider0']}); + expect(() => parse('
', [dirA])) + .toThrowError( + 'Template parse errors:\nNo provider for provider0 ("[ERROR ->]
"): TestComp@0:0'); + }); + + it('should change missing @Host() that are optional to nulls', () => { + const dirA = createDir('[dirA]', {deps: ['optional:host:provider0']}); + const elAst: ElementAst = parse('
', [dirA])[0]; + expect(elAst.providers[0].providers[0].deps[0].isValue).toBe(true); + expect(elAst.providers[0].providers[0].deps[0].value).toBe(null); + }); + }); + + describe('references', () => { + + it('should parse references via #... and not report them as attributes', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); + }); + + it('should parse references via ref-... and not report them as attributes', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); + }); + + it('should parse camel case references', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'someA', null]]); + }); + + it('should assign references with empty value to the element', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); + }); + + it('should assign references to directives via exportAs', () => { + const dirA = + CompileDirectiveMetadata + .create({ + selector: '[a]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), + exportAs: 'dirA' + }) + .toSummary(); + expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ + [ElementAst, 'div'], + [AttrAst, 'a', ''], + [ReferenceAst, 'a', identifierToken(dirA.type)], + [DirectiveAst, dirA], + ]); + }); + + it('should report references with values that dont match a directive as errors', () => { + expect(() => parse('
', [])).toThrowError(`Template parse errors: There is no directive with "exportAs" set to "dirA" ("
]#a="dirA">
"): TestComp@0:5`); - }); + }); - it('should report invalid reference names', () => { - expect(() => parse('
', [])).toThrowError(`Template parse errors: + it('should report invalid reference names', () => { + expect(() => parse('
', [])).toThrowError(`Template parse errors: "-" is not allowed in reference names ("
]#a-b>
"): TestComp@0:5`); - }); + }); - it('should report variables as errors', () => { - expect(() => parse('
', [])).toThrowError(`Template parse errors: + it('should report variables as errors', () => { + expect(() => parse('
', [])).toThrowError(`Template parse errors: "let-" is only supported on template elements. ("
]let-a>
"): TestComp@0:5`); - }); + }); - it('should report duplicate reference names', () => { - expect(() => parse('
', [])) - .toThrowError(`Template parse errors: + it('should report duplicate reference names', () => { + expect(() => parse('
', [])) + .toThrowError(`Template parse errors: Reference "#a" is defined several times ("
]#a>
"): TestComp@0:19`); - }); - - it('should not throw error when there is same reference name in different templates', - () => { - expect(() => parse('
', [])) - .not.toThrowError(); - - }); - - it('should assign references with empty value to components', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: '[a]', - isComponent: true, - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), - exportAs: 'dirA', - template: new CompileTemplateMetadata({ngContentSelectors: []}) - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], - [AttrAst, 'a', ''], - [ReferenceAst, 'a', identifierToken(dirA.type)], - [DirectiveAst, dirA], - ]); - }); - - it('should not locate directives in references', () => { - const dirA = - CompileDirectiveMetadata - .create({ - selector: '[a]', - type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}) - }) - .toSummary(); - expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ - [ElementAst, 'div'], [ReferenceAst, 'a', null] - ]); - }); - }); - - describe('explicit templates', () => { - it('should create embedded templates for