diff --git a/.travis.yml b/.travis.yml index d574adc770..04c0c2a707 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,7 @@ env: - CI_MODE=browserstack_required - CI_MODE=saucelabs_optional - CI_MODE=browserstack_optional - - CI_MODE=docs_test + - CI_MODE=aio_tools_test - CI_MODE=aio - CI_MODE=aio_e2e AIO_SHARD=0 - CI_MODE=aio_e2e AIO_SHARD=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0e3ab0a3..b46f8db760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +<a name="5.0.0-beta.2"></a> +# [5.0.0-beta.2](https://github.com/angular/angular/compare/5.0.0-beta.1...5.0.0-beta.2) (2017-08-02) + + +### Bug Fixes + +* **compiler:** do not consider arguments when determining recursion ([e64b54b](https://github.com/angular/angular/commit/e64b54b)) +* **compiler:** fix for element needing implicit parent placed in top-level ng-container ([381471d](https://github.com/angular/angular/commit/381471d)), closes [#18314](https://github.com/angular/angular/issues/18314) + + +### Features + +* **forms:** add options arg to abstract controls ([ebef5e6](https://github.com/angular/angular/commit/ebef5e6)) +* **router:** add events tracking activation of individual routes ([49cd851](https://github.com/angular/angular/commit/49cd851)) + + + +<a name="4.3.3"></a> +## [4.3.3](https://github.com/angular/angular/compare/4.3.2...4.3.3) (2017-08-02) + + +### Bug Fixes + +* **compiler:** fix for element needing implicit parent placed in top-level ng-container ([f5cbc2e](https://github.com/angular/angular/commit/f5cbc2e)), closes [#18314](https://github.com/angular/angular/issues/18314) + + + <a name="5.0.0-beta.1"></a> # [5.0.0-beta.1](https://github.com/angular/angular/compare/5.0.0-beta.0...5.0.0-beta.1) (2017-07-27) diff --git a/aio/.angular-cli.json b/aio/.angular-cli.json index 6b3ef2df2f..10b25f2812 100644 --- a/aio/.angular-cli.json +++ b/aio/.angular-cli.json @@ -31,8 +31,9 @@ "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", - "stage": "environments/environment.stage.ts", - "prod": "environments/environment.prod.ts" + "next": "environments/environment.next.ts", + "stable": "environments/environment.stable.ts", + "archive": "environments/environment.archive.ts" } } ], diff --git a/aio/content/examples/form-validation/e2e-spec.ts b/aio/content/examples/form-validation/e2e-spec.ts index 1e9788956a..cd17cd4e3d 100644 --- a/aio/content/examples/form-validation/e2e-spec.ts +++ b/aio/content/examples/form-validation/e2e-spec.ts @@ -9,30 +9,20 @@ describe('Form Validation Tests', function () { browser.get(''); }); - describe('Hero Form 1', () => { + describe('Template-driven form', () => { beforeAll(() => { - getPage('hero-form-template1'); + getPage('hero-form-template'); }); - tests(); + tests('Template-Driven Form'); }); - describe('Hero Form 2', () => { + describe('Reactive form', () => { beforeAll(() => { - getPage('hero-form-template2'); + getPage('hero-form-reactive'); }); - tests(); - bobTests(); - }); - - describe('Hero Form 3 (Reactive)', () => { - beforeAll(() => { - getPage('hero-form-reactive3'); - makeNameTooLong(); - }); - - tests(); + tests('Reactive Form'); bobTests(); }); }); @@ -48,6 +38,7 @@ let page: { nameInput: ElementFinder, alterEgoInput: ElementFinder, powerSelect: ElementFinder, + powerOption: ElementFinder, errorMessages: ElementArrayFinder, heroFormButtons: ElementArrayFinder, heroSubmitted: ElementFinder @@ -64,19 +55,21 @@ function getPage(sectionTag: string) { nameInput: section.element(by.css('#name')), alterEgoInput: section.element(by.css('#alterEgo')), powerSelect: section.element(by.css('#power')), + powerOption: section.element(by.css('#power option')), errorMessages: section.all(by.css('div.alert')), heroFormButtons: buttons, - heroSubmitted: section.element(by.css('hero-submitted > div')) + heroSubmitted: section.element(by.css('.submitted-message')) }; } -function tests() { +function tests(title: string) { + it('should display correct title', function () { - expect(page.title.getText()).toContain('Hero Form'); + expect(page.title.getText()).toContain(title); }); it('should not display submitted message before submit', function () { - expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(false); + expect(page.heroSubmitted.isElementPresent(by.css('p'))).toBe(false); }); it('should have form buttons', function () { @@ -130,11 +123,11 @@ function tests() { it('should hide form after submit', function () { page.heroFormButtons.get(0).click(); - expect(page.title.isDisplayed()).toBe(false); + expect(page.heroFormButtons.get(0).isDisplayed()).toBe(false); }); it('submitted form should be displayed', function () { - expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(true); + expect(page.heroSubmitted.isElementPresent(by.css('p'))).toBe(true); }); it('submitted form should have new hero name', function () { @@ -142,9 +135,9 @@ function tests() { }); it('clicking edit button should reveal form again', function () { - const editBtn = page.heroSubmitted.element(by.css('button')); - editBtn.click(); - expect(page.heroSubmitted.isElementPresent(by.css('h2'))) + const newFormBtn = page.heroSubmitted.element(by.css('button')); + newFormBtn.click(); + expect(page.heroSubmitted.isElementPresent(by.css('p'))) .toBe(false, 'submitted hidden again'); expect(page.title.isDisplayed()).toBe(true, 'can see form title'); }); @@ -159,9 +152,13 @@ function expectFormIsInvalid() { } function bobTests() { - const emsg = 'Someone named "Bob" cannot be a hero.'; + const emsg = 'Name cannot be Bob.'; it('should produce "no bob" error after setting name to "Bobby"', function () { + // Re-populate select element + page.powerSelect.click(); + page.powerOption.click(); + page.nameInput.clear(); page.nameInput.sendKeys('Bobby'); expectFormIsInvalid(); @@ -174,8 +171,3 @@ function bobTests() { expectFormIsValid(); }); } - -function makeNameTooLong() { - // make the first name invalid - page.nameInput.sendKeys('ThisHeroNameHasWayWayTooManyLetters'); -} diff --git a/aio/content/examples/form-validation/src/app/app.component.ts b/aio/content/examples/form-validation/src/app/app.component.ts index 2da4dc4d0a..5260b8d9e1 100644 --- a/aio/content/examples/form-validation/src/app/app.component.ts +++ b/aio/content/examples/form-validation/src/app/app.component.ts @@ -3,10 +3,8 @@ import { Component } from '@angular/core'; @Component({ selector: 'my-app', - template: `<hero-form-template1></hero-form-template1> + template: `<hero-form-template></hero-form-template> <hr> - <hero-form-template2></hero-form-template2> - <hr> - <hero-form-reactive3></hero-form-reactive3>` + <hero-form-reactive></hero-form-reactive>` }) export class AppComponent { } diff --git a/aio/content/examples/form-validation/src/app/app.module.ts b/aio/content/examples/form-validation/src/app/app.module.ts index 72b4e3a770..0a5d1cf455 100644 --- a/aio/content/examples/form-validation/src/app/app.module.ts +++ b/aio/content/examples/form-validation/src/app/app.module.ts @@ -1,18 +1,26 @@ // #docregion import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; -import { HeroFormTemplateModule } from './template/hero-form-template.module'; -import { HeroFormReactiveModule } from './reactive/hero-form-reactive.module'; +import { HeroFormTemplateComponent } from './template/hero-form-template.component'; +import { HeroFormReactiveComponent } from './reactive/hero-form-reactive.component'; +import { ForbiddenValidatorDirective } from './shared/forbidden-name.directive'; + @NgModule({ imports: [ BrowserModule, - HeroFormTemplateModule, - HeroFormReactiveModule + FormsModule, + ReactiveFormsModule + ], + declarations: [ + AppComponent, + HeroFormTemplateComponent, + HeroFormReactiveComponent, + ForbiddenValidatorDirective ], - declarations: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html index 149537bd3e..36cebbaca4 100644 --- a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html +++ b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html @@ -1,26 +1,38 @@ <!-- #docregion --> <div class="container"> - <div [hidden]="submitted"> - <h1>Hero Form 3 (Reactive)</h1> - <!-- #docregion form-tag--> - <form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()"> - <!-- #enddocregion form-tag--> - <div class="form-group"> - <!-- #docregion name-with-error-msg --> - <label for="name">Name</label> - <input type="text" id="name" class="form-control" + <h1>Reactive Form</h1> + + <form [formGroup]="heroForm" #formDir="ngForm"> + + <div [hidden]="formDir.submitted"> + + <div class="form-group"> + + <label for="name">Name</label> + <!-- #docregion name-with-error-msg --> + <input id="name" class="form-control" formControlName="name" required > - <div *ngIf="formErrors.name" class="alert alert-danger"> - {{ formErrors.name }} + <div *ngIf="name.invalid && (name.dirty || name.touched)" + class="alert alert-danger"> + + <div *ngIf="name.errors.required"> + Name is required. + </div> + <div *ngIf="name.errors.minlength"> + Name must be at least 4 characters long. + </div> + <div *ngIf="name.errors.forbiddenName"> + Name cannot be Bob. + </div> </div> <!-- #enddocregion name-with-error-msg --> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> - <input type="text" id="alterEgo" class="form-control" + <input id="alterEgo" class="form-control" formControlName="alterEgo" > </div> @@ -31,17 +43,20 @@ <option *ngFor="let p of powers" [value]="p">{{p}}</option> </select> - <div *ngIf="formErrors.power" class="alert alert-danger"> - {{ formErrors.power }} + <div *ngIf="power.invalid && power.touched" class="alert alert-danger"> + <div *ngIf="power.errors.required">Power is required.</div> </div> </div> <button type="submit" class="btn btn-default" - [disabled]="!heroForm.valid">Submit</button> + [disabled]="heroForm.invalid">Submit</button> <button type="button" class="btn btn-default" - (click)="addHero()">New Hero</button> - </form> - </div> + (click)="formDir.resetForm({})">Reset</button> + </div> + </form> - <hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted> + <div class="submitted-message" *ngIf="formDir.submitted"> + <p>You've submitted your hero, {{ heroForm.value.name }}!</p> + <button (click)="formDir.resetForm({})">Add new hero</button> + </div> </div> diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts index 241ff1e782..7ce9e57fa2 100644 --- a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts +++ b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts @@ -2,115 +2,39 @@ // #docplaster // #docregion import { Component, OnInit } from '@angular/core'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; - -import { Hero } from '../shared/hero'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; import { forbiddenNameValidator } from '../shared/forbidden-name.directive'; @Component({ - selector: 'hero-form-reactive3', + selector: 'hero-form-reactive', templateUrl: './hero-form-reactive.component.html' }) export class HeroFormReactiveComponent implements OnInit { powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; - hero = new Hero(18, 'Dr. WhatIsHisName', this.powers[0], 'Dr. What'); + hero = {name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0]}; - submitted = false; - - // #docregion on-submit - onSubmit() { - this.submitted = true; - this.hero = this.heroForm.value; - } - // #enddocregion on-submit -// #enddocregion - - // Reset the form with a new hero AND restore 'pristine' class state - // by toggling 'active' flag which causes the form - // to be removed/re-added in a tick via NgIf - // TODO: Workaround until NgForm has a reset method (#6822) - active = true; -// #docregion class - // #docregion add-hero - addHero() { - this.hero = new Hero(42, '', ''); - this.buildForm(); - // #enddocregion add-hero -// #enddocregion class - - this.active = false; - setTimeout(() => this.active = true, 0); -// #docregion - // #docregion add-hero - } - // #enddocregion add-hero - - // #docregion form-builder heroForm: FormGroup; - constructor(private fb: FormBuilder) { } + // #docregion form-group ngOnInit(): void { - this.buildForm(); - } - - buildForm(): void { - this.heroForm = this.fb.group({ - // #docregion name-validators - 'name': [this.hero.name, [ - Validators.required, - Validators.minLength(4), - Validators.maxLength(24), - forbiddenNameValidator(/bob/i) - ] - ], - // #enddocregion name-validators - 'alterEgo': [this.hero.alterEgo], - 'power': [this.hero.power, Validators.required] + // #docregion custom-validator + this.heroForm = new FormGroup({ + 'name': new FormControl(this.hero.name, [ + Validators.required, + Validators.minLength(4), + forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator. + ]), + 'alterEgo': new FormControl(this.hero.alterEgo), + 'power': new FormControl(this.hero.power, Validators.required) }); - - this.heroForm.valueChanges - .subscribe(data => this.onValueChanged(data)); - - this.onValueChanged(); // (re)set validation messages now + // #enddocregion custom-validator } - // #enddocregion form-builder + get name() { return this.heroForm.get('name'); } - onValueChanged(data?: any) { - if (!this.heroForm) { return; } - const form = this.heroForm; - - for (const field in this.formErrors) { - // clear previous error message (if any) - this.formErrors[field] = ''; - const control = form.get(field); - - if (control && control.dirty && !control.valid) { - const messages = this.validationMessages[field]; - for (const key in control.errors) { - this.formErrors[field] += messages[key] + ' '; - } - } - } - } - - formErrors = { - 'name': '', - 'power': '' - }; - - validationMessages = { - 'name': { - 'required': 'Name is required.', - 'minlength': 'Name must be at least 4 characters long.', - 'maxlength': 'Name cannot be more than 24 characters long.', - 'forbiddenName': 'Someone named "Bob" cannot be a hero.' - }, - 'power': { - 'required': 'Power is required.' - } - }; + get power() { return this.heroForm.get('power'); } + // #enddocregion form-group } // #enddocregion diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.module.ts b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.module.ts deleted file mode 100644 index 6ff9265e92..0000000000 --- a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// #docregion -import { NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; - -import { SharedModule } from '../shared/shared.module'; -import { HeroFormReactiveComponent } from './hero-form-reactive.component'; - -@NgModule({ - imports: [ SharedModule, ReactiveFormsModule ], - declarations: [ HeroFormReactiveComponent ], - exports: [ HeroFormReactiveComponent ] -}) -export class HeroFormReactiveModule { } diff --git a/aio/content/examples/form-validation/src/app/shared/forbidden-name.directive.ts b/aio/content/examples/form-validation/src/app/shared/forbidden-name.directive.ts index 870f514842..f78b8fb25b 100644 --- a/aio/content/examples/form-validation/src/app/shared/forbidden-name.directive.ts +++ b/aio/content/examples/form-validation/src/app/shared/forbidden-name.directive.ts @@ -6,9 +6,8 @@ import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn, Validators } fr /** A hero's name can't match the given regular expression */ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): {[key: string]: any} => { - const name = control.value; - const no = nameRe.test(name); - return no ? {'forbiddenName': {name}} : null; + const forbidden = nameRe.test(control.value); + return forbidden ? {'forbiddenName': {value: control.value}} : null; }; } // #enddocregion custom-validator @@ -20,23 +19,12 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}] // #enddocregion directive-providers }) -export class ForbiddenValidatorDirective implements Validator, OnChanges { +export class ForbiddenValidatorDirective implements Validator { @Input() forbiddenName: string; - private valFn = Validators.nullValidator; - - ngOnChanges(changes: SimpleChanges): void { - const change = changes['forbiddenName']; - if (change) { - const val: string | RegExp = change.currentValue; - const re = val instanceof RegExp ? val : new RegExp(val, 'i'); - this.valFn = forbiddenNameValidator(re); - } else { - this.valFn = Validators.nullValidator; - } - } validate(control: AbstractControl): {[key: string]: any} { - return this.valFn(control); + return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control) + : null; } } // #enddocregion directive diff --git a/aio/content/examples/form-validation/src/app/shared/hero.ts b/aio/content/examples/form-validation/src/app/shared/hero.ts deleted file mode 100644 index fe2b55e51a..0000000000 --- a/aio/content/examples/form-validation/src/app/shared/hero.ts +++ /dev/null @@ -1,9 +0,0 @@ -// #docregion -export class Hero { - constructor( - public id: number, - public name: string, - public power: string, - public alterEgo?: string - ) { } -} diff --git a/aio/content/examples/form-validation/src/app/shared/shared.module.ts b/aio/content/examples/form-validation/src/app/shared/shared.module.ts deleted file mode 100644 index 2b0ada59bd..0000000000 --- a/aio/content/examples/form-validation/src/app/shared/shared.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -// #docregion -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { ForbiddenValidatorDirective } from './forbidden-name.directive'; -import { SubmittedComponent } from './submitted.component'; - -@NgModule({ - imports: [ CommonModule], - declarations: [ ForbiddenValidatorDirective, SubmittedComponent ], - exports: [ ForbiddenValidatorDirective, SubmittedComponent, - CommonModule ] -}) -export class SharedModule { } diff --git a/aio/content/examples/form-validation/src/app/shared/submitted.component.ts b/aio/content/examples/form-validation/src/app/shared/submitted.component.ts deleted file mode 100644 index 18cea6563f..0000000000 --- a/aio/content/examples/form-validation/src/app/shared/submitted.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -// #docregion -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -import { Hero } from './hero'; - -@Component({ - selector: 'hero-submitted', - template: ` - <div *ngIf="submitted"> - <h2>You submitted the following:</h2> - <div class="row"> - <div class="col-xs-3">Name</div> - <div class="col-xs-9 pull-left">{{ hero.name }}</div> - </div> - <div class="row"> - <div class="col-xs-3">Alter Ego</div> - <div class="col-xs-9 pull-left">{{ hero.alterEgo }}</div> - </div> - <div class="row"> - <div class="col-xs-3">Power</div> - <div class="col-xs-9 pull-left">{{ hero.power }}</div> - </div> - <br> - <button class="btn btn-default" (click)="onClick()">Edit</button> - </div>` -}) -export class SubmittedComponent { - @Input() hero: Hero; - @Input() submitted = false; - @Output() submittedChange = new EventEmitter<boolean>(); - onClick() { this.submittedChange.emit(false); } -} diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html new file mode 100644 index 0000000000..c11336a3f2 --- /dev/null +++ b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html @@ -0,0 +1,66 @@ + <!-- #docregion --> +<div class="container"> + + <h1>Template-Driven Form</h1> + <!-- #docregion form-tag--> + <form #heroForm="ngForm"> + <!-- #enddocregion form-tag--> + <div [hidden]="heroForm.submitted"> + + <div class="form-group"> + <label for="name">Name</label> + <!-- #docregion name-with-error-msg --> + <!-- #docregion name-input --> + <input id="name" name="name" class="form-control" + required minlength="4" forbiddenName="bob" + [(ngModel)]="hero.name" #name="ngModel" > + <!-- #enddocregion name-input --> + + <div *ngIf="name.invalid && (name.dirty || name.touched)" + class="alert alert-danger"> + + <div *ngIf="name.errors.required"> + Name is required. + </div> + <div *ngIf="name.errors.minlength"> + Name must be at least 4 characters long. + </div> + <div *ngIf="name.errors.forbiddenName"> + Name cannot be Bob. + </div> + + </div> + <!-- #enddocregion name-with-error-msg --> + </div> + + <div class="form-group"> + <label for="alterEgo">Alter Ego</label> + <input id="alterEgo" class="form-control" + name="alterEgo" [(ngModel)]="hero.alterEgo" > + </div> + + <div class="form-group"> + <label for="power">Hero Power</label> + <select id="power" name="power" class="form-control" + required [(ngModel)]="hero.power" #power="ngModel" > + <option *ngFor="let p of powers" [value]="p">{{p}}</option> + </select> + + <div *ngIf="power.errors && power.touched" class="alert alert-danger"> + <div *ngIf="power.errors.required">Power is required.</div> + </div> + </div> + + <button type="submit" class="btn btn-default" + [disabled]="heroForm.invalid">Submit</button> + <button type="button" class="btn btn-default" + (click)="heroForm.resetForm({})">Reset</button> + </div> + + <div class="submitted-message" *ngIf="heroForm.submitted"> + <p>You've submitted your hero, {{ heroForm.value.name }}!</p> + <button (click)="heroForm.resetForm({})">Add new hero</button> + </div> + </form> + +</div> diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template.component.ts b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.ts new file mode 100644 index 0000000000..2ee1133eae --- /dev/null +++ b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.ts @@ -0,0 +1,16 @@ +/* tslint:disable: member-ordering */ +// #docplaster +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'hero-form-template', + templateUrl: './hero-form-template.component.html' +}) +export class HeroFormTemplateComponent { + + powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; + + hero = {name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0]}; + +} diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template.module.ts b/aio/content/examples/form-validation/src/app/template/hero-form-template.module.ts deleted file mode 100644 index 042c019d5e..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -// #docregion -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -import { SharedModule } from '../shared/shared.module'; -import { HeroFormTemplate1Component } from './hero-form-template1.component'; -import { HeroFormTemplate2Component } from './hero-form-template2.component'; - -@NgModule({ - imports: [ SharedModule, FormsModule ], - declarations: [ HeroFormTemplate1Component, HeroFormTemplate2Component ], - exports: [ HeroFormTemplate1Component, HeroFormTemplate2Component ] -}) -export class HeroFormTemplateModule { } diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.html b/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.html deleted file mode 100644 index 22b374b622..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.html +++ /dev/null @@ -1,61 +0,0 @@ - <!-- #docregion --> -<div class="container"> - <div [hidden]="submitted"> - <h1>Hero Form 1 (Template)</h1> - <!-- #docregion form-tag--> - <form #heroForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()"> - <!-- #enddocregion form-tag--> - <div class="form-group"> - <!-- #docregion name-with-error-msg --> - <label for="name">Name</label> - - <input type="text" id="name" class="form-control" - required minlength="4" maxlength="24" - name="name" [(ngModel)]="hero.name" - #name="ngModel" > - - <div *ngIf="name.errors && (name.dirty || name.touched)" - class="alert alert-danger"> - <div [hidden]="!name.errors.required"> - Name is required - </div> - <div [hidden]="!name.errors.minlength"> - Name must be at least 4 characters long. - </div> - <div [hidden]="!name.errors.maxlength"> - Name cannot be more than 24 characters long. - </div> - </div> - <!-- #enddocregion name-with-error-msg --> - </div> - - <div class="form-group"> - <label for="alterEgo">Alter Ego</label> - <input type="text" id="alterEgo" class="form-control" - name="alterEgo" - [(ngModel)]="hero.alterEgo" > - </div> - - <div class="form-group"> - <label for="power">Hero Power</label> - <select id="power" class="form-control" - name="power" - [(ngModel)]="hero.power" required - #power="ngModel" > - <option *ngFor="let p of powers" [value]="p">{{p}}</option> - </select> - - <div *ngIf="power.errors && power.touched" class="alert alert-danger"> - <div [hidden]="!power.errors.required">Power is required</div> - </div> - </div> - - <button type="submit" class="btn btn-default" - [disabled]="!heroForm.form.valid">Submit</button> - <button type="button" class="btn btn-default" - (click)="addHero()">New Hero</button> - </form> - </div> - - <hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted> -</div> diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.ts b/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.ts deleted file mode 100644 index 1bc29db44d..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* tslint:disable: member-ordering */ -// #docplaster -// #docregion -import { Component } from '@angular/core'; - - -import { Hero } from '../shared/hero'; - -@Component({ - selector: 'hero-form-template1', - templateUrl: './hero-form-template1.component.html' -}) -// #docregion class -export class HeroFormTemplate1Component { - - powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; - - hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What'); - - submitted = false; - - onSubmit() { - this.submitted = true; - } -// #enddocregion class -// #enddocregion - // Reset the form with a new hero AND restore 'pristine' class state - // by toggling 'active' flag which causes the form - // to be removed/re-added in a tick via NgIf - // TODO: Workaround until NgForm has a reset method (#6822) - active = true; -// #docregion -// #docregion class - - addHero() { - this.hero = new Hero(42, '', ''); -// #enddocregion class -// #enddocregion - - this.active = false; - setTimeout(() => this.active = true, 0); -// #docregion -// #docregion class - } -} -// #enddocregion class -// #enddocregion diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.html b/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.html deleted file mode 100644 index 8bb7066541..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.html +++ /dev/null @@ -1,52 +0,0 @@ - <!-- #docregion --> -<div class="container"> - <div [hidden]="submitted"> - <h1>Hero Form 2 (Template & Messages)</h1> - <!-- #docregion form-tag--> - <form #heroForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()"> - <!-- #enddocregion form-tag--> - <div class="form-group"> - <!-- #docregion name-with-error-msg --> - <label for="name">Name</label> - - <!-- #docregion name-input --> - <input type="text" id="name" class="form-control" - required minlength="4" maxlength="24" forbiddenName="bob" - name="name" [(ngModel)]="hero.name" > - <!-- #enddocregion name-input --> - - <div *ngIf="formErrors.name" class="alert alert-danger"> - {{ formErrors.name }} - </div> - <!-- #enddocregion name-with-error-msg --> - </div> - - <div class="form-group"> - <label for="alterEgo">Alter Ego</label> - <input type="text" id="alterEgo" class="form-control" - name="alterEgo" - [(ngModel)]="hero.alterEgo" > - </div> - - <div class="form-group"> - <label for="power">Hero Power</label> - <select id="power" class="form-control" - name="power" - [(ngModel)]="hero.power" required > - <option *ngFor="let p of powers" [value]="p">{{p}}</option> - </select> - - <div *ngIf="formErrors.power" class="alert alert-danger"> - {{ formErrors.power }} - </div> - </div> - - <button type="submit" class="btn btn-default" - [disabled]="!heroForm.form.valid">Submit</button> - <button type="button" class="btn btn-default" - (click)="addHero()">New Hero</button> - </form> - </div> - - <hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted> -</div> diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.ts b/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.ts deleted file mode 100644 index 320ef09efd..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* tslint:disable: member-ordering forin */ -// #docplaster -// #docregion -import { Component, AfterViewChecked, ViewChild } from '@angular/core'; -import { NgForm } from '@angular/forms'; - -import { Hero } from '../shared/hero'; - -@Component({ - selector: 'hero-form-template2', - templateUrl: './hero-form-template2.component.html' -}) -export class HeroFormTemplate2Component implements AfterViewChecked { - - powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; - - hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What'); - - submitted = false; - - onSubmit() { - this.submitted = true; - } -// #enddocregion - - // Reset the form with a new hero AND restore 'pristine' class state - // by toggling 'active' flag which causes the form - // to be removed/re-added in a tick via NgIf - // TODO: Workaround until NgForm has a reset method (#6822) - active = true; -// #docregion - - addHero() { - this.hero = new Hero(42, '', ''); -// #enddocregion - - this.active = false; - setTimeout(() => this.active = true, 0); -// #docregion - } - - // #docregion view-child - heroForm: NgForm; - @ViewChild('heroForm') currentForm: NgForm; - - ngAfterViewChecked() { - this.formChanged(); - } - - formChanged() { - if (this.currentForm === this.heroForm) { return; } - this.heroForm = this.currentForm; - if (this.heroForm) { - this.heroForm.valueChanges - .subscribe(data => this.onValueChanged(data)); - } - } - // #enddocregion view-child - - // #docregion handler - onValueChanged(data?: any) { - if (!this.heroForm) { return; } - const form = this.heroForm.form; - - for (const field in this.formErrors) { - // clear previous error message (if any) - this.formErrors[field] = ''; - const control = form.get(field); - - if (control && control.dirty && !control.valid) { - const messages = this.validationMessages[field]; - for (const key in control.errors) { - this.formErrors[field] += messages[key] + ' '; - } - } - } - } - - formErrors = { - 'name': '', - 'power': '' - }; - // #enddocregion handler - - // #docregion messages - validationMessages = { - 'name': { - 'required': 'Name is required.', - 'minlength': 'Name must be at least 4 characters long.', - 'maxlength': 'Name cannot be more than 24 characters long.', - 'forbiddenName': 'Someone named "Bob" cannot be a hero.' - }, - 'power': { - 'required': 'Power is required.' - } - }; - // #enddocregion messages -} -// #enddocregion diff --git a/aio/content/examples/form-validation/src/forms.css b/aio/content/examples/form-validation/src/forms.css index 67ad13037b..1d388f656d 100644 --- a/aio/content/examples/form-validation/src/forms.css +++ b/aio/content/examples/form-validation/src/forms.css @@ -1,3 +1,4 @@ + .ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */ } diff --git a/aio/content/guide/form-validation.md b/aio/content/guide/form-validation.md index 140fed61f5..90c07ec89b 100644 --- a/aio/content/guide/form-validation.md +++ b/aio/content/guide/form-validation.md @@ -10,15 +10,16 @@ Improve overall data quality by validating user input for accuracy and completen 我们可以通过验证用户输入的准确性和完整性,来增强整体数据质量。 This page shows how to validate user input in the UI and display useful validation messages -using first the Template Driven Forms and then the Reactive Forms approach. +using both reactive and template-driven forms. It assumes some basic knowledge of the two +forms modules. 在本烹饪书中,我们展示在界面中如何验证用户输入,并显示有用的验证信息,先使用模板驱动表单方式,再使用响应式表单方式。 <div class="l-sub-section"> -Read more about these choices in the [Forms](guide/forms) -and the [Reactive Forms](guide/reactive-forms) guides. +If you're new to forms, start by reviewing the [Forms](guide/forms) and +[Reactive Forms](guide/reactive-forms) guides. 参见[表单](guide/forms)和[响应式表单](guide/reactive-forms)了解关于这些选择的更多知识。 @@ -26,102 +27,58 @@ and the [Reactive Forms](guide/reactive-forms) guides. </div> -{@a live-example} +## Template-driven validation +## 模板驱动验证 -**Try the live example to see and download the full cookbook source code.** +To add validation to a template-driven form, you add the same validation attributes as you +would with [native HTML form validation](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation). +Angular uses directives to match these attributes with validator functions in the framework. -**查看在线例子,并下载整个烹饪书的源代码** +为了往模板驱动表单中添加验证机制,我们要添加一些验证属性,就像[原生的HTML表单验证器](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation)。 +Angular 会用指令来匹配这些具有验证功能的指令。 +Every time the value of a form control changes, Angular runs validation and generates +either a list of validation errors, which results in an INVALID status, or null, which results in a VALID status. -<live-example name="form-validation" embedded=true img="guide/form-validation/plunker.png"> - 在线例子 -</live-example> +每当表单控件中的值发生变化时,Angular 就会进行验证,并生成一个验证错误的列表(对应着INVALID状态)或者null(对应着VALID状态)。 -## Simple Template Driven Forms +You can then inspect the control's state by exporting `ngModel` to a local template variable. +The following example exports `NgModel` into a variable called `name`: -## 简单的模板驱动表单 +我们可以通过把`ngModel`导出成局部模板变量来查看该控件的状态。 +比如下面这个例子就把`NgModel`导出成了一个名叫`name`的变量: -In the Template Driven approach, you arrange -[form elements](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms_in_HTML) in the component's template. - -在模板驱动表单方法中,你在组件的模板中组织[表单元素](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms_in_HTML)。 - -You add Angular form directives (mostly directives beginning `ng...`) to help -Angular construct a corresponding internal control model that implements form functionality. -In Template Driven forms, the control model is _implicit_ in the template. - -你可以添加Angular表单指令(通常为以`ng`开头的指令)来帮助Angular构建对应的内部控制模型,以实现表单功能。 -控制模型在模板中是**隐式**的。 - -To validate user input, you add [HTML validation attributes](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) -to the elements. Angular interprets those as well, adding validator functions to the control model. - -要验证用户输入,你添加[HTML验证属性](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation)到元素中。 -Angular拦截这些元素,添加验证器函数到控制模型中。 - -Angular exposes information about the state of the controls including -whether the user has "touched" the control or made changes and if the control values are valid. - -Angular暴露关于控制状态的信息,包括用户是否已经“触摸“了控制器,或者用户已经作了更新和控制器的值是否还有效。 - -In this first template validation example, -notice the HTML that reads the control state and updates the display appropriately. -Here's an excerpt from the template HTML for a single input control bound to the hero name: - -在第一个模板验证例子中,我们添加了更多HTML,来读取控制器状态并适当更新显示。 -下面是模板HTML中提取的,一个绑定到英雄名字的输入框控制器: - -<code-example path="form-validation/src/app/template/hero-form-template1.component.html" region="name-with-error-msg" title="template/hero-form-template1.component.html (Hero name)" linenums="false"> +<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-with-error-msg" title="template/hero-form-template.component.html (name)" linenums="false"> </code-example> - Note the following: 请注意以下几点: -* The `<input>` element carries the HTML validation attributes: `required`, `minlength`, and `maxlength`. +* The `<input>` element carries the HTML validation attributes: `required` and `minlength`. It +also carries a custom validator directive, `forbiddenName`. For more +information, see [Custom validators](guide/form-validation#custom-validators) section. - `<input>`元素带有一些HTML验证属性:`required`、`minlength` 和 `maxlength`。 + `<input>`元素带有一些HTML验证属性:`required`、`minlength` 和 `maxlength`。它还带有一个自定义的验证器指令`forbiddenName`。要了解更多信息,参见[自定义验证器](guide/form-validation#custom-validators)一节。 -* The `name` attribute of the input is set to `"name"` so Angular can track this input element and associate it -with an Angular form control called `name` in its internal control model. +* `#name="ngModel"` exports `NgModel` into a local variable callled `name`. `NgModel` mirrors many of the properties of its underlying +`FormControl` instance, so you can use this in the template to check for control states such as `valid` and `dirty`. For a full list of control properties, see the [AbstractControl](api/forms/AbstractControl) +API reference. - 我们把输入框的`name`属性设置为`"name"`,这样Angular可以跟踪这个输入元素,并将其内部控制器模型的一个名为`name`的Angular表单控制关联起来。 - - -* The `[(ngModel)]` directive allows two-way data binding between the input box to the `hero.name` property. - - 我们使用`[(ngModel)]`指令,将输入框双向数据绑定到`hero.name`属性。 - -* The template variable (`#name`) has the value `"ngModel"` (always `ngModel`). -This gives you a reference to the Angular `NgModel` directive -associated with this control that you can use _in the template_ -to check for control states such as `valid` and `dirty`. - - 我们将模板变量(`#name`)赋值为`"ngModel"` (总是 `ngModel`)。 - 它为我们提供了与这个控制器关联的Angular `NgModel`指令的引用,我们在模板中使用它,以检查控制器状态,比如`valid`和`dirty`。 + `#name="ngModel"`把`NgModel`导出成了一个名叫`name`的局部变量。`NgModel`把自己控制的`FormControl`实例的属性映射出去,让我们能在模板中检查控件的状态,比如`valid`和`dirty`。要了解完整的控件属性,参见 API 参考手册中的[AbstractControl](api/forms/AbstractControl)。 * The `*ngIf` on the `<div>` element reveals a set of nested message `divs` -but only if there are `name` errors and -the control is either `dirty` or `touched`. +but only if the `name` is invalid and the control is either `dirty` or `touched`. `<div>`元素的`*ngIf`揭露了一套嵌套消息`divs`,但是只在有“name”错误和控制器为`dirty`或者`touched`。 * Each nested `<div>` can present a custom message for one of the possible validation errors. -There are messages for `required`, `minlength`, and `maxlength`. - - 每个嵌套的`<div>`为其中一个可能出现的验证错误显示一条自定义消息。我们已经为`required`、`minlength`、和 `maxlength`准备了消息。 - -The full template repeats this kind of layout for each data entry control on the form. - -整个模板为表单上的每种数据输入控制器重复这种布局。 - - -{@a why-check} - +There are messages for `required`, `minlength`, and `forbiddenName`. + + 每个嵌套的`<div>`为其中一个可能出现的验证错误显示一条自定义消息。比如 `required`、`minlength`和 `forbiddenName`。 <div class="l-sub-section"> @@ -131,864 +88,240 @@ The full template repeats this kind of layout for each data entry control on the #### 为何检查**dirty**和**touched**? -The app shouldn't show errors for a new hero before the user has had a chance to edit the value. -The checks for `dirty` and `touched` prevent premature display of errors. -当用户创建一个新英雄时,在还没有机会输入之前,我们不应该显示任何错误。 -检查`dirty`和`touched`防止了这种过早的错误显示。 +You may not want your application to display errors before the user has a chance to edit the form. +The checks for `dirty` and `touched` prevent errors from showing until the user +does one of two things: changes the value, +turning the control dirty; or blurs the form control element, setting the control to touched. -Learn about `dirty` and `touched` in the [Forms](guide/forms) guide. - -参见[表单](guide/forms)章,学习关于`dirty`和`touched`的知识。 +我们肯定不希望应用在用户还没有编辑过表单的时候就给他们显示错误提示。 +对`dirty`和`touched`的检查可以避免这种问题。改变控件的值会改变控件的`dirty`(脏)状态,而当控件失去焦点时,就会改变控件的`touched`(碰过)状态。 </div> +## Reactive form validation +## 响应式表单的验证 -The component class manages the hero model used in the data binding -as well as other code to support the view. +In a reactive form, the source of truth is the component class. Instead of adding validators through attributes in the template, you add validator functions directly to the form control model in the component class. Angular then calls these functions whenever the value of the control changes. -组件类管理用于数据绑定的英雄模型,它还有其他支持视图的代码。 +在响应式表单中,真正的源码都在组件类中。我们不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(`FormControl`)。然后,一旦控件发生了变化,Angular 就会调用这些函数。 +### Validator functions -<code-example path="form-validation/src/app/template/hero-form-template1.component.ts" region="class" title="template/hero-form-template1.component.ts (class)"> +### 验证器函数 -</code-example> +There are two types of validator functions: sync validators and async validators. +有两种验证器函数:同步验证器和异步验证器。 +* **Sync validators**: functions that take a control instance and immediately return either a set of validation errors or `null`. You can pass these in as the second argument when you instantiate a `FormControl`. -Use this Template Driven validation technique when working with static forms with simple, standard validation rules. + **同步验证器**函数接受一个控件实例,然后返回一组验证错误或`null`。我们可以在实例化一个`FormControl`时把它作为构造函数的第二个参数传进去。 -在处理简单的、拥有标准验证规则的静态表单时,使用这种模板驱动验证方法。 +* **Async validators**: functions that take a control instance and return a Promise +or Observable that later emits a set of validation errors or `null`. You can +pass these in as the third argument when you instantiate a `FormControl`. -Here are the complete files for the first version of `HeroFormTemplateCompononent` in the Template Driven approach: + **异步验证器**函数接受一个控件实例,并返回一个承诺(Promise)或可观察对象(Observable),它们稍后会发出一组验证错误或者`null`。我们可以在实例化一个`FormControl`时把它作为构造函数的第三个参数传进去。 -下面是第一个版本的使用模板驱动方法的`HeroFormTemplateComponent`: +Note: for performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set. +注意:出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。 -<code-tabs> +### Built-in validators - <code-pane title="template/hero-form-template1.component.html" path="form-validation/src/app/template/hero-form-template1.component.html"> +### 内置验证器 - </code-pane> +You can choose to [write your own validator functions](guide/form-validation#custom-validators), or you can use some of +Angular's built-in validators. - <code-pane title="template/hero-form-template1.component.ts" path="form-validation/src/app/template/hero-form-template1.component.ts"> +我们可以[写自己的验证器](guide/form-validation#custom-validators),也可以使用一些 Angular 内置的验证器。 - </code-pane> +The same built-in validators that are available as attributes in template-driven forms, such as `required` and `minlength`, are all available to use as functions from the `Validators` class. For a full list of built-in validators, see the [Validators](api/forms/Validators) API reference. -</code-tabs> - - - - - -## Template Driven Forms with validation messages in code - -## 验证消息在代码中的模板驱动表单 - -While the layout is straightforward, -there are obvious shortcomings with the way it's handling validation messages: - -虽然布局很直观,但是我们处理验证消息的方法有明显的缺陷: - -* It takes a lot of HTML to represent all possible error conditions. -This gets out of hand when there are many controls and many validation rules. - - 它使用了很多HTML来表现所有可能出现的错误情况。 - 如果有太多控制器和太多验证规则,我们就失去了控制。 - -* There's a lot of JavaScript logic in theHTML. - - 我们不喜欢在HTML里面插入这么多JavaScript。 - -* The messages are static strings, hard-coded into the template. -It's easier to maintain _dynamic_ messages in the component class. - - 这些消息是静态的字符串,被硬编码到模板中。把这些动态消息放在代码中会更易于维护。 - -In this example, you can move the logic and the messages into the component with a few changes to -the template and component. - -只需要对模板和组件做出一些修改,我们可以将逻辑和消息移到组件中。 - -Here's the hero name again, excerpted from the revised template (template 2), next to the original version: - -下面也是关于英雄名字的控制器,从修改后的模板(“Template 2”)中抽取出来,与原来的版本相比: - -<code-tabs> - - <code-pane title="hero-form-template2.component.html (name #2)" path="form-validation/src/app/template/hero-form-template2.component.html" region="name-with-error-msg"> - - </code-pane> - - <code-pane title="hero-form-template1.component.html (name #1)" path="form-validation/src/app/template/hero-form-template1.component.html" region="name-with-error-msg"> - - </code-pane> - -</code-tabs> - - - -The `<input>` element HTML is almost the same. There are noteworthy differences: - -`<input>`元素的HTML几乎一样。但是下列有值得注意的区别: - -* The hard-code error message `<divs>` are gone. - - 硬编码的错误消息`<div>`消失了。 - -* There's a new attribute, `forbiddenName`, that is actually a custom validation directive. -It invalidates the control if the user enters "bob" in the name `<input>`([try it](guide/form-validation#live-example)). -See the [custom validation](guide/form-validation#custom-validation) section later in this page for more information -on custom validation directives. - - 添加了一个新属性`forbiddenName`,它实际上是一个自定义验证指令。 - 如果用户名`<input>`中的任何地方输入“bob”,该指令变将控制器标记为无效([试试](guide/form-validation#live-example))。 - 我们将在本烹饪书稍后的地方介绍[自定义验证指令](guide/form-validation#custom-validation)。 - -* The `#name` template variable is gone because the app no longer refers to the Angular control for this element. - -模板变量`#name`消失了,因为我们不再需要为这个元素引用Angular控制器。 - -* Binding to the new `formErrors.name` property is sufficient to display all name validation error messages. - - 绑定到新的`formErrors.name`属性,就可以处理所有名字验证错误信息了。 - -{@a component-class} - -### Component class - -### 组件类 - -The original component code for Template 1 stayed the same; however, -Template 2 requires some changes in the component. This section covers the code -necessary in Template 2's component class to acquire the Angular -form control and compose error messages. - -原组件代码的模板一没变化,只是模板二发生了变化。本节包括模板二的组件类,以获取Angular表单控制器和撰写错误信息。 - -The first step is to acquire the form control that Angular created from the template by querying for it. - -第一步是获取Angular通过查询模板而生成的表单控制器。 - -Look back at the top of the component template at the -`#heroForm` template variable in the `<form>` element: - -回头看组件模板顶部,我们在`<form>`元素中设置`#heroForm`模板变量: - - -<code-example path="form-validation/src/app/template/hero-form-template1.component.html" region="form-tag" title="template/hero-form-template1.component.html (form tag)" linenums="false"> - -</code-example> - - - -The `heroForm` variable is a reference to the control model that Angular derived from the template. -Tell Angular to inject that model into the component class's `currentForm` property using a `@ViewChild` query: - -`heroFrom`变量是Angular从模板衍生出来的控制模型的引用。 -我们利用`@ViewChild`来告诉Angular注入这个模型到组件类的`currentForm`属性: - - -<code-example path="form-validation/src/app/template/hero-form-template2.component.ts" region="view-child" title="template/hero-form-template2.component.ts (heroForm)" linenums="false"> - -</code-example> - - - -Some observations: - -一些细节: - -* Angular `@ViewChild` queries for a template variable when you pass it -the name of that variable as a string (`'heroForm'` in this case). - - Angular的`@ViewChild`使用传入的模板变量的字符串名字(这里是`'heroForm'`),来查询对应的模板变量。 - -* The `heroForm` object changes several times during the life of the component, most notably when you add a new hero. -Periodically inspecting it reveals these changes. - - `heroForm`对象在组件的生命周期内变化了好几次,最值得注意的是当我们添加一个新英雄时的变化。我们必须定期重新检测它。 - -* Angular calls the `ngAfterViewChecked()` [lifecycle hook method](guide/lifecycle-hooks#afterview) -when anything changes in the view. -That's the right time to see if there's a new `heroForm` object. - - 当视图有任何变化时,Angular调用`ngAfterViewChecked`[生命周期钩子方法](guide/lifecycle-hooks#afterview)。这是查看是否有新`heroForm`对象的最佳时机。 - -* When there _is_ a new `heroForm` model, `formChanged()` subscribes to its `valueChanges` _Observable_ property. -The `onValueChanged` handler looks for validation errors after every keystroke. - - 当出现新`heroForm`模型时,我们订阅它的`valueChanged`**可观察**属性。 - `onValueChanged`处理器在每次用户键入后查找验证错误。 - - -<code-example path="form-validation/src/app/template/hero-form-template2.component.ts" region="handler" title="template/hero-form-template2.component.ts (handler)" linenums="false"> - -</code-example> - - - -The `onValueChanged` handler interprets user data entry. -The `data` object passed into the handler contains the current element values. -The handler ignores them. Instead, it iterates over the fields of the component's `formErrors` object. - -`onValueChanged`处理器拦截用户数据输入。 -包含当前元素值得`data`对象被传入处理器。 -处理器忽略它们。相反,它迭代组件的`formErrors`对象。 - -The `formErrors` is a dictionary of the hero fields that have validation rules and their current error messages. -Only two hero properties have validation rules, `name` and `power`. -The messages are empty strings when the hero data are valid. - -`formErrors`是一个词典,包含了拥有验证规则和当前错误消息的英雄控件。 - 只有两个英雄属性有验证规则,`name`和`power`。 - 当英雄数据有效时,这些消息的值为空字符串。 - -For each field, the `onValueChanged` handler does the following: - -对于每个字段,这个`onValueChanged`处理器会做这些: - - * Clears the prior error message, if any. - - 清除以前的错误信息(如果有的话) - - * Acquires the field's corresponding Angular form control. - - 获取控件对应的Angular表单控制器 - - * If such a control exists _and_ it's been changed ("dirty") - _and_ it's invalid, the handler composes a consolidated error message for all of the control's errors. - - 如果这样的控制器存在,并且它被更新过(“dirty”),**并且**它无效,处理器就会为所有控制器的错误合成一条错误消息。 - -Next, the component needs some error messages—a set for each validated property with -one message per validation rule: - -很显然,我们需要一些错误消息,每个验证的属性都需要一套,每个验证规则需要一条消息: - - -<code-example path="form-validation/src/app/template/hero-form-template2.component.ts" region="messages" title="template/hero-form-template2.component.ts (messages)" linenums="false"> - -</code-example> - - - -Now every time the user makes a change, the `onValueChanged` handler checks for validation errors and produces messages accordingly. - -现在,每次用户作出变化时,`onValueChanged`处理器检查验证错误并按情况发出错误消息。 - - -{@a improvement} - - -### The benefits of messages in code - -### 在代码中写消息的优点 - -Clearly the template got substantially smaller while the component code got substantially larger. -It's not easy to see the benefit when there are just three fields and only two of them have validation rules. - -很显然,模板变得小多了,组件代码变得大多了。当只有三个控件并且其中只有两个有验证规则时,我们很难看出好处。 - -Consider what happens as the number of validated -fields and rules increases. -In general, HTML is harder to read and maintain than code. -The initial template was already large and threatening to get rapidly worse -with the addition of more validation message `<div>` elements. - -假设增加需要验证的控件和规则后会怎么样。 -通常,HTML比代码更难阅读和维护。 -初始的模板已经很大了,如果我们添加更多验证消息`<div>`,它会迅速变得更大。 - -After moving the validation messaging to the component, -the template grows more slowly and proportionally. -Each field has approximately the same number of lines no matter its number of validation rules. -The component also grows proportionally, at the rate of one line per validated field -and one line per validation message. - -将验证消息移到组件后,模板的增长变得更加缓慢,幅度也小一些。 -不管有多少个验证规则,每个控件的行数是差不多的。 -组件也按比例增长,每增加一个控件增加一行,每个验证消息一行。 - -Now that the messages are in code, you have more flexibility and can compose messages more efficiently. -You can refactor the messages out of the component, perhaps to a service class that retrieves them from the server. -In short, there are more opportunities to improve message handling now that text and logic have moved from template to code. - -现在消息在代码中,我们有更多的灵活度。我们更加智能的撰写消息。 -我们可以将消息重构出组件,比如到一个服务类,从服务端获取消息。 -简而言之,有很多机会增强消息处理,因为文本和逻辑都已经从模板移到代码中。 - - -{@a formmodule} - - -### _FormModule_ and Template Driven forms - -### _FormModule_ 和模板驱动表单 - -Angular has two different forms modules—`FormsModule` and -`ReactiveFormsModule`—that correspond with the -two approaches to form development. Both modules come -from the same `@angular/forms` library package. - -Angular有两种不同的表单模块 - `FormsModule`和`ReactiveFormsModule` - 它们与表单开发的两种方法对应。 - 两种模块都从同一个`@angular/forms`库。 - -You've been reviewing the Template Driven approach which requires the `FormsModule`. -Here's how you imported it in the `HeroFormTemplateModule`. - -我们一直在探讨**模板驱动**方法,它需要`FormsModule`。下面是如何在`HeroFormTemplateModule`中导入它: - - -<code-example path="form-validation/src/app/template/hero-form-template.module.ts" title="template/hero-form-template.module.ts" linenums="false"> - -</code-example> - - - -<div class="l-sub-section"> - - - -This guide hasn't talked about the `SharedModule` or its `SubmittedComponent` which appears at the bottom of every -form template in this cookbook. - -我们还没有讲`SharedModule`或者它的`SubmittedComponent`,它们出现在本烹饪书的每一个表单模板中。 - -They're not germane to the validation story. Look at the [live example](guide/form-validation#live-example) if you're interested. - -它们与表单验证没有紧密的关系。如果你感兴趣,参见[在线例子](guide/form-validation#live-example)。 - - -</div> - - - - -{@a reactive} - - -## Reactive Forms with validation in code - -## 在代码中验证响应式表单 - -In the Template Driven approach, you mark up the template with form elements, validation attributes, -and `ng...` directives from the Angular `FormsModule`. -At runtime, Angular interprets the template and derives its _form control model_. - -在模板驱动方法中,你在模板中标出表单元素、验证属性和Angular`FormsModule`中的`ng...`指令。 -在运行时间,Angular解释模板并从**表单控制器模型**衍生它。 - -**Reactive Forms** takes a different approach. -You create the form control model in code. You write the template with form elements -and `form...` directives from the Angular `ReactiveFormsModule`. -At runtime, Angular binds the template elements to your control model based on your instructions. - -**响应式表单**采用不同的方法。 -你在代码中创建表单控制器模型,并用表单元素和来自Angular `ReactiveFormsModule`中的`form...`指令来编写模板。 -在运行时间,Angular根据你的指示绑定模板元素到你的控制器模型。 - -This approach requires a bit more effort. *You have to write the control model and manage it*. - -这个方法需要做一些额外的工作。*你必须编写并管理控制器模型**。 - -This allows you to do the following: - -这可以让你: - -* Add, change, and remove validation functions on the fly. - - 随时添加、修改和删除验证函数 - -* Manipulate the control model dynamically from within the component. - - 在组件内动态操纵控制器模型 - -* [Test](guide/form-validation#testing-considerations) validation and control logic with isolated unit tests. - - 使用孤立单元测试来[测试](guide/form-validation#testing-considerations)验证和控制器逻辑 - -The following sample re-writes the hero form in Reactive Forms style. - -第三个烹饪书例子用**响应式表单**风格重新编写英雄表格。 - - -{@a reactive-forms-module} - - -### Switch to the _ReactiveFormsModule_ - -### 切换到_ReactiveFormsModule_ - -The Reactive Forms classes and directives come from the Angular `ReactiveFormsModule`, not the `FormsModule`. -The application module for the Reactive Forms feature in this sample looks like this: - -响应式表单类和指令来自于Angular的`ReactiveFormsModule`,不是`FormsModule`。 -本例中,应用模块的“响应式表单”特性是这样的: - - -<code-example path="form-validation/src/app/reactive/hero-form-reactive.module.ts" title="src/app/reactive/hero-form-reactive.module.ts" linenums="false"> - -</code-example> - - - -The Reactive Forms feature module and component are in the `src/app/reactive` folder. -Focus on the `HeroFormReactiveComponent` there, starting with its template. - -“响应式表单”特性模块和组件在`app/reactive`目录。 -让我们关注那里的`HeroFormReactiveComponent`,先看它的模板。 - - -{@a reactive-component-template} - - -### Component template - -### 组件模板 - -Begin by changing the `<form>` tag so that it binds the Angular `formGroup` directive in the template -to the `heroForm` property in the component class. -The `heroForm` is the control model that the component class builds and maintains. - -我们先修改`<form>`标签,让Angular的`formGroup`指令绑定到组件类的`heroForm`属性。 -`heroForm`是组件类创建和维护的控制器模型。 - - -<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="form-tag" title="form-validation/src/app/reactive/hero-form-reactive.component.html" linenums="false"> - -</code-example> - - - -Next, modify the template HTML elements to match the Reactive Forms style. -Here is the "name" portion of the template again, revised for Reactive Forms and compared with the Template Driven version: - -接下来,我们修改模板HTML元素,来匹配**响应式表单**样式。 -下面又是“name”部分的模板,响应式表单修改版本和模板驱动版本的比较: - - -<code-tabs> - - <code-pane title="hero-form-reactive.component.html (name #3)" path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="name-with-error-msg"> - - </code-pane> - - <code-pane title="hero-form-template1.component.html (name #2)" path="form-validation/src/app/template/hero-form-template2.component.html" region="name-with-error-msg"> - - </code-pane> - -</code-tabs> - - - -Key changes are: - -关键变化: - -* The validation attributes are gone (except `required`) because -validating happens in code. - - 验证属性没有了(除了`required`),因为我们将在代码中验证。 - -* `required` remains, not for validation purposes (that's in the code), -but rather for css styling and accessibility. - - 保留`required`,不是为了验证的目的(验证在代码中),而是为了CSS样式和可访问性。 - - -<div class="l-sub-section"> - -Currently, Reactive Forms doesn't add the `required` or `aria-required` -HTML validation attribute to the DOM element -when the control has the `required` validator function. - -未来版本的响应式表单将会在控制器有`required`验证器函数时,添加`required` HTML验证属性到DOM元素(也可能添加`aria-required`属性)。 - -Until then, apply the `required` attribute _and_ add the `Validator.required` function -to the control model, as you'll see below. - -在此之前,添加`required`属性**以及**添加`Validator.required`函数到控制器模型,像我们下面这样做: - - -</div> - - - -* The `formControlName` replaces the `name` attribute; it serves the same -purpose of correlating the input with the Angular form control. - - `formControlName`替换了`name`属性;它起到了关联输入框和Angular表单控制器的同样作用。 - -* The two-way `[(ngModel)]` binding is gone. -The reactive approach does not use data binding to move data into and out of the form controls. -That's all in code. - - 双向`[(ngModel)]`绑定消失了。 -响应式表单方法不使用数据绑定从表单控制器移入和移出数据。我们在代码中做这些。 +模板驱动表单中可用的那些属性型验证器(如`required`、`minlength`等)对应于`Validators`类中的同名函数。要想查看内置验证器的全列表,参见 API 参考手册中的[验证器](api/forms/Validators)部分。 +To update the hero form to be a reactive form, you can use some of the same +built-in validators—this time, in function form. See below: +要想把这个英雄表单改造成一个响应式表单,我们还是用那些内置验证器,但这次改为用它们的函数形态。 {@a reactive-component-class} - -### Component class - -### 组件类 - -The component class is now responsible for defining and managing the form control model. - -组件类现在负责定义和管理表单控制器模型。 - -Angular no longer derives the control model from the template so you can no longer query for it. -You can create the Angular form control model explicitly with the help of the `FormBuilder`class. - -Angular不再从模板衍生控制器模型,所以我们不能再查询它。 -我们利用`FormBuilder`来显式创建Angular表单控制器模型。 - -Here's the section of code devoted to that process, paired with the Template Driven code it replaces: - -下面是负责该进程的代码部分,与被它取代的模板驱动代码相比: - -<code-tabs> - - <code-pane title="reactive/hero-form-reactive.component.ts (FormBuilder)" path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="form-builder"> - - </code-pane> - - <code-pane title="template/hero-form-template2.component.ts (ViewChild)" path="form-validation/src/app/template/hero-form-template2.component.ts" region="view-child"> - - </code-pane> - -</code-tabs> - - - -* Inject `FormBuilder` in a constructor. - - 我们注入`FormBuilder`到构造函数中。 - -* Call a `buildForm` method in the `ngOnInit` [lifecycle hook method](guide/lifecycle-hooks#hooks-overview) -because that's when you'll have the hero data. Call it again in the `addHero` method. - - 我们在`ngOnInit`[生命周期钩子方法](guide/lifecycle-hooks#hooks-overview)中调用`buildForm`方法, - 因为这正是我们拥有英雄数据的时刻。我们将在`addHero`方法中再次调用它。 - - -<div class="l-sub-section"> - - - -A real app would retrieve the hero asynchronously from a data service, a task best performed in the `ngOnInit` hook. - -真实的应用很可能从数据服务异步获取英雄,这个任务最好在`ngOnInit`生命周期钩子中进行。 - -</div> - - - -* The `buildForm` method uses the `FormBuilder`, `fb`, to declare the form control model. -Then it attaches the same `onValueChanged` handler (there's a one line difference) -to the form's `valueChanges` event and calls it immediately -to set error messages for the new control model. - - `buildForm`方法使用`FormBuilder`(`fb`)来声明表单控制器模型。 -然后它将相同的`onValueChanged`(有一行代码不一样)处理器附加到表单的`valueChanged`事件, -并立刻为新的控制器模型设置错误消息。 - -## Built-in validators - -## 内置验证器 - -Angular forms include a number of built-in validator functions, which are functions -that help you check common user input in forms. In addition to the built-in -validators covered here of `minlength`, `maxlength`, -and `required`, there are others such as `email` and `pattern` -for Reactive Forms. -For a full list of built-in validators, -see the [Validators](api/forms/Validators) API reference. - - -#### _FormBuilder_ declaration - -#### _FormBuilder_声明 - -The `FormBuilder` declaration object specifies the three controls of the sample's hero form. - -`FormBuilder`声明对象指定了本例英雄表单的三个控制器。 - -Each control spec is a control name with an array value. -The first array element is the current value of the corresponding hero field. -The optional second value is a validator function or an array of validator functions. - -每个控制器的设置都是控制器名字和数组值。 -第一个数组元素是英雄控件对应的当前值。 -第二个值(可选)是验证器函数或者验证器函数数组。 - -Most of the validator functions are stock validators provided by Angular as static methods of the `Validators` class. -Angular has stock validators that correspond to the standard HTML validation attributes. - -大多数验证器函数是Angular以`Validators`类的静态方法的形式提供的原装验证器。 -Angular有一些原装验证器,与标准HTML验证属性一一对应。 - -The `forbiddenName` validator on the `"name"` control is a custom validator, -discussed in a separate [section below](guide/form-validation#custom-validation). - -`"name"`控制器上的`forbiddenNames`验证器是自定义验证器,在下面单独的[小结](guide/form-validation#custom-validation)有所讨论。 - - -<div class="l-sub-section"> - -Learn more about `FormBuilder` in the [Introduction to FormBuilder](guide/reactive-forms#formbuilder) section of Reactive Forms guide. - -到[响应式表单]的[FormBuilder介绍](guide/reactive-forms#formbuilder)部分,学习更多关于`FormBuilder`的知识。 - - -</div> - -#### Committing hero value changes - -#### 提交英雄值的更新 - -In two-way data binding, the user's changes flow automatically from the controls back to the data model properties. -A Reactive Forms component should not use data binding to -automatically update data model properties. -The developer decides _when and how_ to update the data model from control values. - -在双向数据绑定时,用户的修改自动从控制器流向数据模型属性。 -响应式表单不适用数据绑定来更新数据模型属性。 -开发者决定**何时**与**如何**从控制器的值更新数据模型。 - -This sample updates the model twice: - -本例更新模型两次: - -1. When the user submits the form. - - 当用户提交标单时 - -1. When the user adds a new hero. - - 当用户添加新英雄时 - -The `onSubmit()` method simply replaces the `hero` object with the combined values of the form: - -`onSubmit()`方法直接使用表单的值得合集来替换`hero`对象: - -<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="on-submit" title="form-validation/src/app/reactive/hero-form-reactive.component.ts" linenums="false"> +<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="form-group" title="reactive/hero-form-reactive.component.ts (validator functions)" linenums="false"> </code-example> +Note that: -The `addHero()` method discards pending changes and creates a brand new `hero` model object. +注意 -`addHero()`方法放弃未处理的变化,并创建一个崭新的`hero`模型对象。 +* The name control sets up two built-in validators—`Validators.required` and `Validators.minLength(4)`—and one custom validator, `forbiddenNameValidator`. For more details see the [Custom validators](guide/form-validation#custom-validators) section in this guide. + + `name`控件设置了两个内置验证器:`Validators.required` 和 `Validators.minLength(4)`。要了解更多信息,参见本章的[自定义验证器](guide/form-validation#custom-validators)一节。 + +* As these validators are all sync validators, you pass them in as the second argument. + + 由于这些验证器都是同步验证器,因此我们要把它们作为第二个参数传进去。 + +* Support multiple validators by passing the functions in as an array. + + 可以通过把这些函数放进一个数组后传进去,可以支持多重验证器。 + +* This example adds a few getter methods. In a reactive form, you can always access any form control through the `get` method on its parent group, but sometimes it's useful to define getters as shorthands +for the template. + + 这个例子添加了一些getter方法。在响应式表单中,我们通常会通过它所属的控件组(FormGroup)的`get`方法来访问表单控件,但有时候为模板定义一些getter作为简短形式。 -<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="add-hero" title="form-validation/src/app/reactive/hero-form-reactive.component.ts" linenums="false"> +If you look at the template for the name input again, it is fairly similar to the template-driven example. + +如果我们到模板中找到name输入框,就会发现它和模板驱动的例子很相似。 + +<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="name-with-error-msg" title="reactive/hero-form-reactive.component.html (name with error msg)" linenums="false"> </code-example> +Key takeaways: +关键改动是: + + * The form no longer exports any directives, and instead uses the `name` getter defined in + the component class. + + 该表单不再导出任何指令,而是使用组件类中定义的`name`读取器。 + + * The `required` attribute is still present. While it's not necessary for validation purposes, + you may want to keep it in your template for CSS styling or accessibility reasons. -Then it calls `buildForm()` again which replaces the previous `heroForm` control model with a new one. -The `<form>` tag's `[formGroup]` binding refreshes the page with the new control model. + `required`属性仍然存在,虽然验证不再需要它,但我们仍然在模板中保留它,以支持 CSS 样式或可访问性。 -然后它再次调用`buildForm`,用一个新对象替换了之前的`heroForm`控制器模型。 -`<form>`标签的`[formGroup]`绑定使用这个新的控制器模型更新页面。 +## Custom validators -Here's the complete reactive component file, compared to the two Template Driven component files. +## 自定义验证器 -下面是完整的响应式表单的组件文件,与两个模板驱动组件文件对比: +Since the built-in validators won't always match the exact use case of your application, sometimes you'll want to create a custom validator. -<code-tabs> +由于内置验证器无法适用于所有应用场景,有时候我们还是得创建自定义验证器。 - <code-pane title="reactive/hero-form-reactive.component.ts (#3)" path="form-validation/src/app/reactive/hero-form-reactive.component.ts"> - - </code-pane> - - <code-pane title="template/hero-form-template2.component.ts (#2)" path="form-validation/src/app/template/hero-form-template2.component.ts"> - - </code-pane> - - <code-pane title="template/hero-form-template1.component.ts (#1)" path="form-validation/src/app/template/hero-form-template1.component.ts"> - - </code-pane> - -</code-tabs> - - - -<div class="l-sub-section"> - -Run the [live example](guide/form-validation#live-example) to see how the reactive form behaves, -and to compare all of the files in this sample. -运行[在线例子](guide/form-validation#live-example),查看响应式表单是的行为,并与本章中的例子文件作比较。 - -</div> - -## Custom validation - -## 自定义验证 - -This cookbook sample has a custom `forbiddenNameValidator()` function that's applied to both the -Template Driven and the reactive form controls. It's in the `src/app/shared` folder -and declared in the `SharedModule`. - -本烹饪书例子有一个自定义`forbiddenNameValidator`函数,在模板驱动和响应式表单中都有使用。 -它在`app/shared`目录,在`SharedModule`中被声明。 - -Here's the `forbiddenNameValidator()` function: - -下面是`forbiddenNameValidator`函数: +Consider the `forbiddenNameValidator` function from previous +[examples](guide/form-validation#reactive-component-class) in +this guide. Here's what the definition of that function looks like: +考虑前面的[例子](guide/form-validation#reactive-component-class)中的`forbiddenNameValidator`函数。该函数的定义看起来是这样的: <code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="custom-validator" title="shared/forbidden-name.directive.ts (forbiddenNameValidator)" linenums="false"> </code-example> +The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name and returns a validator function. +这个函数实际上是一个工厂,它接受一个用来检测指定名字是否已被禁用的正则表达式,并返回一个验证器函数。 -The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name -and returns a validator function. - -该函数其实是一个工厂函数,接受一个正则表达式,用来检测**指定**的禁止的名字,并返回验证器函数。 - -In this sample, the forbidden name is "bob"; -the validator rejects any hero name containing "bob". +In this sample, the forbidden name is "bob", so the validator will reject any hero name containing "bob". Elsewhere it could reject "alice" or any name that the configuring regular expression matches. 在本例中,禁止的名字是“bob”; -验证器拒绝任何带有“bob”的英雄名字。 +验证器会拒绝任何带有“bob”的英雄名字。 在其他地方,只要配置的正则表达式可以匹配上,它可能拒绝“alice”或者任何其他名字。 The `forbiddenNameValidator` factory returns the configured validator function. That function takes an Angular control object and returns _either_ null if the control value is valid _or_ a validation error object. The validation error object typically has a property whose name is the validation key, `'forbiddenName'`, -and whose value is an arbitrary dictionary of values that you could insert into an error message (`{name}`). +and whose value is an arbitrary dictionary of values that you could insert into an error message, `{name}`. `forbiddenNameValidator`工厂函数返回配置好的验证器函数。 该函数接受一个Angular控制器对象,并在控制器值有效时返回null,或无效时返回验证错误对象。 验证错误对象通常有一个名为验证秘钥(`forbiddenName`)的属性。其值为一个任意词典,我们可以用来插入错误信息(`{name}`)。 +### Adding to reactive forms +### 添加响应式表单 -### Custom validation directive +In reactive forms, custom validators are fairly simple to add. All you have to do is pass the function directly +to the `FormControl`. -### 自定义验证指令 +在响应式表单组件中,添加自定义验证器相当简单。你所要做的一切就是直接把这个函数传给 `FormControl` 。 -In the Reactive Forms component, the `'name'` control's validator function list -has a `forbiddenNameValidator` at the bottom. - -在响应式表单组件中,我们在`'name'`控制器的验证函数列表的底部添加了一个配置了的`forbiddenNameValidator`。 - - -<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="name-validators" title="reactive/hero-form-reactive.component.ts (name validators)" linenums="false"> +<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="custom-validator" title="reactive/hero-form-reactive.component.ts (validator functions)" linenums="false"> </code-example> +### Adding to template-driven forms +In template-driven forms, you don't have direct access to the `FormControl` instance, so you can't pass the +validator in like you can for reactive forms. Instead, you need to add a directive to the template. -In the Template Driven example, the `<input>` has the selector (`forbiddenName`) -of a custom _attribute directive_, which rejects "bob". +在模板驱动表单中,我们不用直接访问`FormControl`实例。所以我们不能像响应式表单中那样把验证器传进去,而应该在模板中添加一个指令。 -在模板驱动组件的模板中,我们在name的输入框元素中添加了自定义**属性指令**的选择器(`forbiddenName`),并配置它来拒绝“bob”。 +The corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`. +`ForbiddenValidatorDirective`指令相当于`forbiddenNameValidator`的包装器。 -<code-example path="form-validation/src/app/template/hero-form-template2.component.html" region="name-input" title="template/hero-form-template2.component.html (name input)" linenums="false"> -</code-example> +Angular recognizes the directive's role in the validation process because the directive registers itself +with the `NG_VALIDATORS` provider, a provider with an extensible collection of validators. - - -The corresponding `ForbiddenValidatorDirective` is a wrapper around the `forbiddenNameValidator`. - -对应的`ForbiddenValidatorDirective`包装了`forbiddenNamevalidator`。 - -Angular `forms` recognizes the directive's role in the validation process because the directive registers itself -with the `NG_VALIDATORS` provider, a provider with an extensible collection of validation directives. - -Angular表单接受指令在验证流程中的作用,因为指令注册自己到`NG_VALIDATORS`提供商中,该提供商拥有可扩展的验证指令集。 +Angular在验证流程中的识别出指令的作用,是因为指令把自己注册到了`NG_VALIDATORS`提供商中,该提供商拥有一组可扩展的验证器。 <code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive-providers" title="shared/forbidden-name.directive.ts (providers)" linenums="false"> </code-example> +The directive class then implements the `Validator` interface, so that it can easily integrate +with Angular forms. Here is the rest of the directive to help you get an idea of how it all +comes together: - -Here is the rest of the directive to help you get an idea of how it all comes together: - -指令的其它部分是为了帮你理解它们是如何合作的: - +然后该指令类实现了`Validator`接口,以便它能简单的与 Angular 表单集成在一起。这个指令的其余部分有助于你理解它们是如何协作的: <code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" title="shared/forbidden-name.directive.ts (directive)"> </code-example> +Once the `ForbiddenValidatorDirective` is ready, you can simply add its selector, `forbiddenName`, to any input element to activate it. For example: +一旦 `ForbiddenValidatorDirective` 写好了,我们只要把`forbiddenName`选择器添加到输入框上就可以激活这个验证器了。比如: +<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-input" title="template/hero-form-template.component.html (forbidden-name-input)" linenums="false"> + +</code-example> <div class="l-sub-section"> -If you are familiar with Angular validations, you may have noticed -that the custom validation directive is instantiated with `useExisting` +You may have noticed that the custom validation directive is instantiated with `useExisting` rather than `useClass`. The registered validator must be _this instance_ of the `ForbiddenValidatorDirective`—the instance in the form with its `forbiddenName` property bound to “bob". If you were to replace `useExisting` with `useClass`, then you’d be registering a new class instance, one that doesn’t have a `forbiddenName`. -如果你熟悉 Angular 的验证机制,可能会注意到自定义验证指令是使用`useExisting`而不是`useClass`来实例化的。这是因为注册的验证器必须是这个 `ForbiddenValidatorDirective` 实例本身,也就是表单中 `forbiddenName` 属性被绑定到了"bob"的那个。如果用`useClass`来代替`useExisting`,就会注册一个新的类实例,而它是没有`forbiddenName`的。 - -To see this in action, run the example and then type “bob” in the name of Hero Form 2. -Notice that you get a validation error. Now change from `useExisting` to `useClass` and try again. -This time, when you type “bob”, there's no "bob" error message. - -要查看它的运行效果,请打开范例,并在英雄表单2的 name 字段输入 "bob"。 -注意,我们会看到一个验证错误。现在,把 `useExisting` 改成 `useClass` 再试一下。这次,当你敲 "bob" 时,就不会再出现错误信息了。 +你可能注意到了自定义验证器指令是用`useExisting`而不是`useClass`来实例化的。注册的验证器必须是这个 `ForbiddenValidatorDirective` 实例本身,也就是表单中 `forbiddenName` 属性被绑定到了"bob"的那个。如果用`useClass`来代替`useExisting`,就会注册一个新的类实例,而它是没有`forbiddenName`的。 </div> +## Control status CSS classes -<div class="l-sub-section"> +## 表示控件状态的 CSS 类 -For more information on attaching behavior to elements, -see [Attribute Directives](guide/attribute-directives). +Like in AngularJS, Angular automatically mirrors many control properties onto the form control element as CSS classes. You can use these classes to style form control elements according to the state of the form. The following classes are currently supported: -参见[属性型指令](guide/attribute-directives)章节。 +像 AngularJS 中一样,Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。我们可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类: + +* `.ng-valid` +* `.ng-invalid` +* `.ng-pending` +* `.ng-pristine` +* `.ng-dirty` +* `.ng-untouched` +* `.ng-touched` + +The hero form uses the `.ng-valid` and `.ng-invalid` classes to +set the color of each form control's border. + +这个英雄表单使用 `.ng-valid` 和 `.ng-invalid` 来设置每个表单控件的边框颜色。 + +<code-example path="form-validation/src/forms.css" title="forms.css (status classes)"> + +</code-example> -</div> +**You can run the <live-example></live-example> to see the complete reactive and template-driven example code.** - - -## Testing Considerations - -## 测试时的注意事项 - -You can write _isolated unit tests_ of validation and control logic in Reactive Forms. - -我们可以为**响应式表单**的验证器和控制器逻辑编写**孤立单元测试**。 - -_Isolated unit tests_ probe the component class directly, independent of its -interactions with its template, the DOM, other dependencies, or Angular itself. - -**孤立单元测试**直接检测组件类,与组件和它的模板的交互、DOM、其他以来和Angular本省都无关。 - -Such tests have minimal setup, are quick to write, and easy to maintain. -They do not require the `Angular TestBed` or asynchronous testing practices. - -这样的测试具有简单设置#,快速编写和容易维护的特征。它们不需要`Angular TestBed`或异步测试工序。 - -That's not possible with Template Driven forms. -The Template Driven approach relies on Angular to produce the control model and -to derive validation rules from the HTML validation attributes. -You must use the `Angular TestBed` to create component test instances, -write asynchronous tests, and interact with the DOM. - -这对**模板驱动**表单来说是不可能的。 -模板驱动方法依靠Angular来生成控制器模型并从HTML验证属性中衍生验证规则。 -你必须使用`Angular TestBed`来创建组件测试实例,编写异步测试并与DOM交互。 - -While not difficult, this takes more time, work and -skill—factors that tend to diminish test code -coverage and quality. - -虽然这种测试并不困难,但是它需要更多时间、工作和能力 - 这些因素往往会降低测试代码覆盖率和测试质量。 +**你可以运行<live-example></live-example>来查看完整的响应式和模板驱动表单的代码。** \ No newline at end of file diff --git a/aio/content/guide/i18n.md b/aio/content/guide/i18n.md index 0de99ae2bd..e7b0a33c06 100644 --- a/aio/content/guide/i18n.md +++ b/aio/content/guide/i18n.md @@ -642,7 +642,7 @@ This XML element represents the translation of the `<h1>` greeting tag you marke 这个XML元素代表了你使用`i18n`属性标记的`<h1>`问候语标签的翻译。 <div class="l-sub-section"> -Note that the translation unit `id=introductionHeader` is derived from the _custom_ `id`](#custom-id "Set a custom id") that you set earlier, but **without the `@@` prefix** required in the source HTML. +Note that the translation unit `id=introductionHeader` is derived from the [_custom_ `id`](#custom-id "Set a custom id") that you set earlier, but **without the `@@` prefix** required in the source HTML. 注意,翻译单元`id=introductionHeader`派生自[*自定义*`id`](#custom-id "设置自定义id"),它设置起来更简单,但是在HTML源码中**不需要`@@`前缀**。 diff --git a/aio/content/marketing/contributors.json b/aio/content/marketing/contributors.json index 0f67259285..1914caab35 100644 --- a/aio/content/marketing/contributors.json +++ b/aio/content/marketing/contributors.json @@ -342,7 +342,7 @@ "name": "Ralph Wang", "picture": "ralph.jpg", "twitter": "ralph_wang_gde", - "bio": "Ralph(Zhicheng Wang) is a senior consultant at ThoughWorks and also a GDE. He is a technology enthusiast and he is a passionate advocate of 'Simplicity, Professionalism and Sharing'. In his eighteen years of R&D career, he worked as tester, R&D engineer, project manager, product manager and CTO. He is looking forward to the birth of his baby.", + "bio": "Ralph(Zhicheng Wang) is a senior consultant at ThoughtWorks and also a GDE. He is a technology enthusiast and he is a passionate advocate of 'Simplicity, Professionalism and Sharing'. In his eighteen years of R&D career, he worked as tester, R&D engineer, project manager, product manager and CTO. He is immersed in the excitement of the arrival of the baby.", "group": "GDE" }, diff --git a/aio/content/marketing/index.html b/aio/content/marketing/index.html index a91dc4264b..452b1f6048 100755 --- a/aio/content/marketing/index.html +++ b/aio/content/marketing/index.html @@ -1,10 +1,10 @@ -<!--FULL HEADER BLOCK--> +<!-- FULL HEADER BLOCK --> <header> - <!--BACKGROUND IMAGE--> + <!-- BACKGROUND IMAGE --> <div class="background-sky hero"></div> -<!--INTRO SECTION --> + <!-- INTRO SECTION --> <section id="intro"> <!-- LOGO --> @@ -12,31 +12,32 @@ <img src="assets/images/logos/angular/angular.svg"/> </div> - <!-- CONTAINER --> + <!-- CONTAINER --> <div class="homepage-container"> - <!-- container content starts --> - - <div class="hero-headline no-toc">一套框架,多种平台。<br>移动端 & 桌面端</div> + <div class="hero-headline no-toc">一套框架,多种平台<br>移动端 & 桌面端</div> <a class="button hero-cta" href="guide/quickstart">快速上手</a> - </div><!-- CONTAINER END --> + </div> </section> - </header> +<!-- MAIN CONTENT --> <article> + + <h1 class="no-toc" style="display: none"></h1> + <div class="home-rows"> - <!--Announcement Bar--> + <!-- Announcement Bar --> <div class="homepage-container"> <div class="announcement-bar"> <img src="generated/images/marketing/home/angular-mix.png" height="40" width="151"> <p>2017年十月 加入我们的最新活动</p> <a class="button" href="https://angularmix.com/">了解更多</a> - </div> + </div> </div> - <!-- Group 1--> + <!-- Group 1 --> <div layout="row" layout-xs="column" class="home-row homepage-container"> <div class="promo-img-container promo-1"> <div> @@ -52,7 +53,8 @@ </div> </div> <hr> - <!-- Group 2--> + + <!-- Group 2 --> <div layout="row" layout-xs="column" class="home-row"> <div class="text-container"> <div class="text-block"> @@ -70,7 +72,7 @@ </div> <hr> - <!-- Group 3--> + <!-- Group 3 --> <div layout="row" layout-xs="column" class="home-row"> <div class="promo-img-container promo-3"> <div><img src="generated/images/marketing/home/joyful-development.svg" alt="IDE example"></div> @@ -86,9 +88,8 @@ </div> <hr> - <!-- Group 4--> + <!-- Group 4 --> <div layout="row" layout-xs="column" class="home-row"> - <div class="text-container"> <div class="text-block l-pad-top-2"> <div class="text-headline">百万粉丝热捧</div> @@ -103,20 +104,19 @@ </div> </div> - <!-- CTA CARDS --> - <div layout="row" layout-xs="column" class="home-row"> - - <a href="guide/quickstart"> - <div class="card"> + <!-- CTA CARDS --> + <div layout="row" layout-xs="column" class="home-row"> + <a href="guide/quickstart"> + <div class="card"> <img src="generated/images/marketing/home/code-icon.svg" height="70px"> <div class="card-text-container"> <div class="text-headline">立即开始</div> <p>开始构建你的 Angular 应用</p> </div> - </div> - </a> - </div> + </div> + </a> + </div> - </div> <!-- end of home rows --> + </div><!-- end of home rows --> </article> diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 6a3ece8bce..a2f02e63c1 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -40,6 +40,12 @@ "hidden": true }, + { + "url": "guide/webpack", + "title": "Webpack: 简介", + "hidden": true + }, + { "url": "guide/quickstart", "title": "快速上手", @@ -165,7 +171,7 @@ { "url": "guide/forms", "title": "模板驱动表单", - "tooltip": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors." + "tooltip": "表单可以创建集中、高效、引人注目的输入体验。Angular 表单可以协调一组数据绑定控件,跟踪变更,验证输入,并表达错误信息。" }, { "url": "guide/form-validation", @@ -381,7 +387,7 @@ "tooltip": "我们的联系方式、LOGO 和品牌" }, { - "url": "https://blog.angularjs.org/", + "url": "https://blog.angular.io/", "title": "博客", "tooltip": "Angular 官方博客" } diff --git a/aio/firebase.json b/aio/firebase.json index b4710b8c87..628a255a3a 100644 --- a/aio/firebase.json +++ b/aio/firebase.json @@ -6,26 +6,27 @@ "public": "dist", "cleanUrls": true, "redirects": [ - // cli-quickstart.html glossary.html, quickstart.html, http.html, style-guide.html, styleguide + // cli-quickstart.html, glossary.html, quickstart.html, server-communication.html, style-guide.html {"type": 301, "source": "/docs/ts/latest/cli-quickstart.html", "destination": "/guide/quickstart"}, - {"type": 301, "source": "/guide/cli-quickstart", "destination": "/guide/quickstart"}, {"type": 301, "source": "/docs/ts/latest/glossary.html", "destination": "/guide/glossary"}, {"type": 301, "source": "/docs/ts/latest/quickstart.html", "destination": "/guide/quickstart"}, {"type": 301, "source": "/docs/ts/latest/guide/server-communication.html", "destination": "/guide/http"}, {"type": 301, "source": "/docs/ts/latest/guide/style-guide.html", "destination": "/guide/styleguide"}, - {"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"}, - // cookbook/component-communication.html + // guide/cli-quickstart, styleguide + {"type": 301, "source": "/guide/cli-quickstart", "destination": "/guide/quickstart"}, + {"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"}, + + // cookbook/a1-a2-quick-reference.html, cookbook/component-communication.html, cookbook/dependency-injection.html + {"type": 301, "source": "/docs/ts/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"}, {"type": 301, "source": "/docs/ts/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"}, + {"type": 301, "source": "/docs/ts/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"}, // cookbook, cookbook/, cookbook/index.html {"type": 301, "source": "/docs/ts/latest/cookbook", "destination": "/docs"}, {"type": 301, "source": "/docs/ts/latest/cookbook/", "destination": "/docs"}, {"type": 301, "source": "/docs/ts/latest/cookbook/index.html", "destination": "/docs"}, - // cookbook/dependency-injection.html - {"type": 301, "source": "/docs/ts/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"}, - // cookbook/*.html {"type": 301, "source": "/docs/ts/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"}, diff --git a/aio/karma.conf.js b/aio/karma.conf.js index f89999269a..c2d83c7804 100644 --- a/aio/karma.conf.js +++ b/aio/karma.conf.js @@ -16,15 +16,8 @@ module.exports = function (config) { clearContext: false // leave Jasmine Spec Runner output visible in browser }, files: [ - { pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css', included: true }, - { pattern: './src/test.ts', watched: false } + { pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css', included: true } ], - preprocessors: { - './src/test.ts': ['@angular/cli'] - }, - mime: { - 'text/x-typescript': ['ts','tsx'] - }, coverageIstanbulReporter: { reports: [ 'html', 'lcovonly' ], fixWebpackSourcePaths: true @@ -32,14 +25,13 @@ module.exports = function (config) { angularCli: { environment: 'dev' }, - reporters: config.angularCli && config.angularCli.codeCoverage - ? ['progress', 'coverage-istanbul'] - : ['progress', 'kjhtml'], + reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], + browserNoActivityTimeout: 60000, singleRun: false }); }; diff --git a/aio/package.json b/aio/package.json index 6881103fdb..7309a5c78a 100644 --- a/aio/package.json +++ b/aio/package.json @@ -9,7 +9,7 @@ "ng": "yarn check-env && ng", "start": "yarn check-env && ng serve", "prebuild": "yarn check-env && yarn setup", - "build": "ng build -prod -sm -vc=false", + "build": "ng build --target=production --environment=stable -sm -bo", "postbuild": "yarn sw-manifest && yarn sw-copy", "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint", "test": "yarn check-env && ng test", @@ -22,8 +22,7 @@ "example-e2e": "node ./tools/examples/run-example-e2e", "example-lint": "tslint -c \"content/examples/tslint.json\" \"content/examples/**/*.ts\" -e \"content/examples/styleguide/**/*.avoid.ts\"", "deploy-preview": "scripts/deploy-preview.sh", - "deploy-staging": "scripts/deploy-to-firebase.sh staging", - "deploy-production": "scripts/deploy-to-firebase.sh production", + "deploy-production": "scripts/deploy-to-firebase.sh", "check-env": "node scripts/check-environment", "payload-size": "scripts/payload.sh", "predocs": "rimraf src/generated/{docs,*.json}", @@ -31,6 +30,7 @@ "docs-watch": "node tools/transforms/authors-package/watchr.js", "docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms", "docs-test": "node tools/transforms/test.js", + "tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test", "serve-and-sync": "concurrently --kill-others \"yarn docs-watch\" \"yarn start\"", "~~update-webdriver": "webdriver-manager update --standalone false --gecko false", "boilerplate:add": "node ./tools/examples/example-boilerplate add", @@ -40,7 +40,7 @@ "generate-zips": "node ./tools/example-zipper/generateZips", "sw-manifest": "ngu-sw-manifest --dist dist --in ngsw-manifest.json --out dist/ngsw-manifest.json", "sw-copy": "cp node_modules/@angular/service-worker/bundles/worker-basic.min.js dist/", - "postinstall": "node tools/cli-patches/patch.js && uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map", + "postinstall": "uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map", "build-ie-polyfills": "node node_modules/webpack/bin/webpack.js -p src/ie-polyfills.js src/generated/ie-polyfills.min.js" }, "engines": { @@ -66,14 +66,13 @@ "core-js": "^2.4.1", "jasmine": "^2.6.0", "ng-pwa-tools": "^0.0.10", - "ngo": "angular/ngo", "rxjs": "^5.2.0", "tslib": "^1.7.1", "web-animations-js": "^2.2.5", "zone.js": "^0.8.12" }, "devDependencies": { - "@angular/cli": "angular/cli-builds#webpack-next", + "@angular/cli": "1.3.0-rc.3", "@angular/compiler-cli": "^4.3.1", "@types/jasmine": "^2.5.52", "@types/node": "~6.0.60", diff --git a/aio/scripts/deploy-to-firebase.sh b/aio/scripts/deploy-to-firebase.sh index a14ba8965b..1542067de0 100755 --- a/aio/scripts/deploy-to-firebase.sh +++ b/aio/scripts/deploy-to-firebase.sh @@ -3,32 +3,82 @@ # WARNING: FIREBASE_TOKEN should NOT be printed. set +x -eu -o pipefail +# Only deploy if this not a PR. PRs are deployed early in `build.sh`. +if [[ $TRAVIS_PULL_REQUEST != "false" ]]; then + echo "Skipping deploy because this is a PR build." + exit 0 +fi -readonly deployEnv=$1 +# Do not deploy if the current commit is not the latest on its branch. +readonly LATEST_COMMIT=$(git ls-remote origin $TRAVIS_BRANCH | cut -c1-40) +if [[ $TRAVIS_COMMIT != $LATEST_COMMIT ]]; then + echo "Skipping deploy because $TRAVIS_COMMIT is not the latest commit ($LATEST_COMMIT)." + exit 0 +fi + +# The deployment mode is computed based on the branch we are building +if [[ $TRAVIS_BRANCH == master ]]; then + readonly deployEnv=next +elif [[ $TRAVIS_BRANCH == $STABLE_BRANCH ]]; then + readonly deployEnv=stable +else + # Extract the major versions from the branches, e.g. the 4 from 4.3.x + readonly majorVersion=${TRAVIS_BRANCH%%.*} + readonly majorVersionStable=${STABLE_BRANCH%%.*} + + # Do not deploy if the major version is not less than the stable branch major version + if [[ $majorVersion -ge $majorVersionStable ]]; then + echo "Skipping deploy of branch \"${TRAVIS_BRANCH}\" to firebase." + echo "We only deploy archive branches with the major version less than the stable branch: \"${STABLE_BRANCH}\"" + exit 0 + fi + + # Find the branch that has highest minor version for the given `$majorVersion` + readonly mostRecentMinorVersion=$( + # List the branches that start with the major version + git ls-remote origin refs/heads/${majorVersion}.*.x | + # Extract the version number + awk -F'/' '{print $3}' | + # Sort by the minor version + sort -t. -k 2,2n | + # Get the highest version + tail -n1 + ) + + # Do not deploy as it is not the latest branch for the given major version + if [[ $TRAVIS_BRANCH != $mostRecentMinorVersion ]]; then + echo "Skipping deploy of branch \"${TRAVIS_BRANCH}\" to firebase." + echo "There is a more recent branch with the same major version: \"${mostRecentMinorVersion}\"" + exit 0 + fi + + readonly deployEnv=archive +fi case $deployEnv in - staging) - readonly buildEnv=stage + next) readonly projectId=aio-staging - readonly deployedUrl=https://$projectId.firebaseapp.com/ + readonly deployedUrl=https://next.angular.io/ readonly firebaseToken=$FIREBASE_TOKEN ;; - production) - readonly buildEnv=prod + stable) readonly projectId=angular-io readonly deployedUrl=https://angular.io/ readonly firebaseToken=$FIREBASE_TOKEN ;; - *) - echo "Unknown deployment environment ('$deployEnv'). Expected 'staging' or 'production'." - exit 1 + archive) + readonly projectId=angular-io-${majorVersion} + readonly deployedUrl=https://v${majorVersion}.angular.io/ + readonly firebaseToken=$FIREBASE_TOKEN ;; esac -# Do not deploy if the current commit is not the latest on its branch. -readonly LATEST_COMMIT=$(git ls-remote origin $TRAVIS_BRANCH | cut -c1-40) -if [ $TRAVIS_COMMIT != $LATEST_COMMIT ]; then - echo "Skipping deploy because $TRAVIS_COMMIT is not the latest commit ($LATEST_COMMIT)." +echo "Git branch : $TRAVIS_BRANCH" +echo "Build/deploy mode : $deployEnv" +echo "Firebase project : $projectId" +echo "Deployment URL : $deployedUrl" + +if [[ $1 == "--dry-run" ]]; then exit 0 fi @@ -37,7 +87,10 @@ fi cd "`dirname $0`/.." # Build the app - yarn build -- --env=$buildEnv + yarn build -- --env=$deployEnv + + # Include any mode-specific files + cp -rf src/extra-files/$deployEnv/. dist/ # Check payload size yarn payload-size diff --git a/aio/scripts/deploy-to-firebase.test.sh b/aio/scripts/deploy-to-firebase.test.sh new file mode 100755 index 0000000000..dd9a3235fd --- /dev/null +++ b/aio/scripts/deploy-to-firebase.test.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash + +function check { + if [[ $1 == $2 ]]; then + echo Pass + exit 0 + fi + echo Fail + echo ---- Expected ---- + echo "$2" + echo ---- Actual ---- + echo "$1" + exit 1 +} + +( + echo ===== master - skip deploy - pull request + actual=$( + export TRAVIS_PULL_REQUEST=true + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy because this is a PR build." + check "$actual" "$expected" +) + +( + echo ===== master - deploy success + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=master + export TRAVIS_COMMIT=$(git ls-remote origin master | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Git branch : master +Build/deploy mode : next +Firebase project : aio-staging +Deployment URL : https://next.angular.io/" + check "$actual" "$expected" +) + +( + echo ===== master - skip deploy - commit not HEAD + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=master + export TRAVIS_COMMIT=DUMMY_TEST_COMMIT + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy because DUMMY_TEST_COMMIT is not the latest commit ($(git ls-remote origin master | cut -c1-40))." + check "$actual" "$expected" +) + +( + echo ===== stable - deploy success + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=4.3.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=$(git ls-remote origin 4.3.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Git branch : 4.3.x +Build/deploy mode : stable +Firebase project : angular-io +Deployment URL : https://angular.io/" + check "$actual" "$expected" +) + +( + echo ===== stable - skip deploy - commit not HEAD + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=4.3.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=DUMMY_TEST_COMMIT + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy because DUMMY_TEST_COMMIT is not the latest commit ($(git ls-remote origin 4.3.x | cut -c1-40))." + check "$actual" "$expected" +) + +( + echo ===== archive - deploy success + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.4.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=$(git ls-remote origin 2.4.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Git branch : 2.4.x +Build/deploy mode : archive +Firebase project : angular-io-2 +Deployment URL : https://v2.angular.io/" + check "$actual" "$expected" +) + +( + echo ===== archive - skip deploy - commit not HEAD + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.4.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=DUMMY_TEST_COMMIT + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy because DUMMY_TEST_COMMIT is not the latest commit ($(git ls-remote origin 2.4.x | cut -c1-40))." + check "$actual" "$expected" +) + +( + echo ===== archive - skip deploy - major version too high, lower minor + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.1.x + export STABLE_BRANCH=2.2.x + export TRAVIS_COMMIT=$(git ls-remote origin 2.1.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy of branch \"2.1.x\" to firebase. +We only deploy archive branches with the major version less than the stable branch: \"2.2.x\"" + check "$actual" "$expected" +) + +( + echo ===== archive - skip deploy - major version too high, higher minor + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.4.x + export STABLE_BRANCH=2.2.x + export TRAVIS_COMMIT=$(git ls-remote origin 2.1.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy of branch \"2.1.x\" to firebase. +We only deploy archive branches with the major version less than the stable branch: \"2.2.x\"" + check "$actual" "$expected" +) + +( + echo ===== archive - skip deploy - minor version too low + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.1.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=$(git ls-remote origin 2.1.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy of branch \"2.1.x\" to firebase. +There is a more recent branch with the same major version: \"2.4.x\"" + check "$actual" "$expected" +) diff --git a/aio/scripts/payload.sh b/aio/scripts/payload.sh index 0dc0bd9c30..aacd7c615e 100755 --- a/aio/scripts/payload.sh +++ b/aio/scripts/payload.sh @@ -61,7 +61,8 @@ else # Nothing changed in aio/ exit 0 fi -payloadData="$payloadData\"change\": \"$change\"" +message=$(echo $TRAVIS_COMMIT_MESSAGE | sed 's/"/\\"/g' | sed 's/\\/\\\\/g') +payloadData="$payloadData\"change\": \"$change\", \"message\": \"$message\"" payloadData="{${payloadData}}" diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index 22127e12c2..8f42887b50 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -21,12 +21,13 @@ <aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu> <aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="isSideBySide"></aio-nav-menu> - <div class="doc-version" title="Angular docs version {{currentDocVersion?.title}}"> - <aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="docVersions && docVersions[0]"></aio-select> + <div class="doc-version"> + <aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="currentDocVersion"></aio-select> </div> </md-sidenav> <section class="sidenav-content" [id]="pageId" role="content"> + <aio-mode-banner [mode]="deployment.mode" [version]="versionInfo"></aio-mode-banner> <aio-doc-viewer [doc]="currentDocument" (docRendered)="onDocRendered()"></aio-doc-viewer> <aio-dt [on]="dtOn" [(doc)]="currentDocument"></aio-dt> </section> diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index c69ffe6fdd..ea67b3bd6e 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -12,6 +12,7 @@ import { of } from 'rxjs/observable/of'; import { AppComponent } from './app.component'; import { AppModule } from './app.module'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; +import { Deployment } from 'app/shared/deployment.service'; import { GaService } from 'app/shared/ga.service'; import { LocationService } from 'app/shared/location.service'; import { Logger } from 'app/shared/logger.service'; @@ -273,26 +274,49 @@ describe('AppComponent', () => { describe('SideNav version selector', () => { let selectElement: DebugElement; let selectComponent: SelectComponent; - beforeEach(() => { + + function setupSelectorForTesting(mode?: string) { + createTestingModule('a/b', mode); + initializeTest(); component.onResize(sideBySideBreakPoint + 1); // side-by-side selectElement = fixture.debugElement.query(By.directive(SelectComponent)); selectComponent = selectElement.componentInstance; + } + + it('should select the version that matches the deploy mode', () => { + setupSelectorForTesting(); + expect(selectComponent.selected.title).toContain('stable'); + setupSelectorForTesting('next'); + expect(selectComponent.selected.title).toContain('next'); + setupSelectorForTesting('archive'); + expect(selectComponent.selected.title).toContain('v4'); }); - it('should pick first (current) version by default', () => { - expect(selectComponent.selected.title).toEqual(component.versionInfo.raw); + it('should add the current raw version string to the selected version', () => { + setupSelectorForTesting(); + expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`); + setupSelectorForTesting('next'); + expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`); + setupSelectorForTesting('archive'); + expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`); }); // Older docs versions have an href - it('should navigate when change to a version with an href', () => { - selectElement.triggerEventHandler('change', { option: component.docVersions[1] as Option, index: 1}); - expect(locationService.go).toHaveBeenCalledWith(TestHttp.docVersions[0].url); + it('should navigate when change to a version with a url', () => { + setupSelectorForTesting(); + const versionWithUrlIndex = component.docVersions.findIndex(v => !!v.url); + const versionWithUrl = component.docVersions[versionWithUrlIndex]; + selectElement.triggerEventHandler('change', { option: versionWithUrl, index: versionWithUrlIndex}); + expect(locationService.go).toHaveBeenCalledWith(versionWithUrl.url); }); // The current docs version should not have an href // This may change when we perfect our docs versioning approach - it('should not navigate when change to a version without an href', () => { - selectElement.triggerEventHandler('change', { option: component.docVersions[0] as Option, index: 0}); + it('should not navigate when change to a version without a url', () => { + setupSelectorForTesting(); + const versionWithoutUrlIndex = component.docVersions.findIndex(v => !v.url); + const versionWithoutUrl = component.docVersions[versionWithoutUrlIndex]; + selectElement.triggerEventHandler('change', { option: versionWithoutUrl, index: versionWithoutUrlIndex}); expect(locationService.go).not.toHaveBeenCalled(); }); }); @@ -332,10 +356,6 @@ describe('AppComponent', () => { }); describe('hostClasses', () => { - let host: DebugElement; - beforeEach(() => { - host = fixture.debugElement; - }); it('should set the css classes of the host container based on the current doc and navigation view', () => { locationService.go('guide/pipes'); @@ -359,7 +379,7 @@ describe('AppComponent', () => { }); it('should set the css class of the host container based on the open/closed state of the side nav', () => { - const sideNav = host.query(By.directive(MdSidenav)); + const sideNav = fixture.debugElement.query(By.directive(MdSidenav)); locationService.go('guide/pipes'); fixture.detectChanges(); @@ -376,7 +396,14 @@ describe('AppComponent', () => { checkHostClass('sidenav', 'open'); }); + it('should set the css class of the host container based on the initial deployment mode', () => { + createTestingModule('a/b', 'archive'); + initializeTest(); + checkHostClass('mode', 'archive'); + }); + function checkHostClass(type, value) { + const host = fixture.debugElement; const classes = host.properties['className']; const classArray = classes.split(' ').filter(c => c.indexOf(`${type}-`) === 0); expect(classArray.length).toBeLessThanOrEqual(1, `"${classes}" should have only one class matching ${type}-*`); @@ -623,7 +650,25 @@ describe('AppComponent', () => { describe('footer', () => { it('should have version number', () => { const versionEl: HTMLElement = fixture.debugElement.query(By.css('aio-footer')).nativeElement; - expect(versionEl.textContent).toContain(TestHttp.versionFull); + expect(versionEl.textContent).toContain(TestHttp.versionInfo.full); + }); + }); + + describe('deployment banner', () => { + it('should show a message if the deployment mode is "archive"', () => { + createTestingModule('a/b', 'archive'); + initializeTest(); + fixture.detectChanges(); + const banner: HTMLElement = fixture.debugElement.query(By.css('aio-mode-banner')).nativeElement; + expect(banner.textContent).toContain('archived documentation for Angular v4'); + }); + + it('should show no message if the deployment mode is not "archive"', () => { + createTestingModule('a/b', 'stable'); + initializeTest(); + fixture.detectChanges(); + const banner: HTMLElement = fixture.debugElement.query(By.css('aio-mode-banner')).nativeElement; + expect(banner.textContent.trim()).toEqual(''); }); }); @@ -720,6 +765,97 @@ describe('AppComponent', () => { }); }); + describe('archive redirection', () => { + it('should redirect to `docs` if deployment mode is `archive` and not at a docs page', () => { + createTestingModule('', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs'); + + createTestingModule('resources', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs'); + + createTestingModule('guide/aot-compiler', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial/toh-pt1', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('docs', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('api', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + }); + + it('should redirect to `docs` if deployment mode is `next` and not at a docs page', () => { + createTestingModule('', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs'); + + createTestingModule('resources', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs'); + + createTestingModule('guide/aot-compiler', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial/toh-pt1', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('docs', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('api', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + }); + + it('should not redirect to `docs` if deployment mode is `stable` and not at a docs page', () => { + createTestingModule('', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('resources', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('guide/aot-compiler', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial/toh-pt1', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('docs', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('api', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + }); + }); }); describe('with mocked DocViewer', () => { @@ -883,7 +1019,8 @@ describe('AppComponent', () => { //// test helpers //// -function createTestingModule(initialUrl: string) { +function createTestingModule(initialUrl: string, mode: string = 'stable') { + const mockLocationService = new MockLocationService(initialUrl); TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [ AppModule ], @@ -891,9 +1028,14 @@ function createTestingModule(initialUrl: string) { { provide: APP_BASE_HREF, useValue: '/' }, { provide: GaService, useClass: TestGaService }, { provide: Http, useClass: TestHttp }, - { provide: LocationService, useFactory: () => new MockLocationService(initialUrl) }, + { provide: LocationService, useFactory: () => mockLocationService }, { provide: Logger, useClass: MockLogger }, { provide: SearchService, useClass: MockSearchService }, + { provide: Deployment, useFactory: () => { + const deployment = new Deployment(mockLocationService as any); + deployment.mode = mode; + return deployment; + }}, ] }); } @@ -908,7 +1050,21 @@ class TestSearchService { } class TestHttp { - static versionFull = '4.0.0-local+sha.73808dd'; + + static versionInfo = { + raw: '4.0.0-rc.6', + major: 4, + minor: 0, + patch: 0, + prerelease: [ 'local' ], + build: 'sha.73808dd', + version: '4.0.0-local', + codeName: 'snapshot', + isSnapshot: true, + full: '4.0.0-local+sha.73808dd', + branch: 'master', + commitSHA: '73808dd38b5ccd729404936834d1568bd066de81' + }; static docVersions: NavigationNode[] = [ { title: 'v2', url: 'https://v2.angular.cn' } @@ -951,22 +1107,7 @@ class TestHttp { ], "docVersions": TestHttp.docVersions, - "__versionInfo": { - "raw": "4.0.0-rc.6", - "major": 4, - "minor": 0, - "patch": 0, - "prerelease": [ - "local" - ], - "build": "sha.73808dd", - "version": "4.0.0-local", - "codeName": "snapshot", - "isSnapshot": true, - "full": TestHttp.versionFull, - "branch": "master", - "commitSHA": "73808dd38b5ccd729404936834d1568bd066de81" - } + "__versionInfo": TestHttp.versionInfo, }; get(url: string) { diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts index 3ba0293a4b..84c283ded8 100644 --- a/aio/src/app/app.component.ts +++ b/aio/src/app/app.component.ts @@ -5,6 +5,7 @@ import { MdSidenav } from '@angular/material'; import { CurrentNodes, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service'; import { DocumentService, DocumentContents } from 'app/documents/document.service'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; +import { Deployment } from 'app/shared/deployment.service'; import { LocationService } from 'app/shared/location.service'; import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component'; import { ScrollService } from 'app/shared/scroll.service'; @@ -99,6 +100,7 @@ export class AppComponent implements OnInit { sidenav: MdSidenav; constructor( + public deployment: Deployment, private documentService: DocumentService, private hostElement: ElementRef, private locationService: LocationService, @@ -127,6 +129,11 @@ export class AppComponent implements OnInit { }); this.locationService.currentPath.subscribe(path => { + // Redirect to docs if we are in not in stable mode and are not hitting a docs page + // (i.e. we have arrived at a marketing page) + if (this.deployment.mode !== 'stable' && !/^(docs$|api$|guide|tutorial)/.test(path)) { + this.locationService.replace('docs'); + } if (path === this.currentPath) { // scroll only if on same page (most likely a change to the hash) this.autoScroll(); @@ -158,12 +165,24 @@ export class AppComponent implements OnInit { // Compute the version picker list from the current version and the versions in the navigation map combineLatest( - this.navigationService.versionInfo.map(versionInfo => ({ title: versionInfo.raw, url: null })), - this.navigationService.navigationViews.map(views => views['docVersions']), - (currentVersion, otherVersions) => [currentVersion, ...otherVersions]) - .subscribe(versions => { - this.docVersions = versions; - this.currentDocVersion = this.docVersions[0]; + this.navigationService.versionInfo, + this.navigationService.navigationViews.map(views => views['docVersions'])) + .subscribe(([versionInfo, versions]) => { + // TODO(pbd): consider whether we can lookup the stable and next versions from the internet + const computedVersions = [ + { title: 'next', url: 'https://next.angular.io' }, + { title: 'stable', url: 'https://angular.io' }, + ]; + if (this.deployment.mode === 'archive') { + computedVersions.push({ title: `v${versionInfo.major}`, url: null }); + } + this.docVersions = [...computedVersions, ...versions]; + + // Find the current version - eithers title matches the current deployment mode + // or its title matches the major version of the current version info + this.currentDocVersion = this.docVersions.find(version => + version.title === this.deployment.mode || version.title === `v${versionInfo.major}`); + this.currentDocVersion.title += ` (v${versionInfo.raw})`; }); this.navigationService.navigationViews.subscribe(views => { @@ -256,12 +275,13 @@ export class AppComponent implements OnInit { } updateHostClasses() { + const mode = `mode-${this.deployment.mode}`; const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`; const pageClass = `page-${this.pageId}`; const folderClass = `folder-${this.folderId}`; const viewClasses = Object.keys(this.currentNodes || {}).map(view => `view-${view}`).join(' '); - this.hostClasses = `${sideNavOpen} ${pageClass} ${folderClass} ${viewClasses}`; + this.hostClasses = `${mode} ${sideNavOpen} ${pageClass} ${folderClass} ${viewClasses}`; } // Dynamically change height of table of contents container diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index 73e0154342..ca9545ab5b 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -26,8 +26,10 @@ import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module'; import { AppComponent } from 'app/app.component'; import { ApiService } from 'app/embedded/api/api.service'; import { CustomMdIconRegistry, SVG_ICONS } from 'app/shared/custom-md-icon-registry'; +import { Deployment } from 'app/shared/deployment.service'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; import { DtComponent } from 'app/layout/doc-viewer/dt.component'; +import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component'; import { EmbeddedModule } from 'app/embedded/embedded.module'; import { GaService } from 'app/shared/ga.service'; import { Logger } from 'app/shared/logger.service'; @@ -90,14 +92,16 @@ export const svgIconProviders = [ DocViewerComponent, DtComponent, FooterComponent, - TopMenuComponent, + ModeBannerComponent, NavMenuComponent, NavItemComponent, SearchResultsComponent, SearchBoxComponent, + TopMenuComponent, ], providers: [ ApiService, + Deployment, DocumentService, GaService, Logger, diff --git a/aio/src/app/layout/mode-banner/mode-banner.component.ts b/aio/src/app/layout/mode-banner/mode-banner.component.ts new file mode 100644 index 0000000000..366a0154e9 --- /dev/null +++ b/aio/src/app/layout/mode-banner/mode-banner.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { VersionInfo } from 'app/navigation/navigation.service'; + +@Component({ + selector: 'aio-mode-banner', + template: ` + <div *ngIf="mode == 'archive'" class="mode-banner"> + This is the <strong>archived documentation for Angular v{{version?.major}}.</strong> + Please visit <a href="https://angular.io/">angular.io</a> to see documentation for the current version of Angular. + </div> + ` +}) +export class ModeBannerComponent { + @Input() mode: string; + @Input() version: VersionInfo; +} diff --git a/aio/src/app/shared/deployment.service.spec.ts b/aio/src/app/shared/deployment.service.spec.ts new file mode 100644 index 0000000000..34df4ec92e --- /dev/null +++ b/aio/src/app/shared/deployment.service.spec.ts @@ -0,0 +1,32 @@ +import { ReflectiveInjector } from '@angular/core'; +import { environment } from 'environments/environment'; +import { LocationService } from 'app/shared/location.service'; +import { MockLocationService } from 'testing/location.service'; +import { Deployment } from './deployment.service'; + +describe('Deployment service', () => { + describe('mode', () => { + it('should get the mode from the environment', () => { + environment.mode = 'foo'; + const deployment = getInjector().get(Deployment); + expect(deployment.mode).toEqual('foo'); + }); + + it('should get the mode from the `mode` query parameter if available', () => { + const injector = getInjector(); + + const locationService: MockLocationService = injector.get(LocationService); + locationService.search.and.returnValue({ mode: 'bar' }); + + const deployment = injector.get(Deployment); + expect(deployment.mode).toEqual('bar'); + }); + }); +}); + +function getInjector() { + return ReflectiveInjector.resolveAndCreate([ + Deployment, + { provide: LocationService, useFactory: () => new MockLocationService('') } + ]); +} diff --git a/aio/src/app/shared/deployment.service.ts b/aio/src/app/shared/deployment.service.ts new file mode 100644 index 0000000000..6a3f0ee79a --- /dev/null +++ b/aio/src/app/shared/deployment.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { LocationService } from 'app/shared/location.service'; +import { environment } from 'environments/environment'; + +/** + * Information about the deployment of this application. + */ +@Injectable() +export class Deployment { + /** + * The deployment mode set from the environment provided at build time; + * or overridden by the `mode` query parameter: e.g. `...?mode=archive` + */ + mode: string = this.location.search()['mode'] || environment.mode; + + constructor(private location: LocationService) {} +}; diff --git a/aio/src/app/shared/location.service.ts b/aio/src/app/shared/location.service.ts index 00d042ad70..52ecacfa5c 100644 --- a/aio/src/app/shared/location.service.ts +++ b/aio/src/app/shared/location.service.ts @@ -55,6 +55,10 @@ export class LocationService { window.location.assign(url); } + replace(url: string) { + window.location.replace(url); + } + private stripSlashes(url: string) { return url.replace(/^\/+/, '').replace(/\/+(\?|#|$)/, '$1'); } diff --git a/aio/src/environments/environment.archive.ts b/aio/src/environments/environment.archive.ts new file mode 100644 index 0000000000..5100c50a77 --- /dev/null +++ b/aio/src/environments/environment.archive.ts @@ -0,0 +1,6 @@ +// This is for archived sites, which are hosted at https://vX.angular.io, where X is the major Angular version. +export const environment = { + gaId: 'UA-8594346-15', // Production id (since it is linked from the main site) + production: true, + mode: 'archive' +}; diff --git a/aio/src/environments/environment.next.ts b/aio/src/environments/environment.next.ts new file mode 100644 index 0000000000..63682ea65e --- /dev/null +++ b/aio/src/environments/environment.next.ts @@ -0,0 +1,6 @@ +// This is for the staging site, which is hosted at https://next.angular.io (and https://aio-staging.firebaseapp.org) +export const environment = { + gaId: 'UA-8594346-15', // Production id (since it is linked from the main site) + production: true, + mode: 'next' +}; diff --git a/aio/src/environments/environment.prod.ts b/aio/src/environments/environment.stable.ts similarity index 57% rename from aio/src/environments/environment.prod.ts rename to aio/src/environments/environment.stable.ts index 94acf34f85..47e330840b 100644 --- a/aio/src/environments/environment.prod.ts +++ b/aio/src/environments/environment.stable.ts @@ -1,5 +1,6 @@ // This is for the production site, which is hosted at https://angular.io export const environment = { - gaId: 'UA-80456300-1', - production: true + gaId: 'UA-80456300-1', // Production id + production: true, + mode: 'stable' }; diff --git a/aio/src/environments/environment.stage.ts b/aio/src/environments/environment.stage.ts deleted file mode 100644 index 97a992c88f..0000000000 --- a/aio/src/environments/environment.stage.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This is for the staging site, which is hosted at https://aio-staging.firebaseapp.org -export const environment = { - gaId: 'UA-8594346-26', - production: true -}; diff --git a/aio/src/environments/environment.ts b/aio/src/environments/environment.ts index 165a718f55..77d62f0968 100644 --- a/aio/src/environments/environment.ts +++ b/aio/src/environments/environment.ts @@ -13,6 +13,7 @@ import 'core-js/es7/reflect'; export const environment = { - gaId: 'UA-8594346-26', // Staging site - production: false + gaId: 'UA-8594346-26', // Development id + production: false, + mode: 'stable' }; diff --git a/aio/src/extra-files/README.md b/aio/src/extra-files/README.md new file mode 100644 index 0000000000..2aad208a30 --- /dev/null +++ b/aio/src/extra-files/README.md @@ -0,0 +1,9 @@ +# Extra files folder + +This folder is used for extra files that should be included in deployments to firebase. + +After the AIO application had been built and before it is deployed all files and folders +inside the folder with the same name as the current deployment mode (next, stable, archive) +will be copied to the `dist` folder. + +See the `scripts/deploy-to-firebase.sh` script for more detail. \ No newline at end of file diff --git a/aio/src/extra-files/archive/robots.txt b/aio/src/extra-files/archive/robots.txt new file mode 100644 index 0000000000..77470cb39f --- /dev/null +++ b/aio/src/extra-files/archive/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/aio/src/extra-files/next/robots.txt b/aio/src/extra-files/next/robots.txt new file mode 100644 index 0000000000..77470cb39f --- /dev/null +++ b/aio/src/extra-files/next/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/aio/src/styles/2-modules/_deploy-theme.scss b/aio/src/styles/2-modules/_deploy-theme.scss new file mode 100644 index 0000000000..3858b66ce7 --- /dev/null +++ b/aio/src/styles/2-modules/_deploy-theme.scss @@ -0,0 +1,50 @@ + +aio-shell.mode-archive { + + .mat-toolbar.mat-primary, footer { + background: linear-gradient(145deg,#263238,#78909C); + } + + .vertical-menu-item { + &.selected, &:hover { + color: #263238; + } + } + + .toc-inner ul.toc-list li.active a { + color: #263238; + + &:before { + background-color: #263238; + } + } + + .toc-inner ul.toc-list li:hover a { + color: #263238; + } +} + +aio-shell.mode-next { + + .mat-toolbar.mat-primary, footer { + background: linear-gradient(145deg,#DD0031,#C3002F); + } + + .vertical-menu-item { + &.selected, &:hover { + color: #DD0031; + } + } + + .toc-inner ul.toc-list li.active a { + color: #DD0031; + + &:before { + background-color: #DD0031; + } + } + + .toc-inner ul.toc-list li:hover a { + color: #DD0031; + } +} diff --git a/aio/src/styles/2-modules/_modules-dir.scss b/aio/src/styles/2-modules/_modules-dir.scss index c145331196..4828b05e68 100644 --- a/aio/src/styles/2-modules/_modules-dir.scss +++ b/aio/src/styles/2-modules/_modules-dir.scss @@ -29,3 +29,4 @@ @import 'subsection'; @import 'toc'; @import 'select-menu'; + @import 'deploy-theme'; diff --git a/aio/src/testing/location.service.ts b/aio/src/testing/location.service.ts index 2656769b95..1e03a69626 100644 --- a/aio/src/testing/location.service.ts +++ b/aio/src/testing/location.service.ts @@ -10,6 +10,7 @@ export class MockLocationService { go = jasmine.createSpy('Location.go').and .callFake((url: string) => this.urlSubject.next(url)); goExternal = jasmine.createSpy('Location.goExternal'); + replace = jasmine.createSpy('Location.replace'); handleAnchorClick = jasmine.createSpy('Location.handleAnchorClick') .and.returnValue(false); // prevent click from causing a browser navigation diff --git a/aio/tools/cli-patches/ngo.patch b/aio/tools/cli-patches/ngo.patch deleted file mode 100644 index 44c74b987f..0000000000 --- a/aio/tools/cli-patches/ngo.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-12 14:30:22.000000000 -0700 -+++ node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-12 14:32:23.000000000 -0700 -@@ -68,6 +68,11 @@ - } - return { - entry: entryPoints, -+ module: { -+ rules: [ -+ {"test": /\.js$/, "use": {loader: "ngo/webpack-loader", options: { sourceMap: true }}}, -+ ] -+ }, - plugins: [ - new webpack.EnvironmentPlugin({ - 'NODE_ENV': 'production' diff --git a/aio/tools/cli-patches/patch.js b/aio/tools/cli-patches/patch.js deleted file mode 100644 index b5735eee16..0000000000 --- a/aio/tools/cli-patches/patch.js +++ /dev/null @@ -1,12 +0,0 @@ -const fs = require('fs'); -const sh = require('shelljs'); - -PATCH_LOCK = 'node_modules/@angular/cli/models/webpack-configs/.patched'; - -if (!fs.existsSync(PATCH_LOCK)) { - sh.exec('patch -p0 -i tools/cli-patches/ngo.patch'); - sh.exec('patch -p0 -i tools/cli-patches/purify.patch'); - sh.exec('patch -p0 -i tools/cli-patches/scope-hoisting.patch'); - sh.exec('patch -p0 -i tools/cli-patches/uglify-config.patch'); - sh.touch(PATCH_LOCK); -} diff --git a/aio/tools/cli-patches/purify.patch b/aio/tools/cli-patches/purify.patch deleted file mode 100644 index fbe8484169..0000000000 --- a/aio/tools/cli-patches/purify.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-11 12:10:46.000000000 -0700 -+++ node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-11 12:10:11.000000000 -0700 -@@ -73,6 +73,7 @@ - 'NODE_ENV': 'production' - }), - new webpack.HashedModuleIdsPlugin(), -+ new (require("ngo").PurifyPlugin)(), - new webpack.optimize.UglifyJsPlugin({ - mangle: { screw_ie8: true }, - compress: { screw_ie8: true, warnings: buildOptions.verbose }, diff --git a/aio/tools/cli-patches/scope-hoisting.patch b/aio/tools/cli-patches/scope-hoisting.patch deleted file mode 100644 index 15c05ce456..0000000000 --- a/aio/tools/cli-patches/scope-hoisting.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-24 15:36:43.000000000 -0700 -+++ node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-24 15:37:04.000000000 -0700 -@@ -85,6 +85,7 @@ - 'NODE_ENV': 'production' - }), - new webpack.HashedModuleIdsPlugin(), -+ new webpack.optimize.ModuleConcatenationPlugin(), - new (require("ngo").PurifyPlugin)(), - new webpack.optimize.UglifyJsPlugin({ - mangle: true, diff --git a/aio/tools/cli-patches/uglify-config.patch b/aio/tools/cli-patches/uglify-config.patch deleted file mode 100644 index 3d17d3c9d4..0000000000 --- a/aio/tools/cli-patches/uglify-config.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-24 15:36:43.000000000 -0700 -+++ node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-24 15:37:04.000000000 -0700 -@@ -82,7 +82,7 @@ - new (require("purify/purify-webpack-plugin"))(), - new webpack.optimize.UglifyJsPlugin({ - mangle: { screw_ie8: true }, -- compress: { screw_ie8: true, warnings: buildOptions.verbose }, -+ compress: { screw_ie8: true, warnings: buildOptions.verbose, pure_getters: true }, - sourceMap: buildOptions.sourcemaps, - comments: false - }) diff --git a/aio/tools/transforms/angular-base-package/post-processors/h1-checker.js b/aio/tools/transforms/angular-base-package/post-processors/h1-checker.js index ee5356b5e9..e43517e571 100644 --- a/aio/tools/transforms/angular-base-package/post-processors/h1-checker.js +++ b/aio/tools/transforms/angular-base-package/post-processors/h1-checker.js @@ -37,5 +37,5 @@ function getText(h1) { (node.properties.ariaHidden === 'true' || node.properties['aria-hidden'] === 'true') )); - return toString(cleaned); -} \ No newline at end of file + return cleaned ? toString(cleaned) : ''; +} diff --git a/aio/tools/transforms/angular-base-package/post-processors/h1-checker.spec.js b/aio/tools/transforms/angular-base-package/post-processors/h1-checker.spec.js index 00ee5490ff..4dd2537df4 100644 --- a/aio/tools/transforms/angular-base-package/post-processors/h1-checker.spec.js +++ b/aio/tools/transforms/angular-base-package/post-processors/h1-checker.spec.js @@ -70,4 +70,14 @@ describe('h1Checker postprocessor', () => { processor.$process([doc]); expect(doc.vFile.title).toEqual('What is Angular?'); }); -}); \ No newline at end of file + + it('should not break if the h1 is empty (except for an aria-hidden anchor)', () => { + const doc = { + docType: 'a', + renderedContent: ` + <h1><a aria-hidden="true"></a></h1> + ` + }; + expect(() => processor.$process([doc])).not.toThrow(); + }); +}); diff --git a/aio/yarn.lock b/aio/yarn.lock index c7be4d5000..9e32cc0b32 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -2,6 +2,15 @@ # yarn lockfile v1 +"@angular-devkit/build-optimizer@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.0.3.tgz#092bdf732b79a779ce540f9bb99d6590dd971204" + dependencies: + loader-utils "^1.1.0" + magic-string "^0.19.1" + source-map "^0.5.6" + typescript "^2.3.3" + "@angular/animations@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.3.1.tgz#1f7e0bb803efc21c608246e6765a1c647f3d1a5f" @@ -14,51 +23,54 @@ dependencies: tslib "^1.7.1" -"@angular/cli@angular/cli-builds#webpack-next": - version "1.2.0-beta.0-7c33dd4" - resolved "https://codeload.github.com/angular/cli-builds/tar.gz/b4bb5968c04c92fd816c434044e28bbfc60b296c" +"@angular/cli@1.3.0-rc.3": + version "1.3.0-rc.3" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-1.3.0-rc.3.tgz#5a6999382f956b6109d3042569659972bca38a63" dependencies: + "@angular-devkit/build-optimizer" "0.0.3" "@ngtools/json-schema" "1.1.0" - "@ngtools/webpack" "https://github.com/angular/ngtools-webpack-builds.git#7c33dd4" + "@ngtools/webpack" "1.6.0-rc.3" autoprefixer "^6.5.3" - chalk "^1.1.3" + chalk "^2.0.1" + circular-dependency-plugin "^3.0.0" common-tags "^1.3.1" + core-object "^3.1.0" css-loader "^0.28.1" cssnano "^3.10.0" - debug "^2.1.3" denodeify "^1.2.1" diff "^3.1.0" ember-cli-normalize-entity-name "^1.0.0" ember-cli-string-utils "^1.0.0" exports-loader "^0.6.3" - extract-text-webpack-plugin "^2.1.0" + extract-text-webpack-plugin "3.0.0" file-loader "^0.10.0" - fs-extra "^2.0.0" + fs-extra "^4.0.0" get-caller-file "^1.0.0" glob "^7.0.3" - html-webpack-plugin "^2.19.0" + heimdalljs "^0.2.4" + heimdalljs-logger "^0.1.9" + html-webpack-plugin "^2.29.0" inflection "^1.7.0" inquirer "^3.0.0" isbinaryfile "^3.0.0" istanbul-instrumenter-loader "^2.0.0" - json-loader "^0.5.4" + karma-source-map-support "^1.2.0" less "^2.7.2" - less-loader "^4.0.2" + less-loader "^4.0.5" license-webpack-plugin "^0.4.2" lodash "^4.11.1" memory-fs "^0.4.1" minimatch "^3.0.3" node-modules-path "^1.0.0" nopt "^4.0.1" - opn "4.0.2" + opn "~5.1.0" portfinder "~1.0.12" postcss-loader "^1.3.3" postcss-url "^5.1.2" raw-loader "^0.5.1" resolve "^1.1.7" - rimraf "^2.5.3" rsvp "^3.0.17" - rxjs "^5.0.1" + rxjs "^5.4.2" sass-loader "^6.0.3" script-loader "^0.7.0" semver "^5.1.0" @@ -68,14 +80,14 @@ stylus "^0.54.5" stylus-loader "^3.0.1" temp "0.8.3" - typescript ">=2.0.0 <2.4.0" + typescript ">=2.0.0 <2.5.0" url-loader "^0.5.7" walk-sync "^0.3.1" - webpack "3.0.0-rc.1" - webpack-dev-middleware "^1.10.2" - webpack-dev-server "~2.4.5" - webpack-merge "^2.4.0" - zone.js "^0.8.4" + webpack "~3.4.1" + webpack-dev-middleware "^1.11.0" + webpack-dev-server "~2.5.1" + webpack-merge "^4.1.0" + zone.js "^0.8.14" optionalDependencies: node-sass "^4.3.0" @@ -166,13 +178,12 @@ version "1.1.0" resolved "https://registry.yarnpkg.com/@ngtools/json-schema/-/json-schema-1.1.0.tgz#c3a0c544d62392acc2813a42c8a0dc6f58f86922" -"@ngtools/webpack@https://github.com/angular/ngtools-webpack-builds.git#7c33dd4": - version "1.4.0-7c33dd4" - resolved "https://github.com/angular/ngtools-webpack-builds.git#7c33dd4" +"@ngtools/webpack@1.6.0-rc.3": + version "1.6.0-rc.3" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.6.0-rc.3.tgz#1cc0885c5075f66ac322e68eaf256bff172dd134" dependencies: - enhanced-resolve "^3.1.0" loader-utils "^1.0.2" - magic-string "^0.19.0" + magic-string "^0.22.3" source-map "^0.5.6" "@types/jasmine@^2.5.52": @@ -280,14 +291,14 @@ ajv-keywords@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" -ajv@^4.11.2, ajv@^4.7.0, ajv@^4.9.1: +ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" dependencies: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.1.5: +ajv@^5.0.0, ajv@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.1.5.tgz#8734931b601f00d4feef7c65738d77d1b65d1f68" dependencies: @@ -353,6 +364,12 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + any-promise@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -451,7 +468,7 @@ array-flatten@1.1.1, array-flatten@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" -array-flatten@2.1.1: +array-flatten@2.1.1, array-flatten@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" @@ -549,12 +566,18 @@ async@^1.3.0, async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.1.5: +async@^2.0.0: version "2.4.0" resolved "https://registry.yarnpkg.com/async/-/async-2.4.0.tgz#4990200f18ea5b837c2cc4f8c031a6985c385611" dependencies: lodash "^4.14.0" +async@^2.1.2, async@^2.1.4, async@^2.1.5, async@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + async@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" @@ -772,6 +795,17 @@ body-parser@^1.16.1: raw-body "~2.2.0" type-is "~1.6.15" +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -899,6 +933,10 @@ buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" +buffer-indexof@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982" + buffer-shims@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" @@ -971,7 +1009,7 @@ camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" -camelcase@^4.0.0: +camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -1037,6 +1075,14 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + change-case@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.0.0.tgz#6c9c8e35f8790870a82b6b0745be8c3cbef9b081" @@ -1084,7 +1130,7 @@ character-reference-invalid@^1.0.0: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" -chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0: +chokidar@^1.4.1, chokidar@^1.6.0, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -1113,6 +1159,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: dependencies: inherits "^2.0.1" +circular-dependency-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-3.0.0.tgz#9b68692e35b0e3510998d0164b6ae5011bea5760" + circular-json@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" @@ -1236,7 +1286,7 @@ collapse-white-space@^1.0.0, collapse-white-space@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.2.tgz#9c463fb9c6d190d2dcae21a356a01bcae9eeef6d" -color-convert@^1.3.0: +color-convert@^1.3.0, color-convert@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" dependencies: @@ -1502,6 +1552,12 @@ core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" +core-object@^3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/core-object/-/core-object-3.1.3.tgz#df399b3311bdb0c909e8aae8929fc3c1c4b25880" + dependencies: + chalk "^1.1.3" + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1594,7 +1650,7 @@ cross-spawn@^4.0.0: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^5.1.0: +cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: @@ -1796,7 +1852,7 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*, debug@2, debug@2.6.7, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.6.3: +debug@*, debug@2, debug@2.6.7, debug@^2.1.1, debug@^2.2.0, debug@^2.6.3: version "2.6.7" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" dependencies: @@ -1830,6 +1886,10 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -1860,6 +1920,17 @@ del@^2.0.2, del@^2.2.0: pinkie-promise "^2.0.0" rimraf "^2.2.8" +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1992,6 +2063,23 @@ directory-encoder@^0.7.2: handlebars "^1.3.0" img-stats "^0.5.2" +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + +dns-packet@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.1.1.tgz#2369d45038af045f3898e6fa56862aed3f40296c" + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + dependencies: + buffer-indexof "^1.0.0" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -2221,14 +2309,14 @@ engine.io@1.8.3: engine.io-parser "1.3.2" ws "1.1.2" -enhanced-resolve@^3.0.0, enhanced-resolve@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec" +enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" dependencies: graceful-fs "^4.1.2" memory-fs "^0.4.0" object-assign "^4.0.1" - tapable "^0.2.5" + tapable "^0.2.7" ensure-posix-path@^1.0.0: version "1.0.2" @@ -2468,6 +2556,18 @@ execa@^0.4.0: path-key "^1.0.0" strip-eof "^1.0.0" +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exit-code@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/exit-code/-/exit-code-1.0.2.tgz#ce165811c9f117af6a5f882940b96ae7f9aecc34" @@ -2570,14 +2670,14 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" -extract-text-webpack-plugin@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.0.tgz#69315b885f876dbf96d3819f6a9f1cca7aebf159" +extract-text-webpack-plugin@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612" dependencies: - ajv "^4.11.2" - async "^2.1.2" - loader-utils "^1.0.2" - webpack-sources "^0.1.0" + async "^2.4.1" + loader-utils "^1.1.0" + schema-utils "^0.3.0" + webpack-sources "^1.0.1" extsprintf@1.0.2: version "1.0.2" @@ -2687,6 +2787,12 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -2824,13 +2930,21 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^2.0.0, fs-extra@^2.1.2: +fs-extra@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35" dependencies: graceful-fs "^4.1.2" jsonfile "^2.1.0" +fs-extra@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.0.tgz#414fb4ca2d2170ba0014159d3a8aec3303418d9e" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^3.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3128,6 +3242,10 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -3238,6 +3356,19 @@ header-case@^1.0.0: no-case "^2.2.0" upper-case "^1.1.3" +heimdalljs-logger@^0.1.9: + version "0.1.9" + resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.9.tgz#d76ada4e45b7bb6f786fc9c010a68eb2e2faf176" + dependencies: + debug "^2.2.0" + heimdalljs "^0.2.0" + +heimdalljs@^0.2.0, heimdalljs@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.5.tgz#6aa54308eee793b642cff9cf94781445f37730ac" + dependencies: + rsvp "~3.2.1" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -3298,9 +3429,9 @@ html-void-elements@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.1.tgz#f929bea267a19e3535950502ca12c159f1b559af" -html-webpack-plugin@^2.19.0: - version "2.28.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.28.0.tgz#2e7863b57e5fd48fe263303e2ffc934c3064d009" +html-webpack-plugin@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz#e987f421853d3b6938c8c4c8171842e5fd17af23" dependencies: bluebird "^3.4.7" html-minifier "^3.2.3" @@ -3535,6 +3666,12 @@ inquirer@^3.0.0: strip-ansi "^3.0.0" through "^2.3.6" +internal-ip@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" + dependencies: + meow "^3.3.0" + interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" @@ -3549,6 +3686,10 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +ip@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + ipaddr.js@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" @@ -3779,6 +3920,10 @@ is-word-character@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.0.tgz#a3a9e5ddad70c5c2ee36f4a9cfc9a53f44535247" +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -4025,6 +4170,12 @@ jsonfile@^2.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -4105,6 +4256,12 @@ karma-jasmine@^1.0.2, karma-jasmine@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf" +karma-source-map-support@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.2.0.tgz#1bf81e7bb4b089627ab352ec4179e117c406a540" + dependencies: + source-map-support "^0.4.1" + karma@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.0.tgz#6f7a1a406446fa2e187ec95398698f4cee476269" @@ -4211,9 +4368,9 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -less-loader@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.0.3.tgz#d1e6462ca2f090c11248455e14b8dda4616d0521" +less-loader@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.0.5.tgz#ae155a7406cac6acd293d785587fcff0f478c4dd" dependencies: clone "^2.1.1" loader-utils "^1.1.0" @@ -4279,6 +4436,15 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" @@ -4300,6 +4466,13 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + lodash._isnative@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash._isnative/-/lodash._isnative-2.4.1.tgz#3ea6404b784a7be836c7b57580e1cdf79b14832c" @@ -4451,12 +4624,18 @@ macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" -magic-string@^0.19.0, magic-string@^0.19.1: +magic-string@^0.19.1: version "0.19.1" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.19.1.tgz#14d768013caf2ec8fdea16a49af82fc377e75201" dependencies: vlq "^0.2.1" +magic-string@^0.22.3: + version "0.22.4" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.4.tgz#31039b4e40366395618c1d6cf8193c53917475ff" + dependencies: + vlq "^0.2.1" + make-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" @@ -4528,6 +4707,12 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -4535,7 +4720,7 @@ memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.7.0: +meow@^3.3.0, meow@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" dependencies: @@ -4680,6 +4865,17 @@ ms@^0.7.1: version "0.7.3" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + +multicast-dns@^6.0.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.1.1.tgz#6e7de86a570872ab17058adea7160bbeca814dde" + dependencies: + dns-packet "^1.0.1" + thunky "^0.1.0" + mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" @@ -4730,21 +4926,16 @@ ng-pwa-tools@^0.0.10: sha1 "^1.1.1" ts-node "^3.0.2" -ngo@angular/ngo: - version "0.0.11" - resolved "https://codeload.github.com/angular/ngo/tar.gz/09980bf1006a20963a7273467c20d28216035d16" - dependencies: - loader-utils "^1.1.0" - magic-string "^0.19.1" - source-map "^0.5.6" - typescript "^2.3.3" - no-case@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081" dependencies: lower-case "^1.1.1" +node-forge@0.6.33: + version "0.6.33" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" + node-gyp@^3.3.1: version "3.6.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.1.tgz#19561067ff185464aded478212681f47fd578cbc" @@ -4899,6 +5090,12 @@ npm-run-path@^1.0.0: dependencies: path-key "^1.0.0" +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + "npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.0.tgz#dc59bee85f64f00ed424efb2af0783df25d1c0b5" @@ -5022,6 +5219,12 @@ opn@4.0.2: object-assign "^4.0.1" pinkie-promise "^2.0.0" +opn@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.1.0.tgz#72ce2306a17dbea58ff1041853352b4a8fc77519" + dependencies: + is-wsl "^1.1.0" + optimist@0.6.x, optimist@^0.6.1, optimist@~0.6.0, optimist@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -5079,6 +5282,14 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + os-tmpdir@^1.0.0, os-tmpdir@~1.0.0, os-tmpdir@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5090,6 +5301,24 @@ osenv@0, osenv@^0.1.0, osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" + package-json@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" @@ -5221,6 +5450,10 @@ path-exists@^2.0.0: dependencies: pinkie-promise "^2.0.0" +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -5233,6 +5466,10 @@ path-key@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af" +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" @@ -5255,6 +5492,12 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + pbkdf2@^3.0.3: version "3.0.12" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.12.tgz#be36785c5067ea48d806ff923288c5f750b6b8a2" @@ -5273,6 +5516,10 @@ pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -5775,6 +6022,13 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -5783,6 +6037,14 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + readable-stream@1.0, readable-stream@~1.0.2, readable-stream@~1.0.24, readable-stream@~1.0.26: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -6138,7 +6400,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: @@ -6171,6 +6433,10 @@ rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.1.0: version "3.5.0" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.5.0.tgz#a62c573a4ae4e1dfd0697ebc6242e79c681eaa34" +rsvp@~3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a" + run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -6195,12 +6461,18 @@ rx@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" -rxjs@^5.0.1, rxjs@^5.2.0: +rxjs@^5.2.0: version "5.4.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.0.tgz#a7db14ab157f9d7aac6a56e655e7a3860d39bf26" dependencies: symbol-observable "^1.0.1" +rxjs@^5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.2.tgz#2a3236fcbf03df57bae06fd6972fd99e5c08fcf7" + dependencies: + symbol-observable "^1.0.1" + safe-buffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" @@ -6263,6 +6535,12 @@ scandirectory@^2.5.0: safefs "^3.1.2" taskgroup "^4.0.5" +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + script-loader@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/script-loader/-/script-loader-0.7.0.tgz#685dc7e7069e0dee7a92674f0ebc5b0f55baa5ec" @@ -6299,6 +6577,12 @@ selenium-webdriver@^2.53.2: ws "^1.0.1" xml2js "0.4.4" +selfsigned@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.9.1.tgz#cdda4492d70d486570f87c65546023558e1dfa5a" + dependencies: + node-forge "0.6.33" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -6527,7 +6811,7 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" -source-list-map@^0.1.7, source-list-map@~0.1.7: +source-list-map@^0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" @@ -6543,7 +6827,7 @@ source-map-loader@^0.2.0: loader-utils "~0.2.2" source-map "~0.1.33" -source-map-support@^0.4.0, source-map-support@^0.4.15, source-map-support@^0.4.2, source-map-support@~0.4.0: +source-map-support@^0.4.0, source-map-support@^0.4.1, source-map-support@^0.4.15, source-map-support@^0.4.2, source-map-support@~0.4.0: version "0.4.15" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" dependencies: @@ -6848,12 +7132,18 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" -supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-color@^3.2.3: +supports-color@^3.1.1, supports-color@^3.1.2, supports-color@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" dependencies: has-flag "^1.0.0" +supports-color@^4.0.0, supports-color@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836" + dependencies: + has-flag "^2.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -6892,9 +7182,9 @@ table@^3.7.8: slice-ansi "0.0.4" string-width "^2.0.0" -tapable@^0.2.5, tapable@~0.2.5: - version "0.2.6" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" +tapable@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.7.tgz#e46c0daacbb2b8a98b9b0cea0f4052105817ed5c" tar-pack@^3.4.0: version "3.4.0" @@ -6979,6 +7269,10 @@ through2@2.0.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" +thunky@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" + timed-out@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" @@ -7210,7 +7504,7 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@2.3.2, "typescript@>=2.0.0 <2.4.0": +typescript@2.3.2, "typescript@>=2.0.0 <2.5.0": version "2.3.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.2.tgz#f0f045e196f69a72f06b25fd3bd39d01c3ce9984" @@ -7218,7 +7512,7 @@ typescript@^2.3.3, typescript@^2.3.4: version "2.4.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" -uglify-js@^2.6, uglify-js@^2.8.27, uglify-js@~2.8.22: +uglify-js@^2.6, uglify-js@~2.8.22: version "2.8.28" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.28.tgz#e335032df9bb20dcb918f164589d5af47f38834a" dependencies: @@ -7227,6 +7521,15 @@ uglify-js@^2.6, uglify-js@^2.8.27, uglify-js@~2.8.22: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + uglify-js@^3.0.15: version "3.0.15" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.0.15.tgz#aacb323a846b234602270dead8a32441a8806f42" @@ -7246,6 +7549,14 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -7380,6 +7691,10 @@ universal-analytics@^0.3.9: request "2.x" underscore "1.x" +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -7609,12 +7924,12 @@ walkdir@^0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532" -watchpack@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87" +watchpack@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" dependencies: async "^2.1.2" - chokidar "^1.4.3" + chokidar "^1.7.0" graceful-fs "^4.1.2" watchr@^3.0.1: @@ -7670,50 +7985,47 @@ webidl-conversions@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.1.tgz#8015a17ab83e7e1b311638486ace81da6ce206a0" -webpack-dev-middleware@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.2.tgz#2e252ce1dfb020dbda1ccb37df26f30ab014dbd1" +webpack-dev-middleware@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" dependencies: memory-fs "~0.4.1" mime "^1.3.4" path-is-absolute "^1.0.0" range-parser "^1.0.3" -webpack-dev-server@~2.4.5: - version "2.4.5" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.4.5.tgz#31384ce81136be1080b4b4cde0eb9b90e54ee6cf" +webpack-dev-server@~2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.5.1.tgz#a02e726a87bb603db5d71abb7d6d2649bf10c769" dependencies: ansi-html "0.0.7" + bonjour "^3.5.0" chokidar "^1.6.0" compression "^1.5.2" connect-history-api-fallback "^1.3.0" + del "^3.0.0" express "^4.13.3" html-entities "^1.2.0" http-proxy-middleware "~0.17.4" + internal-ip "^1.2.0" opn "4.0.2" portfinder "^1.0.9" + selfsigned "^1.9.1" serve-index "^1.7.2" sockjs "0.3.18" sockjs-client "1.1.2" spdy "^3.4.1" strip-ansi "^3.0.0" supports-color "^3.1.1" - webpack-dev-middleware "^1.10.2" + webpack-dev-middleware "^1.11.0" yargs "^6.0.0" -webpack-merge@^2.4.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-2.6.1.tgz#f1d801d2c5d39f83ffec9f119240b3e3be994a1c" +webpack-merge@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.0.tgz#6ad72223b3e0b837e531e4597c199f909361511e" dependencies: lodash "^4.17.4" -webpack-sources@^0.1.0: - version "0.1.5" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750" - dependencies: - source-list-map "~0.1.7" - source-map "~0.5.3" - webpack-sources@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" @@ -7721,16 +8033,16 @@ webpack-sources@^1.0.1: source-list-map "^2.0.0" source-map "~0.5.3" -webpack@3.0.0-rc.1: - version "3.0.0-rc.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.0.0-rc.1.tgz#41319cda8040f53177ce999a5d8a333e4f264d75" +webpack@~3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.4.1.tgz#4c3f4f3fb318155a4db0cb6a36ff05c5697418f4" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" ajv "^5.1.5" ajv-keywords "^2.0.0" async "^2.1.2" - enhanced-resolve "^3.0.0" + enhanced-resolve "^3.4.0" escope "^3.6.0" interpret "^1.0.0" json-loader "^0.5.4" @@ -7741,12 +8053,12 @@ webpack@3.0.0-rc.1: mkdirp "~0.5.0" node-libs-browser "^2.0.0" source-map "^0.5.3" - supports-color "^3.1.0" - tapable "~0.2.5" - uglify-js "^2.8.27" - watchpack "^1.3.1" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" webpack-sources "^1.0.1" - yargs "^6.0.0" + yargs "^8.0.2" websocket-driver@>=0.5.1: version "0.6.5" @@ -7790,6 +8102,10 @@ which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + which@1, which@^1.2.1, which@^1.2.8, which@^1.2.9: version "1.2.14" resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" @@ -7984,6 +8300,12 @@ yargs-parser@^5.0.0: dependencies: camelcase "^3.0.0" +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + yargs@3.32.0, yargs@^3.32.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" @@ -8032,6 +8354,24 @@ yargs@^7.0.2: y18n "^3.2.1" yargs-parser "^5.0.0" +yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" @@ -8068,6 +8408,10 @@ zip-stream@~0.6.0: lodash "~3.10.1" readable-stream "~1.0.26" -zone.js@^0.8.12, zone.js@^0.8.4: +zone.js@^0.8.12: version "0.8.12" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.12.tgz#86ff5053c98aec291a0bf4bbac501d694a05cfbb" + +zone.js@^0.8.14: + version "0.8.14" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.14.tgz#0c4db24b178232274ccb43f78c99db7f3642b6cf" diff --git a/docs/TRIAGE_AND_LABELS.md b/docs/TRIAGE_AND_LABELS.md index 86736ef745..50da21211b 100644 --- a/docs/TRIAGE_AND_LABELS.md +++ b/docs/TRIAGE_AND_LABELS.md @@ -50,7 +50,7 @@ What kind of problem is this? * `type: RFC / discussion / question` * `type: bug` -* `type: chore` +* `type: docs` * `type: feature` * `type: performance` * `type: refactor` @@ -108,16 +108,31 @@ closing or reviewing PRs is a top priority ahead of other ongoing work. Every triaged PR must have a `pr_action` label assigned to it and an assignee: -* `pr_action: review` - work is complete and comment is needed from the assignee. -* `pr_action: cleanup` - more work is needed from the current assignee. -* `pr_action: discuss` - discussion is needed, to be led by the current assignee. -* `pr_action: merge` - the PR should be merged. Add this to a PR when you would like to - trigger automatic merging following a successful build. This is described in [COMMITTER.md](COMMITTER.md). +* `PR action: review` - work is complete and comment is needed from the assignee. +* `PR action: cleanup` - more work is needed from the current assignee. +* `PR action: discuss` - discussion is needed, to be led by the current assignee. +* `PR action: merge` - the PR is ready to be merged by the caretaker. In addition, PRs can have the following states: -* `pr_state: WIP` - PR is experimental or rapidly changing. Not ready for review or triage. -* `pr_state: blocked` - PR is blocked on an issue or other PR. Not ready for review or triage. +* `PR state: WIP` - PR is experimental or rapidly changing. Not ready for review or triage. +* `PR state: blocked` - PR is blocked on an issue or other PR. Not ready for review or triage. + + +## PR Target + +In our git workflow, we merge changes either to the `master` branch, the most recent patch branch (e.g. `4.3.x`), or to both. + +The decision about the target must be done by the PR author and/or reviewer. This decision is then honored when the PR is being merged. + +To communicate the target we use the following labels: + +* `PR target: master-only` +* `PR target: patch-only` +* `PR target: master & patch` +* `PR target: TBD` - the target is yet to be determined + +If a PR is missing the "PR target" label, or if the label is set to "TBD" when the PR is sent to the caretaker, the caretaker should reject the PR and request the appropriate target label to be applied before the PR is merged. ## PR Approvals diff --git a/modules/e2e_util/perf_util.ts b/modules/e2e_util/perf_util.ts index f49dd92475..d5673ff587 100644 --- a/modules/e2e_util/perf_util.ts +++ b/modules/e2e_util/perf_util.ts @@ -11,7 +11,7 @@ const yargs = require('yargs'); const nodeUuid = require('node-uuid'); import * as fs from 'fs-extra'; -import {SeleniumWebDriverAdapter, Options, JsonFileReporter, Validator, RegressionSlopeValidator, ConsoleReporter, SizeValidator, MultiReporter, MultiMetric, Runner, Provider} from '@angular/benchpress'; +import {SeleniumWebDriverAdapter, Options, JsonFileReporter, Validator, RegressionSlopeValidator, ConsoleReporter, SizeValidator, MultiReporter, MultiMetric, Runner, StaticProvider} from '@angular/benchpress'; import {readCommandLine as readE2eCommandLine, openBrowser} from './e2e_util'; let cmdArgs: {'sample-size': number, 'force-gc': boolean, 'dryrun': boolean, 'bundles': boolean}; @@ -59,7 +59,7 @@ function createBenchpressRunner(): Runner { } const resultsFolder = './dist/benchmark_results'; fs.ensureDirSync(resultsFolder); - const providers: Provider[] = [ + const providers: StaticProvider[] = [ SeleniumWebDriverAdapter.PROTRACTOR_PROVIDERS, {provide: Options.FORCE_GC, useValue: cmdArgs['force-gc']}, {provide: Options.DEFAULT_DESCRIPTION, useValue: {'runId': runId}}, JsonFileReporter.PROVIDERS, diff --git a/modules/playground/README.md b/modules/playground/README.md index 0f8e32d516..213a5faafb 100644 --- a/modules/playground/README.md +++ b/modules/playground/README.md @@ -1,6 +1,8 @@ # How to run the examples locally +``` $ cp -r ./modules/playground ./dist/all/ $ ./node_modules/.bin/tsc -p modules --emitDecoratorMetadata -w $ gulp serve $ open http://localhost:8000/all/playground/src/hello_world/index.html?bundles=false +``` \ No newline at end of file diff --git a/package.json b/package.json index f5bbab4e8b..41e326ee97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "5.0.0-beta.1", + "version": "5.0.0-beta.2", "private": true, "branchPattern": "2.0.*", "description": "Angular - a web framework for modern web apps", diff --git a/packages/animations/browser/src/dsl/animation_ast.ts b/packages/animations/browser/src/dsl/animation_ast.ts index 54e7ffec86..0a8b1309cd 100644 --- a/packages/animations/browser/src/dsl/animation_ast.ts +++ b/packages/animations/browser/src/dsl/animation_ast.ts @@ -82,6 +82,7 @@ export class AnimateAst extends Ast { export class StyleAst extends Ast { public isEmptyStep = false; + public containsDynamicStyles = false; constructor( public styles: (ɵStyleData|string)[], public easing: string|null, diff --git a/packages/animations/browser/src/dsl/animation_ast_builder.ts b/packages/animations/browser/src/dsl/animation_ast_builder.ts index d373ab145c..3fc8d50d65 100644 --- a/packages/animations/browser/src/dsl/animation_ast_builder.ts +++ b/packages/animations/browser/src/dsl/animation_ast_builder.ts @@ -8,7 +8,7 @@ import {AUTO_STYLE, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, style, ɵStyleData} from '@angular/animations'; import {getOrSetAsInMap} from '../render/shared'; -import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, copyObj, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util'; +import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, SUBSTITUTION_EXPR_START, copyObj, extractStyleParams, iteratorToArray, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util'; import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast'; import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor'; @@ -112,7 +112,35 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor { } visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst { - return new StateAst(metadata.name, this.visitStyle(metadata.styles, context)); + const styleAst = this.visitStyle(metadata.styles, context); + const astParams = (metadata.options && metadata.options.params) || null; + if (styleAst.containsDynamicStyles) { + const missingSubs = new Set<string>(); + const params = astParams || {}; + styleAst.styles.forEach(value => { + if (isObject(value)) { + const stylesObj = value as any; + Object.keys(stylesObj).forEach(prop => { + extractStyleParams(stylesObj[prop]).forEach(sub => { + if (!params.hasOwnProperty(sub)) { + missingSubs.add(sub); + } + }); + }); + } + }); + if (missingSubs.size) { + const missingSubsArr = iteratorToArray(missingSubs.values()); + context.errors.push( + `state("${metadata.name}", ...) must define default values for all the following style substitutions: ${missingSubsArr.join(', ')}`); + } + } + + const stateAst = new StateAst(metadata.name, styleAst); + if (astParams) { + stateAst.options = {params: astParams}; + } + return stateAst; } visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext): @@ -201,11 +229,12 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor { } else { styles.push(styleTuple as ɵStyleData); } - }) + }); } else { styles.push(metadata.styles); } + let containsDynamicStyles = false; let collectedEasing: string|null = null; styles.forEach(styleData => { if (isObject(styleData)) { @@ -215,9 +244,21 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor { collectedEasing = easing as string; delete styleMap['easing']; } + if (!containsDynamicStyles) { + for (let prop in styleMap) { + const value = styleMap[prop]; + if (value.toString().indexOf(SUBSTITUTION_EXPR_START) >= 0) { + containsDynamicStyles = true; + break; + } + } + } } }); - return new StyleAst(styles, collectedEasing, metadata.offset); + + const ast = new StyleAst(styles, collectedEasing, metadata.offset); + ast.containsDynamicStyles = containsDynamicStyles; + return ast; } private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void { diff --git a/packages/animations/browser/src/dsl/animation_transition_factory.ts b/packages/animations/browser/src/dsl/animation_transition_factory.ts index 390f77d32f..4eea313a8b 100644 --- a/packages/animations/browser/src/dsl/animation_transition_factory.ts +++ b/packages/animations/browser/src/dsl/animation_transition_factory.ts @@ -9,38 +9,51 @@ import {AnimationOptions, ɵStyleData} from '@angular/animations'; import {AnimationDriver} from '../render/animation_driver'; import {getOrSetAsInMap} from '../render/shared'; -import {iteratorToArray, mergeAnimationOptions} from '../util'; +import {copyObj, interpolateParams, iteratorToArray, mergeAnimationOptions} from '../util'; -import {TransitionAst} from './animation_ast'; +import {StyleAst, TransitionAst} from './animation_ast'; import {buildAnimationTimelines} from './animation_timeline_builder'; import {TransitionMatcherFn} from './animation_transition_expr'; import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction'; import {ElementInstructionMap} from './element_instruction_map'; +const EMPTY_OBJECT = {}; + export class AnimationTransitionFactory { constructor( private _triggerName: string, public ast: TransitionAst, - private _stateStyles: {[stateName: string]: ɵStyleData}) {} + private _stateStyles: {[stateName: string]: AnimationStateStyles}) {} match(currentState: any, nextState: any): boolean { return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState); } + buildStyles(stateName: string, params: {[key: string]: any}, errors: any[]) { + const backupStateStyler = this._stateStyles['*']; + const stateStyler = this._stateStyles[stateName]; + const backupStyles = backupStateStyler ? backupStateStyler.buildStyles(params, errors) : {}; + return stateStyler ? stateStyler.buildStyles(params, errors) : backupStyles; + } + build( driver: AnimationDriver, element: any, currentState: any, nextState: any, - options?: AnimationOptions, + currentOptions?: AnimationOptions, nextOptions?: AnimationOptions, subInstructions?: ElementInstructionMap): AnimationTransitionInstruction { - const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {}); + const errors: any[] = []; + + const transitionAnimationParams = this.ast.options && this.ast.options.params || EMPTY_OBJECT; + const currentAnimationParams = currentOptions && currentOptions.params || EMPTY_OBJECT; + const currentStateStyles = this.buildStyles(currentState, currentAnimationParams, errors); + const nextAnimationParams = nextOptions && nextOptions.params || EMPTY_OBJECT; + const nextStateStyles = this.buildStyles(nextState, nextAnimationParams, errors); - const backupStateStyles = this._stateStyles['*'] || {}; - const currentStateStyles = this._stateStyles[currentState] || backupStateStyles; - const nextStateStyles = this._stateStyles[nextState] || backupStateStyles; const queriedElements = new Set<any>(); const preStyleMap = new Map<any, {[prop: string]: boolean}>(); const postStyleMap = new Map<any, {[prop: string]: boolean}>(); const isRemoval = nextState === 'void'; - const errors: any[] = []; + const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}}; + const timelines = buildAnimationTimelines( driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions, subInstructions, errors); @@ -75,3 +88,31 @@ function oneOrMoreTransitionsMatch( matchFns: TransitionMatcherFn[], currentState: any, nextState: any): boolean { return matchFns.some(fn => fn(currentState, nextState)); } + +export class AnimationStateStyles { + constructor(private styles: StyleAst, private defaultParams: {[key: string]: any}) {} + + buildStyles(params: {[key: string]: any}, errors: string[]): ɵStyleData { + const finalStyles: ɵStyleData = {}; + const combinedParams = copyObj(this.defaultParams); + Object.keys(params).forEach(key => { + const value = params[key]; + if (value != null) { + combinedParams[key] = value; + } + }); + this.styles.styles.forEach(value => { + if (typeof value !== 'string') { + const styleObj = value as any; + Object.keys(styleObj).forEach(prop => { + let val = styleObj[prop]; + if (val.length > 1) { + val = interpolateParams(val, combinedParams, errors); + } + finalStyles[prop] = val; + }); + } + }); + return finalStyles; + } +} diff --git a/packages/animations/browser/src/dsl/animation_trigger.ts b/packages/animations/browser/src/dsl/animation_trigger.ts index 19cfdeaad5..9d820e25c2 100644 --- a/packages/animations/browser/src/dsl/animation_trigger.ts +++ b/packages/animations/browser/src/dsl/animation_trigger.ts @@ -7,10 +7,11 @@ */ import {ɵStyleData} from '@angular/animations'; -import {copyStyles} from '../util'; +import {copyStyles, interpolateParams} from '../util'; + +import {SequenceAst, StyleAst, TransitionAst, TriggerAst} from './animation_ast'; +import {AnimationStateStyles, AnimationTransitionFactory} from './animation_transition_factory'; -import {SequenceAst, TransitionAst, TriggerAst} from './animation_ast'; -import {AnimationTransitionFactory} from './animation_transition_factory'; /** * @experimental Animation support is experimental. @@ -25,16 +26,12 @@ export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger { export class AnimationTrigger { public transitionFactories: AnimationTransitionFactory[] = []; public fallbackTransition: AnimationTransitionFactory; - public states: {[stateName: string]: ɵStyleData} = {}; + public states: {[stateName: string]: AnimationStateStyles} = {}; constructor(public name: string, public ast: TriggerAst) { ast.states.forEach(ast => { - const obj = this.states[ast.name] = {}; - ast.style.styles.forEach(styleTuple => { - if (typeof styleTuple == 'object') { - copyStyles(styleTuple as ɵStyleData, false, obj); - } - }); + const defaultParams = (ast.options && ast.options.params) || {}; + this.states[ast.name] = new AnimationStateStyles(ast.style, defaultParams); }); balanceProperties(this.states, 'true', '1'); @@ -53,10 +50,15 @@ export class AnimationTrigger { const entry = this.transitionFactories.find(f => f.match(currentState, nextState)); return entry || null; } + + matchStyles(currentState: any, params: {[key: string]: any}, errors: any[]): ɵStyleData { + return this.fallbackTransition.buildStyles(currentState, params, errors); + } } function createFallbackTransition( - triggerName: string, states: {[stateName: string]: ɵStyleData}): AnimationTransitionFactory { + triggerName: string, + states: {[stateName: string]: AnimationStateStyles}): AnimationTransitionFactory { const matchers = [(fromState: any, toState: any) => true]; const animation = new SequenceAst([]); const transition = new TransitionAst(matchers, animation); diff --git a/packages/animations/browser/src/render/animation_engine_next.ts b/packages/animations/browser/src/render/animation_engine_next.ts index 4aa25fc0db..d7d598cbe1 100644 --- a/packages/animations/browser/src/render/animation_engine_next.ts +++ b/packages/animations/browser/src/render/animation_engine_next.ts @@ -29,8 +29,8 @@ export class AnimationEngine { this._transitionEngine = new TransitionAnimationEngine(driver, normalizer); this._timelineEngine = new TimelineAnimationEngine(driver, normalizer); - this._transitionEngine.onRemovalComplete = - (element: any, context: any) => { this.onRemovalComplete(element, context); } + this._transitionEngine.onRemovalComplete = (element: any, context: any) => + this.onRemovalComplete(element, context); } registerTrigger( diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index 1f41927a84..721f1bd75b 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -66,6 +66,8 @@ export class StateValue { public value: string; public options: AnimationOptions; + get params(): {[key: string]: any} { return this.options.params as{[key: string]: any}; } + constructor(input: any) { const isObj = input && input.hasOwnProperty('value'); const value = isObj ? input['value'] : input; @@ -213,7 +215,24 @@ export class AnimationTransitionNamespace { // The removal arc here is special cased because the same element is triggered // twice in the event that it contains animations on the outer/inner portions // of the host container - if (!isRemoval && fromState.value === toState.value) return; + if (!isRemoval && fromState.value === toState.value) { + // this means that despite the value not changing, some inner params + // have changed which means that the animation final styles need to be applied + if (!objEquals(fromState.params, toState.params)) { + const errors: any[] = []; + const fromStyles = trigger.matchStyles(fromState.value, fromState.params, errors); + const toStyles = trigger.matchStyles(toState.value, toState.params, errors); + if (errors.length) { + this._engine.reportError(errors); + } else { + this._engine.afterFlush(() => { + eraseStyles(element, fromStyles); + setStyles(element, toStyles); + }); + } + } + return; + } const playersOnElement: TransitionAnimationPlayer[] = getOrSetAsInMap(this._engine.playersByElement, element, []); @@ -490,6 +509,7 @@ export class TransitionAnimationEngine { // this method is designed to be overridden by the code that uses this engine public onRemovalComplete = (element: any, context: any) => {}; + /** @internal */ _onRemovalComplete(element: any, context: any) { this.onRemovalComplete(element, context); } constructor(public driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {} @@ -663,7 +683,7 @@ export class TransitionAnimationEngine { private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) { return entry.transition.build( this.driver, entry.element, entry.fromState.value, entry.toState.value, - entry.toState.options, subTimelines); + entry.fromState.options, entry.toState.options, subTimelines); } destroyInnerAnimations(containerElement: any) { @@ -780,6 +800,11 @@ export class TransitionAnimationEngine { } } + reportError(errors: string[]) { + throw new Error( + `Unable to process animations due to the following failed trigger transitions\n ${errors.join("\n")}`); + } + private _flushAnimations(cleanupFns: Function[], microtaskId: number): TransitionAnimationPlayer[] { const subTimelines = new ElementInstructionMap(); @@ -900,14 +925,14 @@ export class TransitionAnimationEngine { } if (erroneousTransitions.length) { - let msg = `Unable to process animations due to the following failed trigger transitions\n`; + const errors: string[] = []; erroneousTransitions.forEach(instruction => { - msg += `@${instruction.triggerName} has failed due to:\n`; - instruction.errors !.forEach(error => { msg += `- ${error}\n`; }); + errors.push(`@${instruction.triggerName} has failed due to:\n`); + instruction.errors !.forEach(error => errors.push(`- ${error}\n`)); }); allPlayers.forEach(player => player.destroy()); - throw new Error(msg); + this.reportError(errors); } // these can only be detected here since we have a map of all the elements @@ -1168,8 +1193,17 @@ export class TransitionAnimationEngine { if (details && details.removedBeforeQueried) return new NoopAnimationPlayer(); const isQueriedElement = element !== rootElement; - const previousPlayers = flattenGroupPlayers( - (allPreviousPlayersMap.get(element) || EMPTY_PLAYER_ARRAY).map(p => p.getRealPlayer())); + const previousPlayers = + flattenGroupPlayers((allPreviousPlayersMap.get(element) || EMPTY_PLAYER_ARRAY) + .map(p => p.getRealPlayer())) + .filter(p => { + // the `element` is not apart of the AnimationPlayer definition, but + // Mock/WebAnimations + // use the element within their implementation. This will be added in Angular5 to + // AnimationPlayer + const pp = p as any; + return pp.element ? pp.element === element : false; + }); const preStyles = preStylesMap.get(element); const postStyles = postStylesMap.get(element); @@ -1481,3 +1515,14 @@ function _flattenGroupPlayersRecur(players: AnimationPlayer[], finalPlayers: Ani } } } + +function objEquals(a: {[key: string]: any}, b: {[key: string]: any}): boolean { + const k1 = Object.keys(a); + const k2 = Object.keys(b); + if (k1.length != k2.length) return false; + for (let i = 0; i < k1.length; i++) { + const prop = k1[i]; + if (!b.hasOwnProperty(prop) || a[prop] !== b[prop]) return false; + } + return true; +} diff --git a/packages/animations/browser/src/util.ts b/packages/animations/browser/src/util.ts index a6dbb75f56..dbdfa4f213 100644 --- a/packages/animations/browser/src/util.ts +++ b/packages/animations/browser/src/util.ts @@ -9,6 +9,8 @@ import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleDa export const ONE_SECOND = 1000; +export const SUBSTITUTION_EXPR_START = '{{'; +export const SUBSTITUTION_EXPR_END = '}}'; export const ENTER_CLASSNAME = 'ng-enter'; export const LEAVE_CLASSNAME = 'ng-leave'; export const ENTER_SELECTOR = '.ng-enter'; @@ -151,10 +153,8 @@ export function normalizeAnimationEntry(steps: AnimationMetadata | AnimationMeta export function validateStyleParams( value: string | number, options: AnimationOptions, errors: any[]) { const params = options.params || {}; - if (typeof value !== 'string') return; - - const matches = value.toString().match(PARAM_REGEX); - if (matches) { + const matches = extractStyleParams(value); + if (matches.length) { matches.forEach(varName => { if (!params.hasOwnProperty(varName)) { errors.push( @@ -164,7 +164,22 @@ export function validateStyleParams( } } -const PARAM_REGEX = /\{\{\s*(.+?)\s*\}\}/g; +const PARAM_REGEX = + new RegExp(`${SUBSTITUTION_EXPR_START}\\s*(.+?)\\s*${SUBSTITUTION_EXPR_END}`, 'g'); +export function extractStyleParams(value: string | number): string[] { + let params: string[] = []; + if (typeof value === 'string') { + const val = value.toString(); + + let match: any; + while (match = PARAM_REGEX.exec(val)) { + params.push(match[1] as string); + } + PARAM_REGEX.lastIndex = 0; + } + return params; +} + export function interpolateParams( value: string | number, params: {[name: string]: any}, errors: any[]): string|number { const original = value.toString(); diff --git a/packages/animations/browser/test/dsl/animation_spec.ts b/packages/animations/browser/test/dsl/animation_spec.ts index d022379e12..bc6da97a92 100644 --- a/packages/animations/browser/test/dsl/animation_spec.ts +++ b/packages/animations/browser/test/dsl/animation_spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, animation, group, keyframes, query, sequence, style, transition, trigger, useAnimation, ɵStyleData} from '@angular/animations'; +import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, animation, group, keyframes, query, sequence, state, style, transition, trigger, useAnimation, ɵStyleData} from '@angular/animations'; import {AnimationOptions} from '@angular/core/src/animation/dsl'; import {Animation} from '../../src/dsl/animation'; @@ -174,6 +174,30 @@ export function main() { validateAndThrowAnimationSequence(steps2); }).toThrowError(/keyframes\(\) must be placed inside of a call to animate\(\)/); }); + + it('should throw if dynamic style substitutions are used without defaults within state() definitions', + () => { + const steps = [state('final', style({ + 'width': '{{ one }}px', + 'borderRadius': '{{ two }}px {{ three }}px', + }))]; + + expect(() => { validateAndThrowAnimationSequence(steps); }) + .toThrowError( + /state\("final", ...\) must define default values for all the following style substitutions: one, two, three/); + + const steps2 = [state( + 'panfinal', style({ + 'color': '{{ greyColor }}', + 'borderColor': '1px solid {{ greyColor }}', + 'backgroundColor': '{{ redColor }}', + }), + {params: {redColor: 'maroon'}})]; + + expect(() => { validateAndThrowAnimationSequence(steps2); }) + .toThrowError( + /state\("panfinal", ...\) must define default values for all the following style substitutions: greyColor/); + }); }); describe('keyframe building', () => { @@ -427,17 +451,17 @@ export function main() { it('should throw an error when an input variable is not provided when invoked and is not a default value', () => { - expect(() => {invokeAnimationSequence(rootElement, [style({color: '{{ color }}'})])}) + expect(() => invokeAnimationSequence(rootElement, [style({color: '{{ color }}'})])) .toThrowError(/Please provide a value for the animation param color/); expect( - () => {invokeAnimationSequence( + () => invokeAnimationSequence( rootElement, [ style({color: '{{ start }}'}), animate('{{ time }}', style({color: '{{ end }}'})), ], - buildParams({start: 'blue', end: 'red'}))}) + buildParams({start: 'blue', end: 'red'}))) .toThrowError(/Please provide a value for the animation param time/); }); }); diff --git a/packages/animations/browser/test/dsl/animation_trigger_spec.ts b/packages/animations/browser/test/dsl/animation_trigger_spec.ts index eeb200005f..9db8e2404b 100644 --- a/packages/animations/browser/test/dsl/animation_trigger_spec.ts +++ b/packages/animations/browser/test/dsl/animation_trigger_spec.ts @@ -51,12 +51,14 @@ export function main() { describe('trigger usage', () => { it('should construct a trigger based on the states and transition data', () => { const result = makeTrigger('name', [ - state('on', style({width: 0})), state('off', style({width: 100})), - transition('on => off', animate(1000)), transition('off => on', animate(1000)) + state('on', style({width: 0})), + state('off', style({width: 100})), + transition('on => off', animate(1000)), + transition('off => on', animate(1000)), ]); - expect(result.states).toEqual({'on': {width: 0}, 'off': {width: 100}}); - + expect(result.states['on'].buildStyles({}, [])).toEqual({width: 0}); + expect(result.states['off'].buildStyles({}, [])).toEqual({width: 100}); expect(result.transitionFactories.length).toEqual(2); }); @@ -66,7 +68,9 @@ export function main() { transition('off => on', animate(1000)) ]); - expect(result.states).toEqual({'on': {width: 50}, 'off': {width: 50}}); + + expect(result.states['on'].buildStyles({}, [])).toEqual({width: 50}); + expect(result.states['off'].buildStyles({}, [])).toEqual({width: 50}); }); it('should find the first transition that matches', () => { @@ -145,7 +149,7 @@ export function main() { 'a => b', [style({height: '{{ a }}'}), animate(1000, style({height: '{{ b }}'}))], buildParams({a: '100px', b: '200px'}))]); - const trans = buildTransition(result, element, 'a', 'b', buildParams({a: '300px'})) !; + const trans = buildTransition(result, element, 'a', 'b', {}, buildParams({a: '300px'})) !; const keyframes = trans.timelines[0].keyframes; expect(keyframes).toEqual([{height: '300px', offset: 0}, {height: '200px', offset: 1}]); @@ -182,7 +186,7 @@ export function main() { const trans = buildTransition(result, element, false, true) !; expect(trans.timelines[0].keyframes).toEqual([ {offset: 0, color: 'red'}, {offset: 1, color: 'green'} - ]) + ]); }); it('should match `1` and `0` state styles on a `true <=> false` boolean transition given boolean values', @@ -195,7 +199,7 @@ export function main() { const trans = buildTransition(result, element, false, true) !; expect(trans.timelines[0].keyframes).toEqual([ {offset: 0, color: 'orange'}, {offset: 1, color: 'blue'} - ]) + ]); }); describe('aliases', () => { @@ -219,11 +223,12 @@ export function main() { function buildTransition( trigger: AnimationTrigger, element: any, fromState: any, toState: any, - params?: AnimationOptions): AnimationTransitionInstruction|null { + fromOptions?: AnimationOptions, toOptions?: AnimationOptions): AnimationTransitionInstruction| + null { const trans = trigger.matchTransition(fromState, toState) !; if (trans) { const driver = new MockAnimationDriver(); - return trans.build(driver, element, fromState, toState, params) !; + return trans.build(driver, element, fromState, toState, fromOptions, toOptions) !; } return null; } diff --git a/packages/animations/src/animation_metadata.ts b/packages/animations/src/animation_metadata.ts index cabf6dd429..d0f907508b 100755 --- a/packages/animations/src/animation_metadata.ts +++ b/packages/animations/src/animation_metadata.ts @@ -105,6 +105,7 @@ export interface AnimationTriggerMetadata extends AnimationMetadata { export interface AnimationStateMetadata extends AnimationMetadata { name: string; styles: AnimationStyleMetadata; + options?: {params: {[name: string]: any}}; } /** @@ -567,8 +568,10 @@ export function style( * * @experimental Animation support is experimental. */ -export function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata { - return {type: AnimationMetadataType.State, name, styles}; +export function state( + name: string, styles: AnimationStyleMetadata, + options?: {params: {[name: string]: any}}): AnimationStateMetadata { + return {type: AnimationMetadataType.State, name, styles, options}; } /** diff --git a/packages/benchpress/index.ts b/packages/benchpress/index.ts index f91106e540..1b11272642 100644 --- a/packages/benchpress/index.ts +++ b/packages/benchpress/index.ts @@ -9,7 +9,7 @@ // Must be imported first, because Angular decorators throw on load. import 'reflect-metadata'; -export {InjectionToken, Injector, Provider, ReflectiveInjector} from '@angular/core'; +export {InjectionToken, Injector, Provider, ReflectiveInjector, StaticProvider} from '@angular/core'; export {Options} from './src/common_options'; export {MeasureValues} from './src/measure_values'; export {Metric} from './src/metric'; diff --git a/packages/benchpress/src/metric/perflog_metric.ts b/packages/benchpress/src/metric/perflog_metric.ts index 391a97ff90..f31fc6186d 100644 --- a/packages/benchpress/src/metric/perflog_metric.ts +++ b/packages/benchpress/src/metric/perflog_metric.ts @@ -20,7 +20,14 @@ import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_e export class PerflogMetric extends Metric { static SET_TIMEOUT = new InjectionToken('PerflogMetric.setTimeout'); static PROVIDERS = [ - PerflogMetric, { + { + provide: PerflogMetric, + deps: [ + WebDriverExtension, PerflogMetric.SET_TIMEOUT, Options.MICRO_METRICS, Options.FORCE_GC, + Options.CAPTURE_FRAMES, Options.RECEIVED_DATA, Options.REQUEST_COUNT + ] + }, + { provide: PerflogMetric.SET_TIMEOUT, useValue: (fn: Function, millis: number) => <any>setTimeout(fn, millis) } @@ -156,7 +163,7 @@ export class PerflogMetric extends Metric { return result; } let resolve: (result: any) => void; - const promise = new Promise(res => { resolve = res; }); + const promise = new Promise<{[key: string]: number}>(res => { resolve = res; }); this._setTimeout(() => resolve(this._readUntilEndMark(markName, loopCount + 1)), 100); return promise; }); diff --git a/packages/benchpress/src/metric/user_metric.ts b/packages/benchpress/src/metric/user_metric.ts index 19b8d57dfe..fe253d913d 100644 --- a/packages/benchpress/src/metric/user_metric.ts +++ b/packages/benchpress/src/metric/user_metric.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, StaticProvider} from '@angular/core'; import {Options} from '../common_options'; import {Metric} from '../metric'; @@ -14,7 +14,8 @@ import {WebDriverAdapter} from '../web_driver_adapter'; @Injectable() export class UserMetric extends Metric { - static PROVIDERS = [UserMetric]; + static PROVIDERS = + <StaticProvider[]>[{provide: UserMetric, deps: [Options.USER_METRICS, WebDriverAdapter]}]; constructor( @Inject(Options.USER_METRICS) private _userMetrics: {[key: string]: string}, diff --git a/packages/benchpress/src/reporter/console_reporter.ts b/packages/benchpress/src/reporter/console_reporter.ts index 72fd545175..1659640010 100644 --- a/packages/benchpress/src/reporter/console_reporter.ts +++ b/packages/benchpress/src/reporter/console_reporter.ts @@ -22,7 +22,11 @@ export class ConsoleReporter extends Reporter { static PRINT = new InjectionToken('ConsoleReporter.print'); static COLUMN_WIDTH = new InjectionToken('ConsoleReporter.columnWidth'); static PROVIDERS = [ - ConsoleReporter, {provide: ConsoleReporter.COLUMN_WIDTH, useValue: 18}, { + { + provide: ConsoleReporter, + deps: [ConsoleReporter.COLUMN_WIDTH, SampleDescription, ConsoleReporter.PRINT] + }, + {provide: ConsoleReporter.COLUMN_WIDTH, useValue: 18}, { provide: ConsoleReporter.PRINT, useValue: function(v: any) { // tslint:disable-next-line:no-console diff --git a/packages/benchpress/src/reporter/json_file_reporter.ts b/packages/benchpress/src/reporter/json_file_reporter.ts index 2a8d0fb629..aaca71e916 100644 --- a/packages/benchpress/src/reporter/json_file_reporter.ts +++ b/packages/benchpress/src/reporter/json_file_reporter.ts @@ -22,7 +22,13 @@ import {formatStats, sortedProps} from './util'; @Injectable() export class JsonFileReporter extends Reporter { static PATH = new InjectionToken('JsonFileReporter.path'); - static PROVIDERS = [JsonFileReporter, {provide: JsonFileReporter.PATH, useValue: '.'}]; + static PROVIDERS = [ + { + provide: JsonFileReporter, + deps: [SampleDescription, JsonFileReporter.PATH, Options.WRITE_FILE, Options.NOW] + }, + {provide: JsonFileReporter.PATH, useValue: '.'} + ]; constructor( private _description: SampleDescription, @Inject(JsonFileReporter.PATH) private _path: string, diff --git a/packages/benchpress/src/runner.ts b/packages/benchpress/src/runner.ts index 97a188f4d5..0d9a201f8d 100644 --- a/packages/benchpress/src/runner.ts +++ b/packages/benchpress/src/runner.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider, ReflectiveInjector} from '@angular/core'; +import {Injector, StaticProvider} from '@angular/core'; import {Options} from './common_options'; import {Metric} from './metric'; @@ -34,17 +34,17 @@ import {IOsDriverExtension} from './webdriver/ios_driver_extension'; * It provides defaults, creates the injector and calls the sampler. */ export class Runner { - constructor(private _defaultProviders: Provider[] = []) {} + constructor(private _defaultProviders: StaticProvider[] = []) {} sample({id, execute, prepare, microMetrics, providers, userMetrics}: { id: string, execute?: Function, prepare?: Function, microMetrics?: {[key: string]: string}, - providers?: Provider[], + providers?: StaticProvider[], userMetrics?: {[key: string]: string} }): Promise<SampleState> { - const sampleProviders: Provider[] = [ + const sampleProviders: StaticProvider[] = [ _DEFAULT_PROVIDERS, this._defaultProviders, {provide: Options.SAMPLE_ID, useValue: id}, {provide: Options.EXECUTE, useValue: execute} ]; @@ -61,7 +61,7 @@ export class Runner { sampleProviders.push(providers); } - const inj = ReflectiveInjector.resolveAndCreate(sampleProviders); + const inj = Injector.create(sampleProviders); const adapter: WebDriverAdapter = inj.get(WebDriverAdapter); return Promise @@ -75,7 +75,7 @@ export class Runner { // Only WebDriverAdapter is reused. // TODO vsavkin consider changing it when toAsyncFactory is added back or when child // injectors are handled better. - const injector = ReflectiveInjector.resolveAndCreate([ + const injector = Injector.create([ sampleProviders, {provide: Options.CAPABILITIES, useValue: capabilities}, {provide: Options.USER_AGENT, useValue: userAgent}, {provide: WebDriverAdapter, useValue: adapter} diff --git a/packages/benchpress/src/sampler.ts b/packages/benchpress/src/sampler.ts index beb190852c..bd9d0fc868 100644 --- a/packages/benchpress/src/sampler.ts +++ b/packages/benchpress/src/sampler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, StaticProvider} from '@angular/core'; import {Options} from './common_options'; import {MeasureValues} from './measure_values'; @@ -26,8 +26,12 @@ import {WebDriverAdapter} from './web_driver_adapter'; */ @Injectable() export class Sampler { - static PROVIDERS = [Sampler]; - + static PROVIDERS = <StaticProvider[]>[{ + provide: Sampler, + deps: [ + WebDriverAdapter, Metric, Reporter, Validator, Options.PREPARE, Options.EXECUTE, Options.NOW + ] + }]; constructor( private _driver: WebDriverAdapter, private _metric: Metric, private _reporter: Reporter, private _validator: Validator, @Inject(Options.PREPARE) private _prepare: Function, diff --git a/packages/benchpress/src/validator/regression_slope_validator.ts b/packages/benchpress/src/validator/regression_slope_validator.ts index f0e2fa0980..8f590e2b24 100644 --- a/packages/benchpress/src/validator/regression_slope_validator.ts +++ b/packages/benchpress/src/validator/regression_slope_validator.ts @@ -21,7 +21,11 @@ export class RegressionSlopeValidator extends Validator { static SAMPLE_SIZE = new InjectionToken('RegressionSlopeValidator.sampleSize'); static METRIC = new InjectionToken('RegressionSlopeValidator.metric'); static PROVIDERS = [ - RegressionSlopeValidator, {provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: 10}, + { + provide: RegressionSlopeValidator, + deps: [RegressionSlopeValidator.SAMPLE_SIZE, RegressionSlopeValidator.METRIC] + }, + {provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: 10}, {provide: RegressionSlopeValidator.METRIC, useValue: 'scriptTime'} ]; diff --git a/packages/benchpress/src/validator/size_validator.ts b/packages/benchpress/src/validator/size_validator.ts index 711eb9edbb..7d546c186a 100644 --- a/packages/benchpress/src/validator/size_validator.ts +++ b/packages/benchpress/src/validator/size_validator.ts @@ -17,7 +17,10 @@ import {Validator} from '../validator'; @Injectable() export class SizeValidator extends Validator { static SAMPLE_SIZE = new InjectionToken('SizeValidator.sampleSize'); - static PROVIDERS = [SizeValidator, {provide: SizeValidator.SAMPLE_SIZE, useValue: 10}]; + static PROVIDERS = [ + {provide: SizeValidator, deps: [SizeValidator.SAMPLE_SIZE]}, + {provide: SizeValidator.SAMPLE_SIZE, useValue: 10} + ]; constructor(@Inject(SizeValidator.SAMPLE_SIZE) private _sampleSize: number) { super(); } diff --git a/packages/benchpress/src/webdriver/chrome_driver_extension.ts b/packages/benchpress/src/webdriver/chrome_driver_extension.ts index 0cf41cb38d..d1ceba67b1 100644 --- a/packages/benchpress/src/webdriver/chrome_driver_extension.ts +++ b/packages/benchpress/src/webdriver/chrome_driver_extension.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, StaticProvider} from '@angular/core'; import {Options} from '../common_options'; import {WebDriverAdapter} from '../web_driver_adapter'; @@ -21,7 +21,10 @@ import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_e */ @Injectable() export class ChromeDriverExtension extends WebDriverExtension { - static PROVIDERS = [ChromeDriverExtension]; + static PROVIDERS = <StaticProvider>[{ + provide: ChromeDriverExtension, + deps: [WebDriverAdapter, Options.USER_AGENT] + }]; private _majorChromeVersion: number; private _firstRun = true; diff --git a/packages/benchpress/src/webdriver/firefox_driver_extension.ts b/packages/benchpress/src/webdriver/firefox_driver_extension.ts index 0c3c360632..2d34742505 100644 --- a/packages/benchpress/src/webdriver/firefox_driver_extension.ts +++ b/packages/benchpress/src/webdriver/firefox_driver_extension.ts @@ -13,7 +13,7 @@ import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_e @Injectable() export class FirefoxDriverExtension extends WebDriverExtension { - static PROVIDERS = [FirefoxDriverExtension]; + static PROVIDERS = [{provide: FirefoxDriverExtension, deps: [WebDriverAdapter]}]; private _profilerStarted: boolean; @@ -40,7 +40,7 @@ export class FirefoxDriverExtension extends WebDriverExtension { return this._driver.executeScript(script); } - readPerfLog(): Promise<PerfLogEvent> { + readPerfLog(): Promise<PerfLogEvent[]> { return this._driver.executeAsyncScript('var cb = arguments[0]; window.getProfile(cb);'); } diff --git a/packages/benchpress/src/webdriver/ios_driver_extension.ts b/packages/benchpress/src/webdriver/ios_driver_extension.ts index 8ceb3e2eeb..fefef6314c 100644 --- a/packages/benchpress/src/webdriver/ios_driver_extension.ts +++ b/packages/benchpress/src/webdriver/ios_driver_extension.ts @@ -13,7 +13,7 @@ import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_e @Injectable() export class IOsDriverExtension extends WebDriverExtension { - static PROVIDERS = [IOsDriverExtension]; + static PROVIDERS = [{provide: IOsDriverExtension, deps: [WebDriverAdapter]}]; constructor(private _driver: WebDriverAdapter) { super(); } diff --git a/packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts b/packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts index efb1d91f4b..4aa0ad27e0 100644 --- a/packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts +++ b/packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts @@ -6,15 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ +import {StaticProvider} from '@angular/core'; + import {WebDriverAdapter} from '../web_driver_adapter'; + /** * Adapter for the selenium-webdriver. */ export class SeleniumWebDriverAdapter extends WebDriverAdapter { - static PROTRACTOR_PROVIDERS = [{ + static PROTRACTOR_PROVIDERS = <StaticProvider[]>[{ provide: WebDriverAdapter, - useFactory: () => new SeleniumWebDriverAdapter((<any>global).browser) + useFactory: () => new SeleniumWebDriverAdapter((<any>global).browser), + deps: [] }]; constructor(private _driver: any) { super(); } diff --git a/packages/benchpress/test/metric/multi_metric_spec.ts b/packages/benchpress/test/metric/multi_metric_spec.ts index 53f2892bbd..3d8ff95467 100644 --- a/packages/benchpress/test/metric/multi_metric_spec.ts +++ b/packages/benchpress/test/metric/multi_metric_spec.ts @@ -7,12 +7,13 @@ */ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {Metric, MultiMetric, ReflectiveInjector} from '../../index'; + +import {Injector, Metric, MultiMetric} from '../../index'; export function main() { function createMetric(ids: any[]) { - const m = ReflectiveInjector - .resolveAndCreate([ + const m = Injector + .create([ ids.map(id => ({provide: id, useValue: new MockMetric(id)})), MultiMetric.provideWith(ids) ]) diff --git a/packages/benchpress/test/metric/perflog_metric_spec.ts b/packages/benchpress/test/metric/perflog_metric_spec.ts index 66b81cc160..68822092f9 100644 --- a/packages/benchpress/test/metric/perflog_metric_spec.ts +++ b/packages/benchpress/test/metric/perflog_metric_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider} from '@angular/core'; +import {StaticProvider} from '@angular/core'; import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {Metric, Options, PerfLogEvent, PerfLogFeatures, PerflogMetric, ReflectiveInjector, WebDriverExtension} from '../../index'; +import {Injector, Metric, Options, PerfLogEvent, PerfLogFeatures, PerflogMetric, WebDriverExtension} from '../../index'; import {TraceEventFactory} from '../trace_event_factory'; export function main() { @@ -33,7 +33,7 @@ export function main() { if (!microMetrics) { microMetrics = {}; } - const providers: Provider[] = [ + const providers: StaticProvider[] = [ Options.DEFAULT_PROVIDERS, PerflogMetric.PROVIDERS, {provide: Options.MICRO_METRICS, useValue: microMetrics}, { provide: PerflogMetric.SET_TIMEOUT, @@ -59,7 +59,7 @@ export function main() { if (requestCount != null) { providers.push({provide: Options.REQUEST_COUNT, useValue: requestCount}); } - return ReflectiveInjector.resolveAndCreate(providers).get(PerflogMetric); + return Injector.create(providers).get(PerflogMetric); } describe('perflog metric', () => { diff --git a/packages/benchpress/test/metric/user_metric_spec.ts b/packages/benchpress/test/metric/user_metric_spec.ts index fe0c382efd..6884620b68 100644 --- a/packages/benchpress/test/metric/user_metric_spec.ts +++ b/packages/benchpress/test/metric/user_metric_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider, ReflectiveInjector} from '@angular/core'; +import {Injector, StaticProvider} from '@angular/core'; import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; import {Options, PerfLogEvent, PerfLogFeatures, UserMetric, WebDriverAdapter} from '../../index'; @@ -25,12 +25,12 @@ export function main() { userMetrics = {}; } wdAdapter = new MockDriverAdapter(); - const providers: Provider[] = [ + const providers: StaticProvider[] = [ Options.DEFAULT_PROVIDERS, UserMetric.PROVIDERS, {provide: Options.USER_METRICS, useValue: userMetrics}, {provide: WebDriverAdapter, useValue: wdAdapter} ]; - return ReflectiveInjector.resolveAndCreate(providers).get(UserMetric); + return Injector.create(providers).get(UserMetric); } describe('user metric', () => { diff --git a/packages/benchpress/test/reporter/console_reporter_spec.ts b/packages/benchpress/test/reporter/console_reporter_spec.ts index bfe30e7547..7e3c15b6cb 100644 --- a/packages/benchpress/test/reporter/console_reporter_spec.ts +++ b/packages/benchpress/test/reporter/console_reporter_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider} from '@angular/core'; +import {StaticProvider} from '@angular/core'; import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; -import {ConsoleReporter, MeasureValues, ReflectiveInjector, SampleDescription} from '../../index'; +import {ConsoleReporter, Injector, MeasureValues, SampleDescription} from '../../index'; export function main() { describe('console reporter', () => { @@ -30,7 +30,7 @@ export function main() { if (sampleId == null) { sampleId = 'null'; } - const providers: Provider[] = [ + const providers: StaticProvider[] = [ ConsoleReporter.PROVIDERS, { provide: SampleDescription, useValue: new SampleDescription(sampleId, descriptions, metrics !) @@ -40,7 +40,7 @@ export function main() { if (columnWidth != null) { providers.push({provide: ConsoleReporter.COLUMN_WIDTH, useValue: columnWidth}); } - reporter = ReflectiveInjector.resolveAndCreate(providers).get(ConsoleReporter); + reporter = Injector.create(providers).get(ConsoleReporter); } it('should print the sample id, description and table header', () => { diff --git a/packages/benchpress/test/reporter/json_file_reporter_spec.ts b/packages/benchpress/test/reporter/json_file_reporter_spec.ts index 27c4b25584..cbdbf5d9d4 100644 --- a/packages/benchpress/test/reporter/json_file_reporter_spec.ts +++ b/packages/benchpress/test/reporter/json_file_reporter_spec.ts @@ -8,7 +8,7 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {JsonFileReporter, MeasureValues, Options, ReflectiveInjector, SampleDescription} from '../../index'; +import {Injector, JsonFileReporter, MeasureValues, Options, SampleDescription} from '../../index'; export function main() { describe('file reporter', () => { @@ -34,7 +34,7 @@ export function main() { } } ]; - return ReflectiveInjector.resolveAndCreate(providers).get(JsonFileReporter); + return Injector.create(providers).get(JsonFileReporter); } it('should write all data into a file', diff --git a/packages/benchpress/test/reporter/multi_reporter_spec.ts b/packages/benchpress/test/reporter/multi_reporter_spec.ts index 1ee54f2cde..aa1502e36b 100644 --- a/packages/benchpress/test/reporter/multi_reporter_spec.ts +++ b/packages/benchpress/test/reporter/multi_reporter_spec.ts @@ -8,12 +8,12 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {MeasureValues, MultiReporter, ReflectiveInjector, Reporter} from '../../index'; +import {Injector, MeasureValues, MultiReporter, Reporter} from '../../index'; export function main() { function createReporters(ids: any[]) { - const r = ReflectiveInjector - .resolveAndCreate([ + const r = Injector + .create([ ids.map(id => ({provide: id, useValue: new MockReporter(id)})), MultiReporter.provideWith(ids) ]) diff --git a/packages/benchpress/test/runner_spec.ts b/packages/benchpress/test/runner_spec.ts index 7c4ee88847..27683730c5 100644 --- a/packages/benchpress/test/runner_spec.ts +++ b/packages/benchpress/test/runner_spec.ts @@ -8,11 +8,11 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {Injector, Metric, Options, ReflectiveInjector, Runner, SampleDescription, SampleState, Sampler, Validator, WebDriverAdapter} from '../index'; +import {Injector, Metric, Options, Runner, SampleDescription, SampleState, Sampler, Validator, WebDriverAdapter} from '../index'; export function main() { describe('runner', () => { - let injector: ReflectiveInjector; + let injector: Injector; let runner: Runner; function createRunner(defaultProviders?: any[]): Runner { @@ -22,7 +22,7 @@ export function main() { runner = new Runner([ defaultProviders, { provide: Sampler, - useFactory: (_injector: ReflectiveInjector) => { + useFactory: (_injector: Injector) => { injector = _injector; return new MockSampler(); }, diff --git a/packages/benchpress/test/sampler_spec.ts b/packages/benchpress/test/sampler_spec.ts index e7e8339ee7..82ca5d6d64 100644 --- a/packages/benchpress/test/sampler_spec.ts +++ b/packages/benchpress/test/sampler_spec.ts @@ -8,7 +8,7 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {MeasureValues, Metric, Options, ReflectiveInjector, Reporter, Sampler, Validator, WebDriverAdapter} from '../index'; +import {Injector, MeasureValues, Metric, Options, Reporter, Sampler, Validator, WebDriverAdapter} from '../index'; export function main() { const EMPTY_EXECUTE = () => {}; @@ -44,7 +44,7 @@ export function main() { providers.push({provide: Options.PREPARE, useValue: prepare}); } - sampler = ReflectiveInjector.resolveAndCreate(providers).get(Sampler); + sampler = Injector.create(providers).get(Sampler); } it('should call the prepare and execute callbacks using WebDriverAdapter.waitFor', diff --git a/packages/benchpress/test/validator/regression_slope_validator_spec.ts b/packages/benchpress/test/validator/regression_slope_validator_spec.ts index 75be174bef..3b8c3e02ce 100644 --- a/packages/benchpress/test/validator/regression_slope_validator_spec.ts +++ b/packages/benchpress/test/validator/regression_slope_validator_spec.ts @@ -8,15 +8,15 @@ import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; -import {MeasureValues, ReflectiveInjector, RegressionSlopeValidator} from '../../index'; +import {Injector, MeasureValues, RegressionSlopeValidator} from '../../index'; export function main() { describe('regression slope validator', () => { let validator: RegressionSlopeValidator; function createValidator({size, metric}: {size: number, metric: string}) { - validator = ReflectiveInjector - .resolveAndCreate([ + validator = Injector + .create([ RegressionSlopeValidator.PROVIDERS, {provide: RegressionSlopeValidator.METRIC, useValue: metric}, {provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: size} diff --git a/packages/benchpress/test/validator/size_validator_spec.ts b/packages/benchpress/test/validator/size_validator_spec.ts index 99bc4b9951..da5a14ad57 100644 --- a/packages/benchpress/test/validator/size_validator_spec.ts +++ b/packages/benchpress/test/validator/size_validator_spec.ts @@ -8,7 +8,7 @@ import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; -import {MeasureValues, ReflectiveInjector, SizeValidator} from '../../index'; +import {Injector, MeasureValues, SizeValidator} from '../../index'; export function main() { describe('size validator', () => { @@ -16,8 +16,8 @@ export function main() { function createValidator(size: number) { validator = - ReflectiveInjector - .resolveAndCreate( + Injector + .create( [SizeValidator.PROVIDERS, {provide: SizeValidator.SAMPLE_SIZE, useValue: size}]) .get(SizeValidator); } diff --git a/packages/benchpress/test/web_driver_extension_spec.ts b/packages/benchpress/test/web_driver_extension_spec.ts index 37fdab29ab..178f851262 100644 --- a/packages/benchpress/test/web_driver_extension_spec.ts +++ b/packages/benchpress/test/web_driver_extension_spec.ts @@ -8,14 +8,14 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {Options, ReflectiveInjector, WebDriverExtension} from '../index'; +import {Injector, Options, WebDriverExtension} from '../index'; export function main() { function createExtension(ids: any[], caps: any) { return new Promise<any>((res, rej) => { try { - res(ReflectiveInjector - .resolveAndCreate([ + res(Injector + .create([ ids.map((id) => ({provide: id, useValue: new MockExtension(id)})), {provide: Options.CAPABILITIES, useValue: caps}, WebDriverExtension.provideFirstSupported(ids) diff --git a/packages/benchpress/test/webdriver/chrome_driver_extension_spec.ts b/packages/benchpress/test/webdriver/chrome_driver_extension_spec.ts index f12d8a8bd6..ebb17e3d9b 100644 --- a/packages/benchpress/test/webdriver/chrome_driver_extension_spec.ts +++ b/packages/benchpress/test/webdriver/chrome_driver_extension_spec.ts @@ -8,7 +8,7 @@ import {AsyncTestCompleter, describe, expect, iit, inject, it} from '@angular/core/testing/src/testing_internal'; -import {ChromeDriverExtension, Options, ReflectiveInjector, WebDriverAdapter, WebDriverExtension} from '../../index'; +import {ChromeDriverExtension, Injector, Options, WebDriverAdapter, WebDriverExtension} from '../../index'; import {TraceEventFactory} from '../trace_event_factory'; export function main() { @@ -41,8 +41,8 @@ export function main() { userAgent = CHROME45_USER_AGENT; } log = []; - extension = ReflectiveInjector - .resolveAndCreate([ + extension = Injector + .create([ ChromeDriverExtension.PROVIDERS, { provide: WebDriverAdapter, useValue: new MockDriverAdapter(log, perfRecords, messageMethod) diff --git a/packages/benchpress/test/webdriver/ios_driver_extension_spec.ts b/packages/benchpress/test/webdriver/ios_driver_extension_spec.ts index a91fc2982d..7ff0cd6b13 100644 --- a/packages/benchpress/test/webdriver/ios_driver_extension_spec.ts +++ b/packages/benchpress/test/webdriver/ios_driver_extension_spec.ts @@ -8,7 +8,7 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {IOsDriverExtension, ReflectiveInjector, WebDriverAdapter, WebDriverExtension} from '../../index'; +import {IOsDriverExtension, Injector, WebDriverAdapter, WebDriverExtension} from '../../index'; import {TraceEventFactory} from '../trace_event_factory'; export function main() { @@ -24,8 +24,8 @@ export function main() { } log = []; extension = - ReflectiveInjector - .resolveAndCreate([ + Injector + .create([ IOsDriverExtension.PROVIDERS, {provide: WebDriverAdapter, useValue: new MockDriverAdapter(log, perfRecords)} ]) diff --git a/packages/common/http/src/request.ts b/packages/common/http/src/request.ts index 7673e71f0e..a77da15f7d 100644 --- a/packages/common/http/src/request.ts +++ b/packages/common/http/src/request.ts @@ -15,8 +15,11 @@ import {HttpParams} from './params'; * All values are optional and will override default values if provided. */ interface HttpRequestInit { - headers?: HttpHeaders, reportProgress?: boolean, params?: HttpParams, - responseType?: 'arraybuffer'|'blob'|'json'|'text', withCredentials?: boolean, + headers?: HttpHeaders; + reportProgress?: boolean; + params?: HttpParams; + responseType?: 'arraybuffer'|'blob'|'json'|'text'; + withCredentials?: boolean; } /** diff --git a/packages/common/http/src/response.ts b/packages/common/http/src/response.ts index 0850505cc5..a56e4da14d 100644 --- a/packages/common/http/src/response.ts +++ b/packages/common/http/src/response.ts @@ -120,7 +120,10 @@ export interface HttpUserEvent<T> { type: HttpEventType.User; } * * @experimental */ -export interface HttpJsonParseError { error: Error, text: string, } +export interface HttpJsonParseError { + error: Error; + text: string; +} /** * Union type for all possible events on the response stream. @@ -233,7 +236,7 @@ export class HttpHeaderResponse extends HttpResponseBase { status: update.status !== undefined ? update.status : this.status, statusText: update.statusText || this.statusText, url: update.url || this.url || undefined, - }) + }); } } diff --git a/packages/common/http/src/xhr.ts b/packages/common/http/src/xhr.ts index 8f35a98ffe..88ad2e2658 100644 --- a/packages/common/http/src/xhr.ts +++ b/packages/common/http/src/xhr.ts @@ -268,27 +268,26 @@ export class HttpXhrBackend implements HttpBackend { // The upload progress event handler, which is only registered if // progress events are enabled. - const onUpProgress = - (event: ProgressEvent) => { - // Upload progress events are simpler. Begin building the progress - // event. - let progress: HttpUploadProgressEvent = { - type: HttpEventType.UploadProgress, - loaded: event.loaded, - }; + const onUpProgress = (event: ProgressEvent) => { + // Upload progress events are simpler. Begin building the progress + // event. + let progress: HttpUploadProgressEvent = { + type: HttpEventType.UploadProgress, + loaded: event.loaded, + }; - // If the total number of bytes being uploaded is available, include - // it. - if (event.lengthComputable) { - progress.total = event.total; - } + // If the total number of bytes being uploaded is available, include + // it. + if (event.lengthComputable) { + progress.total = event.total; + } - // Send the event. - observer.next(progress); - } + // Send the event. + observer.next(progress); + }; - // By default, register for load and error events. - xhr.addEventListener('load', onLoad); + // By default, register for load and error events. + xhr.addEventListener('load', onLoad); xhr.addEventListener('error', onError); // Progress events are only enabled if requested. diff --git a/packages/common/http/test/jsonp_spec.ts b/packages/common/http/test/jsonp_spec.ts index 1a7bb49254..141cd544db 100644 --- a/packages/common/http/test/jsonp_spec.ts +++ b/packages/common/http/test/jsonp_spec.ts @@ -56,11 +56,11 @@ export function main() { }); describe('throws an error', () => { it('when request method is not JSONP', - () => {expect(() => backend.handle(SAMPLE_REQ.clone<never>({method: 'GET'}))) - .toThrowError(JSONP_ERR_WRONG_METHOD)}); + () => expect(() => backend.handle(SAMPLE_REQ.clone<never>({method: 'GET'}))) + .toThrowError(JSONP_ERR_WRONG_METHOD)); it('when response type is not json', - () => {expect(() => backend.handle(SAMPLE_REQ.clone<never>({responseType: 'text'}))) - .toThrowError(JSONP_ERR_WRONG_RESPONSE_TYPE)}); + () => expect(() => backend.handle(SAMPLE_REQ.clone<never>({responseType: 'text'}))) + .toThrowError(JSONP_ERR_WRONG_RESPONSE_TYPE)); it('when callback is never called', (done: DoneFn) => { backend.handle(SAMPLE_REQ).subscribe(undefined, (err: HttpErrorResponse) => { expect(err.status).toBe(0); @@ -69,7 +69,7 @@ export function main() { done(); }); document.mockLoad(); - }) + }); }); }); } diff --git a/packages/common/http/test/params_spec.ts b/packages/common/http/test/params_spec.ts index 27c2d83d76..8bc167836b 100644 --- a/packages/common/http/test/params_spec.ts +++ b/packages/common/http/test/params_spec.ts @@ -13,7 +13,7 @@ export function main() { describe('initialization', () => { it('should be empty at construction', () => { const body = new HttpParams(); - expect(body.toString()).toEqual('') + expect(body.toString()).toEqual(''); }); it('should parse an existing url', () => { diff --git a/packages/common/http/test/xhr_spec.ts b/packages/common/http/test/xhr_spec.ts index cdc3162a95..fc9dff6422 100644 --- a/packages/common/http/test/xhr_spec.ts +++ b/packages/common/http/test/xhr_spec.ts @@ -271,7 +271,7 @@ export function main() { done(); }); factory.mock.mockFlush(200, 'OK', 'Test'); - }) + }); }); describe('corrects for quirks', () => { it('by normalizing 1223 status to 204', (done: DoneFn) => { diff --git a/packages/common/http/test/xsrf_spec.ts b/packages/common/http/test/xsrf_spec.ts index f59b7df84d..1c5c235432 100644 --- a/packages/common/http/test/xsrf_spec.ts +++ b/packages/common/http/test/xsrf_spec.ts @@ -64,7 +64,7 @@ export function main() { }); describe('HttpXsrfCookieExtractor', () => { let document: {[key: string]: string}; - let extractor: HttpXsrfCookieExtractor + let extractor: HttpXsrfCookieExtractor; beforeEach(() => { document = { cookie: 'XSRF-TOKEN=test', diff --git a/packages/common/http/testing/src/backend.ts b/packages/common/http/testing/src/backend.ts index 1cb5e2b125..3de345b984 100644 --- a/packages/common/http/testing/src/backend.ts +++ b/packages/common/http/testing/src/backend.ts @@ -126,7 +126,7 @@ export class HttpClientTestingBackend implements HttpBackend, HttpTestingControl const requests = open.map(testReq => { const url = testReq.request.urlWithParams.split('?')[0]; const method = testReq.request.method; - return `${method} ${url}` + return `${method} ${url}`; }) .join(', '); throw new Error(`Expected no open requests, found ${open.length}: ${requests}`); diff --git a/packages/common/src/directives/ng_component_outlet.ts b/packages/common/src/directives/ng_component_outlet.ts index 34e32c8e71..3c018a7803 100644 --- a/packages/common/src/directives/ng_component_outlet.ts +++ b/packages/common/src/directives/ng_component_outlet.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, NgModuleFactory, NgModuleRef, OnChanges, OnDestroy, Provider, SimpleChanges, Type, ViewContainerRef} from '@angular/core'; +import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, NgModuleFactory, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, StaticProvider, Type, ViewContainerRef} from '@angular/core'; + /** * Instantiates a single {@link Component} type and inserts its Host View into current View. diff --git a/packages/common/src/directives/ng_template_outlet.ts b/packages/common/src/directives/ng_template_outlet.ts index ccfa8ecee4..800eed23ec 100644 --- a/packages/common/src/directives/ng_template_outlet.ts +++ b/packages/common/src/directives/ng_template_outlet.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; +import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; /** * @ngModule CommonModule @@ -49,13 +49,58 @@ export class NgTemplateOutlet implements OnChanges { set ngOutletContext(context: Object) { this.ngTemplateOutletContext = context; } ngOnChanges(changes: SimpleChanges) { - if (this._viewRef) { - this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef)); - } + const recreateView = this._shouldRecreateView(changes); - if (this.ngTemplateOutlet) { - this._viewRef = this._viewContainerRef.createEmbeddedView( - this.ngTemplateOutlet, this.ngTemplateOutletContext); + if (recreateView) { + if (this._viewRef) { + this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef)); + } + + if (this.ngTemplateOutlet) { + this._viewRef = this._viewContainerRef.createEmbeddedView( + this.ngTemplateOutlet, this.ngTemplateOutletContext); + } + } else { + if (this._viewRef && this.ngTemplateOutletContext) { + this._updateExistingContext(this.ngTemplateOutletContext); + } + } + } + + /** + * We need to re-create existing embedded view if: + * - templateRef has changed + * - context has changes + * + * To mark context object as changed when the corresponding object + * shape changes (new properties are added or existing properties are removed). + * In other words we consider context with the same properties as "the same" even + * if object reference changes (see https://github.com/angular/angular/issues/13407). + */ + private _shouldRecreateView(changes: SimpleChanges): boolean { + const ctxChange = changes['ngTemplateOutletContext']; + return !!changes['ngTemplateOutlet'] || (ctxChange && this._hasContextShapeChanged(ctxChange)); + } + + private _hasContextShapeChanged(ctxChange: SimpleChange): boolean { + const prevCtxKeys = Object.keys(ctxChange.previousValue || {}); + const currCtxKeys = Object.keys(ctxChange.currentValue || {}); + + if (prevCtxKeys.length === currCtxKeys.length) { + for (let propName of currCtxKeys) { + if (prevCtxKeys.indexOf(propName) === -1) { + return true; + } + } + return false; + } else { + return true; + } + } + + private _updateExistingContext(ctx: Object): void { + for (let propName of Object.keys(ctx)) { + (<any>this._viewRef.context)[propName] = (<any>this.ngTemplateOutletContext)[propName]; } } } diff --git a/packages/common/src/pipes/date_pipe.ts b/packages/common/src/pipes/date_pipe.ts index 12a5aa7340..aec4cee835 100644 --- a/packages/common/src/pipes/date_pipe.ts +++ b/packages/common/src/pipes/date_pipe.ts @@ -43,8 +43,8 @@ const ISO8601_DATE_REGEX = * | month | M | L (S) | MMM (Sep) | MMMM (September) | M (9) | MM (09) | * | day | d | - | - | - | d (3) | dd (03) | * | weekday | E | E (S) | EEE (Sun) | EEEE (Sunday) | - | - | - * | hour | j | - | - | - | j (13) | jj (13) | - * | hour12 | h | - | - | - | h (1 PM) | hh (01 PM)| + * | hour | j | - | - | - | j (1 PM) | jj (1 PM) | + * | hour12 | h | - | - | - | h (1) | hh (01) | * | hour24 | H | - | - | - | H (13) | HH (13) | * | minute | m | - | - | - | m (5) | mm (05) | * | second | s | - | - | - | s (9) | ss (09) | diff --git a/packages/common/test/directives/ng_component_outlet_spec.ts b/packages/common/test/directives/ng_component_outlet_spec.ts index 0d897a97c1..45f2c5e8b3 100644 --- a/packages/common/test/directives/ng_component_outlet_spec.ts +++ b/packages/common/test/directives/ng_component_outlet_spec.ts @@ -8,7 +8,7 @@ import {CommonModule} from '@angular/common'; import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_outlet'; -import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, Optional, Provider, QueryList, ReflectiveInjector, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; +import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, Optional, QueryList, StaticProvider, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {TestBed, async, fakeAsync} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -96,7 +96,7 @@ export function main() { const uniqueValue = {}; fixture.componentInstance.currentComponent = InjectedComponent; - fixture.componentInstance.injector = ReflectiveInjector.resolveAndCreate( + fixture.componentInstance.injector = Injector.create( [{provide: TEST_TOKEN, useValue: uniqueValue}], fixture.componentRef.injector); fixture.detectChanges(); diff --git a/packages/common/test/directives/ng_template_outlet_spec.ts b/packages/common/test/directives/ng_template_outlet_spec.ts index 8f1d25ac95..13135629cd 100644 --- a/packages/common/test/directives/ng_template_outlet_spec.ts +++ b/packages/common/test/directives/ng_template_outlet_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Component, ContentChildren, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef} from '@angular/core'; +import {Component, ContentChildren, Directive, Injectable, NO_ERRORS_SCHEMA, OnDestroy, QueryList, TemplateRef} from '@angular/core'; import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -26,11 +26,9 @@ export function main() { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [ - TestComponent, - CaptureTplRefs, - ], + declarations: [TestComponent, CaptureTplRefs, DestroyableCmpt], imports: [CommonModule], + providers: [DestroyedSpyService] }); }); @@ -125,9 +123,105 @@ export function main() { fixture.componentInstance.context = {shawshank: 'was here'}; detectChangesAndExpectText('was here'); })); + + it('should update but not destroy embedded view when context values change', () => { + const template = + `<ng-template let-foo="foo" #tpl><destroyable-cmpt></destroyable-cmpt>:{{foo}}</ng-template>` + + `<ng-template [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="{foo: value}"></ng-template>`; + + fixture = createTestComponent(template); + const spyService = fixture.debugElement.injector.get(DestroyedSpyService); + + detectChangesAndExpectText('Content to destroy:bar'); + expect(spyService.destroyed).toBeFalsy(); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText('Content to destroy:baz'); + expect(spyService.destroyed).toBeFalsy(); + }); + + it('should recreate embedded view when context shape changes', () => { + const template = + `<ng-template let-foo="foo" #tpl><destroyable-cmpt></destroyable-cmpt>:{{foo}}</ng-template>` + + `<ng-template [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="context"></ng-template>`; + + fixture = createTestComponent(template); + const spyService = fixture.debugElement.injector.get(DestroyedSpyService); + + detectChangesAndExpectText('Content to destroy:bar'); + expect(spyService.destroyed).toBeFalsy(); + + fixture.componentInstance.context = {foo: 'baz', other: true}; + detectChangesAndExpectText('Content to destroy:baz'); + expect(spyService.destroyed).toBeTruthy(); + }); + + it('should destroy embedded view when context value changes and templateRef becomes undefined', + () => { + const template = + `<ng-template let-foo="foo" #tpl><destroyable-cmpt></destroyable-cmpt>:{{foo}}</ng-template>` + + `<ng-template [ngTemplateOutlet]="value === 'bar' ? tpl : undefined" [ngTemplateOutletContext]="{foo: value}"></ng-template>`; + + fixture = createTestComponent(template); + const spyService = fixture.debugElement.injector.get(DestroyedSpyService); + + detectChangesAndExpectText('Content to destroy:bar'); + expect(spyService.destroyed).toBeFalsy(); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText(''); + expect(spyService.destroyed).toBeTruthy(); + }); + + it('should not try to update null / undefined context when context changes but template stays the same', + () => { + const template = `<ng-template let-foo="foo" #tpl>{{foo}}</ng-template>` + + `<ng-template [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="value === 'bar' ? null : undefined"></ng-template>`; + + fixture = createTestComponent(template); + detectChangesAndExpectText(''); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText(''); + }); + + it('should not try to update null / undefined context when template changes', () => { + const template = `<ng-template let-foo="foo" #tpl1>{{foo}}</ng-template>` + + `<ng-template let-foo="foo" #tpl2>{{foo}}</ng-template>` + + `<ng-template [ngTemplateOutlet]="value === 'bar' ? tpl1 : tpl2" [ngTemplateOutletContext]="value === 'bar' ? null : undefined"></ng-template>`; + + fixture = createTestComponent(template); + detectChangesAndExpectText(''); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText(''); + }); + + it('should not try to update context on undefined view', () => { + const template = `<ng-template let-foo="foo" #tpl>{{foo}}</ng-template>` + + `<ng-template [ngTemplateOutlet]="value === 'bar' ? null : undefined" [ngTemplateOutletContext]="{foo: value}"></ng-template>`; + + fixture = createTestComponent(template); + detectChangesAndExpectText(''); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText(''); + }); }); } +@Injectable() +class DestroyedSpyService { + destroyed = false; +} + +@Component({selector: 'destroyable-cmpt', template: 'Content to destroy'}) +class DestroyableCmpt implements OnDestroy { + constructor(private _spyService: DestroyedSpyService) {} + + ngOnDestroy(): void { this._spyService.destroyed = true; } +} + @Directive({selector: 'tpl-refs', exportAs: 'tplRefs'}) class CaptureTplRefs { @ContentChildren(TemplateRef) tplRefs: QueryList<TemplateRef<any>>; @@ -137,6 +231,7 @@ class CaptureTplRefs { class TestComponent { currentTplRef: TemplateRef<any>; context: any = {foo: 'bar'}; + value = 'bar'; } function createTestComponent(template: string): ComponentFixture<TestComponent> { diff --git a/packages/common/test/pipes/date_pipe_spec.ts b/packages/common/test/pipes/date_pipe_spec.ts index 07a009d0dc..a4fb977011 100644 --- a/packages/common/test/pipes/date_pipe_spec.ts +++ b/packages/common/test/pipes/date_pipe_spec.ts @@ -124,9 +124,12 @@ export function main() { expectDateFormatAs(date, pattern, dateFixtures[pattern]); }); - Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => { - expectDateFormatAs(isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]); - }); + if (!browserDetection.isOldChrome) { + Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => { + expectDateFormatAs( + isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]); + }); + } expect(pipe.transform(date, 'Z')).toBeDefined(); }); diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index f242a5b64b..482b8ecba8 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -20,8 +20,7 @@ export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Sp export * from './src/transformers/api'; export * from './src/transformers/entry_points'; -export {main as ngc} from './src/ngc'; -export {performCompilation} from './src/ngc'; +export {performCompilation} from './src/perform-compile'; // TODO(hansl): moving to Angular 4 need to update this API. export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api'; diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json index ef72109112..ef1ce4b573 100644 --- a/packages/compiler-cli/package.json +++ b/packages/compiler-cli/package.json @@ -9,7 +9,7 @@ "ng-xi18n": "./src/extract_i18n.js" }, "dependencies": { - "@angular/tsc-wrapped": "5.0.0-beta.1", + "@angular/tsc-wrapped": "5.0.0-beta.2", "reflect-metadata": "^0.1.2", "minimist": "^1.2.0" }, diff --git a/packages/compiler-cli/src/codegen.ts b/packages/compiler-cli/src/codegen.ts index cf8ea0a07e..f861db7382 100644 --- a/packages/compiler-cli/src/codegen.ts +++ b/packages/compiler-cli/src/codegen.ts @@ -96,7 +96,7 @@ export class CodeGenerator { } } if (!transContent) { - missingTranslation = MissingTranslationStrategy.Ignore + missingTranslation = MissingTranslationStrategy.Ignore; } const {compiler: aotCompiler} = compiler.createAotCompiler(ngCompilerHost, { translations: transContent, diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 04d5083770..e7c5c03954 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -25,8 +25,9 @@ export interface CompilerHostContext extends ts.ModuleResolutionHost { assumeFileExists(fileName: string): void; } +export interface MetadataProvider { getMetadata(source: ts.SourceFile): ModuleMetadata|undefined; } + export class CompilerHost implements AotCompilerHost { - protected metadataCollector = new MetadataCollector(); private isGenDirChildOfRootDir: boolean; protected basePath: string; private genDir: string; @@ -39,7 +40,8 @@ export class CompilerHost implements AotCompilerHost { constructor( protected program: ts.Program, protected options: AngularCompilerOptions, - protected context: CompilerHostContext, collectorOptions?: CollectorOptions) { + protected context: CompilerHostContext, collectorOptions?: CollectorOptions, + protected metadataProvider: MetadataProvider = new MetadataCollector()) { // normalize the path so that it never ends with '/'. this.basePath = path.normalize(path.join(this.options.basePath !, '.')).replace(/\\/g, '/'); this.genDir = path.normalize(path.join(this.options.genDir !, '.')).replace(/\\/g, '/'); @@ -206,7 +208,7 @@ export class CompilerHost implements AotCompilerHost { } const sf = this.getSourceFile(filePath); - const metadata = this.metadataCollector.getMetadata(sf); + const metadata = this.metadataProvider.getMetadata(sf); return metadata ? [metadata] : []; } @@ -245,7 +247,7 @@ export class CompilerHost implements AotCompilerHost { v3Metadata.metadata[prop] = v1Metadata.metadata[prop]; } - const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath)); + const exports = this.metadataProvider.getMetadata(this.getSourceFile(dtsFilePath)); if (exports) { for (let prop in exports.metadata) { if (!v3Metadata.metadata[prop]) { diff --git a/packages/compiler-cli/src/diagnostics/check_types.ts b/packages/compiler-cli/src/diagnostics/check_types.ts index 3a2030043d..0029f2fe60 100644 --- a/packages/compiler-cli/src/diagnostics/check_types.ts +++ b/packages/compiler-cli/src/diagnostics/check_types.ts @@ -203,11 +203,9 @@ class TypeCheckingHost implements ts.CompilerHost { } writeFile: ts.WriteFileCallback = - () => { throw new Error('Unexpected write in diagnostic program'); } + () => { throw new Error('Unexpected write in diagnostic program'); }; - getCurrentDirectory(): string { - return this.host.getCurrentDirectory(); - } + getCurrentDirectory(): string { return this.host.getCurrentDirectory(); } getDirectories(path: string): string[] { return this.host.getDirectories(path); } diff --git a/packages/compiler-cli/src/ngc.ts b/packages/compiler-cli/src/ngc.ts index 6d21ff851d..658ed31d6e 100644 --- a/packages/compiler-cli/src/ngc.ts +++ b/packages/compiler-cli/src/ngc.ts @@ -10,185 +10,14 @@ import 'reflect-metadata'; import {isSyntaxError, syntaxError} from '@angular/compiler'; -import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; -import * as ts from 'typescript'; -import * as api from './transformers/api'; -import * as ng from './transformers/entry_points'; - -const TS_EXT = /\.ts$/; - -type Diagnostics = ts.Diagnostic[] | api.Diagnostic[]; - -function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] { - return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText); -} - -function formatDiagnostics(cwd: string, diags: Diagnostics): string { - if (diags && diags.length) { - if (isTsDiagnostics(diags)) { - return ts.formatDiagnostics(diags, { - getCurrentDirectory: () => cwd, - getCanonicalFileName: fileName => fileName, - getNewLine: () => ts.sys.newLine - }); - } else { - return diags - .map(d => { - let res = api.DiagnosticCategory[d.category]; - if (d.span) { - res += - ` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`; - } - if (d.span && d.span.details) { - res += `: ${d.span.details}, ${d.message}\n`; - } else { - res += `: ${d.message}\n`; - } - return res; - }) - .join(); - } - } else - return ''; -} - -function check(cwd: string, ...args: Diagnostics[]) { - if (args.some(diags => !!(diags && diags[0]))) { - throw syntaxError(args.map(diags => { - if (diags && diags[0]) { - return formatDiagnostics(cwd, diags); - } - }) - .filter(message => !!message) - .join('')); - } -} - -function syntheticError(message: string): ts.Diagnostic { - return { - file: null as any as ts.SourceFile, - start: 0, - length: 0, - messageText: message, - category: ts.DiagnosticCategory.Error, - code: 0 - }; -} - -export function readConfiguration( - project: string, basePath: string, checkFunc: (cwd: string, ...args: any[]) => void = check, - existingOptions?: ts.CompilerOptions) { - // Allow a directory containing tsconfig.json as the project value - // Note, TS@next returns an empty array, while earlier versions throw - const projectFile = - fs.lstatSync(project).isDirectory() ? path.join(project, 'tsconfig.json') : project; - let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile); - - if (error) checkFunc(basePath, [error]); - const parseConfigHost = { - useCaseSensitiveFileNames: true, - fileExists: fs.existsSync, - readDirectory: ts.sys.readDirectory, - readFile: ts.sys.readFile - }; - const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions); - - checkFunc(basePath, parsed.errors); - - // Default codegen goes to the current directory - // Parsed options are already converted to absolute paths - const ngOptions = config.angularCompilerOptions || {}; - // Ignore the genDir option - ngOptions.genDir = basePath; - - return {parsed, ngOptions}; -} - -function getProjectDirectory(project: string): string { - let isFile: boolean; - try { - isFile = fs.lstatSync(project).isFile(); - } catch (e) { - // Project doesn't exist. Assume it is a file has an extension. This case happens - // when the project file is passed to set basePath but no tsconfig.json file exists. - // It is used in tests to ensure that the options can be passed in without there being - // an actual config file. - isFile = path.extname(project) !== ''; - } - - // If project refers to a file, the project directory is the file's parent directory - // otherwise project is the project directory. - return isFile ? path.dirname(project) : project; -} - -export function performCompilation( - basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: any, - consoleError: (s: string) => void = console.error, - checkFunc: (cwd: string, ...args: any[]) => void = check, tsCompilerHost?: ts.CompilerHost) { - try { - ngOptions.basePath = basePath; - ngOptions.genDir = basePath; - - let host = tsCompilerHost || ts.createCompilerHost(options, true); - host.realpath = p => p; - - const rootFileNames = files.map(f => path.normalize(f)); - - const addGeneratedFileName = - (fileName: string) => { - if (fileName.startsWith(basePath) && TS_EXT.exec(fileName)) { - rootFileNames.push(fileName); - } - } - - if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) { - const {host: bundleHost, indexName, errors} = - createBundleIndexHost(ngOptions, rootFileNames, host); - if (errors) checkFunc(basePath, errors); - if (indexName) addGeneratedFileName(indexName); - host = bundleHost; - } - - const ngHostOptions = {...options, ...ngOptions}; - const ngHost = ng.createHost({tsHost: host, options: ngHostOptions}); - - const ngProgram = - ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngHostOptions}); - - // Check parameter diagnostics - checkFunc(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics()); - - // Check syntactic diagnostics - checkFunc(basePath, ngProgram.getTsSyntacticDiagnostics()); - - // Check TypeScript semantic and Angular structure diagnostics - checkFunc( - basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics()); - - // Check Angular semantic diagnostics - checkFunc(basePath, ngProgram.getNgSemanticDiagnostics()); - - ngProgram.emit({ - emitFlags: api.EmitFlags.Default | - ((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) - }); - } catch (e) { - if (isSyntaxError(e)) { - console.error(e.message); - consoleError(e.message); - return 1; - } - } - - return 0; -} +import {performCompilation, readConfiguration, throwOnDiagnostics} from './perform-compile'; export function main( args: string[], consoleError: (s: string) => void = console.error, - checkFunc: (cwd: string, ...args: any[]) => void = check): number { + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics): number { try { const parsedArgs = require('minimist')(args); const project = parsedArgs.p || parsedArgs.project || '.'; diff --git a/packages/compiler-cli/src/path_mapped_compiler_host.ts b/packages/compiler-cli/src/path_mapped_compiler_host.ts index 2463b5b4ee..f17b0a5800 100644 --- a/packages/compiler-cli/src/path_mapped_compiler_host.ts +++ b/packages/compiler-cli/src/path_mapped_compiler_host.ts @@ -132,7 +132,7 @@ export class PathMappedCompilerHost extends CompilerHost { } else { const sf = this.getSourceFile(rootedPath); sf.fileName = sf.fileName; - const metadata = this.metadataCollector.getMetadata(sf); + const metadata = this.metadataProvider.getMetadata(sf); return metadata ? [metadata] : []; } } diff --git a/packages/compiler-cli/src/perform-compile.ts b/packages/compiler-cli/src/perform-compile.ts new file mode 100644 index 0000000000..55feddb347 --- /dev/null +++ b/packages/compiler-cli/src/perform-compile.ts @@ -0,0 +1,192 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {isSyntaxError, syntaxError} from '@angular/compiler'; +import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import * as api from './transformers/api'; +import * as ng from './transformers/entry_points'; + +const TS_EXT = /\.ts$/; + +export type Diagnostics = ts.Diagnostic[] | api.Diagnostic[]; + +function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] { + return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText); +} + +function formatDiagnostics(cwd: string, diags: Diagnostics): string { + if (diags && diags.length) { + if (isTsDiagnostics(diags)) { + return ts.formatDiagnostics(diags, { + getCurrentDirectory: () => cwd, + getCanonicalFileName: fileName => fileName, + getNewLine: () => ts.sys.newLine + }); + } else { + return diags + .map(d => { + let res = api.DiagnosticCategory[d.category]; + if (d.span) { + res += + ` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`; + } + if (d.span && d.span.details) { + res += `: ${d.span.details}, ${d.message}\n`; + } else { + res += `: ${d.message}\n`; + } + return res; + }) + .join(); + } + } else + return ''; +} + +/** + * Throw a syntax error exception with a message formatted for output + * if the args parameter contains diagnostics errors. + * + * @param cwd The directory to report error as relative to. + * @param args A list of potentially empty diagnostic errors. + */ +export function throwOnDiagnostics(cwd: string, ...args: Diagnostics[]) { + if (args.some(diags => !!(diags && diags[0]))) { + throw syntaxError(args.map(diags => { + if (diags && diags[0]) { + return formatDiagnostics(cwd, diags); + } + }) + .filter(message => !!message) + .join('')); + } +} + +function syntheticError(message: string): ts.Diagnostic { + return { + file: null as any as ts.SourceFile, + start: 0, + length: 0, + messageText: message, + category: ts.DiagnosticCategory.Error, + code: 0 + }; +} + +export function readConfiguration( + project: string, basePath: string, + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics, + existingOptions?: ts.CompilerOptions) { + // Allow a directory containing tsconfig.json as the project value + // Note, TS@next returns an empty array, while earlier versions throw + const projectFile = + fs.lstatSync(project).isDirectory() ? path.join(project, 'tsconfig.json') : project; + let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile); + + if (error) checkFunc(basePath, [error]); + const parseConfigHost = { + useCaseSensitiveFileNames: true, + fileExists: fs.existsSync, + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile + }; + const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions); + + checkFunc(basePath, parsed.errors); + + // Default codegen goes to the current directory + // Parsed options are already converted to absolute paths + const ngOptions = config.angularCompilerOptions || {}; + // Ignore the genDir option + ngOptions.genDir = basePath; + + return {parsed, ngOptions}; +} + +function getProjectDirectory(project: string): string { + let isFile: boolean; + try { + isFile = fs.lstatSync(project).isFile(); + } catch (e) { + // Project doesn't exist. Assume it is a file has an extension. This case happens + // when the project file is passed to set basePath but no tsconfig.json file exists. + // It is used in tests to ensure that the options can be passed in without there being + // an actual config file. + isFile = path.extname(project) !== ''; + } + + // If project refers to a file, the project directory is the file's parent directory + // otherwise project is the project directory. + return isFile ? path.dirname(project) : project; +} + +export function performCompilation( + basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: any, + consoleError: (s: string) => void = console.error, + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics, + tsCompilerHost?: ts.CompilerHost) { + try { + ngOptions.basePath = basePath; + ngOptions.genDir = basePath; + + let host = tsCompilerHost || ts.createCompilerHost(options, true); + host.realpath = p => p; + + const rootFileNames = files.map(f => path.normalize(f)); + + const addGeneratedFileName = (fileName: string) => { + if (fileName.startsWith(basePath) && TS_EXT.exec(fileName)) { + rootFileNames.push(fileName); + } + }; + + if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) { + const {host: bundleHost, indexName, errors} = + createBundleIndexHost(ngOptions, rootFileNames, host); + if (errors) checkFunc(basePath, errors); + if (indexName) addGeneratedFileName(indexName); + host = bundleHost; + } + + const ngHostOptions = {...options, ...ngOptions}; + const ngHost = ng.createHost({tsHost: host, options: ngHostOptions}); + + const ngProgram = + ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngHostOptions}); + + // Check parameter diagnostics + checkFunc(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics()); + + // Check syntactic diagnostics + checkFunc(basePath, ngProgram.getTsSyntacticDiagnostics()); + + // Check TypeScript semantic and Angular structure diagnostics + checkFunc( + basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics()); + + // Check Angular semantic diagnostics + checkFunc(basePath, ngProgram.getNgSemanticDiagnostics()); + + ngProgram.emit({ + emitFlags: api.EmitFlags.Default | + ((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) + }); + } catch (e) { + if (isSyntaxError(e)) { + console.error(e.message); + consoleError(e.message); + return 1; + } + throw e; + } + + return 0; +} diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index ea0919c3b5..de1ccc3b9d 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -91,6 +91,10 @@ export interface CompilerOptions extends ts.CompilerOptions { // Whether to enable support for <template> and the template attribute (true by default) enableLegacyTemplate?: boolean; + + // Whether to enable lowering expressions lambdas and expressions in a reference value + // position. + disableExpressionLowering?: boolean; } export interface ModuleFilenameResolver { diff --git a/packages/compiler-cli/src/transformers/entry_points.ts b/packages/compiler-cli/src/transformers/entry_points.ts index b55b4c81ad..4e4f5b1721 100644 --- a/packages/compiler-cli/src/transformers/entry_points.ts +++ b/packages/compiler-cli/src/transformers/entry_points.ts @@ -1,3 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + import * as ts from 'typescript'; import {CompilerHost, CompilerOptions, Program} from './api'; diff --git a/packages/compiler-cli/src/transformers/lower_expressions.ts b/packages/compiler-cli/src/transformers/lower_expressions.ts new file mode 100644 index 0000000000..286ce3b4fa --- /dev/null +++ b/packages/compiler-cli/src/transformers/lower_expressions.ts @@ -0,0 +1,176 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CollectorOptions, MetadataCollector, MetadataValue, ModuleMetadata} from '@angular/tsc-wrapped'; +import * as ts from 'typescript'; + +export interface LoweringRequest { + kind: ts.SyntaxKind; + location: number; + end: number; + name: string; +} + +export type RequestLocationMap = Map<number, LoweringRequest>; + +interface Declaration { + name: string; + node: ts.Node; +} + +interface DeclarationInsert { + declarations: Declaration[]; + priorTo: ts.Node; +} + +function toMap<T, K>(items: T[], select: (item: T) => K): Map<K, T> { + return new Map(items.map<[K, T]>(i => [select(i), i])); +} + +function transformSourceFile( + sourceFile: ts.SourceFile, requests: RequestLocationMap, + context: ts.TransformationContext): ts.SourceFile { + const inserts: DeclarationInsert[] = []; + + // Calculate the range of intersting locations. The transform will only visit nodes in this + // range to improve the performance on large files. + const locations = Array.from(requests.keys()); + const min = Math.min(...locations); + const max = Math.max(...locations); + + function visitSourceFile(sourceFile: ts.SourceFile): ts.SourceFile { + function topLevelStatement(node: ts.Node): ts.Node { + const declarations: Declaration[] = []; + + function visitNode(node: ts.Node): ts.Node { + const nodeRequest = requests.get(node.pos); + if (nodeRequest && nodeRequest.kind == node.kind && nodeRequest.end == node.end) { + // This node is requested to be rewritten as a reference to the exported name. + // Record that the node needs to be moved to an exported variable with the given name + const name = nodeRequest.name; + declarations.push({name, node}); + return ts.createIdentifier(name); + } + if (node.pos <= max && node.end >= min) return ts.visitEachChild(node, visitNode, context); + return node; + } + + const result = ts.visitEachChild(node, visitNode, context); + + if (declarations.length) { + inserts.push({priorTo: result, declarations}); + } + return result; + } + + const traversedSource = ts.visitEachChild(sourceFile, topLevelStatement, context); + if (inserts.length) { + // Insert the declarations before the rewritten statement that references them. + const insertMap = toMap(inserts, i => i.priorTo); + const newStatements: ts.Statement[] = [...traversedSource.statements]; + for (let i = newStatements.length; i >= 0; i--) { + const statement = newStatements[i]; + const insert = insertMap.get(statement); + if (insert) { + const declarations = insert.declarations.map( + i => ts.createVariableDeclaration( + i.name, /* type */ undefined, i.node as ts.Expression)); + const statement = ts.createVariableStatement( + /* modifiers */ undefined, + ts.createVariableDeclarationList(declarations, ts.NodeFlags.Const)); + newStatements.splice(i, 0, statement); + } + } + + // Insert an exports clause to export the declarations + newStatements.push(ts.createExportDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, + ts.createNamedExports( + inserts + .reduce( + (accumulator, insert) => [...accumulator, ...insert.declarations], + [] as Declaration[]) + .map( + declaration => ts.createExportSpecifier( + /* propertyName */ undefined, declaration.name))))); + return ts.updateSourceFileNode(traversedSource, newStatements); + } + return traversedSource; + } + + return visitSourceFile(sourceFile); +} + +export function getExpressionLoweringTransformFactory(requestsMap: RequestsMap): + (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile { + // Return the factory + return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => { + const requests = requestsMap.getRequests(sourceFile); + if (requests && requests.size) { + return transformSourceFile(sourceFile, requests, context); + } + return sourceFile; + }; +} + +export interface RequestsMap { getRequests(sourceFile: ts.SourceFile): RequestLocationMap; } + +interface MetadataAndLoweringRequests { + metadata: ModuleMetadata|undefined; + requests: RequestLocationMap; +} + +export class LowerMetadataCache implements RequestsMap { + private collector: MetadataCollector; + private metadataCache = new Map<string, MetadataAndLoweringRequests>(); + + constructor(options: CollectorOptions, private strict?: boolean) { + this.collector = new MetadataCollector(options); + } + + getMetadata(sourceFile: ts.SourceFile): ModuleMetadata|undefined { + return this.ensureMetadataAndRequests(sourceFile).metadata; + } + + getRequests(sourceFile: ts.SourceFile): RequestLocationMap { + return this.ensureMetadataAndRequests(sourceFile).requests; + } + + private ensureMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests { + let result = this.metadataCache.get(sourceFile.fileName); + if (!result) { + result = this.getMetadataAndRequests(sourceFile); + this.metadataCache.set(sourceFile.fileName, result); + } + return result; + } + + private getMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests { + let identNumber = 0; + const freshIdent = () => '\u0275' + identNumber++; + const requests = new Map<number, LoweringRequest>(); + const replaceNode = (node: ts.Node) => { + const name = freshIdent(); + requests.set(node.pos, {name, kind: node.kind, location: node.pos, end: node.end}); + return {__symbolic: 'reference', name}; + }; + + const substituteExpression = (value: MetadataValue, node: ts.Node): MetadataValue => { + if (node.kind === ts.SyntaxKind.ArrowFunction || + node.kind === ts.SyntaxKind.FunctionExpression) { + return replaceNode(node); + } + return value; + }; + + const metadata = this.collector.getMetadata(sourceFile, this.strict, substituteExpression); + + return {metadata, requests}; + } +} \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/module_filename_resolver.ts b/packages/compiler-cli/src/transformers/module_filename_resolver.ts index 9c224d85de..d0a55da56a 100644 --- a/packages/compiler-cli/src/transformers/module_filename_resolver.ts +++ b/packages/compiler-cli/src/transformers/module_filename_resolver.ts @@ -1,8 +1,15 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + import * as path from 'path'; import * as ts from 'typescript'; -import {ModuleFilenameResolver} from './api'; -import {CompilerOptions} from './api'; +import {CompilerOptions, ModuleFilenameResolver} from './api'; const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const DTS = /\.d\.ts$/; diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts index f5b81ec6c0..8adc7e8f64 100644 --- a/packages/compiler-cli/src/transformers/node_emitter.ts +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -32,7 +32,7 @@ export class TypeScriptNodeEmitter { } statements[0] = ts.setSyntheticLeadingComments( statements[0], - [{kind: ts.SyntaxKind.MultiLineCommentTrivia, text: preamble, pos: -1, end: -1}]) + [{kind: ts.SyntaxKind.MultiLineCommentTrivia, text: preamble, pos: -1, end: -1}]); } return [newSourceFile, converter.getNodeMap()]; } @@ -279,7 +279,7 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor { return this.record( expr, ts.createCall( expr.fn.visitExpression(this, null), /* typeArguments */ undefined, - expr.args.map(arg => arg.visitExpression(this, null)))) + expr.args.map(arg => arg.visitExpression(this, null)))); } visitInstantiateExpr(expr: InstantiateExpr): RecordedNode<ts.NewExpression> { diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 17b5d34024..b2753555fa 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -15,8 +15,8 @@ import * as ts from 'typescript'; import {CompilerHost as AotCompilerHost, CompilerHostContext} from '../compiler_host'; import {TypeChecker} from '../diagnostics/check_types'; -import {CompilerHost, CompilerOptions, DiagnosticCategory} from './api'; -import {Diagnostic, EmitFlags, Program} from './api'; +import {CompilerHost, CompilerOptions, Diagnostic, DiagnosticCategory, EmitFlags, Program} from './api'; +import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; const GENERATED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$|\.ngsummary\.js$/; @@ -35,7 +35,7 @@ class AngularCompilerProgram implements Program { private aotCompilerHost: AotCompilerHost; private compiler: AotCompiler; private srcNames: string[]; - private collector: MetadataCollector; + private metadataCache: LowerMetadataCache; // Lazily initialized fields private _analyzedModules: NgAnalyzedModules|undefined; private _structuralDiagnostics: Diagnostic[] = []; @@ -55,13 +55,14 @@ class AngularCompilerProgram implements Program { this.tsProgram = ts.createProgram(rootNames, options, host, this.oldTsProgram); this.srcNames = this.tsProgram.getSourceFiles().map(sf => sf.fileName); - this.aotCompilerHost = new AotCompilerHost(this.tsProgram, options, host); + this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit); + this.aotCompilerHost = new AotCompilerHost( + this.tsProgram, options, host, /* collectorOptions */ undefined, this.metadataCache); if (host.readResource) { this.aotCompilerHost.loadResource = host.readResource.bind(host); } const {compiler} = createAotCompiler(this.aotCompilerHost, options); this.compiler = compiler; - this.collector = new MetadataCollector({quotedNames: true}); } // Program implementation @@ -118,11 +119,9 @@ class AngularCompilerProgram implements Program { const emitMap = new Map<string, string>(); const result = this.programWithStubs.emit( /* targetSourceFile */ undefined, - createWriteFileCallback(emitFlags, this.host, this.collector, this.options, emitMap), - cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, { - after: this.options.skipTemplateCodegen ? [] : [getAngularEmitterTransformFactory( - this.generatedFiles)] - }); + createWriteFileCallback(emitFlags, this.host, this.metadataCache, emitMap), + cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, + this.calculateTransforms()); this.generatedFiles.forEach(file => { if (file.source && file.source.length && SUMMARY_JSON_FILES.test(file.genFileUrl)) { @@ -144,7 +143,7 @@ class AngularCompilerProgram implements Program { } private get structuralDiagnostics(): Diagnostic[] { - return this.analyzedModules && this._structuralDiagnostics + return this.analyzedModules && this._structuralDiagnostics; } private get stubs(): GeneratedFile[] { @@ -171,7 +170,7 @@ class AngularCompilerProgram implements Program { } private get generatedFiles(): GeneratedFile[] { - return this._generatedFiles || (this._generatedFiles = this.generateFiles()) + return this._generatedFiles || (this._generatedFiles = this.generateFiles()); } private get typeChecker(): TypeChecker { @@ -184,6 +183,22 @@ class AngularCompilerProgram implements Program { return this.generatedFiles && this._generatedFileDiagnostics !; } + private calculateTransforms(): ts.CustomTransformers { + const before: ts.TransformerFactory<ts.SourceFile>[] = []; + const after: ts.TransformerFactory<ts.SourceFile>[] = []; + if (!this.options.disableExpressionLowering) { + // TODO(chuckj): fix and re-enable + tests - see https://github.com/angular/angular/pull/18388 + // before.push(getExpressionLoweringTransformFactory(this.metadataCache)); + } + if (!this.options.skipTemplateCodegen) { + after.push(getAngularEmitterTransformFactory(this.generatedFiles)); + } + const result: ts.CustomTransformers = {}; + if (before.length) result.before = before; + if (after.length) result.after = after; + return result; + } + private catchAnalysisError(e: any): NgAnalyzedModules { if (isSyntaxError(e)) { const parserErrors = getParseErrors(e); @@ -257,8 +272,7 @@ export function createProgram( } function writeMetadata( - emitFilePath: string, sourceFile: ts.SourceFile, collector: MetadataCollector, - ngOptions: CompilerOptions) { + emitFilePath: string, sourceFile: ts.SourceFile, metadataCache: LowerMetadataCache) { if (/\.js$/.test(emitFilePath)) { const path = emitFilePath.replace(/\.js$/, '.metadata.json'); @@ -271,7 +285,7 @@ function writeMetadata( collectableFile = (collectableFile as any).original; } - const metadata = collector.getMetadata(collectableFile, !!ngOptions.strictMetadataEmit); + const metadata = metadataCache.getMetadata(collectableFile); if (metadata) { const metadataText = JSON.stringify([metadata]); writeFileSync(path, metadataText, {encoding: 'utf-8'}); @@ -280,8 +294,8 @@ function writeMetadata( } function createWriteFileCallback( - emitFlags: EmitFlags, host: ts.CompilerHost, collector: MetadataCollector, - ngOptions: CompilerOptions, emitMap: Map<string, string>) { + emitFlags: EmitFlags, host: ts.CompilerHost, metadataCache: LowerMetadataCache, + emitMap: Map<string, string>) { const withMetadata = (fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { @@ -291,7 +305,7 @@ function createWriteFileCallback( } if (!generatedFile && sourceFiles && sourceFiles.length == 1) { emitMap.set(sourceFiles[0].fileName, fileName); - writeMetadata(fileName, sourceFiles[0], collector, ngOptions); + writeMetadata(fileName, sourceFiles[0], metadataCache); } }; const withoutMetadata = @@ -329,7 +343,7 @@ function createProgramWithStubsHost( generatedFiles: GeneratedFile[], originalProgram: ts.Program, originalHost: ts.CompilerHost): ts.CompilerHost { interface FileData { - g: GeneratedFile + g: GeneratedFile; s?: ts.SourceFile; } return new class implements ts.CompilerHost { diff --git a/packages/compiler-cli/test/diagnostics/mocks.ts b/packages/compiler-cli/test/diagnostics/mocks.ts index 6cd06d8616..c87acd9f96 100644 --- a/packages/compiler-cli/test/diagnostics/mocks.ts +++ b/packages/compiler-cli/test/diagnostics/mocks.ts @@ -110,12 +110,14 @@ const summaryResolver = new AotSummaryResolver( staticSymbolCache); export class DiagnosticContext { + // tslint:disable _analyzedModules: NgAnalyzedModules; _staticSymbolResolver: StaticSymbolResolver|undefined; _reflector: StaticReflector|undefined; _errors: {e: any, path?: string}[] = []; _resolver: CompileMetadataResolver|undefined; _refletor: StaticReflector; + // tslint:enable constructor( public service: ts.LanguageService, public program: ts.Program, diff --git a/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts b/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts index cf7d7697e6..317b26ed3e 100644 --- a/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts +++ b/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts @@ -45,7 +45,7 @@ describe('symbol query', () => { options.basePath = '/quickstart'; const aotHost = new CompilerHost(program, options, host, {verboseInvalidExpression: true}); context = new DiagnosticContext(service, program, checker, aotHost); - query = getSymbolQuery(program, checker, sourceFile, emptyPipes) + query = getSymbolQuery(program, checker, sourceFile, emptyPipes); }); it('should be able to get undefined for an unknown symbol', () => { diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 2c272d0c96..0a994cf6e8 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -11,7 +11,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {main, performCompilation} from '../src/ngc'; +import {main} from '../src/ngc'; +import {performCompilation} from '../src/perform-compile'; function getNgRootDir() { const moduleFilename = module.filename.replace(/\\/g, '/'); @@ -308,7 +309,6 @@ describe('ngc command-line', () => { const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]); expect(exitCode).toEqual(0); - expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true); expect(fs.existsSync(path.resolve( outDir, 'node_modules', '@angular', 'core', 'src', @@ -316,17 +316,133 @@ describe('ngc command-line', () => { .toBe(true); }); + xdescribe('expression lowering', () => { + beforeEach(() => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["mymodule.ts"] + }`); + }); + + function compile(): number { + const errors: string[] = []; + const result = main(['-p', path.join(basePath, 'tsconfig.json')], s => errors.push(s)); + expect(errors).toEqual([]); + return result; + } + + it('should be able to lower a lambda expression in a provider', () => { + write('mymodule.ts', ` + import {CommonModule} from '@angular/common'; + import {NgModule} from '@angular/core'; + + class Foo {} + + @NgModule({ + imports: [CommonModule], + providers: [{provide: 'someToken', useFactory: () => new Foo()}] + }) + export class MyModule {} + `); + expect(compile()).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }'); + expect(mymoduleSource).toContain('export { ɵ0'); + + const mymodulefactory = path.resolve(outDir, 'mymodule.ngfactory.js'); + const mymodulefactorySource = fs.readFileSync(mymodulefactory, 'utf8'); + expect(mymodulefactorySource).toContain('"someToken", i1.ɵ0'); + }); + + it('should be able to lower a function expression in a provider', () => { + write('mymodule.ts', ` + import {CommonModule} from '@angular/common'; + import {NgModule} from '@angular/core'; + + class Foo {} + + @NgModule({ + imports: [CommonModule], + providers: [{provide: 'someToken', useFactory: function() {return new Foo();}}] + }) + export class MyModule {} + `); + expect(compile()).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }'); + expect(mymoduleSource).toContain('export { ɵ0'); + + const mymodulefactory = path.resolve(outDir, 'mymodule.ngfactory.js'); + const mymodulefactorySource = fs.readFileSync(mymodulefactory, 'utf8'); + expect(mymodulefactorySource).toContain('"someToken", i1.ɵ0'); + }); + + it('should able to lower multiple expressions', () => { + write('mymodule.ts', ` + import {CommonModule} from '@angular/common'; + import {NgModule} from '@angular/core'; + + class Foo {} + + @NgModule({ + imports: [CommonModule], + providers: [ + {provide: 'someToken', useFactory: () => new Foo()}, + {provide: 'someToken', useFactory: () => new Foo()}, + {provide: 'someToken', useFactory: () => new Foo()}, + {provide: 'someToken', useFactory: () => new Foo()} + ] + }) + export class MyModule {} + `); + expect(compile()).toEqual(0); + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('ɵ0 = function () { return new Foo(); }'); + expect(mymoduleSource).toContain('ɵ1 = function () { return new Foo(); }'); + expect(mymoduleSource).toContain('ɵ2 = function () { return new Foo(); }'); + expect(mymoduleSource).toContain('ɵ3 = function () { return new Foo(); }'); + expect(mymoduleSource).toContain('export { ɵ0, ɵ1, ɵ2, ɵ3'); + }); + + it('should be able to lower an indirect expression', () => { + write('mymodule.ts', ` + import {CommonModule} from '@angular/common'; + import {NgModule} from '@angular/core'; + + class Foo {} + + const factory = () => new Foo(); + + @NgModule({ + imports: [CommonModule], + providers: [{provide: 'someToken', useFactory: factory}] + }) + export class MyModule {} + `); + expect(compile()).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }'); + expect(mymoduleSource).toContain('export { ɵ0'); + }); + }); + const shouldExist = (fileName: string) => { if (!fs.existsSync(path.resolve(outDir, fileName))) { throw new Error(`Expected ${fileName} to be emitted (outDir: ${outDir})`); } }; - const shouldNotExist = - (fileName: string) => { - if (fs.existsSync(path.resolve(outDir, fileName))) { - throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${outDir})`); - } - } + const shouldNotExist = (fileName: string) => { + if (fs.existsSync(path.resolve(outDir, fileName))) { + throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${outDir})`); + } + }; it('should be able to generate a flat module library', () => { writeConfig(` diff --git a/packages/compiler-cli/test/transformers/lower_expressions_spec.ts b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts new file mode 100644 index 0000000000..7588982b03 --- /dev/null +++ b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts @@ -0,0 +1,107 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {LoweringRequest, RequestLocationMap, getExpressionLoweringTransformFactory} from '../../src/transformers/lower_expressions'; +import {Directory, MockAotContext, MockCompilerHost} from '../mocks'; + +describe('Expression lowering', () => { + it('should be able to lower a simple expression', () => { + expect(convert('const a = 1 +◊b: 2◊;')).toBe('const b = 2; const a = 1 + b; export { b };'); + }); + + it('should be able to lower an expression in a decorator', () => { + expect(convert(` + import {Component} from '@angular/core'; + + @Component({ + provider: [{provide: 'someToken', useFactory:◊l: () => null◊}] + }) + class MyClass {} + `)).toContain('const l = () => null; exports.l = l;'); + }); +}); + +function convert(annotatedSource: string) { + const annotations: {start: number, length: number, name: string}[] = []; + let adjustment = 0; + const unannotatedSource = annotatedSource.replace( + /◊([a-zA-Z]+):(.*)◊/g, + (text: string, name: string, source: string, index: number): string => { + annotations.push({start: index + adjustment, length: source.length, name}); + adjustment -= text.length - source.length; + return source; + }); + + const baseFileName = 'someFile'; + const moduleName = '/' + baseFileName; + const fileName = moduleName + '.ts'; + const context = new MockAotContext('/', {[baseFileName + '.ts']: unannotatedSource}); + const host = new MockCompilerHost(context); + + const sourceFile = ts.createSourceFile( + fileName, unannotatedSource, ts.ScriptTarget.Latest, /* setParentNodes */ true); + const requests = new Map<number, LoweringRequest>(); + + for (const annotation of annotations) { + const node = findNode(sourceFile, annotation.start, annotation.length); + expect(node).toBeDefined(); + if (node) { + const location = node.pos; + requests.set(location, {name: annotation.name, kind: node.kind, location, end: node.end}); + } + } + + const program = ts.createProgram( + [fileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host); + const moduleSourceFile = program.getSourceFile(fileName); + const transformers: ts.CustomTransformers = { + before: [getExpressionLoweringTransformFactory({ + getRequests(sourceFile: ts.SourceFile): RequestLocationMap{ + if (sourceFile.fileName == moduleSourceFile.fileName) { + return requests; + } else {return new Map();} + } + })] + }; + let result: string = ''; + const emitResult = program.emit( + moduleSourceFile, (emittedFileName, data, writeByteOrderMark, onError, sourceFiles) => { + if (fileName.startsWith(moduleName)) { + result = data; + } + }, undefined, undefined, transformers); + return normalizeResult(result); +}; + +function findNode(node: ts.Node, start: number, length: number): ts.Node|undefined { + function find(node: ts.Node): ts.Node|undefined { + if (node.getFullStart() == start && node.getEnd() == start + length) { + return node; + } + if (node.getFullStart() <= start && node.getEnd() >= start + length) { + return ts.forEachChild(node, find); + } + } + return ts.forEachChild(node, find); +} + +function normalizeResult(result: string): string { + // Remove TypeScript prefixes + // Remove new lines + // Squish adjacent spaces + // Remove prefix and postfix spaces + return result.replace('"use strict";', ' ') + .replace('exports.__esModule = true;', ' ') + .replace('Object.defineProperty(exports, "__esModule", { value: true });', ' ') + .replace(/\n/g, ' ') + .replace(/ +/g, ' ') + .replace(/^ /g, '') + .replace(/ $/g, ''); +} diff --git a/packages/compiler/src/aot/compiler_options.ts b/packages/compiler/src/aot/compiler_options.ts index 772ce3ca12..ed260b3a6a 100644 --- a/packages/compiler/src/aot/compiler_options.ts +++ b/packages/compiler/src/aot/compiler_options.ts @@ -14,5 +14,5 @@ export interface AotCompilerOptions { translations?: string; missingTranslation?: MissingTranslationStrategy; enableLegacyTemplate?: boolean; - enableSummariesForJit?: boolean + enableSummariesForJit?: boolean; } diff --git a/packages/compiler/src/aot/static_reflector.ts b/packages/compiler/src/aot/static_reflector.ts index 5a9a74ed2b..fb76750878 100644 --- a/packages/compiler/src/aot/static_reflector.ts +++ b/packages/compiler/src/aot/static_reflector.ts @@ -376,7 +376,6 @@ export class StaticReflector implements CompileReflector { if (calling.get(functionSymbol)) { throw new Error('Recursion not supported'); } - calling.set(functionSymbol, true); try { const value = targetFunction['value']; if (value && (depth != 0 || value.__symbolic != 'error')) { @@ -387,6 +386,7 @@ export class StaticReflector implements CompileReflector { if (defaults && defaults.length > args.length) { args.push(...defaults.slice(args.length).map((value: any) => simplify(value))); } + calling.set(functionSymbol, true); const functionScope = BindingScope.build(); for (let i = 0; i < parameters.length; i++) { functionScope.define(parameters[i], args[i]); @@ -621,7 +621,7 @@ export class StaticReflector implements CompileReflector { } return simplifyInContext(context, value, depth, references + 1); } - return simplify(value) + return simplify(value); }); } return IGNORE; diff --git a/packages/compiler/src/aot/summary_serializer.ts b/packages/compiler/src/aot/summary_serializer.ts index f5d9432374..11e420a2d1 100644 --- a/packages/compiler/src/aot/summary_serializer.ts +++ b/packages/compiler/src/aot/summary_serializer.ts @@ -345,4 +345,4 @@ class FromJsonDeserializer extends ValueTransformer { return super.visitStringMap(map, context); } } -} \ No newline at end of file +} diff --git a/packages/compiler/src/jit/compiler_factory.ts b/packages/compiler/src/jit/compiler_factory.ts index fc2458ea0d..1e51157f0d 100644 --- a/packages/compiler/src/jit/compiler_factory.ts +++ b/packages/compiler/src/jit/compiler_factory.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, Inject, InjectionToken, MissingTranslationStrategy, Optional, PlatformRef, Provider, ReflectiveInjector, TRANSLATIONS, TRANSLATIONS_FORMAT, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore, ɵConsole as Console} from '@angular/core'; +import {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, Inject, InjectionToken, Injector, MissingTranslationStrategy, Optional, PACKAGE_ROOT_URL, PlatformRef, StaticProvider, TRANSLATIONS, TRANSLATIONS_FORMAT, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore, ɵConsole as Console} from '@angular/core'; +import {StaticSymbolCache} from '../aot/static_symbol'; import {CompileReflector} from '../compile_reflector'; import {CompilerConfig} from '../config'; import {DirectiveNormalizer} from '../directive_normalizer'; @@ -16,7 +17,7 @@ import {Lexer} from '../expression_parser/lexer'; import {Parser} from '../expression_parser/parser'; import * as i18n from '../i18n/index'; import {CompilerInjectable} from '../injectable'; -import {CompileMetadataResolver} from '../metadata_resolver'; +import {CompileMetadataResolver, ERROR_COLLECTOR_TOKEN} from '../metadata_resolver'; import {HtmlParser} from '../ml_parser/html_parser'; import {NgModuleCompiler} from '../ng_module_compiler'; import {NgModuleResolver} from '../ng_module_resolver'; @@ -26,7 +27,7 @@ import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry'; import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {StyleCompiler} from '../style_compiler'; import {JitSummaryResolver, SummaryResolver} from '../summary_resolver'; -import {TemplateParser} from '../template_parser/template_parser'; +import {TEMPLATE_TRANSFORMS, TemplateParser} from '../template_parser/template_parser'; import {DEFAULT_PACKAGE_URL_PROVIDER, UrlResolver} from '../url_resolver'; import {ViewCompiler} from '../view_compiler/view_compiler'; @@ -45,17 +46,18 @@ const baseHtmlParser = new InjectionToken('HtmlParser'); * A set of providers that provide `JitCompiler` and its dependencies to use for * template compilation. */ -export const COMPILER_PROVIDERS: Array<any|Type<any>|{[k: string]: any}|any[]> = [ +export const COMPILER_PROVIDERS = <StaticProvider[]>[ {provide: CompileReflector, useValue: new JitReflector()}, {provide: ResourceLoader, useValue: _NO_RESOURCE_LOADER}, - JitSummaryResolver, + {provide: JitSummaryResolver, deps: []}, {provide: SummaryResolver, useExisting: JitSummaryResolver}, - Console, - Lexer, - Parser, + {provide: Console, deps: []}, + {provide: Lexer, deps: []}, + {provide: Parser, deps: [Lexer]}, { provide: baseHtmlParser, useClass: HtmlParser, + deps: [], }, { provide: i18n.I18NHtmlParser, @@ -78,22 +80,37 @@ export const COMPILER_PROVIDERS: Array<any|Type<any>|{[k: string]: any}|any[]> = provide: HtmlParser, useExisting: i18n.I18NHtmlParser, }, - TemplateParser, - DirectiveNormalizer, - CompileMetadataResolver, + { + provide: TemplateParser, deps: [CompilerConfig, CompileReflector, + Parser, ElementSchemaRegistry, + i18n.I18NHtmlParser, Console, [Optional, TEMPLATE_TRANSFORMS]] + }, + { provide: DirectiveNormalizer, deps: [ResourceLoader, UrlResolver, HtmlParser, CompilerConfig]}, + { provide: CompileMetadataResolver, deps: [CompilerConfig, NgModuleResolver, + DirectiveResolver, PipeResolver, + SummaryResolver, + ElementSchemaRegistry, + DirectiveNormalizer, Console, + [Optional, StaticSymbolCache], + CompileReflector, + [Optional, ERROR_COLLECTOR_TOKEN]]}, DEFAULT_PACKAGE_URL_PROVIDER, - StyleCompiler, - ViewCompiler, - NgModuleCompiler, - {provide: CompilerConfig, useValue: new CompilerConfig()}, - JitCompiler, - {provide: Compiler, useExisting: JitCompiler}, - DomElementSchemaRegistry, - {provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry}, - UrlResolver, - DirectiveResolver, - PipeResolver, - NgModuleResolver, + { provide: StyleCompiler, deps: [UrlResolver]}, + { provide: ViewCompiler, deps: [CompilerConfig, CompileReflector, ElementSchemaRegistry]}, + { provide: NgModuleCompiler, deps: [CompileReflector] }, + { provide: CompilerConfig, useValue: new CompilerConfig()}, + { provide: JitCompiler, deps: [Injector, CompileMetadataResolver, + TemplateParser, StyleCompiler, + ViewCompiler, NgModuleCompiler, + SummaryResolver, CompilerConfig, + Console]}, + { provide: Compiler, useExisting: JitCompiler}, + { provide: DomElementSchemaRegistry, deps: []}, + { provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry}, + { provide: UrlResolver, deps: [PACKAGE_ROOT_URL]}, + { provide: DirectiveResolver, deps: [CompileReflector]}, + { provide: PipeResolver, deps: [CompileReflector]}, + { provide: NgModuleResolver, deps: [CompileReflector]}, ]; @CompilerInjectable() @@ -112,7 +129,7 @@ export class JitCompilerFactory implements CompilerFactory { } createCompiler(options: CompilerOptions[] = []): Compiler { const opts = _mergeOptions(this._defaultOptions.concat(options)); - const injector = ReflectiveInjector.resolveAndCreate([ + const injector = Injector.create([ COMPILER_PROVIDERS, { provide: CompilerConfig, useFactory: () => { @@ -142,7 +159,7 @@ export class JitCompilerFactory implements CompilerFactory { */ export const platformCoreDynamic = createPlatformFactory(platformCore, 'coreDynamic', [ {provide: COMPILER_OPTIONS, useValue: {}, multi: true}, - {provide: CompilerFactory, useClass: JitCompilerFactory}, + {provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS]}, ]); function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions { diff --git a/packages/compiler/src/ml_parser/ast.ts b/packages/compiler/src/ml_parser/ast.ts index 0a5b0e6449..3bd8742fed 100644 --- a/packages/compiler/src/ml_parser/ast.ts +++ b/packages/compiler/src/ml_parser/ast.ts @@ -142,7 +142,7 @@ export function findNode(nodes: Node[], position: number): HtmlAstPath { return true; } } - } + }; visitAll(visitor, nodes); diff --git a/packages/compiler/src/ml_parser/parser.ts b/packages/compiler/src/ml_parser/parser.ts index e617156536..ab6a7c29f7 100644 --- a/packages/compiler/src/ml_parser/parser.ts +++ b/packages/compiler/src/ml_parser/parser.ts @@ -230,12 +230,9 @@ class _TreeBuilder { } private _closeVoidElement(): void { - if (this._elementStack.length > 0) { - const el = this._elementStack[this._elementStack.length - 1]; - - if (this.getTagDefinition(el.name).isVoid) { - this._elementStack.pop(); - } + const el = this._getParentElement(); + if (el && this.getTagDefinition(el.name).isVoid) { + this._elementStack.pop(); } } @@ -274,11 +271,10 @@ class _TreeBuilder { } private _pushElement(el: html.Element) { - if (this._elementStack.length > 0) { - const parentEl = this._elementStack[this._elementStack.length - 1]; - if (this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) { - this._elementStack.pop(); - } + const parentEl = this._getParentElement(); + + if (parentEl && this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) { + this._elementStack.pop(); } const tagDef = this.getTagDefinition(el.name); @@ -353,7 +349,7 @@ class _TreeBuilder { * `<ng-container>` elements are skipped as they are not rendered as DOM element. */ private _getParentElementSkippingContainers(): - {parent: html.Element, container: html.Element|null} { + {parent: html.Element | null, container: html.Element|null} { let container: html.Element|null = null; for (let i = this._elementStack.length - 1; i >= 0; i--) { @@ -363,7 +359,7 @@ class _TreeBuilder { container = this._elementStack[i]; } - return {parent: this._elementStack[this._elementStack.length - 1], container}; + return {parent: null, container}; } private _addToParent(node: html.Node) { diff --git a/packages/compiler/src/ml_parser/tags.ts b/packages/compiler/src/ml_parser/tags.ts index 0a882b6c51..5f06c3fd25 100644 --- a/packages/compiler/src/ml_parser/tags.ts +++ b/packages/compiler/src/ml_parser/tags.ts @@ -56,274 +56,272 @@ export function isNgTemplate(tagName: string): boolean { return splitNsName(tagName)[1] === 'ng-template'; } -export function getNsPrefix(fullName: string): string +export function getNsPrefix(fullName: string): string; export function getNsPrefix(fullName: null): null; - export function getNsPrefix(fullName: string | null): string | - null { - return fullName === null ? null : splitNsName(fullName)[0]; - } +export function getNsPrefix(fullName: string | null): string|null { + return fullName === null ? null : splitNsName(fullName)[0]; +} - export function mergeNsAndName(prefix: string, localName: string): - string { - return prefix ? `:${prefix}:${localName}` : localName; - } +export function mergeNsAndName(prefix: string, localName: string): string { + return prefix ? `:${prefix}:${localName}` : localName; +} - // see http://www.w3.org/TR/html51/syntax.html#named-character-references - // see https://html.spec.whatwg.org/multipage/entities.json - // This list is not exhaustive to keep the compiler footprint low. - // The `{` / `ƫ` syntax should be used when the named character reference does not - // exist. - export const NAMED_ENTITIES: {[k: string]: string} = { - 'Aacute': '\u00C1', - 'aacute': '\u00E1', - 'Acirc': '\u00C2', - 'acirc': '\u00E2', - 'acute': '\u00B4', - 'AElig': '\u00C6', - 'aelig': '\u00E6', - 'Agrave': '\u00C0', - 'agrave': '\u00E0', - 'alefsym': '\u2135', - 'Alpha': '\u0391', - 'alpha': '\u03B1', - 'amp': '&', - 'and': '\u2227', - 'ang': '\u2220', - 'apos': '\u0027', - 'Aring': '\u00C5', - 'aring': '\u00E5', - 'asymp': '\u2248', - 'Atilde': '\u00C3', - 'atilde': '\u00E3', - 'Auml': '\u00C4', - 'auml': '\u00E4', - 'bdquo': '\u201E', - 'Beta': '\u0392', - 'beta': '\u03B2', - 'brvbar': '\u00A6', - 'bull': '\u2022', - 'cap': '\u2229', - 'Ccedil': '\u00C7', - 'ccedil': '\u00E7', - 'cedil': '\u00B8', - 'cent': '\u00A2', - 'Chi': '\u03A7', - 'chi': '\u03C7', - 'circ': '\u02C6', - 'clubs': '\u2663', - 'cong': '\u2245', - 'copy': '\u00A9', - 'crarr': '\u21B5', - 'cup': '\u222A', - 'curren': '\u00A4', - 'dagger': '\u2020', - 'Dagger': '\u2021', - 'darr': '\u2193', - 'dArr': '\u21D3', - 'deg': '\u00B0', - 'Delta': '\u0394', - 'delta': '\u03B4', - 'diams': '\u2666', - 'divide': '\u00F7', - 'Eacute': '\u00C9', - 'eacute': '\u00E9', - 'Ecirc': '\u00CA', - 'ecirc': '\u00EA', - 'Egrave': '\u00C8', - 'egrave': '\u00E8', - 'empty': '\u2205', - 'emsp': '\u2003', - 'ensp': '\u2002', - 'Epsilon': '\u0395', - 'epsilon': '\u03B5', - 'equiv': '\u2261', - 'Eta': '\u0397', - 'eta': '\u03B7', - 'ETH': '\u00D0', - 'eth': '\u00F0', - 'Euml': '\u00CB', - 'euml': '\u00EB', - 'euro': '\u20AC', - 'exist': '\u2203', - 'fnof': '\u0192', - 'forall': '\u2200', - 'frac12': '\u00BD', - 'frac14': '\u00BC', - 'frac34': '\u00BE', - 'frasl': '\u2044', - 'Gamma': '\u0393', - 'gamma': '\u03B3', - 'ge': '\u2265', - 'gt': '>', - 'harr': '\u2194', - 'hArr': '\u21D4', - 'hearts': '\u2665', - 'hellip': '\u2026', - 'Iacute': '\u00CD', - 'iacute': '\u00ED', - 'Icirc': '\u00CE', - 'icirc': '\u00EE', - 'iexcl': '\u00A1', - 'Igrave': '\u00CC', - 'igrave': '\u00EC', - 'image': '\u2111', - 'infin': '\u221E', - 'int': '\u222B', - 'Iota': '\u0399', - 'iota': '\u03B9', - 'iquest': '\u00BF', - 'isin': '\u2208', - 'Iuml': '\u00CF', - 'iuml': '\u00EF', - 'Kappa': '\u039A', - 'kappa': '\u03BA', - 'Lambda': '\u039B', - 'lambda': '\u03BB', - 'lang': '\u27E8', - 'laquo': '\u00AB', - 'larr': '\u2190', - 'lArr': '\u21D0', - 'lceil': '\u2308', - 'ldquo': '\u201C', - 'le': '\u2264', - 'lfloor': '\u230A', - 'lowast': '\u2217', - 'loz': '\u25CA', - 'lrm': '\u200E', - 'lsaquo': '\u2039', - 'lsquo': '\u2018', - 'lt': '<', - 'macr': '\u00AF', - 'mdash': '\u2014', - 'micro': '\u00B5', - 'middot': '\u00B7', - 'minus': '\u2212', - 'Mu': '\u039C', - 'mu': '\u03BC', - 'nabla': '\u2207', - 'nbsp': '\u00A0', - 'ndash': '\u2013', - 'ne': '\u2260', - 'ni': '\u220B', - 'not': '\u00AC', - 'notin': '\u2209', - 'nsub': '\u2284', - 'Ntilde': '\u00D1', - 'ntilde': '\u00F1', - 'Nu': '\u039D', - 'nu': '\u03BD', - 'Oacute': '\u00D3', - 'oacute': '\u00F3', - 'Ocirc': '\u00D4', - 'ocirc': '\u00F4', - 'OElig': '\u0152', - 'oelig': '\u0153', - 'Ograve': '\u00D2', - 'ograve': '\u00F2', - 'oline': '\u203E', - 'Omega': '\u03A9', - 'omega': '\u03C9', - 'Omicron': '\u039F', - 'omicron': '\u03BF', - 'oplus': '\u2295', - 'or': '\u2228', - 'ordf': '\u00AA', - 'ordm': '\u00BA', - 'Oslash': '\u00D8', - 'oslash': '\u00F8', - 'Otilde': '\u00D5', - 'otilde': '\u00F5', - 'otimes': '\u2297', - 'Ouml': '\u00D6', - 'ouml': '\u00F6', - 'para': '\u00B6', - 'permil': '\u2030', - 'perp': '\u22A5', - 'Phi': '\u03A6', - 'phi': '\u03C6', - 'Pi': '\u03A0', - 'pi': '\u03C0', - 'piv': '\u03D6', - 'plusmn': '\u00B1', - 'pound': '\u00A3', - 'prime': '\u2032', - 'Prime': '\u2033', - 'prod': '\u220F', - 'prop': '\u221D', - 'Psi': '\u03A8', - 'psi': '\u03C8', - 'quot': '\u0022', - 'radic': '\u221A', - 'rang': '\u27E9', - 'raquo': '\u00BB', - 'rarr': '\u2192', - 'rArr': '\u21D2', - 'rceil': '\u2309', - 'rdquo': '\u201D', - 'real': '\u211C', - 'reg': '\u00AE', - 'rfloor': '\u230B', - 'Rho': '\u03A1', - 'rho': '\u03C1', - 'rlm': '\u200F', - 'rsaquo': '\u203A', - 'rsquo': '\u2019', - 'sbquo': '\u201A', - 'Scaron': '\u0160', - 'scaron': '\u0161', - 'sdot': '\u22C5', - 'sect': '\u00A7', - 'shy': '\u00AD', - 'Sigma': '\u03A3', - 'sigma': '\u03C3', - 'sigmaf': '\u03C2', - 'sim': '\u223C', - 'spades': '\u2660', - 'sub': '\u2282', - 'sube': '\u2286', - 'sum': '\u2211', - 'sup': '\u2283', - 'sup1': '\u00B9', - 'sup2': '\u00B2', - 'sup3': '\u00B3', - 'supe': '\u2287', - 'szlig': '\u00DF', - 'Tau': '\u03A4', - 'tau': '\u03C4', - 'there4': '\u2234', - 'Theta': '\u0398', - 'theta': '\u03B8', - 'thetasym': '\u03D1', - 'thinsp': '\u2009', - 'THORN': '\u00DE', - 'thorn': '\u00FE', - 'tilde': '\u02DC', - 'times': '\u00D7', - 'trade': '\u2122', - 'Uacute': '\u00DA', - 'uacute': '\u00FA', - 'uarr': '\u2191', - 'uArr': '\u21D1', - 'Ucirc': '\u00DB', - 'ucirc': '\u00FB', - 'Ugrave': '\u00D9', - 'ugrave': '\u00F9', - 'uml': '\u00A8', - 'upsih': '\u03D2', - 'Upsilon': '\u03A5', - 'upsilon': '\u03C5', - 'Uuml': '\u00DC', - 'uuml': '\u00FC', - 'weierp': '\u2118', - 'Xi': '\u039E', - 'xi': '\u03BE', - 'Yacute': '\u00DD', - 'yacute': '\u00FD', - 'yen': '\u00A5', - 'yuml': '\u00FF', - 'Yuml': '\u0178', - 'Zeta': '\u0396', - 'zeta': '\u03B6', - 'zwj': '\u200D', - 'zwnj': '\u200C', - }; +// see http://www.w3.org/TR/html51/syntax.html#named-character-references +// see https://html.spec.whatwg.org/multipage/entities.json +// This list is not exhaustive to keep the compiler footprint low. +// The `{` / `ƫ` syntax should be used when the named character reference does not +// exist. +export const NAMED_ENTITIES: {[k: string]: string} = { + 'Aacute': '\u00C1', + 'aacute': '\u00E1', + 'Acirc': '\u00C2', + 'acirc': '\u00E2', + 'acute': '\u00B4', + 'AElig': '\u00C6', + 'aelig': '\u00E6', + 'Agrave': '\u00C0', + 'agrave': '\u00E0', + 'alefsym': '\u2135', + 'Alpha': '\u0391', + 'alpha': '\u03B1', + 'amp': '&', + 'and': '\u2227', + 'ang': '\u2220', + 'apos': '\u0027', + 'Aring': '\u00C5', + 'aring': '\u00E5', + 'asymp': '\u2248', + 'Atilde': '\u00C3', + 'atilde': '\u00E3', + 'Auml': '\u00C4', + 'auml': '\u00E4', + 'bdquo': '\u201E', + 'Beta': '\u0392', + 'beta': '\u03B2', + 'brvbar': '\u00A6', + 'bull': '\u2022', + 'cap': '\u2229', + 'Ccedil': '\u00C7', + 'ccedil': '\u00E7', + 'cedil': '\u00B8', + 'cent': '\u00A2', + 'Chi': '\u03A7', + 'chi': '\u03C7', + 'circ': '\u02C6', + 'clubs': '\u2663', + 'cong': '\u2245', + 'copy': '\u00A9', + 'crarr': '\u21B5', + 'cup': '\u222A', + 'curren': '\u00A4', + 'dagger': '\u2020', + 'Dagger': '\u2021', + 'darr': '\u2193', + 'dArr': '\u21D3', + 'deg': '\u00B0', + 'Delta': '\u0394', + 'delta': '\u03B4', + 'diams': '\u2666', + 'divide': '\u00F7', + 'Eacute': '\u00C9', + 'eacute': '\u00E9', + 'Ecirc': '\u00CA', + 'ecirc': '\u00EA', + 'Egrave': '\u00C8', + 'egrave': '\u00E8', + 'empty': '\u2205', + 'emsp': '\u2003', + 'ensp': '\u2002', + 'Epsilon': '\u0395', + 'epsilon': '\u03B5', + 'equiv': '\u2261', + 'Eta': '\u0397', + 'eta': '\u03B7', + 'ETH': '\u00D0', + 'eth': '\u00F0', + 'Euml': '\u00CB', + 'euml': '\u00EB', + 'euro': '\u20AC', + 'exist': '\u2203', + 'fnof': '\u0192', + 'forall': '\u2200', + 'frac12': '\u00BD', + 'frac14': '\u00BC', + 'frac34': '\u00BE', + 'frasl': '\u2044', + 'Gamma': '\u0393', + 'gamma': '\u03B3', + 'ge': '\u2265', + 'gt': '>', + 'harr': '\u2194', + 'hArr': '\u21D4', + 'hearts': '\u2665', + 'hellip': '\u2026', + 'Iacute': '\u00CD', + 'iacute': '\u00ED', + 'Icirc': '\u00CE', + 'icirc': '\u00EE', + 'iexcl': '\u00A1', + 'Igrave': '\u00CC', + 'igrave': '\u00EC', + 'image': '\u2111', + 'infin': '\u221E', + 'int': '\u222B', + 'Iota': '\u0399', + 'iota': '\u03B9', + 'iquest': '\u00BF', + 'isin': '\u2208', + 'Iuml': '\u00CF', + 'iuml': '\u00EF', + 'Kappa': '\u039A', + 'kappa': '\u03BA', + 'Lambda': '\u039B', + 'lambda': '\u03BB', + 'lang': '\u27E8', + 'laquo': '\u00AB', + 'larr': '\u2190', + 'lArr': '\u21D0', + 'lceil': '\u2308', + 'ldquo': '\u201C', + 'le': '\u2264', + 'lfloor': '\u230A', + 'lowast': '\u2217', + 'loz': '\u25CA', + 'lrm': '\u200E', + 'lsaquo': '\u2039', + 'lsquo': '\u2018', + 'lt': '<', + 'macr': '\u00AF', + 'mdash': '\u2014', + 'micro': '\u00B5', + 'middot': '\u00B7', + 'minus': '\u2212', + 'Mu': '\u039C', + 'mu': '\u03BC', + 'nabla': '\u2207', + 'nbsp': '\u00A0', + 'ndash': '\u2013', + 'ne': '\u2260', + 'ni': '\u220B', + 'not': '\u00AC', + 'notin': '\u2209', + 'nsub': '\u2284', + 'Ntilde': '\u00D1', + 'ntilde': '\u00F1', + 'Nu': '\u039D', + 'nu': '\u03BD', + 'Oacute': '\u00D3', + 'oacute': '\u00F3', + 'Ocirc': '\u00D4', + 'ocirc': '\u00F4', + 'OElig': '\u0152', + 'oelig': '\u0153', + 'Ograve': '\u00D2', + 'ograve': '\u00F2', + 'oline': '\u203E', + 'Omega': '\u03A9', + 'omega': '\u03C9', + 'Omicron': '\u039F', + 'omicron': '\u03BF', + 'oplus': '\u2295', + 'or': '\u2228', + 'ordf': '\u00AA', + 'ordm': '\u00BA', + 'Oslash': '\u00D8', + 'oslash': '\u00F8', + 'Otilde': '\u00D5', + 'otilde': '\u00F5', + 'otimes': '\u2297', + 'Ouml': '\u00D6', + 'ouml': '\u00F6', + 'para': '\u00B6', + 'permil': '\u2030', + 'perp': '\u22A5', + 'Phi': '\u03A6', + 'phi': '\u03C6', + 'Pi': '\u03A0', + 'pi': '\u03C0', + 'piv': '\u03D6', + 'plusmn': '\u00B1', + 'pound': '\u00A3', + 'prime': '\u2032', + 'Prime': '\u2033', + 'prod': '\u220F', + 'prop': '\u221D', + 'Psi': '\u03A8', + 'psi': '\u03C8', + 'quot': '\u0022', + 'radic': '\u221A', + 'rang': '\u27E9', + 'raquo': '\u00BB', + 'rarr': '\u2192', + 'rArr': '\u21D2', + 'rceil': '\u2309', + 'rdquo': '\u201D', + 'real': '\u211C', + 'reg': '\u00AE', + 'rfloor': '\u230B', + 'Rho': '\u03A1', + 'rho': '\u03C1', + 'rlm': '\u200F', + 'rsaquo': '\u203A', + 'rsquo': '\u2019', + 'sbquo': '\u201A', + 'Scaron': '\u0160', + 'scaron': '\u0161', + 'sdot': '\u22C5', + 'sect': '\u00A7', + 'shy': '\u00AD', + 'Sigma': '\u03A3', + 'sigma': '\u03C3', + 'sigmaf': '\u03C2', + 'sim': '\u223C', + 'spades': '\u2660', + 'sub': '\u2282', + 'sube': '\u2286', + 'sum': '\u2211', + 'sup': '\u2283', + 'sup1': '\u00B9', + 'sup2': '\u00B2', + 'sup3': '\u00B3', + 'supe': '\u2287', + 'szlig': '\u00DF', + 'Tau': '\u03A4', + 'tau': '\u03C4', + 'there4': '\u2234', + 'Theta': '\u0398', + 'theta': '\u03B8', + 'thetasym': '\u03D1', + 'thinsp': '\u2009', + 'THORN': '\u00DE', + 'thorn': '\u00FE', + 'tilde': '\u02DC', + 'times': '\u00D7', + 'trade': '\u2122', + 'Uacute': '\u00DA', + 'uacute': '\u00FA', + 'uarr': '\u2191', + 'uArr': '\u21D1', + 'Ucirc': '\u00DB', + 'ucirc': '\u00FB', + 'Ugrave': '\u00D9', + 'ugrave': '\u00F9', + 'uml': '\u00A8', + 'upsih': '\u03D2', + 'Upsilon': '\u03A5', + 'upsilon': '\u03C5', + 'Uuml': '\u00DC', + 'uuml': '\u00FC', + 'weierp': '\u2118', + 'Xi': '\u039E', + 'xi': '\u03BE', + 'Yacute': '\u00DD', + 'yacute': '\u00FD', + 'yen': '\u00A5', + 'yuml': '\u00FF', + 'Yuml': '\u0178', + 'Zeta': '\u0396', + 'zeta': '\u03B6', + 'zwj': '\u200D', + 'zwnj': '\u200C', +}; diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index 8cd09a93e1..d98f67f4d1 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -228,7 +228,7 @@ export class ReadVarExpr extends Expression { set(value: Expression): WriteVarExpr { if (!this.name) { - throw new Error(`Built in variable ${this.builtin} can not be assigned to.`) + throw new Error(`Built in variable ${this.builtin} can not be assigned to.`); } return new WriteVarExpr(this.name, value, null, this.sourceSpan); } diff --git a/packages/compiler/src/style_url_resolver.ts b/packages/compiler/src/style_url_resolver.ts index 1f2bbb5dd6..1a45ff0ddc 100644 --- a/packages/compiler/src/style_url_resolver.ts +++ b/packages/compiler/src/style_url_resolver.ts @@ -43,5 +43,5 @@ export function extractStyleUrls( } const CSS_IMPORT_REGEXP = /@import\s+(?:url\()?\s*(?:(?:['"]([^'"]*))|([^;\)\s]*))[^;]*;?/g; -const CSS_COMMENT_REGEXP = /\/\*.+?\*\//g; +const CSS_COMMENT_REGEXP = /\/\*[\s\S]+?\*\//g; const URL_WITH_SCHEMA_REGEXP = /^([^:/?#]+):/; diff --git a/packages/compiler/src/view_compiler/view_compiler.ts b/packages/compiler/src/view_compiler/view_compiler.ts index 6398c06199..f0dca508b5 100644 --- a/packages/compiler/src/view_compiler/view_compiler.ts +++ b/packages/compiler/src/view_compiler/view_compiler.ts @@ -1071,4 +1071,4 @@ function calcStaticDynamicQueryFlags( flags |= NodeFlags.DynamicQuery; } return flags; -} \ No newline at end of file +} diff --git a/packages/compiler/test/aot/compiler_spec.ts b/packages/compiler/test/aot/compiler_spec.ts index faf041d9a8..a515126e51 100644 --- a/packages/compiler/test/aot/compiler_spec.ts +++ b/packages/compiler/test/aot/compiler_spec.ts @@ -28,7 +28,7 @@ describe('compiler (unbundled Angular)', () => { describe('aot source mapping', () => { const componentPath = '/app/app.component.ts'; - const ngComponentPath = 'ng:///app/app.component.ts' + const ngComponentPath = 'ng:///app/app.component.ts'; let rootDir: MockDirectory; let appDir: MockDirectory; diff --git a/packages/compiler/test/aot/static_reflector_spec.ts b/packages/compiler/test/aot/static_reflector_spec.ts index 8d72f75166..c594c15a30 100644 --- a/packages/compiler/test/aot/static_reflector_spec.ts +++ b/packages/compiler/test/aot/static_reflector_spec.ts @@ -462,6 +462,20 @@ describe('StaticReflector', () => { expect(annotations[0].providers[0].useValue.members[0]).toEqual('staticMethod'); }); + it('should be able to get metadata for a class calling a macro function', () => { + const annotations = reflector.annotations( + reflector.getStaticSymbol('/tmp/src/call-macro-function.ts', 'MyComponent')); + expect(annotations.length).toBe(1); + expect(annotations[0].providers.useValue).toBe(100); + }); + + it('should be able to get metadata for a class calling a nested macro function', () => { + const annotations = reflector.annotations( + reflector.getStaticSymbol('/tmp/src/call-macro-function.ts', 'MyComponentNested')); + expect(annotations.length).toBe(1); + expect(annotations[0].providers.useValue.useValue).toBe(100); + }); + // #13605 it('should not throw on unknown decorators', () => { const data = Object.create(DEFAULT_TEST_DATA); @@ -1392,6 +1406,25 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = { static VALUE = 'Some string'; } `, + '/tmp/src/macro-function.ts': ` + export function v(value: any) { + return { provide: 'a', useValue: value }; + } + `, + '/tmp/src/call-macro-function.ts': ` + import {Component} from '@angular/core'; + import {v} from './macro-function'; + + @Component({ + providers: v(100) + }) + export class MyComponent { } + + @Component({ + providers: v(v(100)) + }) + export class MyComponentNested { } + `, '/tmp/src/static-field-reference.ts': ` import {Component} from '@angular/core'; import {MyModule} from './static-field'; diff --git a/packages/compiler/test/aot/static_symbol_resolver_spec.ts b/packages/compiler/test/aot/static_symbol_resolver_spec.ts index eba341aac7..4ee26ff77c 100644 --- a/packages/compiler/test/aot/static_symbol_resolver_spec.ts +++ b/packages/compiler/test/aot/static_symbol_resolver_spec.ts @@ -459,7 +459,7 @@ export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost { const errors = diagnostics .map(d => { - const {line, character} = ts.getLineAndCharacterOfPosition(d.file, d.start); + const {line, character} = ts.getLineAndCharacterOfPosition(d.file !, d.start !); return `(${line}:${character}): ${d.messageText}`; }) .join('\n'); diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index 93b3c7b25b..7ea8633c78 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -559,8 +559,8 @@ export function expectNoDiagnostics(program: ts.Program) { function lineInfo(diagnostic: ts.Diagnostic): string { if (diagnostic.file) { - const start = diagnostic.start; - let end = diagnostic.start + diagnostic.length; + const start = diagnostic.start !; + let end = diagnostic.start ! + diagnostic.length !; const source = diagnostic.file.text; let lineStart = start; let lineEnd = end; diff --git a/packages/compiler/test/directive_normalizer_spec.ts b/packages/compiler/test/directive_normalizer_spec.ts index 114f2672a0..bd68115eb1 100644 --- a/packages/compiler/test/directive_normalizer_spec.ts +++ b/packages/compiler/test/directive_normalizer_spec.ts @@ -328,10 +328,7 @@ export function main() { describe('normalizeExternalStylesheets', () => { - beforeEach(() => { - TestBed.configureCompiler( - {providers: [{provide: ResourceLoader, useClass: SpyResourceLoader}]}); - }); + beforeEach(() => { TestBed.configureCompiler({providers: [SpyResourceLoader.PROVIDE]}); }); it('should load an external stylesheet', inject( diff --git a/packages/compiler/test/i18n/extractor_merger_spec.ts b/packages/compiler/test/i18n/extractor_merger_spec.ts index 641b769c3f..0618e0408f 100644 --- a/packages/compiler/test/i18n/extractor_merger_spec.ts +++ b/packages/compiler/test/i18n/extractor_merger_spec.ts @@ -490,7 +490,7 @@ export function main() { .toEqual( '<span someAttr="ok">foo</span><div>{count, plural, =0 {<p title="foo"></p>}}</div>'); }); - }) + }); }); } diff --git a/packages/compiler/test/i18n/integration_common.ts b/packages/compiler/test/i18n/integration_common.ts index b2abdb8d0d..4d7e37d620 100644 --- a/packages/compiler/test/i18n/integration_common.ts +++ b/packages/compiler/test/i18n/integration_common.ts @@ -26,6 +26,7 @@ export class I18nComponent { } export class FrLocalization extends NgLocalization { + public static PROVIDE = {provide: NgLocalization, useClass: FrLocalization, deps: []}; getPluralCategory(value: number): string { switch (value) { case 0: diff --git a/packages/compiler/test/i18n/integration_xliff2_spec.ts b/packages/compiler/test/i18n/integration_xliff2_spec.ts index 3594db225a..add927a8c1 100644 --- a/packages/compiler/test/i18n/integration_xliff2_spec.ts +++ b/packages/compiler/test/i18n/integration_xliff2_spec.ts @@ -26,8 +26,8 @@ export function main() { beforeEach(async(() => { TestBed.configureCompiler({ providers: [ - {provide: ResourceLoader, useClass: SpyResourceLoader}, - {provide: NgLocalization, useClass: FrLocalization}, + SpyResourceLoader.PROVIDE, + FrLocalization.PROVIDE, {provide: TRANSLATIONS, useValue: XLIFF2_TOMERGE}, {provide: TRANSLATIONS_FORMAT, useValue: 'xlf2'}, ] diff --git a/packages/compiler/test/i18n/integration_xliff_spec.ts b/packages/compiler/test/i18n/integration_xliff_spec.ts index 1bcc2f0cc6..7355fff485 100644 --- a/packages/compiler/test/i18n/integration_xliff_spec.ts +++ b/packages/compiler/test/i18n/integration_xliff_spec.ts @@ -26,8 +26,8 @@ export function main() { beforeEach(async(() => { TestBed.configureCompiler({ providers: [ - {provide: ResourceLoader, useClass: SpyResourceLoader}, - {provide: NgLocalization, useClass: FrLocalization}, + SpyResourceLoader.PROVIDE, + FrLocalization.PROVIDE, {provide: TRANSLATIONS, useValue: XLIFF_TOMERGE}, {provide: TRANSLATIONS_FORMAT, useValue: 'xliff'}, ] diff --git a/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts b/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts index ebd7c324f7..88f249a346 100644 --- a/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts +++ b/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts @@ -26,8 +26,8 @@ export function main() { beforeEach(async(() => { TestBed.configureCompiler({ providers: [ - {provide: ResourceLoader, useClass: SpyResourceLoader}, - {provide: NgLocalization, useClass: FrLocalization}, + SpyResourceLoader.PROVIDE, + FrLocalization.PROVIDE, {provide: TRANSLATIONS, useValue: XTB}, {provide: TRANSLATIONS_FORMAT, useValue: 'xtb'}, ] diff --git a/packages/compiler/test/ml_parser/html_parser_spec.ts b/packages/compiler/test/ml_parser/html_parser_spec.ts index 1a8b2bd630..837ec0ebb0 100644 --- a/packages/compiler/test/ml_parser/html_parser_spec.ts +++ b/packages/compiler/test/ml_parser/html_parser_spec.ts @@ -152,6 +152,16 @@ export function main() { ]); }); + it('should append the required parent considering top level ng-container', () => { + expect(humanizeDom( + parser.parse('<ng-container><tr></tr></ng-container><p></p>', 'TestComp'))) + .toEqual([ + [html.Element, 'ng-container', 0], + [html.Element, 'tr', 1], + [html.Element, 'p', 0], + ]); + }); + it('should special case ng-container when adding a required parent', () => { expect(humanizeDom(parser.parse( '<table><thead><ng-container><tr></tr></ng-container></thead></table>', diff --git a/packages/compiler/test/output/output_jit_spec.ts b/packages/compiler/test/output/output_jit_spec.ts index 017ddf8cb6..b9aad8dadf 100644 --- a/packages/compiler/test/output/output_jit_spec.ts +++ b/packages/compiler/test/output/output_jit_spec.ts @@ -31,5 +31,5 @@ export function main() { expect(Object.keys(args).length).toBe(20); }); }); - }) + }); } \ No newline at end of file diff --git a/packages/compiler/test/parse_util_spec.ts b/packages/compiler/test/parse_util_spec.ts index 65521cf451..9c9ce43f0b 100644 --- a/packages/compiler/test/parse_util_spec.ts +++ b/packages/compiler/test/parse_util_spec.ts @@ -6,23 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ -import {ParseError, ParseErrorLevel, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../src/parse_util' +import {ParseError, ParseErrorLevel, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../src/parse_util'; export function main() { - describe( - 'ParseError', - () => { - it('should reflect the level in the message', () => { - const file = new ParseSourceFile(`foo\nbar\nfoo`, 'url'); - const start = new ParseLocation(file, 4, 1, 0); - const end = new ParseLocation(file, 6, 1, 2); - const span = new ParseSourceSpan(start, end); + describe('ParseError', () => { + it('should reflect the level in the message', () => { + const file = new ParseSourceFile(`foo\nbar\nfoo`, 'url'); + const start = new ParseLocation(file, 4, 1, 0); + const end = new ParseLocation(file, 6, 1, 2); + const span = new ParseSourceSpan(start, end); - const fatal = new ParseError(span, 'fatal', ParseErrorLevel.ERROR); - expect(fatal.toString()).toEqual('fatal ("foo\n[ERROR ->]bar\nfoo"): url@1:0'); + const fatal = new ParseError(span, 'fatal', ParseErrorLevel.ERROR); + expect(fatal.toString()).toEqual('fatal ("foo\n[ERROR ->]bar\nfoo"): url@1:0'); - const warning = new ParseError(span, 'warning', ParseErrorLevel.WARNING); - expect(warning.toString()).toEqual('warning ("foo\n[WARNING ->]bar\nfoo"): url@1:0'); - }); - }); + const warning = new ParseError(span, 'warning', ParseErrorLevel.WARNING); + expect(warning.toString()).toEqual('warning ("foo\n[WARNING ->]bar\nfoo"): url@1:0'); + }); + }); } \ No newline at end of file diff --git a/packages/compiler/test/runtime_compiler_spec.ts b/packages/compiler/test/runtime_compiler_spec.ts index 318ca06a1b..199e9e2827 100644 --- a/packages/compiler/test/runtime_compiler_spec.ts +++ b/packages/compiler/test/runtime_compiler_spec.ts @@ -36,7 +36,7 @@ export function main() { beforeEach(() => { TestBed.configureCompiler( - {providers: [{provide: ResourceLoader, useClass: StubResourceLoader}]}); + {providers: [{provide: ResourceLoader, useClass: StubResourceLoader, deps: []}]}); }); it('should throw when using a templateUrl that has not been compiled before', async(() => { @@ -68,7 +68,7 @@ export function main() { beforeEach(() => { TestBed.configureCompiler( - {providers: [{provide: ResourceLoader, useClass: StubResourceLoader}]}); + {providers: [{provide: ResourceLoader, useClass: StubResourceLoader, deps: []}]}); }); it('should allow to use templateUrl components that have been loaded before', async(() => { @@ -88,10 +88,7 @@ export function main() { let dirResolver: MockDirectiveResolver; let injector: Injector; - beforeEach(() => { - TestBed.configureCompiler( - {providers: [{provide: ResourceLoader, useClass: SpyResourceLoader}]}); - }); + beforeEach(() => { TestBed.configureCompiler({providers: [SpyResourceLoader.PROVIDE]}); }); beforeEach(fakeAsync(inject( [Compiler, ResourceLoader, DirectiveResolver, Injector], diff --git a/packages/compiler/test/spies.ts b/packages/compiler/test/spies.ts index b86939489a..54c2263c4a 100644 --- a/packages/compiler/test/spies.ts +++ b/packages/compiler/test/spies.ts @@ -11,5 +11,6 @@ import {ResourceLoader} from '@angular/compiler/src/resource_loader'; import {SpyObject} from '@angular/core/testing/src/testing_internal'; export class SpyResourceLoader extends SpyObject { + public static PROVIDE = {provide: ResourceLoader, useClass: SpyResourceLoader, deps: []}; constructor() { super(ResourceLoader); } } diff --git a/packages/compiler/test/style_url_resolver_spec.ts b/packages/compiler/test/style_url_resolver_spec.ts index 5b7b548f1e..3b135e9925 100644 --- a/packages/compiler/test/style_url_resolver_spec.ts +++ b/packages/compiler/test/style_url_resolver_spec.ts @@ -40,11 +40,15 @@ export function main() { const css = ` @import '1.css'; /*@import '2.css';*/ + /* + @import '3.css'; + */ `; const styleWithImports = extractStyleUrls(urlResolver, 'http://ng.io', css); expect(styleWithImports.style.trim()).toEqual(''); expect(styleWithImports.styleUrls).toContain('http://ng.io/1.css'); expect(styleWithImports.styleUrls).not.toContain('http://ng.io/2.css'); + expect(styleWithImports.styleUrls).not.toContain('http://ng.io/3.css'); }); it('should extract "@import url()" urls', () => { diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index c0a2fe8335..8204b2f67c 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -308,7 +308,7 @@ export function main() { TestBed.configureCompiler({ providers: [ TEST_COMPILER_PROVIDERS, - {provide: ElementSchemaRegistry, useClass: DomElementSchemaRegistry} + {provide: ElementSchemaRegistry, useClass: DomElementSchemaRegistry, deps: []} ] }); }); diff --git a/packages/compiler/testing/src/test_bindings.ts b/packages/compiler/testing/src/test_bindings.ts index 8812ac8ecb..4956bf64f4 100644 --- a/packages/compiler/testing/src/test_bindings.ts +++ b/packages/compiler/testing/src/test_bindings.ts @@ -20,6 +20,6 @@ export function createUrlResolverWithoutPackagePrefix(): UrlResolver { // TODO: get rid of it or move to a separate @angular/internal_testing package export const TEST_COMPILER_PROVIDERS: Provider[] = [ {provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {}, [], [])}, - {provide: ResourceLoader, useClass: MockResourceLoader}, - {provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix} + {provide: ResourceLoader, useClass: MockResourceLoader, deps: []}, + {provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix, deps: []} ]; diff --git a/packages/compiler/testing/src/testing.ts b/packages/compiler/testing/src/testing.ts index c77be42fd8..0d259fa709 100644 --- a/packages/compiler/testing/src/testing.ts +++ b/packages/compiler/testing/src/testing.ts @@ -28,7 +28,7 @@ export * from './pipe_resolver_mock'; import {createPlatformFactory, ModuleWithComponentFactories, Injectable, CompilerOptions, COMPILER_OPTIONS, CompilerFactory, ComponentFactory, NgModuleFactory, Injector, NgModule, Component, Directive, Pipe, Type, PlatformRef, ɵstringify} from '@angular/core'; import {MetadataOverride, ɵTestingCompilerFactory as TestingCompilerFactory, ɵTestingCompiler as TestingCompiler} from '@angular/core/testing'; -import {platformCoreDynamic, JitCompiler, DirectiveResolver, NgModuleResolver, PipeResolver, CompileMetadataResolver} from '@angular/compiler'; +import {platformCoreDynamic, JitCompiler, DirectiveResolver, NgModuleResolver, PipeResolver, CompileMetadataResolver, CompileReflector} from '@angular/compiler'; import {MockDirectiveResolver} from './directive_resolver_mock'; import {MockNgModuleResolver} from './ng_module_resolver_mock'; import {MockPipeResolver} from './pipe_resolver_mock'; @@ -124,15 +124,19 @@ export const platformCoreDynamicTesting: (extraProviders?: any[]) => PlatformRef provide: COMPILER_OPTIONS, useValue: { providers: [ - MockPipeResolver, + {provide: MockPipeResolver, deps: [Injector, CompileReflector]}, {provide: PipeResolver, useExisting: MockPipeResolver}, - MockDirectiveResolver, + {provide: MockDirectiveResolver, deps: [Injector, CompileReflector]}, {provide: DirectiveResolver, useExisting: MockDirectiveResolver}, - MockNgModuleResolver, + {provide: MockNgModuleResolver, deps: [Injector, CompileReflector]}, {provide: NgModuleResolver, useExisting: MockNgModuleResolver}, ] }, multi: true }, - {provide: TestingCompilerFactory, useClass: TestingCompilerFactoryImpl} + { + provide: TestingCompilerFactory, + useClass: TestingCompilerFactoryImpl, + deps: [CompilerFactory] + } ]); diff --git a/packages/core/src/application_init.ts b/packages/core/src/application_init.ts index fdc1966a00..b801ab26b4 100644 --- a/packages/core/src/application_init.ts +++ b/packages/core/src/application_init.ts @@ -45,11 +45,10 @@ export class ApplicationInitStatus { const asyncInitPromises: Promise<any>[] = []; - const complete = - () => { - this._done = true; - this.resolve(); - } + const complete = () => { + this._done = true; + this.resolve(); + }; if (this.appInits) { for (let i = 0; i < this.appInits.length; i++) { diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index ebb6a28b2d..a3dd39d563 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -19,7 +19,7 @@ import {isPromise} from '../src/util/lang'; import {ApplicationInitStatus} from './application_init'; import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens'; import {Console} from './console'; -import {Injectable, InjectionToken, Injector, Provider, ReflectiveInjector} from './di'; +import {Injectable, InjectionToken, Injector, StaticProvider} from './di'; import {CompilerFactory, CompilerOptions} from './linker/compiler'; import {ComponentFactory, ComponentRef} from './linker/component_factory'; import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from './linker/component_factory_resolver'; @@ -99,17 +99,18 @@ export function createPlatform(injector: Injector): PlatformRef { * @experimental APIs related to application bootstrap are currently under review. */ export function createPlatformFactory( - parentPlatformFactory: ((extraProviders?: Provider[]) => PlatformRef) | null, name: string, - providers: Provider[] = []): (extraProviders?: Provider[]) => PlatformRef { + parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef) | null, + name: string, providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => + PlatformRef { const marker = new InjectionToken(`Platform: ${name}`); - return (extraProviders: Provider[] = []) => { + return (extraProviders: StaticProvider[] = []) => { let platform = getPlatform(); if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { parentPlatformFactory( providers.concat(extraProviders).concat({provide: marker, useValue: true})); } else { - createPlatform(ReflectiveInjector.resolveAndCreate( + createPlatform(Injector.create( providers.concat(extraProviders).concat({provide: marker, useValue: true}))); } } @@ -292,8 +293,7 @@ export class PlatformRef_ extends PlatformRef { // Attention: Don't use ApplicationRef.run here, // as we want to be sure that all possible constructor calls are inside `ngZone.run`! return ngZone.run(() => { - const ngZoneInjector = - ReflectiveInjector.resolveAndCreate([{provide: NgZone, useValue: ngZone}], this.injector); + const ngZoneInjector = Injector.create([{provide: NgZone, useValue: ngZone}], this.injector); const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector); const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null); if (!exceptionHandler) { diff --git a/packages/core/src/change_detection/differs/default_iterable_differ.ts b/packages/core/src/change_detection/differs/default_iterable_differ.ts index 6f5686b165..a73a597904 100644 --- a/packages/core/src/change_detection/differs/default_iterable_differ.ts +++ b/packages/core/src/change_detection/differs/default_iterable_differ.ts @@ -52,11 +52,9 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan // Keeps track of records where custom track by is the same, but item identity has changed private _identityChangesHead: IterableChangeRecord_<V>|null = null; private _identityChangesTail: IterableChangeRecord_<V>|null = null; - private _trackByFn: TrackByFunction<V> + private _trackByFn: TrackByFunction<V>; - constructor(trackByFn?: TrackByFunction<V>) { - this._trackByFn = trackByFn || trackByIdentity; - } + constructor(trackByFn?: TrackByFunction<V>) { this._trackByFn = trackByFn || trackByIdentity; } get collection() { return this._collection; } diff --git a/packages/core/src/change_detection/differs/iterable_differs.ts b/packages/core/src/change_detection/differs/iterable_differs.ts index 72bad7296b..d1daadc3b5 100644 --- a/packages/core/src/change_detection/differs/iterable_differs.ts +++ b/packages/core/src/change_detection/differs/iterable_differs.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Optional, Provider, SkipSelf} from '../../di'; +import {Optional, SkipSelf, StaticProvider} from '../../di'; import {ChangeDetectorRef} from '../change_detector_ref'; + /** * A type describing supported iterable types. * @@ -181,7 +182,7 @@ export class IterableDiffers { * }) * ``` */ - static extend(factories: IterableDifferFactory[]): Provider { + static extend(factories: IterableDifferFactory[]): StaticProvider { return { provide: IterableDiffers, useFactory: (parent: IterableDiffers) => { diff --git a/packages/core/src/change_detection/differs/keyvalue_differs.ts b/packages/core/src/change_detection/differs/keyvalue_differs.ts index f401ab8389..c6637ecf9d 100644 --- a/packages/core/src/change_detection/differs/keyvalue_differs.ts +++ b/packages/core/src/change_detection/differs/keyvalue_differs.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Optional, Provider, SkipSelf} from '../../di'; +import {Optional, SkipSelf, StaticProvider} from '../../di'; import {ChangeDetectorRef} from '../change_detector_ref'; @@ -156,7 +156,7 @@ export class KeyValueDiffers { * }) * ``` */ - static extend<S>(factories: KeyValueDifferFactory[]): Provider { + static extend<S>(factories: KeyValueDifferFactory[]): StaticProvider { return { provide: KeyValueDiffers, useFactory: (parent: KeyValueDiffers) => { diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index cbfcbaf62d..976fd3f5e4 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -18,7 +18,7 @@ export {forwardRef, resolveForwardRef, ForwardRefFn} from './di/forward_ref'; export {Injector} from './di/injector'; export {ReflectiveInjector} from './di/reflective_injector'; -export {Provider, TypeProvider, ValueProvider, ClassProvider, ExistingProvider, FactoryProvider} from './di/provider'; +export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider'; export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider'; export {ReflectiveKey} from './di/reflective_key'; export {InjectionToken, OpaqueToken} from './di/injection_token'; diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index d640c81a08..a0a380a414 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -9,7 +9,10 @@ import {Type} from '../type'; import {stringify} from '../util'; +import {resolveForwardRef} from './forward_ref'; import {InjectionToken} from './injection_token'; +import {Inject, Optional, Self, SkipSelf} from './metadata'; +import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './provider'; const _THROW_IF_NOT_FOUND = new Object(); export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; @@ -17,7 +20,7 @@ export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; class _NullInjector implements Injector { get(token: any, notFoundValue: any = _THROW_IF_NOT_FOUND): any { if (notFoundValue === _THROW_IF_NOT_FOUND) { - throw new Error(`No provider for ${stringify(token)}!`); + throw new Error(`NullInjectorError: No provider for ${stringify(token)}!`); } return notFoundValue; } @@ -60,4 +63,307 @@ export abstract class Injector { * @suppress {duplicate} */ abstract get(token: any, notFoundValue?: any): any; + + /** + * Create a new Injector which is configure using `StaticProvider`s. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ConstructorProvider'} + */ + static create(providers: StaticProvider[], parent?: Injector): Injector { + return new StaticInjector(providers, parent); + } +} + + + +const IDENT = function<T>(value: T): T { + return value; +}; +const EMPTY = <any[]>[]; +const CIRCULAR = IDENT; +const MULTI_PROVIDER_FN = function(): any[] { + return Array.prototype.slice.call(arguments); +}; +const GET_PROPERTY_NAME = {} as any; +const USE_VALUE = + getClosureSafeProperty<ValueProvider>({provide: String, useValue: GET_PROPERTY_NAME}); +const NG_TOKEN_PATH = 'ngTokenPath'; +const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath'; +const enum OptionFlags { + Optional = 1 << 0, + CheckSelf = 1 << 1, + CheckParent = 1 << 2, + Default = CheckSelf | CheckParent +} +const NULL_INJECTOR = Injector.NULL; +const NEW_LINE = /\n/gm; +const NO_NEW_LINE = 'ɵ'; + +export class StaticInjector implements Injector { + readonly parent: Injector; + + private _records: Map<any, Record>; + + constructor(providers: StaticProvider[], parent: Injector = NULL_INJECTOR) { + this.parent = parent; + const records = this._records = new Map<any, Record>(); + records.set( + Injector, <Record>{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}); + recursivelyProcessProviders(records, providers); + } + + get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T): T; + get(token: any, notFoundValue?: any): any; + get(token: any, notFoundValue?: any): any { + const record = this._records.get(token); + try { + return tryResolveToken(token, record, this._records, this.parent, notFoundValue); + } catch (e) { + const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH]; + e.message = formatError('\n' + e.message, tokenPath); + e[NG_TOKEN_PATH] = tokenPath; + e[NG_TEMP_TOKEN_PATH] = null; + throw e; + } + } + + toString() { + const tokens = <string[]>[], records = this._records; + records.forEach((v, token) => tokens.push(stringify(token))); + return `StaticInjector[${tokens.join(', ')}]`; + } +} + +type SupportedProvider = + ValueProvider | ExistingProvider | StaticClassProvider | ConstructorProvider | FactoryProvider; + +interface Record { + fn: Function; + useNew: boolean; + deps: DependencyRecord[]; + value: any; +} + +interface DependencyRecord { + token: any; + options: number; +} + +type TokenPath = Array<any>; + +function resolveProvider(provider: SupportedProvider): Record { + const deps = computeDeps(provider); + let fn: Function = IDENT; + let value: any = EMPTY; + let useNew: boolean = false; + let provide = resolveForwardRef(provider.provide); + if (USE_VALUE in provider) { + // We need to use USE_VALUE in provider since provider.useValue could be defined as undefined. + value = (provider as ValueProvider).useValue; + } else if ((provider as FactoryProvider).useFactory) { + fn = (provider as FactoryProvider).useFactory; + } else if ((provider as ExistingProvider).useExisting) { + // Just use IDENT + } else if ((provider as StaticClassProvider).useClass) { + useNew = true; + fn = resolveForwardRef((provider as StaticClassProvider).useClass); + } else if (typeof provide == 'function') { + useNew = true; + fn = provide; + } else { + throw staticError( + 'StaticProvider does not have [useValue|useFactory|useExisting|useClass] or [provide] is not newable', + provider); + } + return {deps, fn, useNew, value}; +} + +function multiProviderMixError(token: any) { + return staticError('Cannot mix multi providers and regular providers', token); +} + +function recursivelyProcessProviders(records: Map<any, Record>, provider: StaticProvider) { + if (provider) { + provider = resolveForwardRef(provider); + if (provider instanceof Array) { + // if we have an array recurse into the array + for (let i = 0; i < provider.length; i++) { + recursivelyProcessProviders(records, provider[i]); + } + } else if (typeof provider === 'function') { + // Functions were supported in ReflectiveInjector, but are not here. For safety give useful + // error messages + throw staticError('Function/Class not supported', provider); + } else if (provider && typeof provider === 'object' && provider.provide) { + // At this point we have what looks like a provider: {provide: ?, ....} + let token = resolveForwardRef(provider.provide); + const resolvedProvider = resolveProvider(provider); + if (provider.multi === true) { + // This is a multi provider. + let multiProvider: Record|undefined = records.get(token); + if (multiProvider) { + if (multiProvider.fn !== MULTI_PROVIDER_FN) { + throw multiProviderMixError(token); + } + } else { + // Create a placeholder factory which will look up the constituents of the multi provider. + records.set(token, multiProvider = <Record>{ + token: provider.provide, + deps: [], + useNew: false, + fn: MULTI_PROVIDER_FN, + value: EMPTY + }); + } + // Treat the provider as the token. + token = provider; + multiProvider.deps.push({token, options: OptionFlags.Default}); + } + const record = records.get(token); + if (record && record.fn == MULTI_PROVIDER_FN) { + throw multiProviderMixError(token); + } + records.set(token, resolvedProvider); + } else { + throw staticError('Unexpected provider', provider); + } + } +} + +function tryResolveToken( + token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector, + notFoundValue: any): any { + try { + return resolveToken(token, record, records, parent, notFoundValue); + } catch (e) { + // ensure that 'e' is of type Error. + if (!(e instanceof Error)) { + e = new Error(e); + } + const path: any[] = e[NG_TEMP_TOKEN_PATH] = e[NG_TEMP_TOKEN_PATH] || []; + path.unshift(token); + if (record && record.value == CIRCULAR) { + // Reset the Circular flag. + record.value = EMPTY; + } + throw e; + } +} + +function resolveToken( + token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector, + notFoundValue: any): any { + let value; + if (record) { + // If we don't have a record, this implies that we don't own the provider hence don't know how + // to resolve it. + value = record.value; + if (value == CIRCULAR) { + throw Error(NO_NEW_LINE + 'Circular dependency'); + } else if (value === EMPTY) { + record.value = CIRCULAR; + let obj = undefined; + let useNew = record.useNew; + let fn = record.fn; + let depRecords = record.deps; + let deps = EMPTY; + if (depRecords.length) { + deps = []; + for (let i = 0; i < depRecords.length; i++) { + const depRecord: DependencyRecord = depRecords[i]; + const options = depRecord.options; + const childRecord = + options & OptionFlags.CheckSelf ? records.get(depRecord.token) : undefined; + deps.push(tryResolveToken( + // Current Token to resolve + depRecord.token, + // A record which describes how to resolve the token. + // If undefined, this means we don't have such a record + childRecord, + // Other records we know about. + records, + // If we don't know how to resolve dependency and we should not check parent for it, + // than pass in Null injector. + !childRecord && !(options & OptionFlags.CheckParent) ? NULL_INJECTOR : parent, + options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND)); + } + } + record.value = value = useNew ? new (fn as any)(...deps) : fn.apply(obj, deps); + } + } else { + value = parent.get(token, notFoundValue); + } + return value; +} + + +function computeDeps(provider: StaticProvider): DependencyRecord[] { + let deps: DependencyRecord[] = EMPTY; + const providerDeps: any[] = + (provider as ExistingProvider & StaticClassProvider & ConstructorProvider).deps; + if (providerDeps && providerDeps.length) { + deps = []; + for (let i = 0; i < providerDeps.length; i++) { + let options = OptionFlags.Default; + let token = resolveForwardRef(providerDeps[i]); + if (token instanceof Array) { + for (let j = 0, annotations = token; j < annotations.length; j++) { + const annotation = annotations[j]; + if (annotation instanceof Optional || annotation == Optional) { + options = options | OptionFlags.Optional; + } else if (annotation instanceof SkipSelf || annotation == SkipSelf) { + options = options & ~OptionFlags.CheckSelf; + } else if (annotation instanceof Self || annotation == Self) { + options = options & ~OptionFlags.CheckParent; + } else if (annotation instanceof Inject) { + token = (annotation as Inject).token; + } else { + token = resolveForwardRef(annotation); + } + } + } + deps.push({token, options}); + } + } else if ((provider as ExistingProvider).useExisting) { + const token = resolveForwardRef((provider as ExistingProvider).useExisting); + deps = [{token, options: OptionFlags.Default}]; + } else if (!providerDeps && !(USE_VALUE in provider)) { + // useValue & useExisting are the only ones which are exempt from deps all others need it. + throw staticError('\'deps\' required', provider); + } + return deps; +} + +function formatError(text: string, obj: any): string { + text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text; + let context = stringify(obj); + if (obj instanceof Array) { + context = obj.map(stringify).join(' -> '); + } else if (typeof obj === 'object') { + let parts = <string[]>[]; + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + let value = obj[key]; + parts.push( + key + ':' + (typeof value === 'string' ? JSON.stringify(value) : stringify(value))); + } + } + context = `{${parts.join(', ')}}`; + } + return `StaticInjectorError[${context}]: ${text.replace(NEW_LINE, '\n ')}`; +} + +function staticError(text: string, obj: any): Error { + return new Error(formatError(text, obj)); +} + +function getClosureSafeProperty<T>(objWithPropertyToExtract: T): string { + for (let key in objWithPropertyToExtract) { + if (objWithPropertyToExtract[key] === GET_PROPERTY_NAME) { + return key; + } + } + throw Error('!prop'); } diff --git a/packages/core/src/di/provider.ts b/packages/core/src/di/provider.ts index acb8f23cb3..9ad449e3ff 100644 --- a/packages/core/src/di/provider.ts +++ b/packages/core/src/di/provider.ts @@ -8,32 +8,6 @@ import {Type} from '../type'; -/** - * @whatItDoes Configures the {@link Injector} to return an instance of `Type` when `Type' is used - * as token. - * @howToUse - * ``` - * @Injectable() - * class MyService {} - * - * const provider: TypeProvider = MyService; - * ``` - * - * @description - * - * Create an instance by invoking the `new` operator and supplying additional arguments. - * This form is a short form of `TypeProvider`; - * - * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. - * - * ### Example - * - * {@example core/di/ts/provider_spec.ts region='TypeProvider'} - * - * @stable - */ -export interface TypeProvider extends Type<any> {} - /** * @whatItDoes Configures the {@link Injector} to return a value for a token. * @howToUse @@ -79,7 +53,7 @@ export interface ValueProvider { * @Injectable() * class MyService {} * - * const provider: ClassProvider = {provide: 'someToken', useClass: MyService}; + * const provider: ClassProvider = {provide: 'someToken', useClass: MyService, deps: []}; * ``` * * @description @@ -87,24 +61,74 @@ export interface ValueProvider { * * ### Example * - * {@example core/di/ts/provider_spec.ts region='ClassProvider'} + * {@example core/di/ts/provider_spec.ts region='StaticClassProvider'} * * Note that following two providers are not equal: - * {@example core/di/ts/provider_spec.ts region='ClassProviderDifference'} + * {@example core/di/ts/provider_spec.ts region='StaticClassProviderDifference'} * * @stable */ -export interface ClassProvider { +export interface StaticClassProvider { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ provide: any; /** - * Class to instantiate for the `token`. + * An optional class to instantiate for the `token`. (If not provided `provide` is assumed to be a + * class to + * instantiate) */ useClass: Type<any>; + /** + * A list of `token`s which need to be resolved by the injector. The list of values is then + * used as arguments to the `useClass` constructor. + */ + deps: any[]; + + /** + * If true, then injector returns an array of instances. This is useful to allow multiple + * providers spread across many files to provide configuration information to a common token. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='MultiProviderAspect'} + */ + multi?: boolean; +} + +/** + * @whatItDoes Configures the {@link Injector} to return an instance of a token. + * @howToUse + * ``` + * @Injectable() + * class MyService {} + * + * const provider: ClassProvider = {provide: MyClass, deps: []}; + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ConstructorProvider'} + * + * @stable + */ +export interface ConstructorProvider { + /** + * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). + */ + provide: Type<any>; + + /** + * A list of `token`s which need to be resolved by the injector. The list of values is then + * used as arguments to the `useClass` constructor. + */ + deps: any[]; + /** * If true, then injector returns an array of instances. This is useful to allow multiple * providers spread across many files to provide configuration information to a common token. @@ -205,11 +229,95 @@ export interface FactoryProvider { multi?: boolean; } +/** + * @whatItDoes Describes how the {@link Injector} should be configured in a static way (Without + * reflection). + * @howToUse + * See {@link ValueProvider}, {@link ExistingProvider}, {@link FactoryProvider}. + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * @stable + */ +export type StaticProvider = ValueProvider | ExistingProvider | StaticClassProvider | + ConstructorProvider | FactoryProvider | any[]; + + +/** + * @whatItDoes Configures the {@link Injector} to return an instance of `Type` when `Type' is used + * as token. + * @howToUse + * ``` + * @Injectable() + * class MyService {} + * + * const provider: TypeProvider = MyService; + * ``` + * + * @description + * + * Create an instance by invoking the `new` operator and supplying additional arguments. + * This form is a short form of `TypeProvider`; + * + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='TypeProvider'} + * + * @stable + */ +export interface TypeProvider extends Type<any> {} + +/** + * @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token. + * @howToUse + * ``` + * @Injectable() + * class MyService {} + * + * const provider: ClassProvider = {provide: 'someToken', useClass: MyService}; + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ClassProvider'} + * + * Note that following two providers are not equal: + * {@example core/di/ts/provider_spec.ts region='ClassProviderDifference'} + * + * @stable + */ +export interface ClassProvider { + /** + * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). + */ + provide: any; + + /** + * Class to instantiate for the `token`. + */ + useClass: Type<any>; + + /** + * If true, then injector returns an array of instances. This is useful to allow multiple + * providers spread across many files to provide configuration information to a common token. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='MultiProviderAspect'} + */ + multi?: boolean; +} + /** * @whatItDoes Describes how the {@link Injector} should be configured. * @howToUse - * See {@link TypeProvider}, {@link ValueProvider}, {@link ClassProvider}, {@link ExistingProvider}, - * {@link FactoryProvider}. + * See {@link TypeProvider}, {@link ClassProvider}, {@link StaticProvider}. * * @description * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. diff --git a/packages/core/src/di/reflective_injector.ts b/packages/core/src/di/reflective_injector.ts index a0e939dddd..2dd381b657 100644 --- a/packages/core/src/di/reflective_injector.ts +++ b/packages/core/src/di/reflective_injector.ts @@ -13,6 +13,8 @@ import {cyclicDependencyError, instantiationError, noProviderError, outOfBoundsE import {ReflectiveKey} from './reflective_key'; import {ReflectiveDependency, ResolvedReflectiveFactory, ResolvedReflectiveProvider, resolveReflectiveProviders} from './reflective_provider'; + + // Threshold for the dynamic version const UNDEFINED = new Object(); diff --git a/packages/core/src/di/reflective_key.ts b/packages/core/src/di/reflective_key.ts index e23df7ad0a..765f3fded6 100644 --- a/packages/core/src/di/reflective_key.ts +++ b/packages/core/src/di/reflective_key.ts @@ -24,7 +24,7 @@ import {resolveForwardRef} from './forward_ref'; * `Key` should not be created directly. {@link ReflectiveInjector} creates keys automatically when * resolving * providers. - * @experimental + * @deprecated No replacement */ export class ReflectiveKey { /** diff --git a/packages/core/src/linker/compiler.ts b/packages/core/src/linker/compiler.ts index 28320877b8..f92bfc1f87 100644 --- a/packages/core/src/linker/compiler.ts +++ b/packages/core/src/linker/compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, InjectionToken} from '../di'; +import {Injectable, InjectionToken, StaticProvider} from '../di'; import {MissingTranslationStrategy} from '../i18n/tokens'; import {ViewEncapsulation} from '../metadata'; import {Type} from '../type'; @@ -14,6 +14,7 @@ import {Type} from '../type'; import {ComponentFactory} from './component_factory'; import {NgModuleFactory} from './ng_module_factory'; + /** * Combination of NgModuleFactory and ComponentFactorys. * @@ -101,7 +102,7 @@ export type CompilerOptions = { useDebug?: boolean, useJit?: boolean, defaultEncapsulation?: ViewEncapsulation, - providers?: any[], + providers?: StaticProvider[], missingTranslation?: MissingTranslationStrategy, // Whether to support the `<template>` tag and the `template` attribute to define angular // templates. They have been deprecated in 4.x, `<ng-template>` should be used instead. diff --git a/packages/core/src/platform_core_providers.ts b/packages/core/src/platform_core_providers.ts index 7b7cd5f693..6ff1967188 100644 --- a/packages/core/src/platform_core_providers.ts +++ b/packages/core/src/platform_core_providers.ts @@ -9,22 +9,16 @@ import {PlatformRef, PlatformRef_, createPlatformFactory} from './application_ref'; import {PLATFORM_ID} from './application_tokens'; import {Console} from './console'; -import {Provider} from './di'; -import {Reflector, reflector} from './reflection/reflection'; +import {Injector, StaticProvider} from './di'; import {TestabilityRegistry} from './testability/testability'; -function _reflector(): Reflector { - return reflector; -} - -const _CORE_PLATFORM_PROVIDERS: Provider[] = [ +const _CORE_PLATFORM_PROVIDERS: StaticProvider[] = [ // Set a default platform name for platforms that don't set it explicitly. {provide: PLATFORM_ID, useValue: 'unknown'}, - PlatformRef_, + {provide: PlatformRef_, deps: [Injector]}, {provide: PlatformRef, useExisting: PlatformRef_}, - {provide: Reflector, useFactory: _reflector, deps: []}, - TestabilityRegistry, - Console, + {provide: TestabilityRegistry, deps: []}, + {provide: Console, deps: []}, ]; /** diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index 3e256b4797..393cf40488 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -60,6 +60,10 @@ export function stringify(token: any): string { return token; } + if (token instanceof Array) { + return '[' + token.map(stringify).join(', ') + ']'; + } + if (token == null) { return '' + token; } diff --git a/packages/core/src/view/query.ts b/packages/core/src/view/query.ts index c93b624a2e..842216207e 100644 --- a/packages/core/src/view/query.ts +++ b/packages/core/src/view/query.ts @@ -190,4 +190,4 @@ export function getQueryValue( } return value; } -} \ No newline at end of file +} diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index 2318ca6f45..9db6f7c44d 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -466,7 +466,8 @@ export function createNgModuleRef( class NgModuleRef_ implements NgModuleData, InternalNgModuleRef<any> { private _destroyListeners: (() => void)[] = []; private _destroyed: boolean = false; - public _providers: any[]; + /** @internal */ + _providers: any[]; constructor( private _moduleType: Type<any>, public _parent: Injector, diff --git a/packages/core/src/view/view.ts b/packages/core/src/view/view.ts index 18d07766c3..7572791207 100644 --- a/packages/core/src/view/view.ts +++ b/packages/core/src/view/view.ts @@ -161,7 +161,7 @@ function validateNode(parent: NodeDef | null, node: NodeDef, nodeCount: number) const parentFlags = parent ? parent.flags : 0; if ((parentFlags & NodeFlags.TypeElement) === 0) { throw new Error( - `Illegal State: Provider/Directive nodes need to be children of elements or anchors, at index ${node.index}!`); + `Illegal State: StaticProvider/Directive nodes need to be children of elements or anchors, at index ${node.index}!`); } } if (node.query) { diff --git a/packages/core/test/animation/animation_integration_spec.ts b/packages/core/test/animation/animation_integration_spec.ts index ee5d8ec6cd..e7e5ee5186 100644 --- a/packages/core/test/animation/animation_integration_spec.ts +++ b/packages/core/test/animation/animation_integration_spec.ts @@ -868,6 +868,87 @@ export function main() { expect(pp[2].currentSnapshot).toEqual({opacity: AUTO_STYLE}); }); + it('should provide the styling of previous players that are grouped and queried and make sure match the players with the correct elements', + () => { + @Component({ + selector: 'ani-cmp', + template: ` + <div class="container" [@myAnimation]="exp"> + <div class="inner"></div> + </div> + `, + animations: [ + trigger( + 'myAnimation', + [ + transition( + '1 => 2', + [ + style({fontSize: '10px'}), + query( + '.inner', + [ + style({fontSize: '20px'}), + ]), + animate('1s', style({fontSize: '100px'})), + query( + '.inner', + [ + animate('1s', style({fontSize: '200px'})), + ]), + ]), + transition( + '2 => 3', + [ + animate('1s', style({fontSize: '0px'})), + query( + '.inner', + [ + animate('1s', style({fontSize: '0px'})), + ]), + ]), + ]), + ], + }) + class Cmp { + exp: any = false; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + fixture.detectChanges(); + + cmp.exp = '1'; + fixture.detectChanges(); + resetLog(); + + cmp.exp = '2'; + fixture.detectChanges(); + resetLog(); + + cmp.exp = '3'; + fixture.detectChanges(); + const players = getLog(); + expect(players.length).toEqual(2); + const [p1, p2] = players as MockAnimationPlayer[]; + + const pp1 = p1.previousPlayers as MockAnimationPlayer[]; + expect(p1.element.classList.contains('container')).toBeTruthy(); + for (let i = 0; i < pp1.length; i++) { + expect(pp1[i].element).toEqual(p1.element); + } + + const pp2 = p2.previousPlayers as MockAnimationPlayer[]; + expect(p2.element.classList.contains('inner')).toBeTruthy(); + for (let i = 0; i < pp2.length; i++) { + expect(pp2[i].element).toEqual(p2.element); + } + }); + it('should properly balance styles between states even if there are no destination state styles', () => { @Component({ @@ -1445,6 +1526,60 @@ export function main() { expect(players.length).toEqual(0); }); + it('should update the final state styles when params update even if the expression hasn\'t changed', + fakeAsync(() => { + @Component({ + selector: 'ani-cmp', + template: ` + <div [@myAnimation]="{value:exp,params:{color:color}}"></div> + `, + animations: [ + trigger( + 'myAnimation', + [ + state('*', style({color: '{{ color }}'}), {params: {color: 'black'}}), + transition('* => 1', animate(500)) + ]), + ] + }) + class Cmp { + public exp: any; + public color: string|null; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + cmp.exp = '1'; + cmp.color = 'red'; + fixture.detectChanges(); + const player = getLog()[0] !; + const element = player.element; + player.finish(); + + flushMicrotasks(); + expect(getDOM().hasStyle(element, 'color', 'red')).toBeTruthy(); + + cmp.exp = '1'; + cmp.color = 'blue'; + fixture.detectChanges(); + resetLog(); + + flushMicrotasks(); + expect(getDOM().hasStyle(element, 'color', 'blue')).toBeTruthy(); + + cmp.exp = '1'; + cmp.color = null; + fixture.detectChanges(); + resetLog(); + + flushMicrotasks(); + expect(getDOM().hasStyle(element, 'color', 'black')).toBeTruthy(); + })); + it('should substitute in values if the provided state match is an object with values', () => { @Component({ selector: 'ani-cmp', @@ -1482,6 +1617,138 @@ export function main() { ]); }); + it('should retain substituted styles on the element once the animation is complete if referenced in the final state', + fakeAsync(() => { + @Component({ + selector: 'ani-cmp', + template: ` + <div [@myAnimation]="{value:exp, params: { color: color }}"></div> + `, + animations: [ + trigger( + 'myAnimation', + [ + state( + 'start', style({ + color: '{{ color }}', + fontSize: '{{ fontSize }}px', + width: '{{ width }}' + }), + {params: {color: 'red', fontSize: '200', width: '10px'}}), + + state( + 'final', + style( + {color: '{{ color }}', fontSize: '{{ fontSize }}px', width: '888px'}), + {params: {color: 'green', fontSize: '50', width: '100px'}}), + + transition('start => final', animate(500)), + ]), + ] + }) + class Cmp { + public exp: any; + public color: any; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + cmp.exp = 'start'; + cmp.color = 'red'; + fixture.detectChanges(); + resetLog(); + + cmp.exp = 'final'; + cmp.color = 'blue'; + fixture.detectChanges(); + + const players = getLog(); + expect(players.length).toEqual(1); + const [p1] = players; + + expect(p1.keyframes).toEqual([ + {color: 'red', fontSize: '200px', width: '10px', offset: 0}, + {color: 'blue', fontSize: '50px', width: '888px', offset: 1} + ]); + + const element = p1.element; + p1.finish(); + flushMicrotasks(); + + expect(getDOM().hasStyle(element, 'color', 'blue')).toBeTruthy(); + expect(getDOM().hasStyle(element, 'fontSize', '50px')).toBeTruthy(); + expect(getDOM().hasStyle(element, 'width', '888px')).toBeTruthy(); + })); + + it('should only evaluate final state param substitutions from the expression and state values and not from the transition options ', + fakeAsync(() => { + @Component({ + selector: 'ani-cmp', + template: ` + <div [@myAnimation]="exp"></div> + `, + animations: [ + trigger( + 'myAnimation', + [ + state( + 'start', style({ + width: '{{ width }}', + height: '{{ height }}', + }), + {params: {width: '0px', height: '0px'}}), + + state( + 'final', style({ + width: '{{ width }}', + height: '{{ height }}', + }), + {params: {width: '100px', height: '100px'}}), + + transition( + 'start => final', [animate(500)], + {params: {width: '333px', height: '666px'}}), + ]), + ] + }) + class Cmp { + public exp: any; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + cmp.exp = 'start'; + fixture.detectChanges(); + resetLog(); + + cmp.exp = 'final'; + fixture.detectChanges(); + + const players = getLog(); + expect(players.length).toEqual(1); + const [p1] = players; + + expect(p1.keyframes).toEqual([ + {width: '0px', height: '0px', offset: 0}, + {width: '100px', height: '100px', offset: 1}, + ]); + + const element = p1.element; + p1.finish(); + flushMicrotasks(); + + expect(getDOM().hasStyle(element, 'width', '100px')).toBeTruthy(); + expect(getDOM().hasStyle(element, 'height', '100px')).toBeTruthy(); + })); + it('should not flush animations twice when an inner component runs change detection', () => { @Component({ selector: 'outer-cmp', @@ -1882,7 +2149,7 @@ export function main() { class Cmp { exp: string; log: any[] = []; - callback = (event: any) => { this.log.push(`${event.phaseName} => ${event.toState}`); } + callback = (event: any) => this.log.push(`${event.phaseName} => ${event.toState}`); } TestBed.configureTestingModule({ diff --git a/packages/core/test/application_init_spec.ts b/packages/core/test/application_init_spec.ts index ae0bd543c4..693d538876 100644 --- a/packages/core/test/application_init_spec.ts +++ b/packages/core/test/application_init_spec.ts @@ -35,7 +35,7 @@ export function main() { return () => { const initStatus = injector.get(ApplicationInitStatus); initStatus.donePromise.then(() => { expect(completerResolver).toBe(true); }); - } + }; }; promise = new Promise((res) => { resolve = res; }); TestBed.configureTestingModule({ diff --git a/packages/core/test/change_detection/differs/iterable_differs_spec.ts b/packages/core/test/change_detection/differs/iterable_differs_spec.ts index 8a94bb1799..7d4b99be47 100644 --- a/packages/core/test/change_detection/differs/iterable_differs_spec.ts +++ b/packages/core/test/change_detection/differs/iterable_differs_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ReflectiveInjector} from '@angular/core'; +import {Injector} from '@angular/core'; import {IterableDiffers} from '@angular/core/src/change_detection/differs/iterable_differs'; import {SpyIterableDifferFactory} from '../../spies'; @@ -50,7 +50,7 @@ export function main() { describe('.extend()', () => { it('should throw if calling extend when creating root injector', () => { - const injector = ReflectiveInjector.resolveAndCreate([IterableDiffers.extend([])]); + const injector = Injector.create([IterableDiffers.extend([])]); expect(() => injector.get(IterableDiffers)) .toThrowError(/Cannot extend IterableDiffers without a parent injector/); @@ -58,9 +58,8 @@ export function main() { it('should extend di-inherited differs', () => { const parent = new IterableDiffers([factory1]); - const injector = - ReflectiveInjector.resolveAndCreate([{provide: IterableDiffers, useValue: parent}]); - const childInjector = injector.resolveAndCreateChild([IterableDiffers.extend([factory2])]); + const injector = Injector.create([{provide: IterableDiffers, useValue: parent}]); + const childInjector = Injector.create([IterableDiffers.extend([factory2])], injector); expect(injector.get(IterableDiffers).factories).toEqual([factory1]); expect(childInjector.get(IterableDiffers).factories).toEqual([factory2, factory1]); diff --git a/packages/core/test/di/injector_spec.ts b/packages/core/test/di/injector_spec.ts index 753741fe25..820d5069da 100644 --- a/packages/core/test/di/injector_spec.ts +++ b/packages/core/test/di/injector_spec.ts @@ -13,12 +13,13 @@ import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; export function main() { describe('Injector.NULL', () => { it('should throw if no arg is given', () => { - expect(() => Injector.NULL.get('someToken')).toThrowError('No provider for someToken!'); + expect(() => Injector.NULL.get('someToken')) + .toThrowError('NullInjectorError: No provider for someToken!'); }); it('should throw if THROW_IF_NOT_FOUND is given', () => { expect(() => Injector.NULL.get('someToken', Injector.THROW_IF_NOT_FOUND)) - .toThrowError('No provider for someToken!'); + .toThrowError('NullInjectorError: No provider for someToken!'); }); it('should return the default value', diff --git a/packages/core/test/di/static_injector_spec.ts b/packages/core/test/di/static_injector_spec.ts new file mode 100644 index 0000000000..f2b8df2f54 --- /dev/null +++ b/packages/core/test/di/static_injector_spec.ts @@ -0,0 +1,479 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, InjectionToken, Injector, Optional, ReflectiveKey, Self, SkipSelf, forwardRef} from '@angular/core'; +import {getOriginalError} from '@angular/core/src/errors'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; + +import {stringify} from '../../src/util'; + +class Engine { + static PROVIDER = {provide: Engine, useClass: Engine, deps: []}; +} + +class BrokenEngine { + static PROVIDER = {provide: Engine, useClass: BrokenEngine, deps: []}; + constructor() { throw new Error('Broken Engine'); } +} + +class DashboardSoftware { + static PROVIDER = {provide: DashboardSoftware, useClass: DashboardSoftware, deps: []}; +} + +class Dashboard { + static PROVIDER = {provide: Dashboard, useClass: Dashboard, deps: [DashboardSoftware]}; + constructor(software: DashboardSoftware) {} +} + +class TurboEngine extends Engine { + static PROVIDER = {provide: Engine, useClass: TurboEngine, deps: []}; +} + +class Car { + static PROVIDER = {provide: Car, useClass: Car, deps: [Engine]}; + constructor(public engine: Engine) {} +} + +class CarWithOptionalEngine { + static PROVIDER = { + provide: CarWithOptionalEngine, + useClass: CarWithOptionalEngine, + deps: [[new Optional(), Engine]] + }; + constructor(public engine: Engine) {} +} + +class CarWithDashboard { + static PROVIDER = { + provide: CarWithDashboard, + useClass: CarWithDashboard, + deps: [Engine, Dashboard] + }; + engine: Engine; + dashboard: Dashboard; + constructor(engine: Engine, dashboard: Dashboard) { + this.engine = engine; + this.dashboard = dashboard; + } +} + +class SportsCar extends Car { + static PROVIDER = {provide: Car, useClass: SportsCar, deps: [Engine]}; +} + +class CyclicEngine { + static PROVIDER = {provide: Engine, useClass: CyclicEngine, deps: [Car]}; + constructor(car: Car) {} +} + +class NoAnnotations { + constructor(secretDependency: any) {} +} + +function factoryFn(a: any) {} + +export function main() { + const dynamicProviders = [ + {provide: 'provider0', useValue: 1}, {provide: 'provider1', useValue: 1}, + {provide: 'provider2', useValue: 1}, {provide: 'provider3', useValue: 1}, + {provide: 'provider4', useValue: 1}, {provide: 'provider5', useValue: 1}, + {provide: 'provider6', useValue: 1}, {provide: 'provider7', useValue: 1}, + {provide: 'provider8', useValue: 1}, {provide: 'provider9', useValue: 1}, + {provide: 'provider10', useValue: 1} + ]; + + describe(`StaticInjector`, () => { + + it('should instantiate a class without dependencies', () => { + const injector = Injector.create([Engine.PROVIDER]); + const engine = injector.get(Engine); + + expect(engine).toBeAnInstanceOf(Engine); + }); + + it('should resolve dependencies based on type information', () => { + const injector = Injector.create([Engine.PROVIDER, Car.PROVIDER]); + const car = injector.get(Car); + + expect(car).toBeAnInstanceOf(Car); + expect(car.engine).toBeAnInstanceOf(Engine); + }); + + it('should cache instances', () => { + const injector = Injector.create([Engine.PROVIDER]); + + const e1 = injector.get(Engine); + const e2 = injector.get(Engine); + + expect(e1).toBe(e2); + }); + + it('should provide to a value', () => { + const injector = Injector.create([{provide: Engine, useValue: 'fake engine'}]); + + const engine = injector.get(Engine); + expect(engine).toEqual('fake engine'); + }); + + it('should inject dependencies instance of InjectionToken', () => { + const TOKEN = new InjectionToken<string>('token'); + + const injector = Injector.create([ + {provide: TOKEN, useValue: 'by token'}, + {provide: Engine, useFactory: (v: string) => v, deps: [[TOKEN]]}, + ]); + + const engine = injector.get(Engine); + expect(engine).toEqual('by token'); + }); + + it('should provide to a factory', () => { + function sportsCarFactory(e: any) { return new SportsCar(e); } + + const injector = Injector.create( + [Engine.PROVIDER, {provide: Car, useFactory: sportsCarFactory, deps: [Engine]}]); + + const car = injector.get(Car); + expect(car).toBeAnInstanceOf(SportsCar); + expect(car.engine).toBeAnInstanceOf(Engine); + }); + + it('should supporting provider to null', () => { + const injector = Injector.create([{provide: Engine, useValue: null}]); + const engine = injector.get(Engine); + expect(engine).toBeNull(); + }); + + it('should provide to an alias', () => { + const injector = Injector.create([ + Engine.PROVIDER, {provide: SportsCar, useClass: SportsCar, deps: [Engine]}, + {provide: Car, useExisting: SportsCar} + ]); + + const car = injector.get(Car); + const sportsCar = injector.get(SportsCar); + expect(car).toBeAnInstanceOf(SportsCar); + expect(car).toBe(sportsCar); + }); + + it('should support multiProviders', () => { + const injector = Injector.create([ + Engine.PROVIDER, {provide: Car, useClass: SportsCar, deps: [Engine], multi: true}, + {provide: Car, useClass: CarWithOptionalEngine, deps: [Engine], multi: true} + ]); + + const cars = injector.get(Car) as any as Car[]; + expect(cars.length).toEqual(2); + expect(cars[0]).toBeAnInstanceOf(SportsCar); + expect(cars[1]).toBeAnInstanceOf(CarWithOptionalEngine); + }); + + it('should support multiProviders that are created using useExisting', () => { + const injector = Injector.create([ + Engine.PROVIDER, {provide: SportsCar, useClass: SportsCar, deps: [Engine]}, + {provide: Car, useExisting: SportsCar, multi: true} + ]); + + const cars = injector.get(Car) as any as Car[]; + expect(cars.length).toEqual(1); + expect(cars[0]).toBe(injector.get(SportsCar)); + }); + + it('should throw when the aliased provider does not exist', () => { + const injector = Injector.create([{provide: 'car', useExisting: SportsCar}]); + const e = + `StaticInjectorError[car -> ${stringify(SportsCar)}]: \n NullInjectorError: No provider for ${stringify(SportsCar)}!`; + expect(() => injector.get('car')).toThrowError(e); + }); + + it('should handle forwardRef in useExisting', () => { + const injector = Injector.create([ + {provide: 'originalEngine', useClass: forwardRef(() => Engine), deps: []}, { + provide: 'aliasedEngine', + useExisting: <any>forwardRef(() => 'originalEngine'), + deps: [] + } + ]); + expect(injector.get('aliasedEngine')).toBeAnInstanceOf(Engine); + }); + + it('should support overriding factory dependencies', () => { + const injector = Injector.create([ + Engine.PROVIDER, + {provide: Car, useFactory: (e: Engine) => new SportsCar(e), deps: [Engine]} + ]); + + const car = injector.get(Car); + expect(car).toBeAnInstanceOf(SportsCar); + expect(car.engine).toBeAnInstanceOf(Engine); + }); + + it('should support optional dependencies', () => { + const injector = Injector.create([CarWithOptionalEngine.PROVIDER]); + + const car = injector.get(CarWithOptionalEngine); + expect(car.engine).toEqual(null); + }); + + it('should flatten passed-in providers', () => { + const injector = Injector.create([[[Engine.PROVIDER, Car.PROVIDER]]]); + + const car = injector.get(Car); + expect(car).toBeAnInstanceOf(Car); + }); + + it('should use the last provider when there are multiple providers for same token', () => { + const injector = Injector.create([ + {provide: Engine, useClass: Engine, deps: []}, + {provide: Engine, useClass: TurboEngine, deps: []} + ]); + + expect(injector.get(Engine)).toBeAnInstanceOf(TurboEngine); + }); + + it('should use non-type tokens', () => { + const injector = Injector.create([{provide: 'token', useValue: 'value'}]); + + expect(injector.get('token')).toEqual('value'); + }); + + it('should throw when given invalid providers', () => { + expect(() => Injector.create(<any>['blah'])) + .toThrowError('StaticInjectorError[blah]: Unexpected provider'); + }); + + it('should throw when missing deps', () => { + expect(() => Injector.create(<any>[{provide: Engine, useClass: Engine}])) + .toThrowError( + 'StaticInjectorError[{provide:Engine, useClass:Engine}]: \'deps\' required'); + }); + + it('should throw when using reflective API', () => { + expect(() => Injector.create(<any>[Engine])) + .toThrowError('StaticInjectorError[Engine]: Function/Class not supported'); + }); + + it('should throw when unknown provider shape API', () => { + expect(() => Injector.create(<any>[{provide: 'abc', deps: [Engine]}])) + .toThrowError( + 'StaticInjectorError[{provide:"abc", deps:[Engine]}]: StaticProvider does not have [useValue|useFactory|useExisting|useClass] or [provide] is not newable'); + }); + + it('should throw when given invalid providers and serialize the provider', () => { + expect(() => Injector.create(<any>[{foo: 'bar', bar: Car}])) + .toThrowError('StaticInjectorError[{foo:"bar", bar:Car}]: Unexpected provider'); + }); + + it('should provide itself', () => { + const parent = Injector.create([]); + const child = Injector.create([], parent); + + expect(child.get(Injector)).toBe(child); + }); + + it('should throw when no provider defined', () => { + const injector = Injector.create([]); + expect(() => injector.get('NonExisting')) + .toThrowError( + 'StaticInjectorError[NonExisting]: \n NullInjectorError: No provider for NonExisting!'); + }); + + it('should show the full path when no provider', () => { + const injector = + Injector.create([CarWithDashboard.PROVIDER, Engine.PROVIDER, Dashboard.PROVIDER]); + expect(() => injector.get(CarWithDashboard)) + .toThrowError( + `StaticInjectorError[${stringify(CarWithDashboard)} -> ${stringify(Dashboard)} -> DashboardSoftware]: + NullInjectorError: No provider for DashboardSoftware!`); + }); + + it('should throw when trying to instantiate a cyclic dependency', () => { + const injector = Injector.create([Car.PROVIDER, CyclicEngine.PROVIDER]); + + expect(() => injector.get(Car)) + .toThrowError( + `StaticInjectorError[${stringify(Car)} -> ${stringify(Engine)} -> ${stringify(Car)}]: Circular dependency`); + }); + + it('should show the full path when error happens in a constructor', () => { + const error = new Error('MyError'); + const injector = Injector.create( + [Car.PROVIDER, {provide: Engine, useFactory: () => { throw error; }, deps: []}]); + + try { + injector.get(Car); + throw 'Must throw'; + } catch (e) { + expect(e).toBe(error); + expect(e.message).toContain( + `StaticInjectorError[${stringify(Car)} -> Engine]: \n MyError`); + expect(e.ngTokenPath[0]).toEqual(Car); + expect(e.ngTokenPath[1]).toEqual(Engine); + } + }); + + it('should instantiate an object after a failed attempt', () => { + let isBroken = true; + + const injector = Injector.create([ + Car.PROVIDER, { + provide: Engine, + useFactory: (() => isBroken ? new BrokenEngine() : new Engine()), + deps: [] + } + ]); + + expect(() => injector.get(Car)) + .toThrowError('StaticInjectorError[Car -> Engine]: \n Broken Engine'); + + isBroken = false; + + expect(injector.get(Car)).toBeAnInstanceOf(Car); + }); + + it('should support null/undefined values', () => { + const injector = Injector.create([ + {provide: 'null', useValue: null}, + {provide: 'undefined', useValue: undefined}, + ]); + expect(injector.get('null')).toBe(null); + expect(injector.get('undefined')).toBe(undefined); + }); + + }); + + + describe('child', () => { + it('should load instances from parent injector', () => { + const parent = Injector.create([Engine.PROVIDER]); + const child = Injector.create([], parent); + + const engineFromParent = parent.get(Engine); + const engineFromChild = child.get(Engine); + + expect(engineFromChild).toBe(engineFromParent); + }); + + it('should not use the child providers when resolving the dependencies of a parent provider', + () => { + const parent = Injector.create([Car.PROVIDER, Engine.PROVIDER]); + const child = Injector.create([TurboEngine.PROVIDER], parent); + + const carFromChild = child.get(Car); + expect(carFromChild.engine).toBeAnInstanceOf(Engine); + }); + + it('should create new instance in a child injector', () => { + const parent = Injector.create([Engine.PROVIDER]); + const child = Injector.create([TurboEngine.PROVIDER], parent); + + const engineFromParent = parent.get(Engine); + const engineFromChild = child.get(Engine); + + expect(engineFromParent).not.toBe(engineFromChild); + expect(engineFromChild).toBeAnInstanceOf(TurboEngine); + }); + + it('should give access to parent', () => { + const parent = Injector.create([]); + const child = Injector.create([], parent); + expect((child as any).parent).toBe(parent); + }); + }); + + + describe('instantiate', () => { + it('should instantiate an object in the context of the injector', () => { + const inj = Injector.create([Engine.PROVIDER]); + const childInj = Injector.create([Car.PROVIDER], inj); + const car = childInj.get(Car); + expect(car).toBeAnInstanceOf(Car); + expect(car.engine).toBe(inj.get(Engine)); + }); + }); + + describe('depedency resolution', () => { + describe('@Self()', () => { + it('should return a dependency from self', () => { + const inj = Injector.create([ + Engine.PROVIDER, + {provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[Engine, new Self()]]} + ]); + + expect(inj.get(Car)).toBeAnInstanceOf(Car); + }); + + it('should throw when not requested provider on self', () => { + const parent = Injector.create([Engine.PROVIDER]); + const child = Injector.create( + [{provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[Engine, new Self()]]}], + parent); + + expect(() => child.get(Car)) + .toThrowError(`StaticInjectorError[${stringify(Car)} -> ${stringify(Engine)}]: + NullInjectorError: No provider for Engine!`); + }); + }); + + describe('default', () => { + it('should skip self', () => { + const parent = Injector.create([Engine.PROVIDER]); + const child = Injector.create( + [ + TurboEngine.PROVIDER, + {provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[SkipSelf, Engine]]} + ], + parent); + + expect(child.get(Car).engine).toBeAnInstanceOf(Engine); + }); + }); + }); + + describe('resolve', () => { + it('should throw when mixing multi providers with regular providers', () => { + expect(() => { + Injector.create( + [{provide: Engine, useClass: BrokenEngine, deps: [], multi: true}, Engine.PROVIDER]); + }).toThrowError(/Cannot mix multi providers and regular providers/); + + expect(() => { + Injector.create( + [Engine.PROVIDER, {provide: Engine, useClass: BrokenEngine, deps: [], multi: true}]); + }).toThrowError(/Cannot mix multi providers and regular providers/); + }); + + it('should resolve forward references', () => { + const injector = Injector.create([ + [{provide: forwardRef(() => BrokenEngine), useClass: forwardRef(() => Engine), deps: []}], { + provide: forwardRef(() => String), + useFactory: (e: any) => e, + deps: [forwardRef(() => BrokenEngine)] + } + ]); + expect(injector.get(String)).toBeAnInstanceOf(Engine); + expect(injector.get(BrokenEngine)).toBeAnInstanceOf(Engine); + }); + + it('should support overriding factory dependencies with dependency annotations', () => { + const injector = Injector.create([ + Engine.PROVIDER, + {provide: 'token', useFactory: (e: any) => e, deps: [[new Inject(Engine)]]} + ]); + + expect(injector.get('token')).toBeAnInstanceOf(Engine); + }); + }); + + describe('displayName', () => { + it('should work', () => { + expect(Injector.create([Engine.PROVIDER, {provide: BrokenEngine, useValue: null}]).toString()) + .toEqual('StaticInjector[Injector, Engine, BrokenEngine]'); + }); + }); +} diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index 8db439d1a4..e5d7b3f508 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -1353,7 +1353,7 @@ export function main() { tpl: TemplateRef<any>; @Input() - outerTpl: TemplateRef<any> + outerTpl: TemplateRef<any>; constructor(public cdRef: ChangeDetectorRef) {} log(id: string) { log.push(`inner-${id}`); } diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 40eb1e4380..ce9ef724f8 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Compiler, ComponentFactory, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, ReflectiveInjector, SkipSelf} from '@angular/core'; +import {Compiler, ComponentFactory, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, SkipSelf} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection'; import {getDebugContext} from '@angular/core/src/errors'; import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver'; @@ -1850,8 +1850,7 @@ class DynamicViewport { const myService = new MyService(); myService.greeting = 'dynamic greet'; - this.injector = ReflectiveInjector.resolveAndCreate( - [{provide: MyService, useValue: myService}], vc.injector); + this.injector = Injector.create([{provide: MyService, useValue: myService}], vc.injector); this.componentFactory = componentFactoryResolver.resolveComponentFactory(ChildCompUsingService) !; } diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index 0991efe7dc..56660ed27e 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -720,7 +720,7 @@ function declareTests({useJit}: {useJit: boolean}) { it('should throw when the aliased provider does not exist', () => { const injector = createInjector([{provide: 'car', useExisting: SportsCar}]); - const e = `No provider for ${stringify(SportsCar)}!`; + const e = `NullInjectorError: No provider for ${stringify(SportsCar)}!`; expect(() => injector.get('car')).toThrowError(e); }); @@ -830,7 +830,8 @@ function declareTests({useJit}: {useJit: boolean}) { it('should throw when no provider defined', () => { const injector = createInjector([]); - expect(() => injector.get('NonExisting')).toThrowError('No provider for NonExisting!'); + expect(() => injector.get('NonExisting')) + .toThrowError('NullInjectorError: No provider for NonExisting!'); }); it('should throw when trying to instantiate a cyclic dependency', () => { diff --git a/packages/core/test/view/provider_spec.ts b/packages/core/test/view/provider_spec.ts index 98f15f1ea2..7732af73ac 100644 --- a/packages/core/test/view/provider_spec.ts +++ b/packages/core/test/view/provider_spec.ts @@ -163,13 +163,19 @@ export function main() { // root elements expect(() => createAndGetRootNodes(compViewDef(nodes))) - .toThrowError('No provider for Dep!'); + .toThrowError( + 'StaticInjectorError[Dep]: \n' + + ' StaticInjectorError[Dep]: \n' + + ' NullInjectorError: No provider for Dep!'); // non root elements expect( () => createAndGetRootNodes(compViewDef( [elementDef(NodeFlags.None, null !, null !, 4, 'span')].concat(nodes)))) - .toThrowError('No provider for Dep!'); + .toThrowError( + 'StaticInjectorError[Dep]: \n' + + ' StaticInjectorError[Dep]: \n' + + ' NullInjectorError: No provider for Dep!'); }); it('should inject from a parent element in a parent view', () => { @@ -191,7 +197,10 @@ export function main() { elementDef(NodeFlags.None, null !, null !, 1, 'span'), directiveDef(NodeFlags.None, null !, 0, SomeService, ['nonExistingDep']) ]))) - .toThrowError('No provider for nonExistingDep!'); + .toThrowError( + 'StaticInjectorError[nonExistingDep]: \n' + + ' StaticInjectorError[nonExistingDep]: \n' + + ' NullInjectorError: No provider for nonExistingDep!'); }); it('should use null for optional missing dependencies', () => { diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 962dd11126..ff169116ca 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, ReflectiveInjector, SchemaMetadata, SkipSelf, Type, ɵDepFlags as DepFlags, ɵERROR_COMPONENT_TYPE, ɵNodeFlags as NodeFlags, ɵclearProviderOverrides as clearProviderOverrides, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; +import {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, SchemaMetadata, SkipSelf, Type, ɵDepFlags as DepFlags, ɵERROR_COMPONENT_TYPE, ɵNodeFlags as NodeFlags, ɵclearProviderOverrides as clearProviderOverrides, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; import {AsyncTestCompleter} from './async_test_completer'; import {ComponentFixture} from './component_fixture'; @@ -308,8 +308,8 @@ export class TestBed implements Injector { } } const ngZone = new NgZone({enableLongStackTrace: true}); - const ngZoneInjector = ReflectiveInjector.resolveAndCreate( - [{provide: NgZone, useValue: ngZone}], this.platform.injector); + const ngZoneInjector = + Injector.create([{provide: NgZone, useValue: ngZone}], this.platform.injector); this._moduleRef = this._moduleFactory.create(ngZoneInjector); // ApplicationInitStatus.runInitializers() is marked @internal to core. So casting to any // before accessing it. diff --git a/packages/examples/core/di/ts/provider_spec.ts b/packages/examples/core/di/ts/provider_spec.ts index fce21ac041..4a273df1dc 100644 --- a/packages/examples/core/di/ts/provider_spec.ts +++ b/packages/examples/core/di/ts/provider_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable, InjectionToken, Optional, ReflectiveInjector} from '@angular/core'; +import {Injectable, InjectionToken, Injector, Optional, ReflectiveInjector} from '@angular/core'; export function main() { describe('Provider examples', () => { @@ -30,8 +30,7 @@ export function main() { describe('ValueProvider', () => { it('works', () => { // #docregion ValueProvider - const injector = - ReflectiveInjector.resolveAndCreate([{provide: String, useValue: 'Hello'}]); + const injector = Injector.create([{provide: String, useValue: 'Hello'}]); expect(injector.get(String)).toEqual('Hello'); // #enddocregion @@ -41,12 +40,13 @@ export function main() { describe('MultiProviderAspect', () => { it('works', () => { // #docregion MultiProviderAspect - const injector = ReflectiveInjector.resolveAndCreate([ - {provide: 'local', multi: true, useValue: 'en'}, - {provide: 'local', multi: true, useValue: 'sk'}, + const locale = new InjectionToken<string[]>('locale'); + const injector = Injector.create([ + {provide: locale, multi: true, useValue: 'en'}, + {provide: locale, multi: true, useValue: 'sk'}, ]); - const locales: string[] = injector.get('local'); + const locales: string[] = injector.get(locale); expect(locales).toEqual(['en', 'sk']); // #enddocregion }); @@ -89,6 +89,61 @@ export function main() { }); }); + describe('StaticClassProvider', () => { + it('works', () => { + // #docregion StaticClassProvider + abstract class Shape { name: string; } + + class Square extends Shape { + name = 'square'; + } + + const injector = Injector.create([{provide: Shape, useClass: Square, deps: []}]); + + const shape: Shape = injector.get(Shape); + expect(shape.name).toEqual('square'); + expect(shape instanceof Square).toBe(true); + // #enddocregion + }); + + it('is different then useExisting', () => { + // #docregion StaticClassProviderDifference + class Greeting { + salutation = 'Hello'; + } + + class FormalGreeting extends Greeting { + salutation = 'Greetings'; + } + + const injector = Injector.create([ + {provide: FormalGreeting, useClass: FormalGreeting, deps: []}, + {provide: Greeting, useClass: FormalGreeting, deps: []} + ]); + + // The injector returns different instances. + // See: {provide: ?, useExisting: ?} if you want the same instance. + expect(injector.get(FormalGreeting)).not.toBe(injector.get(Greeting)); + // #enddocregion + }); + }); + + describe('ConstructorProvider', () => { + it('works', () => { + // #docregion ConstructorProvider + class Square { + name = 'square'; + } + + const injector = Injector.create([{provide: Square, deps: []}]); + + const shape: Square = injector.get(Square); + expect(shape.name).toEqual('square'); + expect(shape instanceof Square).toBe(true); + // #enddocregion + }); + }); + describe('ExistingProvider', () => { it('works', () => { // #docregion ExistingProvider @@ -100,8 +155,9 @@ export function main() { salutation = 'Greetings'; } - const injector = ReflectiveInjector.resolveAndCreate( - [FormalGreeting, {provide: Greeting, useExisting: FormalGreeting}]); + const injector = Injector.create([ + {provide: FormalGreeting, deps: []}, {provide: Greeting, useExisting: FormalGreeting} + ]); expect(injector.get(Greeting).salutation).toEqual('Greetings'); expect(injector.get(FormalGreeting).salutation).toEqual('Greetings'); @@ -116,7 +172,7 @@ export function main() { const Location = new InjectionToken('location'); const Hash = new InjectionToken('hash'); - const injector = ReflectiveInjector.resolveAndCreate([ + const injector = Injector.create([ {provide: Location, useValue: 'http://angular.io/#someLocation'}, { provide: Hash, useFactory: (location: string) => location.split('#')[1], @@ -133,7 +189,7 @@ export function main() { const Location = new InjectionToken('location'); const Hash = new InjectionToken('hash'); - const injector = ReflectiveInjector.resolveAndCreate([{ + const injector = Injector.create([{ provide: Hash, useFactory: (location: string) => `Hash for: ${location}`, // use a nested array to define metadata for dependencies. diff --git a/packages/forms/src/directives/default_value_accessor.ts b/packages/forms/src/directives/default_value_accessor.ts index f550dc6213..6208323338 100644 --- a/packages/forms/src/directives/default_value_accessor.ts +++ b/packages/forms/src/directives/default_value_accessor.ts @@ -83,14 +83,17 @@ export class DefaultValueAccessor implements ControlValueAccessor { this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); } + /** @internal */ _handleInput(value: any): void { if (!this._compositionMode || (this._compositionMode && !this._composing)) { this.onChange(value); } } + /** @internal */ _compositionStart(): void { this._composing = true; } + /** @internal */ _compositionEnd(value: any): void { this._composing = false; this._compositionMode && this.onChange(value); diff --git a/packages/forms/src/directives/ng_control_status.ts b/packages/forms/src/directives/ng_control_status.ts index e1cd164c4a..51c91d3f4e 100644 --- a/packages/forms/src/directives/ng_control_status.ts +++ b/packages/forms/src/directives/ng_control_status.ts @@ -38,7 +38,16 @@ export const ngControlStatusHost = { /** * Directive automatically applied to Angular form controls that sets CSS classes - * based on control status (valid/invalid/dirty/etc). + * based on control status. The following classes are applied as the properties + * become true: + * + * * ng-valid + * * ng-invalid + * * ng-pending + * * ng-pristine + * * ng-dirty + * * ng-untouched + * * ng-touched * * @stable */ diff --git a/packages/forms/src/directives/range_value_accessor.ts b/packages/forms/src/directives/range_value_accessor.ts index 65cf42abb6..e695fde116 100644 --- a/packages/forms/src/directives/range_value_accessor.ts +++ b/packages/forms/src/directives/range_value_accessor.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Provider, Renderer2, forwardRef} from '@angular/core'; +import {Directive, ElementRef, Renderer2, StaticProvider, forwardRef} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; -export const RANGE_VALUE_ACCESSOR: Provider = { +export const RANGE_VALUE_ACCESSOR: StaticProvider = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RangeValueAccessor), multi: true diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index 01fe3bf2af..15d08eeab3 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -134,6 +134,7 @@ export class FormGroupDirective extends ControlContainer implements Form, onSubmit($event: Event): boolean { this._submitted = true; + this._syncPendingControls(); this.ngSubmit.emit($event); return false; } @@ -145,6 +146,16 @@ export class FormGroupDirective extends ControlContainer implements Form, this._submitted = false; } + /** @internal */ + _syncPendingControls() { + this.form._syncPendingControls(); + this.directives.forEach(dir => { + if (dir.control._updateOn === 'submit') { + dir.viewToModelUpdate(dir.control._pendingValue); + } + }); + } + /** @internal */ _updateDomValue() { this.directives.forEach(dir => { diff --git a/packages/forms/src/directives/select_control_value_accessor.ts b/packages/forms/src/directives/select_control_value_accessor.ts index aec3b02f7a..43e6322ae3 100644 --- a/packages/forms/src/directives/select_control_value_accessor.ts +++ b/packages/forms/src/directives/select_control_value_accessor.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer2, forwardRef, ɵlooseIdentical as looseIdentical} from '@angular/core'; +import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Renderer2, StaticProvider, forwardRef, ɵlooseIdentical as looseIdentical} from '@angular/core'; + import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; -export const SELECT_VALUE_ACCESSOR: Provider = { +export const SELECT_VALUE_ACCESSOR: StaticProvider = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectControlValueAccessor), multi: true diff --git a/packages/forms/src/directives/select_multiple_control_value_accessor.ts b/packages/forms/src/directives/select_multiple_control_value_accessor.ts index 7db6202ead..c8e5b692e1 100644 --- a/packages/forms/src/directives/select_multiple_control_value_accessor.ts +++ b/packages/forms/src/directives/select_multiple_control_value_accessor.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer2, forwardRef, ɵlooseIdentical as looseIdentical} from '@angular/core'; +import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Renderer2, StaticProvider, forwardRef, ɵlooseIdentical as looseIdentical} from '@angular/core'; + import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; -export const SELECT_MULTIPLE_VALUE_ACCESSOR: Provider = { +export const SELECT_MULTIPLE_VALUE_ACCESSOR: StaticProvider = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectMultipleControlValueAccessor), multi: true diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index 8823b62818..428f502743 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -38,23 +38,10 @@ export function setUpControl(control: FormControl, dir: NgControl): void { control.asyncValidator = Validators.composeAsync([control.asyncValidator !, dir.asyncValidator]); dir.valueAccessor !.writeValue(control.value); - // view -> model - dir.valueAccessor !.registerOnChange((newValue: any) => { - dir.viewToModelUpdate(newValue); - control.markAsDirty(); - control.setValue(newValue, {emitModelToViewChange: false}); - }); + setUpViewChangePipeline(control, dir); + setUpModelChangePipeline(control, dir); - // touched - dir.valueAccessor !.registerOnTouched(() => control.markAsTouched()); - - control.registerOnChange((newValue: any, emitModelEvent: boolean) => { - // control -> view - dir.valueAccessor !.writeValue(newValue); - - // control -> ngModel - if (emitModelEvent) dir.viewToModelUpdate(newValue); - }); + setUpBlurPipeline(control, dir); if (dir.valueAccessor !.setDisabledState) { control.registerOnDisabledChange( @@ -92,6 +79,40 @@ export function cleanUpControl(control: FormControl, dir: NgControl) { if (control) control._clearChangeFns(); } +function setUpViewChangePipeline(control: FormControl, dir: NgControl): void { + dir.valueAccessor !.registerOnChange((newValue: any) => { + control._pendingValue = newValue; + control._pendingDirty = true; + + if (control._updateOn === 'change') updateControl(control, dir); + }); +} + +function setUpBlurPipeline(control: FormControl, dir: NgControl): void { + dir.valueAccessor !.registerOnTouched(() => { + control._pendingTouched = true; + + if (control._updateOn === 'blur') updateControl(control, dir); + if (control._updateOn !== 'submit') control.markAsTouched(); + }); +} + +function updateControl(control: FormControl, dir: NgControl): void { + dir.viewToModelUpdate(control._pendingValue); + if (control._pendingDirty) control.markAsDirty(); + control.setValue(control._pendingValue, {emitModelToViewChange: false}); +} + +function setUpModelChangePipeline(control: FormControl, dir: NgControl): void { + control.registerOnChange((newValue: any, emitModelEvent: boolean) => { + // control -> view + dir.valueAccessor !.writeValue(newValue); + + // control -> ngModel + if (emitModelEvent) dir.viewToModelUpdate(newValue); + }); +} + export function setUpFormContainer( control: FormGroup | FormArray, dir: AbstractFormGroupDirective | FormArrayName) { if (control == null) _throwError(dir, 'Cannot find control with'); diff --git a/packages/forms/src/directives/validators.ts b/packages/forms/src/directives/validators.ts index 7ec13a6208..00882f016a 100644 --- a/packages/forms/src/directives/validators.ts +++ b/packages/forms/src/directives/validators.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Input, OnChanges, Provider, SimpleChanges, forwardRef} from '@angular/core'; +import {Directive, Input, OnChanges, SimpleChanges, StaticProvider, forwardRef} from '@angular/core'; import {Observable} from 'rxjs/Observable'; + import {AbstractControl} from '../model'; import {NG_VALIDATORS, Validators} from '../validators'; + /** @experimental */ export type ValidationErrors = { [key: string]: any @@ -45,13 +47,13 @@ export interface AsyncValidator extends Validator { validate(c: AbstractControl): Promise<ValidationErrors|null>|Observable<ValidationErrors|null>; } -export const REQUIRED_VALIDATOR: Provider = { +export const REQUIRED_VALIDATOR: StaticProvider = { provide: NG_VALIDATORS, useExisting: forwardRef(() => RequiredValidator), multi: true }; -export const CHECKBOX_REQUIRED_VALIDATOR: Provider = { +export const CHECKBOX_REQUIRED_VALIDATOR: StaticProvider = { provide: NG_VALIDATORS, useExisting: forwardRef(() => CheckboxRequiredValidator), multi: true diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index 0f215ffbbb..b0d1e64058 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -55,16 +55,45 @@ function _find(control: AbstractControl, path: Array<string|number>| string, del }, control); } -function coerceToValidator(validator?: ValidatorFn | ValidatorFn[] | null): ValidatorFn|null { +function coerceToValidator( + validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): ValidatorFn| + null { + const validator = + (isOptionsObj(validatorOrOpts) ? (validatorOrOpts as AbstractControlOptions).validators : + validatorOrOpts) as ValidatorFn | + ValidatorFn[] | null; + return Array.isArray(validator) ? composeValidators(validator) : validator || null; } -function coerceToAsyncValidator(asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): - AsyncValidatorFn|null { - return Array.isArray(asyncValidator) ? composeAsyncValidators(asyncValidator) : - asyncValidator || null; +function coerceToAsyncValidator( + asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, validatorOrOpts?: ValidatorFn | + ValidatorFn[] | AbstractControlOptions | null): AsyncValidatorFn|null { + const origAsyncValidator = + (isOptionsObj(validatorOrOpts) ? (validatorOrOpts as AbstractControlOptions).asyncValidators : + asyncValidator) as AsyncValidatorFn | + AsyncValidatorFn | null; + + return Array.isArray(origAsyncValidator) ? composeAsyncValidators(origAsyncValidator) : + origAsyncValidator || null; } +export type FormHooks = 'change' | 'blur' | 'submit'; + +export interface AbstractControlOptions { + validators?: ValidatorFn|ValidatorFn[]|null; + asyncValidators?: AsyncValidatorFn|AsyncValidatorFn[]|null; + updateOn?: FormHooks; +} + + +function isOptionsObj( + validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): boolean { + return validatorOrOpts != null && !Array.isArray(validatorOrOpts) && + typeof validatorOrOpts === 'object'; +} + + /** * @whatItDoes This is the base class for {@link FormControl}, {@link FormGroup}, and * {@link FormArray}. @@ -79,6 +108,13 @@ function coerceToAsyncValidator(asyncValidator?: AsyncValidatorFn | AsyncValidat export abstract class AbstractControl { /** @internal */ _value: any; + + /** @internal */ + _pendingDirty: boolean; + + /** @internal */ + _pendingTouched: boolean; + /** @internal */ _onCollectionChange = () => {}; @@ -255,6 +291,7 @@ export abstract class AbstractControl { */ markAsUntouched(opts: {onlySelf?: boolean} = {}): void { this._touched = false; + this._pendingTouched = false; this._forEachChild( (control: AbstractControl) => { control.markAsUntouched({onlySelf: true}); }); @@ -287,6 +324,7 @@ export abstract class AbstractControl { */ markAsPristine(opts: {onlySelf?: boolean} = {}): void { this._pristine = true; + this._pendingDirty = false; this._forEachChild((control: AbstractControl) => { control.markAsPristine({onlySelf: true}); }); @@ -539,6 +577,9 @@ export abstract class AbstractControl { /** @internal */ abstract _allControlsDisabled(): boolean; + /** @internal */ + abstract _syncPendingControls(): boolean; + /** @internal */ _anyControlsHaveStatus(status: string): boolean { return this._anyControls((control: AbstractControl) => control.status === status); @@ -612,9 +653,12 @@ export abstract class AbstractControl { * console.log(ctrl.status); // 'DISABLED' * ``` * - * To include a sync validator (or an array of sync validators) with the control, - * pass it in as the second argument. Async validators are also supported, but - * have to be passed in separately as the third arg. + * The second {@link FormControl} argument can accept one of three things: + * * a sync validator function + * * an array of sync validator functions + * * an options object containing validator and/or async validator functions + * + * Example of a single sync validator function: * * ```ts * const ctrl = new FormControl('', Validators.required); @@ -622,6 +666,27 @@ export abstract class AbstractControl { * console.log(ctrl.status); // 'INVALID' * ``` * + * Example using options object: + * + * ```ts + * const ctrl = new FormControl('', { + * validators: Validators.required, + * asyncValidators: myAsyncValidator + * }); + * ``` + * + * The options object can also be used to define when the control should update. + * By default, the value and validity of a control updates whenever the value + * changes. You can configure it to update on the blur event instead by setting + * the `updateOn` option to `'blur'`. + * + * ```ts + * const c = new FormControl('', { updateOn: 'blur' }); + * ``` + * + * You can also set `updateOn` to `'submit'`, which will delay value and validity + * updates until the parent form of the control fires a submit event. + * * See its superclass, {@link AbstractControl}, for more properties and methods. * * * **npm package**: `@angular/forms` @@ -632,11 +697,21 @@ export class FormControl extends AbstractControl { /** @internal */ _onChange: Function[] = []; + /** @internal */ + _updateOn: FormHooks = 'change'; + + /** @internal */ + _pendingValue: any; + constructor( - formState: any = null, validator?: ValidatorFn|ValidatorFn[]|null, + formState: any = null, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { - super(coerceToValidator(validator), coerceToAsyncValidator(asyncValidator)); + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._applyFormState(formState); + this._setUpdateStrategy(validatorOrOpts); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); this._initObservables(); } @@ -664,7 +739,7 @@ export class FormControl extends AbstractControl { emitModelToViewChange?: boolean, emitViewToModelChange?: boolean } = {}): void { - this._value = value; + this._value = this._pendingValue = value; if (this._onChange.length && options.emitModelToViewChange !== false) { this._onChange.forEach( (changeFn) => changeFn(this._value, options.emitViewToModelChange !== false)); @@ -764,13 +839,30 @@ export class FormControl extends AbstractControl { */ _forEachChild(cb: Function): void {} + /** @internal */ + _syncPendingControls(): boolean { + if (this._updateOn === 'submit') { + this.setValue(this._pendingValue, {onlySelf: true, emitModelToViewChange: false}); + if (this._pendingDirty) this.markAsDirty(); + if (this._pendingTouched) this.markAsTouched(); + return true; + } + return false; + } + private _applyFormState(formState: any) { if (this._isBoxedValue(formState)) { - this._value = formState.value; + this._value = this._pendingValue = formState.value; formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) : this.enable({onlySelf: true, emitEvent: false}); } else { - this._value = formState; + this._value = this._pendingValue = formState; + } + } + + private _setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void { + if (isOptionsObj(opts) && (opts as AbstractControlOptions).updateOn != null) { + this._updateOn = (opts as AbstractControlOptions).updateOn !; } } } @@ -823,15 +915,28 @@ export class FormControl extends AbstractControl { * } * ``` * + * Like {@link FormControl} instances, you can alternatively choose to pass in + * validators and async validators as part of an options object. + * + * ``` + * const form = new FormGroup({ + * password: new FormControl('') + * passwordConfirm: new FormControl('') + * }, {validators: passwordMatchValidator, asyncValidators: otherValidator}); + * ``` + * * * **npm package**: `@angular/forms` * * @stable */ export class FormGroup extends AbstractControl { constructor( - public controls: {[key: string]: AbstractControl}, validator?: ValidatorFn|null, - asyncValidator?: AsyncValidatorFn|null) { - super(validator || null, asyncValidator || null); + public controls: {[key: string]: AbstractControl}, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._initObservables(); this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); @@ -1009,6 +1114,15 @@ export class FormGroup extends AbstractControl { }); } + /** @internal */ + _syncPendingControls(): boolean { + let subtreeUpdated = this._reduceChildren(false, (updated: boolean, child: AbstractControl) => { + return child._syncPendingControls() ? true : updated; + }); + if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true}); + return subtreeUpdated; + } + /** @internal */ _throwIfControlMissing(name: string): void { if (!Object.keys(this.controls).length) { @@ -1114,9 +1228,19 @@ export class FormGroup extends AbstractControl { * console.log(arr.status); // 'VALID' * ``` * - * You can also include array-level validators as the second arg, or array-level async - * validators as the third arg. These come in handy when you want to perform validation - * that considers the value of more than one child control. + * You can also include array-level validators and async validators. These come in handy + * when you want to perform validation that considers the value of more than one child + * control. + * + * The two types of validators can be passed in separately as the second and third arg + * respectively, or together as part of an options object. + * + * ``` + * const arr = new FormArray([ + * new FormControl('Nancy'), + * new FormControl('Drew') + * ], {validators: myValidator, asyncValidators: myAsyncValidator}); + * ``` * * ### Adding or removing controls * @@ -1132,9 +1256,12 @@ export class FormGroup extends AbstractControl { */ export class FormArray extends AbstractControl { constructor( - public controls: AbstractControl[], validator?: ValidatorFn|null, - asyncValidator?: AsyncValidatorFn|null) { - super(validator || null, asyncValidator || null); + public controls: AbstractControl[], + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._initObservables(); this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); @@ -1308,6 +1435,15 @@ export class FormArray extends AbstractControl { }); } + /** @internal */ + _syncPendingControls(): boolean { + let subtreeUpdated = this.controls.reduce((updated: boolean, child: AbstractControl) => { + return child._syncPendingControls() ? true : updated; + }, false); + if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true}); + return subtreeUpdated; + } + /** @internal */ _throwIfControlMissing(index: number): void { if (!this.controls.length) { diff --git a/packages/forms/test/form_array_spec.ts b/packages/forms/test/form_array_spec.ts index 2b94b80ec7..11b0605ae3 100644 --- a/packages/forms/test/form_array_spec.ts +++ b/packages/forms/test/form_array_spec.ts @@ -8,8 +8,8 @@ import {fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; -import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms'; - +import {AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors} from '@angular/forms'; +import {of } from 'rxjs/observable/of'; import {Validators} from '../src/validators'; export function main() { @@ -725,18 +725,113 @@ export function main() { }); }); + describe('validator', () => { + function simpleValidator(c: AbstractControl): ValidationErrors|null { + return c.get([0]) !.value === 'correct' ? null : {'broken': true}; + } + + function arrayRequiredValidator(c: AbstractControl): ValidationErrors|null { + return Validators.required(c.get([0]) as AbstractControl); + } + + it('should set a single validator', () => { + const a = new FormArray([new FormControl()], simpleValidator); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set a single validator from options obj', () => { + const a = new FormArray([new FormControl()], {validators: simpleValidator}); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set multiple validators from an array', () => { + const a = new FormArray([new FormControl()], [simpleValidator, arrayRequiredValidator]); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'required': true, 'broken': true}); + + a.setValue(['c']); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set multiple validators from options obj', () => { + const a = new FormArray( + [new FormControl()], {validators: [simpleValidator, arrayRequiredValidator]}); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'required': true, 'broken': true}); + + a.setValue(['c']); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + }); + describe('asyncValidator', () => { + function otherObservableValidator() { return of ({'other': true}); } + it('should run the async validator', fakeAsync(() => { const c = new FormControl('value'); const g = new FormArray([c], null !, asyncValidator('expected')); expect(g.pending).toEqual(true); - tick(1); + tick(); expect(g.errors).toEqual({'async': true}); expect(g.pending).toEqual(false); })); + + it('should set a single async validator from options obj', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], {asyncValidators: asyncValidator('expected')}); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from an array', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], null !, + [asyncValidator('expected'), otherObservableValidator]); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from options obj', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], + {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); }); describe('disable() & enable()', () => { diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index 4fa4d9f3a2..a424a5619a 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -76,7 +76,27 @@ export function main() { }); + describe('updateOn', () => { + + it('should default to on change', () => { + const c = new FormControl(''); + expect(c._updateOn).toEqual('change'); + }); + + it('should default to on change with an options obj', () => { + const c = new FormControl('', {validators: Validators.required}); + expect(c._updateOn).toEqual('change'); + }); + + it('should set updateOn when updating on blur', () => { + const c = new FormControl('', {updateOn: 'blur'}); + expect(c._updateOn).toEqual('blur'); + }); + + }); + describe('validator', () => { + it('should run validator with the initial value', () => { const c = new FormControl('value', Validators.required); expect(c.valid).toEqual(true); @@ -97,6 +117,39 @@ export function main() { expect(c.valid).toEqual(true); }); + it('should support single validator from options obj', () => { + const c = new FormControl(null, {validators: Validators.required}); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({required: true}); + + c.setValue('value'); + expect(c.valid).toEqual(true); + }); + + it('should support multiple validators from options obj', () => { + const c = + new FormControl(null, {validators: [Validators.required, Validators.minLength(3)]}); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({required: true}); + + c.setValue('aa'); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({minlength: {requiredLength: 3, actualLength: 2}}); + + c.setValue('aaa'); + expect(c.valid).toEqual(true); + }); + + it('should support a null validators value', () => { + const c = new FormControl(null, {validators: null}); + expect(c.valid).toEqual(true); + }); + + it('should support an empty options obj', () => { + const c = new FormControl(null, {}); + expect(c.valid).toEqual(true); + }); + it('should return errors', () => { const c = new FormControl(null, Validators.required); expect(c.errors).toEqual({'required': true}); @@ -222,6 +275,40 @@ export function main() { expect(c.errors).toEqual({'async': true, 'other': true}); })); + + it('should support a single async validator from options obj', fakeAsync(() => { + const c = new FormControl('value', {asyncValidators: asyncValidator('expected')}); + expect(c.pending).toEqual(true); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + + it('should support multiple async validators from options obj', fakeAsync(() => { + const c = new FormControl( + 'value', {asyncValidators: [asyncValidator('expected'), otherAsyncValidator]}); + expect(c.pending).toEqual(true); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true, 'other': true}); + })); + + it('should support a mix of validators from options obj', fakeAsync(() => { + const c = new FormControl( + '', {validators: Validators.required, asyncValidators: asyncValidator('expected')}); + tick(); + expect(c.errors).toEqual({required: true}); + + c.setValue('value'); + expect(c.pending).toBe(true); + + tick(); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + it('should add single async validator', fakeAsync(() => { const c = new FormControl('value', null !); diff --git a/packages/forms/test/form_group_spec.ts b/packages/forms/test/form_group_spec.ts index aa62b78c9c..feb20cdf0b 100644 --- a/packages/forms/test/form_group_spec.ts +++ b/packages/forms/test/form_group_spec.ts @@ -9,10 +9,15 @@ import {EventEmitter} from '@angular/core'; import {async, fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; -import {AbstractControl, FormArray, FormControl, FormGroup, Validators} from '@angular/forms'; +import {AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators} from '@angular/forms'; +import {of } from 'rxjs/observable/of'; export function main() { + function simpleValidator(c: AbstractControl): ValidationErrors|null { + return c.get('one') !.value === 'correct' ? null : {'broken': true}; + } + function asyncValidator(expected: string, timeouts = {}) { return (c: AbstractControl) => { let resolve: (result: any) => void = undefined !; @@ -36,6 +41,8 @@ export function main() { return e; } + function otherObservableValidator() { return of ({'other': true}); } + describe('FormGroup', () => { describe('value', () => { it('should be the reduced value of the child controls', () => { @@ -104,26 +111,6 @@ export function main() { }); }); - describe('errors', () => { - it('should run the validator when the value changes', () => { - const simpleValidator = (c: FormGroup) => - c.controls['one'].value != 'correct' ? {'broken': true} : null; - - const c = new FormControl(null); - const g = new FormGroup({'one': c}, simpleValidator); - - c.setValue('correct'); - - expect(g.valid).toEqual(true); - expect(g.errors).toEqual(null); - - c.setValue('incorrect'); - - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({'broken': true}); - }); - }); - describe('dirty', () => { let c: FormControl, g: FormGroup; @@ -629,7 +616,7 @@ export function main() { it('should return true when the component is enabled', () => { expect(group.contains('required')).toEqual(true); - group.enable('optional'); + group.enable(); expect(group.contains('optional')).toEqual(true); }); @@ -687,6 +674,66 @@ export function main() { }); }); + describe('validator', () => { + + function containsValidator(c: AbstractControl): ValidationErrors|null { + return c.get('one') !.value && c.get('one') !.value.indexOf('c') !== -1 ? null : + {'missing': true}; + } + + it('should run a single validator when the value changes', () => { + const c = new FormControl(null); + const g = new FormGroup({'one': c}, simpleValidator); + + c.setValue('correct'); + + expect(g.valid).toEqual(true); + expect(g.errors).toEqual(null); + + c.setValue('incorrect'); + + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({'broken': true}); + }); + + it('should support multiple validators from array', () => { + const g = new FormGroup({one: new FormControl()}, [simpleValidator, containsValidator]); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({missing: true, broken: true}); + + g.setValue({one: 'c'}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + it('should set single validator from options obj', () => { + const g = new FormGroup({one: new FormControl()}, {validators: simpleValidator}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + it('should set multiple validators from options obj', () => { + const g = new FormGroup( + {one: new FormControl()}, {validators: [simpleValidator, containsValidator]}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({missing: true, broken: true}); + + g.setValue({one: 'c'}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + }); + describe('asyncValidator', () => { it('should run the async validator', fakeAsync(() => { const c = new FormControl('value'); @@ -700,6 +747,38 @@ export function main() { expect(g.pending).toEqual(false); })); + it('should set multiple async validators from array', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, null !, + [asyncValidator('expected'), otherObservableValidator]); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set single async validator from options obj', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, {asyncValidators: asyncValidator('expected')}); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from options obj', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, + {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + it('should set the parent group\'s status to pending', fakeAsync(() => { const c = new FormControl('value', null !, asyncValidator('expected')); const g = new FormGroup({'one': c}); diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 3e758b0aed..4db317ca6e 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -12,8 +12,10 @@ import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MO import {By} from '@angular/platform-browser/src/dom/debug/by'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; +import {merge} from 'rxjs/observable/merge'; import {timer} from 'rxjs/observable/timer'; import {_do} from 'rxjs/operator/do'; + import {MyInput, MyInputForm} from './value_accessor_integration_spec'; export function main() { @@ -731,6 +733,493 @@ export function main() { }); + describe('updateOn options', () => { + + describe('on blur', () => { + + it('should not update value or validity based on user input until blur', () => { + const fixture = initTest(FormControlComp); + const control = new FormControl('', {validators: Validators.required, updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(control.value).toEqual('', 'Expected value to remain unchanged until blur.'); + expect(control.valid).toBe(false, 'Expected no validation to occur until blur.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(control.value) + .toEqual('Nancy', 'Expected value to change once control is blurred.'); + expect(control.valid).toBe(true, 'Expected validation to run once control is blurred.'); + }); + + it('should not update parent group value/validity from child until blur', () => { + const fixture = initTest(FormGroupComp); + const form = new FormGroup( + {login: new FormControl('', {validators: Validators.required, updateOn: 'blur'})}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(form.value) + .toEqual({login: ''}, 'Expected group value to remain unchanged until blur.'); + expect(form.valid).toBe(false, 'Expected no validation to occur on group until blur.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(form.value) + .toEqual({login: 'Nancy'}, 'Expected group value to change once input blurred.'); + expect(form.valid).toBe(true, 'Expected validation to run once input blurred.'); + }); + + it('should not wait for blur event to update if value is set programmatically', () => { + const fixture = initTest(FormControlComp); + const control = new FormControl('', {validators: Validators.required, updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + control.setValue('Nancy'); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + expect(input.value).toEqual('Nancy', 'Expected value to propagate to view immediately.'); + expect(control.value).toEqual('Nancy', 'Expected model value to update immediately.'); + expect(control.valid).toBe(true, 'Expected validation to run immediately.'); + }); + + it('should not update dirty state until control is blurred', () => { + const fixture = initTest(FormControlComp); + const control = new FormControl('', {updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + expect(control.dirty).toBe(false, 'Expected control to start out pristine.'); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(control.dirty).toBe(false, 'Expected control to stay pristine until blurred.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(control.dirty).toBe(true, 'Expected control to update dirty state when blurred.'); + }); + + it('should continue waiting for blur to update if previously blurred', () => { + const fixture = initTest(FormControlComp); + const control = + new FormControl('Nancy', {validators: Validators.required, updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + dispatchEvent(input, 'focus'); + input.value = ''; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(control.value) + .toEqual('Nancy', 'Expected value to remain unchanged until second blur.'); + expect(control.valid).toBe(true, 'Expected validation not to run until second blur.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(control.value).toEqual('', 'Expected value to update when blur occurs again.'); + expect(control.valid).toBe(false, 'Expected validation to run when blur occurs again.'); + }); + + it('should not use stale pending value if value set programmatically', () => { + const fixture = initTest(FormControlComp); + const control = new FormControl('', {validators: Validators.required, updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'aa'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + control.setValue('Nancy'); + fixture.detectChanges(); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(input.value).toEqual('Nancy', 'Expected programmatic value to stick after blur.'); + }); + + it('should set initial value and validity on init', () => { + const fixture = initTest(FormControlComp); + const control = + new FormControl('Nancy', {validators: Validators.maxLength(3), updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + + expect(input.value).toEqual('Nancy', 'Expected value to be set in the view.'); + expect(control.value).toEqual('Nancy', 'Expected initial model value to be set.'); + expect(control.valid).toBe(false, 'Expected validation to run on initial value.'); + }); + + it('should reset properly', () => { + const fixture = initTest(FormControlComp); + const control = new FormControl('', {validators: Validators.required, updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'aa'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + expect(control.dirty).toBe(true, 'Expected control to be dirty on blur.'); + + control.reset(); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(input.value).toEqual('', 'Expected view value to reset'); + expect(control.value).toBe(null, 'Expected pending value to reset.'); + expect(control.dirty).toBe(false, 'Expected pending dirty value to reset.'); + }); + + it('should not emit valueChanges or statusChanges until blur', () => { + const fixture = initTest(FormControlComp); + const control = new FormControl('', {validators: Validators.required, updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + const values: string[] = []; + + const sub = + merge(control.valueChanges, control.statusChanges).subscribe(val => values.push(val)); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(values).toEqual([], 'Expected no valueChanges or statusChanges on input.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(values).toEqual( + ['Nancy', 'VALID'], 'Expected valueChanges and statusChanges on blur.'); + + sub.unsubscribe(); + }); + + + it('should mark as pristine properly if pending dirty', () => { + const fixture = initTest(FormControlComp); + const control = new FormControl('', {updateOn: 'blur'}); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'aa'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + control.markAsPristine(); + expect(control.dirty).toBe(false, 'Expected control to become pristine.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(control.dirty).toBe(false, 'Expected pending dirty value to reset.'); + }); + + }); + + describe('on submit', () => { + + it('should set initial value and validity on init', () => { + const fixture = initTest(FormGroupComp); + const form = new FormGroup({ + login: + new FormControl('Nancy', {validators: Validators.required, updateOn: 'submit'}) + }); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + expect(input.value).toEqual('Nancy', 'Expected initial value to propagate to view.'); + expect(form.value).toEqual({login: 'Nancy'}, 'Expected initial value to be set.'); + expect(form.valid).toBe(true, 'Expected form to run validation on initial value.'); + }); + + it('should not update value or validity until submit', () => { + const fixture = initTest(FormGroupComp); + const formGroup = new FormGroup( + {login: new FormControl('', {validators: Validators.required, updateOn: 'submit'})}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(formGroup.value) + .toEqual({login: ''}, 'Expected form value to remain unchanged on input.'); + expect(formGroup.valid).toBe(false, 'Expected form validation not to run on input.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(formGroup.value) + .toEqual({login: ''}, 'Expected form value to remain unchanged on blur.'); + expect(formGroup.valid).toBe(false, 'Expected form validation not to run on blur.'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(formGroup.value) + .toEqual({login: 'Nancy'}, 'Expected form value to update on submit.'); + expect(formGroup.valid).toBe(true, 'Expected form validation to run on submit.'); + }); + + it('should not update after submit until a second submit', () => { + const fixture = initTest(FormGroupComp); + const formGroup = new FormGroup( + {login: new FormControl('', {validators: Validators.required, updateOn: 'submit'})}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + input.value = ''; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(formGroup.value) + .toEqual({login: 'Nancy'}, 'Expected value not to change until a second submit.'); + expect(formGroup.valid) + .toBe(true, 'Expected validation not to run until a second submit.'); + + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(formGroup.value) + .toEqual({login: ''}, 'Expected value to update on the second submit.'); + expect(formGroup.valid).toBe(false, 'Expected validation to run on a second submit.'); + }); + + it('should not wait for submit to set value programmatically', () => { + const fixture = initTest(FormGroupComp); + const formGroup = new FormGroup( + {login: new FormControl('', {validators: Validators.required, updateOn: 'submit'})}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + formGroup.setValue({login: 'Nancy'}); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + expect(input.value).toEqual('Nancy', 'Expected view value to update immediately.'); + expect(formGroup.value) + .toEqual({login: 'Nancy'}, 'Expected form value to update immediately.'); + expect(formGroup.valid).toBe(true, 'Expected form validation to run immediately.'); + }); + + it('should not update dirty until submit', () => { + const fixture = initTest(FormGroupComp); + const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(formGroup.dirty).toBe(false, 'Expected dirty not to change on input.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(formGroup.dirty).toBe(false, 'Expected dirty not to change on blur.'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(formGroup.dirty).toBe(true, 'Expected dirty to update on submit.'); + }); + + it('should not update touched until submit', () => { + const fixture = initTest(FormGroupComp); + const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(formGroup.touched).toBe(false, 'Expected touched not to change until submit.'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(formGroup.touched).toBe(true, 'Expected touched to update on submit.'); + }); + + it('should reset properly', () => { + const fixture = initTest(FormGroupComp); + const formGroup = new FormGroup( + {login: new FormControl('', {validators: Validators.required, updateOn: 'submit'})}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + formGroup.reset(); + fixture.detectChanges(); + + expect(input.value).toEqual('', 'Expected view value to reset.'); + expect(formGroup.value).toEqual({login: null}, 'Expected form value to reset'); + expect(formGroup.dirty).toBe(false, 'Expected dirty to stay false on reset.'); + expect(formGroup.touched).toBe(false, 'Expected touched to stay false on reset.'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(formGroup.value) + .toEqual({login: null}, 'Expected form value to stay empty on submit'); + expect(formGroup.dirty).toBe(false, 'Expected dirty to stay false on submit.'); + expect(formGroup.touched).toBe(false, 'Expected touched to stay false on submit.'); + }); + + it('should not emit valueChanges or statusChanges until submit', () => { + const fixture = initTest(FormGroupComp); + const control = + new FormControl('', {validators: Validators.required, updateOn: 'submit'}); + const formGroup = new FormGroup({login: control}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const values: string[] = []; + const streams = merge( + control.valueChanges, control.statusChanges, formGroup.valueChanges, + formGroup.statusChanges); + const sub = streams.subscribe(val => values.push(val)); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(values).toEqual([], 'Expected no valueChanges or statusChanges on input'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(values).toEqual([], 'Expected no valueChanges or statusChanges on blur'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(values).toEqual( + ['Nancy', 'VALID', {login: 'Nancy'}, 'VALID'], + 'Expected valueChanges and statusChanges to update on submit.'); + + sub.unsubscribe(); + }); + + it('should not run validation for onChange controls on submit', () => { + const validatorSpy = jasmine.createSpy('validator'); + const groupValidatorSpy = jasmine.createSpy('groupValidatorSpy'); + + const fixture = initTest(NestedFormGroupComp); + const formGroup = new FormGroup({ + signin: new FormGroup({login: new FormControl(), password: new FormControl()}), + email: new FormControl('', {updateOn: 'submit'}) + }); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + formGroup.get('signin.login') !.setValidators(validatorSpy); + formGroup.get('signin') !.setValidators(groupValidatorSpy); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(validatorSpy).not.toHaveBeenCalled(); + expect(groupValidatorSpy).not.toHaveBeenCalled(); + + }); + + + it('should mark as untouched properly if pending touched', () => { + const fixture = initTest(FormGroupComp); + const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + formGroup.markAsUntouched(); + fixture.detectChanges(); + + expect(formGroup.touched).toBe(false, 'Expected group to become untouched.'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(formGroup.touched).toBe(false, 'Expected touched to stay false on submit.'); + }); + + }); + + }); + describe('ngModel interactions', () => { it('should support ngModel for complex forms', fakeAsync(() => { @@ -785,6 +1274,33 @@ export function main() { expect(input.selectionStart).toEqual(1); })); + it('should work with updateOn submit', fakeAsync(() => { + const fixture = initTest(FormGroupNgModel); + const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})}); + fixture.componentInstance.form = formGroup; + fixture.componentInstance.login = 'initial'; + fixture.detectChanges(); + tick(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.login) + .toEqual('initial', 'Expected ngModel value to remain unchanged on input.'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.login) + .toEqual('Nancy', 'Expected ngModel value to update on submit.'); + + })); + }); describe('validations', () => { @@ -1116,7 +1632,7 @@ export function main() { tick(100); expect(resultArr.length) - .toEqual(2, `Expected original observable to be canceled on the next value change.`) + .toEqual(2, `Expected original observable to be canceled on the next value change.`); })); @@ -1238,14 +1754,14 @@ export function main() { TestBed.overrideComponent(FormGroupComp, { set: { template: ` - <div [formGroup]="myGroup"> + <div [formGroup]="form"> <input type="text" [(ngModel)]="data"> </div> ` } }); const fixture = initTest(FormGroupComp); - fixture.componentInstance.myGroup = new FormGroup({}); + fixture.componentInstance.form = new FormGroup({}); expect(() => fixture.detectChanges()) .toThrowError(new RegExp( @@ -1256,14 +1772,14 @@ export function main() { TestBed.overrideComponent(FormGroupComp, { set: { template: ` - <div [formGroup]="myGroup"> + <div [formGroup]="form"> <input type="text" [(ngModel)]="data" [ngModelOptions]="{standalone: true}"> </div> ` } }); const fixture = initTest(FormGroupComp); - fixture.componentInstance.myGroup = new FormGroup({}); + fixture.componentInstance.form = new FormGroup({}); expect(() => fixture.detectChanges()).not.toThrowError(); }); @@ -1272,7 +1788,7 @@ export function main() { TestBed.overrideComponent(FormGroupComp, { set: { template: ` - <div [formGroup]="myGroup"> + <div [formGroup]="form"> <div formGroupName="person"> <input type="text" [(ngModel)]="data"> </div> @@ -1281,8 +1797,7 @@ export function main() { } }); const fixture = initTest(FormGroupComp); - const myGroup = new FormGroup({person: new FormGroup({})}); - fixture.componentInstance.myGroup = new FormGroup({person: new FormGroup({})}); + fixture.componentInstance.form = new FormGroup({person: new FormGroup({})}); expect(() => fixture.detectChanges()) .toThrowError(new RegExp( @@ -1293,7 +1808,7 @@ export function main() { TestBed.overrideComponent(FormGroupComp, { set: { template: ` - <div [formGroup]="myGroup"> + <div [formGroup]="form"> <div ngModelGroup="person"> <input type="text" [(ngModel)]="data"> </div> @@ -1302,7 +1817,7 @@ export function main() { } }); const fixture = initTest(FormGroupComp); - fixture.componentInstance.myGroup = new FormGroup({}); + fixture.componentInstance.form = new FormGroup({}); expect(() => fixture.detectChanges()) .toThrowError( @@ -1406,7 +1921,9 @@ export function main() { // formControl should update normally expect(fixture.componentInstance.control.value).toEqual('updatedValue'); }); + }); + }); } @@ -1470,7 +1987,6 @@ class FormControlComp { class FormGroupComp { control: FormControl; form: FormGroup; - myGroup: FormGroup; event: Event; } @@ -1522,13 +2038,12 @@ class FormArrayNestedGroup { cityArray: FormArray; } - @Component({ selector: 'form-group-ng-model', template: ` - <div [formGroup]="form"> + <form [formGroup]="form"> <input type="text" formControlName="login" [(ngModel)]="login"> - </div>` + </form>` }) class FormGroupNgModel { form: FormGroup; diff --git a/packages/forms/test/value_accessor_integration_spec.ts b/packages/forms/test/value_accessor_integration_spec.ts index e8c2c5cad6..b708121882 100644 --- a/packages/forms/test/value_accessor_integration_spec.ts +++ b/packages/forms/test/value_accessor_integration_spec.ts @@ -1,3 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + import {Component, Directive, EventEmitter, Input, Output, Type} from '@angular/core'; import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing'; import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, NgForm, ReactiveFormsModule, Validators} from '@angular/forms'; diff --git a/packages/http/test/backends/jsonp_backend_spec.ts b/packages/http/test/backends/jsonp_backend_spec.ts index ecef2c529e..b750a3e680 100644 --- a/packages/http/test/backends/jsonp_backend_spec.ts +++ b/packages/http/test/backends/jsonp_backend_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ReflectiveInjector} from '@angular/core'; +import {Injector} from '@angular/core'; import {AsyncTestCompleter, SpyObject, afterEach, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {BrowserJsonp} from '../../src/backends/browser_jsonp'; @@ -52,10 +52,10 @@ export function main() { let sampleRequest: Request; beforeEach(() => { - const injector = ReflectiveInjector.resolveAndCreate([ - {provide: ResponseOptions, useClass: BaseResponseOptions}, - {provide: BrowserJsonp, useClass: MockBrowserJsonp}, - {provide: JSONPBackend, useClass: JSONPBackend_} + const injector = Injector.create([ + {provide: ResponseOptions, useClass: BaseResponseOptions, deps: []}, + {provide: BrowserJsonp, useClass: MockBrowserJsonp, deps: []}, + {provide: JSONPBackend, useClass: JSONPBackend_, deps: [BrowserJsonp, ResponseOptions]} ]); backend = injector.get(JSONPBackend); const base = new BaseRequestOptions(); diff --git a/packages/http/test/backends/mock_backend_spec.ts b/packages/http/test/backends/mock_backend_spec.ts index 3b4640decc..73a2d9df29 100644 --- a/packages/http/test/backends/mock_backend_spec.ts +++ b/packages/http/test/backends/mock_backend_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ReflectiveInjector} from '@angular/core'; +import {Injector} from '@angular/core'; import {AsyncTestCompleter, beforeEach, describe, inject, it, xit} from '@angular/core/testing/src/testing_internal'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {ReplaySubject} from 'rxjs/ReplaySubject'; @@ -27,8 +27,10 @@ export function main() { let sampleResponse2: Response; beforeEach(() => { - const injector = ReflectiveInjector.resolveAndCreate( - [{provide: ResponseOptions, useClass: BaseResponseOptions}, MockBackend]); + const injector = Injector.create([ + {provide: ResponseOptions, useClass: BaseResponseOptions, deps: []}, + {provide: MockBackend, deps: []} + ]); backend = injector.get(MockBackend); const base = new BaseRequestOptions(); sampleRequest1 = diff --git a/packages/http/test/http_spec.ts b/packages/http/test/http_spec.ts index 3f0fb7b5c0..7b0a751fd7 100644 --- a/packages/http/test/http_spec.ts +++ b/packages/http/test/http_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, ReflectiveInjector} from '@angular/core'; +import {Injector} from '@angular/core'; import {TestBed, getTestBed} from '@angular/core/testing'; import {AsyncTestCompleter, afterEach, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -79,8 +79,8 @@ export function main() { let jsonp: Jsonp; beforeEach(() => { - injector = ReflectiveInjector.resolveAndCreate([ - BaseRequestOptions, MockBackend, { + injector = Injector.create([ + {provide: BaseRequestOptions, deps: []}, {provide: MockBackend, deps: []}, { provide: Http, useFactory: function(backend: ConnectionBackend, defaultOptions: BaseRequestOptions) { return new Http(backend, defaultOptions); diff --git a/packages/http/testing/src/mock_backend.ts b/packages/http/testing/src/mock_backend.ts index 4f3128dad9..dbaa2877f0 100644 --- a/packages/http/testing/src/mock_backend.ts +++ b/packages/http/testing/src/mock_backend.ts @@ -114,7 +114,7 @@ export class MockConnection implements Connection { * ### Example * * ``` - * import {Injectable, ReflectiveInjector} from '@angular/core'; + * import {Injectable, Injector} from '@angular/core'; * import {async, fakeAsync, tick} from '@angular/core/testing'; * import {BaseRequestOptions, ConnectionBackend, Http, RequestOptions} from '@angular/http'; * import {Response, ResponseOptions} from '@angular/http'; @@ -142,7 +142,7 @@ export class MockConnection implements Connection { * * describe('MockBackend HeroService Example', () => { * beforeEach(() => { - * this.injector = ReflectiveInjector.resolveAndCreate([ + * this.injector = Injector.create([ * {provide: ConnectionBackend, useClass: MockBackend}, * {provide: RequestOptions, useClass: BaseRequestOptions}, * Http, @@ -202,7 +202,7 @@ export class MockBackend implements ConnectionBackend { * ### Example * * ``` - * import {ReflectiveInjector} from '@angular/core'; + * import {Injector} from '@angular/core'; * import {fakeAsync, tick} from '@angular/core/testing'; * import {BaseRequestOptions, ConnectionBackend, Http, RequestOptions} from '@angular/http'; * import {Response, ResponseOptions} from '@angular/http'; @@ -213,7 +213,7 @@ export class MockBackend implements ConnectionBackend { * MockConnection; // this will be set when a new connection is emitted from the * // backend. * let text: string; // this will be set from mock response - * let injector = ReflectiveInjector.resolveAndCreate([ + * let injector = Injector.create([ * {provide: ConnectionBackend, useClass: MockBackend}, * {provide: RequestOptions, useClass: BaseRequestOptions}, * Http, diff --git a/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts b/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts index d66060f889..9b65cf4e95 100644 --- a/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts +++ b/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts @@ -7,7 +7,7 @@ */ import {ResourceLoader, platformCoreDynamic} from '@angular/compiler'; -import {PlatformRef, Provider, createPlatformFactory} from '@angular/core'; +import {PlatformRef, Provider, StaticProvider, createPlatformFactory} from '@angular/core'; import {INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS} from './platform_providers'; import {CachedResourceLoader} from './resource_loader/resource_loader_cache'; diff --git a/packages/platform-browser-dynamic/src/platform_providers.ts b/packages/platform-browser-dynamic/src/platform_providers.ts index 88e937643a..f477ee77b7 100644 --- a/packages/platform-browser-dynamic/src/platform_providers.ts +++ b/packages/platform-browser-dynamic/src/platform_providers.ts @@ -8,17 +8,17 @@ import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; import {ResourceLoader} from '@angular/compiler'; -import {COMPILER_OPTIONS, PLATFORM_ID, Provider} from '@angular/core'; +import {COMPILER_OPTIONS, PLATFORM_ID, StaticProvider} from '@angular/core'; import {ɵINTERNAL_BROWSER_PLATFORM_PROVIDERS as INTERNAL_BROWSER_PLATFORM_PROVIDERS} from '@angular/platform-browser'; import {ResourceLoaderImpl} from './resource_loader/resource_loader_impl'; -export const INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS: Provider[] = [ +export const INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS: StaticProvider[] = [ INTERNAL_BROWSER_PLATFORM_PROVIDERS, { provide: COMPILER_OPTIONS, - useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl}]}, + useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl, deps: []}]}, multi: true }, {provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}, diff --git a/packages/platform-browser-dynamic/src/resource_loader/resource_loader_impl.ts b/packages/platform-browser-dynamic/src/resource_loader/resource_loader_impl.ts index b047058b69..4e5dfd6e5e 100644 --- a/packages/platform-browser-dynamic/src/resource_loader/resource_loader_impl.ts +++ b/packages/platform-browser-dynamic/src/resource_loader/resource_loader_impl.ts @@ -14,7 +14,7 @@ export class ResourceLoaderImpl extends ResourceLoader { get(url: string): Promise<string> { let resolve: (result: any) => void; let reject: (error: any) => void; - const promise = new Promise((res, rej) => { + const promise = new Promise<string>((res, rej) => { resolve = res; reject = rej; }); diff --git a/packages/platform-browser-dynamic/test/resource_loader/resource_loader_cache_spec.ts b/packages/platform-browser-dynamic/test/resource_loader/resource_loader_cache_spec.ts index 28a9e8aacc..0e878e5db6 100644 --- a/packages/platform-browser-dynamic/test/resource_loader/resource_loader_cache_spec.ts +++ b/packages/platform-browser-dynamic/test/resource_loader/resource_loader_cache_spec.ts @@ -26,8 +26,8 @@ export function main() { beforeEach(fakeAsync(() => { TestBed.configureCompiler({ providers: [ - {provide: UrlResolver, useClass: TestUrlResolver}, - {provide: ResourceLoader, useFactory: createCachedResourceLoader} + {provide: UrlResolver, useClass: TestUrlResolver, deps: []}, + {provide: ResourceLoader, useFactory: createCachedResourceLoader, deps: []} ] }); diff --git a/packages/platform-browser-dynamic/testing/src/testing.ts b/packages/platform-browser-dynamic/testing/src/testing.ts index fea035e9e4..f0b6dc26da 100644 --- a/packages/platform-browser-dynamic/testing/src/testing.ts +++ b/packages/platform-browser-dynamic/testing/src/testing.ts @@ -7,7 +7,7 @@ */ import {platformCoreDynamicTesting} from '@angular/compiler/testing'; -import {NgModule, PlatformRef, Provider, createPlatformFactory} from '@angular/core'; +import {NgModule, PlatformRef, StaticProvider, createPlatformFactory} from '@angular/core'; import {TestComponentRenderer} from '@angular/core/testing'; import {ɵINTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS as INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS} from '@angular/platform-browser-dynamic'; import {BrowserTestingModule} from '@angular/platform-browser/testing'; diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index 070b9ae1f1..7043cdb8b5 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -7,7 +7,7 @@ */ import {CommonModule, PlatformLocation, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; -import {APP_ID, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, Testability, createPlatformFactory, platformCore} from '@angular/core'; +import {APP_ID, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore} from '@angular/core'; import {BrowserDomAdapter} from './browser/browser_adapter'; import {BrowserPlatformLocation} from './browser/location/browser_platform_location'; @@ -26,10 +26,10 @@ import {KeyEventsPlugin} from './dom/events/key_events'; import {DomSharedStylesHost, SharedStylesHost} from './dom/shared_styles_host'; import {DomSanitizer, DomSanitizerImpl} from './security/dom_sanitization_service'; -export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [ +export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: StaticProvider[] = [ {provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}, {provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true}, - {provide: PlatformLocation, useClass: BrowserPlatformLocation}, + {provide: PlatformLocation, useClass: BrowserPlatformLocation, deps: [DOCUMENT]}, {provide: DOCUMENT, useFactory: _document, deps: []}, ]; @@ -39,15 +39,15 @@ export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [ * application to XSS risks. For more detail, see the [Security Guide](http://g.co/ng/security). * @experimental */ -export const BROWSER_SANITIZATION_PROVIDERS: Array<any> = [ +export const BROWSER_SANITIZATION_PROVIDERS: StaticProvider[] = [ {provide: Sanitizer, useExisting: DomSanitizer}, - {provide: DomSanitizer, useClass: DomSanitizerImpl}, + {provide: DomSanitizer, useClass: DomSanitizerImpl, deps: [DOCUMENT]}, ]; /** * @stable */ -export const platformBrowser: (extraProviders?: Provider[]) => PlatformRef = +export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); export function initDomAdapter() { diff --git a/packages/platform-browser/src/browser/server-transition.ts b/packages/platform-browser/src/browser/server-transition.ts index 2bfb9de470..fde6a705f7 100644 --- a/packages/platform-browser/src/browser/server-transition.ts +++ b/packages/platform-browser/src/browser/server-transition.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_INITIALIZER, ApplicationInitStatus, Inject, InjectionToken, Injector, Provider} from '@angular/core'; +import {APP_INITIALIZER, ApplicationInitStatus, Inject, InjectionToken, Injector, StaticProvider} from '@angular/core'; import {getDOM} from '../dom/dom_adapter'; import {DOCUMENT} from '../dom/dom_tokens'; @@ -31,7 +31,7 @@ export function appInitializerFactory(transitionId: string, document: any, injec }; } -export const SERVER_TRANSITION_PROVIDERS: Provider[] = [ +export const SERVER_TRANSITION_PROVIDERS: StaticProvider[] = [ { provide: APP_INITIALIZER, useFactory: appInitializerFactory, diff --git a/packages/platform-browser/test/browser/bootstrap_spec.ts b/packages/platform-browser/test/browser/bootstrap_spec.ts index ad2851092c..f1b0cf2926 100644 --- a/packages/platform-browser/test/browser/bootstrap_spec.ts +++ b/packages/platform-browser/test/browser/bootstrap_spec.ts @@ -7,7 +7,7 @@ */ import {isPlatformBrowser} from '@angular/common'; -import {APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, Directive, ErrorHandler, Inject, Input, LOCALE_ID, NgModule, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Pipe, Provider, VERSION, createPlatformFactory, ɵstringify as stringify} from '@angular/core'; +import {APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, Directive, ErrorHandler, Inject, Input, LOCALE_ID, NgModule, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Pipe, Provider, StaticProvider, VERSION, createPlatformFactory, ɵstringify as stringify} from '@angular/core'; import {ApplicationRef, destroyPlatform} from '@angular/core/src/application_ref'; import {Console} from '@angular/core/src/console'; import {ComponentRef} from '@angular/core/src/linker/component_factory'; @@ -112,8 +112,8 @@ class DummyConsole implements Console { class TestModule {} -function bootstrap( - cmpType: any, providers: Provider[] = [], platformProviders: Provider[] = []): Promise<any> { +function bootstrap(cmpType: any, providers: Provider[] = [], platformProviders: StaticProvider[] = [ +]): Promise<any> { @NgModule({ imports: [BrowserModule], declarations: [cmpType], diff --git a/packages/platform-browser/test/browser/tools/spies.ts b/packages/platform-browser/test/browser/tools/spies.ts index 11d27ad1bd..f59a76d970 100644 --- a/packages/platform-browser/test/browser/tools/spies.ts +++ b/packages/platform-browser/test/browser/tools/spies.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ReflectiveInjector, ɵglobal as global} from '@angular/core'; +import {Injector, ɵglobal as global} from '@angular/core'; import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref'; import {SpyObject} from '@angular/core/testing/src/testing_internal'; @@ -18,8 +18,8 @@ export class SpyComponentRef extends SpyObject { injector: any /** TODO #9100 */; constructor() { super(); - this.injector = ReflectiveInjector.resolveAndCreate( - [{provide: ApplicationRef, useClass: SpyApplicationRef}]); + this.injector = + Injector.create([{provide: ApplicationRef, useClass: SpyApplicationRef, deps: []}]); } } diff --git a/packages/platform-browser/test/dom/events/event_manager_spec.ts b/packages/platform-browser/test/dom/events/event_manager_spec.ts index 8d924ba190..638a70d918 100644 --- a/packages/platform-browser/test/dom/events/event_manager_spec.ts +++ b/packages/platform-browser/test/dom/events/event_manager_spec.ts @@ -109,7 +109,6 @@ class FakeEventManagerPlugin extends EventManagerPlugin { class FakeNgZone extends NgZone { constructor() { super({enableLongStackTrace: false}); } - run(fn: Function) { fn(); } - + run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T { return fn(); } runOutsideAngular(fn: Function) { return fn(); } } diff --git a/packages/platform-browser/test/security/html_sanitizer_spec.ts b/packages/platform-browser/test/security/html_sanitizer_spec.ts index 144a052168..51afd6d159 100644 --- a/packages/platform-browser/test/security/html_sanitizer_spec.ts +++ b/packages/platform-browser/test/security/html_sanitizer_spec.ts @@ -123,7 +123,7 @@ export function main() { // depending on the browser, we might ge an exception } try { - sanitizeHtml(defaultDoc, '<form><input name="nextSibling" /></form>') + sanitizeHtml(defaultDoc, '<form><input name="nextSibling" /></form>'); } catch (e) { // depending on the browser, we might ge an exception } diff --git a/packages/platform-browser/testing/src/browser.ts b/packages/platform-browser/testing/src/browser.ts index e698f67726..b9020df987 100644 --- a/packages/platform-browser/testing/src/browser.ts +++ b/packages/platform-browser/testing/src/browser.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {APP_ID, NgModule, NgZone, PLATFORM_INITIALIZER, PlatformRef, Provider, createPlatformFactory, platformCore} from '@angular/core'; +import {APP_ID, NgModule, NgZone, PLATFORM_INITIALIZER, PlatformRef, StaticProvider, createPlatformFactory, platformCore} from '@angular/core'; import {BrowserModule, ɵBrowserDomAdapter as BrowserDomAdapter, ɵELEMENT_PROBE_PROVIDERS as ELEMENT_PROBE_PROVIDERS} from '@angular/platform-browser'; import {BrowserDetection, createNgZone} from './browser_util'; @@ -14,7 +14,7 @@ function initBrowserTests() { BrowserDetection.setup(); } -const _TEST_BROWSER_PLATFORM_PROVIDERS: Provider[] = +const _TEST_BROWSER_PLATFORM_PROVIDERS: StaticProvider[] = [{provide: PLATFORM_INITIALIZER, useValue: initBrowserTests, multi: true}]; /** diff --git a/packages/platform-server/src/server.ts b/packages/platform-server/src/server.ts index facb7b3a39..b109411804 100644 --- a/packages/platform-server/src/server.ts +++ b/packages/platform-server/src/server.ts @@ -10,9 +10,9 @@ import {ɵAnimationEngine} from '@angular/animations/browser'; import {PlatformLocation, ɵPLATFORM_SERVER_ID as PLATFORM_SERVER_ID} from '@angular/common'; import {HttpClientModule} from '@angular/common/http'; import {platformCoreDynamic} from '@angular/compiler'; -import {Injectable, InjectionToken, Injector, NgModule, NgZone, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, Testability, createPlatformFactory, isDevMode, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS} from '@angular/core'; +import {Injectable, InjectionToken, Injector, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, StaticProvider, Testability, createPlatformFactory, isDevMode, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS} from '@angular/core'; import {HttpModule} from '@angular/http'; -import {BrowserModule, DOCUMENT, ɵSharedStylesHost as SharedStylesHost, ɵgetDOM as getDOM} from '@angular/platform-browser'; +import {BrowserModule, DOCUMENT, ɵSharedStylesHost as SharedStylesHost, ɵTRANSITION_ID, ɵgetDOM as getDOM} from '@angular/platform-browser'; import {NoopAnimationsModule, ɵAnimationRendererFactory} from '@angular/platform-browser/animations'; import {SERVER_HTTP_PROVIDERS} from './http'; @@ -27,11 +27,15 @@ function notSupported(feature: string): Error { throw new Error(`platform-server does not support '${feature}'.`); } -export const INTERNAL_SERVER_PLATFORM_PROVIDERS: Array<any /*Type | Provider | any[]*/> = [ +export const INTERNAL_SERVER_PLATFORM_PROVIDERS: StaticProvider[] = [ {provide: DOCUMENT, useFactory: _document, deps: [Injector]}, {provide: PLATFORM_ID, useValue: PLATFORM_SERVER_ID}, - {provide: PLATFORM_INITIALIZER, useFactory: initParse5Adapter, multi: true, deps: [Injector]}, - {provide: PlatformLocation, useClass: ServerPlatformLocation}, PlatformState, + {provide: PLATFORM_INITIALIZER, useFactory: initParse5Adapter, multi: true, deps: [Injector]}, { + provide: PlatformLocation, + useClass: ServerPlatformLocation, + deps: [DOCUMENT, [Optional, INITIAL_CONFIG]] + }, + {provide: PlatformState, deps: [DOCUMENT]}, // Add special provider that allows multiple instances of platformServer* to be created. {provide: ALLOW_MULTIPLE_PLATFORMS, useValue: true} ]; diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index f768a24709..3239cc82ba 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Type} from '@angular/core'; +import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type} from '@angular/core'; import {ɵTRANSITION_ID} from '@angular/platform-browser'; import {filter} from 'rxjs/operator/filter'; import {first} from 'rxjs/operator/first'; @@ -21,11 +21,11 @@ const parse5 = require('parse5'); interface PlatformOptions { document?: string; url?: string; - extraProviders?: Provider[]; + extraProviders?: StaticProvider[]; } function _getPlatform( - platformFactory: (extraProviders: Provider[]) => PlatformRef, + platformFactory: (extraProviders: StaticProvider[]) => PlatformRef, options: PlatformOptions): PlatformRef { const extraProviders = options.extraProviders ? options.extraProviders : []; return platformFactory([ @@ -67,8 +67,8 @@ the server-rendered app can be properly bootstrapped into a client app.`); * @experimental */ export function renderModule<T>( - module: Type<T>, - options: {document?: string, url?: string, extraProviders?: Provider[]}): Promise<string> { + module: Type<T>, options: {document?: string, url?: string, extraProviders?: StaticProvider[]}): + Promise<string> { const platform = _getPlatform(platformDynamicServer, options); return _render(platform, platform.bootstrapModule(module)); } @@ -84,7 +84,8 @@ export function renderModule<T>( */ export function renderModuleFactory<T>( moduleFactory: NgModuleFactory<T>, - options: {document?: string, url?: string, extraProviders?: Provider[]}): Promise<string> { + options: {document?: string, url?: string, extraProviders?: StaticProvider[]}): + Promise<string> { const platform = _getPlatform(platformServer, options); return _render(platform, platform.bootstrapModuleFactory(moduleFactory)); } diff --git a/packages/platform-server/testing/src/server.ts b/packages/platform-server/testing/src/server.ts index 85272e89d1..6ea98413b6 100644 --- a/packages/platform-server/testing/src/server.ts +++ b/packages/platform-server/testing/src/server.ts @@ -7,7 +7,7 @@ */ import {platformCoreDynamicTesting} from '@angular/compiler/testing'; -import {NgModule, PlatformRef, Provider, createPlatformFactory} from '@angular/core'; +import {NgModule, PlatformRef, StaticProvider, createPlatformFactory} from '@angular/core'; import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as INTERNAL_SERVER_PLATFORM_PROVIDERS, ɵSERVER_RENDER_PROVIDERS as SERVER_RENDER_PROVIDERS} from '@angular/platform-server'; diff --git a/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts b/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts index 72aeb121f4..57c2a2bedb 100644 --- a/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts +++ b/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts @@ -8,7 +8,7 @@ import {ɵPLATFORM_WORKER_UI_ID as PLATFORM_WORKER_UI_ID} from '@angular/common'; import {ResourceLoader, platformCoreDynamic} from '@angular/compiler'; -import {COMPILER_OPTIONS, PLATFORM_ID, PlatformRef, Provider, createPlatformFactory} from '@angular/core'; +import {COMPILER_OPTIONS, PLATFORM_ID, PlatformRef, StaticProvider, createPlatformFactory} from '@angular/core'; import {ɵResourceLoaderImpl as ResourceLoaderImpl} from '@angular/platform-browser-dynamic'; export {VERSION} from './version'; @@ -19,7 +19,7 @@ export const platformWorkerAppDynamic = createPlatformFactory(platformCoreDynamic, 'workerAppDynamic', [ { provide: COMPILER_OPTIONS, - useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl}]}, + useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl, deps: []}]}, multi: true }, {provide: PLATFORM_ID, useValue: PLATFORM_WORKER_UI_ID} diff --git a/packages/platform-webworker/src/platform-webworker.ts b/packages/platform-webworker/src/platform-webworker.ts index fa193e8db7..8ecc2a4b57 100644 --- a/packages/platform-webworker/src/platform-webworker.ts +++ b/packages/platform-webworker/src/platform-webworker.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {PlatformRef, Provider} from '@angular/core'; +import {PlatformRef, StaticProvider} from '@angular/core'; import {WORKER_SCRIPT, platformWorkerUi} from './worker_render'; @@ -26,7 +26,7 @@ export {platformWorkerUi} from './worker_render'; * @experimental */ export function bootstrapWorkerUi( - workerScriptUri: string, customProviders: Provider[] = []): Promise<PlatformRef> { + workerScriptUri: string, customProviders: StaticProvider[] = []): Promise<PlatformRef> { // For now, just creates the worker ui platform... const platform = platformWorkerUi([ {provide: WORKER_SCRIPT, useValue: workerScriptUri}, diff --git a/packages/platform-webworker/src/web_workers/ui/location_providers.ts b/packages/platform-webworker/src/web_workers/ui/location_providers.ts index 71192b3547..f399cba7a8 100644 --- a/packages/platform-webworker/src/web_workers/ui/location_providers.ts +++ b/packages/platform-webworker/src/web_workers/ui/location_providers.ts @@ -6,9 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, NgZone, PLATFORM_INITIALIZER, Provider} from '@angular/core'; - +import {DOCUMENT} from '@angular/common'; +import {Injector, NgZone, PLATFORM_INITIALIZER, StaticProvider} from '@angular/core'; import {ɵBrowserPlatformLocation as BrowserPlatformLocation} from '@angular/platform-browser'; + +import {MessageBus} from '../shared/message_bus'; +import {Serializer} from '../shared/serializer'; +import {ServiceMessageBrokerFactory} from '../shared/service_message_broker'; + import {MessageBasedPlatformLocation} from './platform_location'; @@ -18,8 +23,10 @@ import {MessageBasedPlatformLocation} from './platform_location'; * include these providers when setting up the render thread. * @experimental */ -export const WORKER_UI_LOCATION_PROVIDERS: Provider[] = [ - MessageBasedPlatformLocation, BrowserPlatformLocation, +export const WORKER_UI_LOCATION_PROVIDERS = <StaticProvider[]>[ + {provide: MessageBasedPlatformLocation, deps: [ServiceMessageBrokerFactory, + BrowserPlatformLocation, MessageBus, Serializer]}, + {provide: BrowserPlatformLocation, deps: [DOCUMENT]}, {provide: PLATFORM_INITIALIZER, useFactory: initUiLocation, multi: true, deps: [Injector]} ]; diff --git a/packages/platform-webworker/src/worker_app.ts b/packages/platform-webworker/src/worker_app.ts index 9f992b5924..2daf468a10 100644 --- a/packages/platform-webworker/src/worker_app.ts +++ b/packages/platform-webworker/src/worker_app.ts @@ -7,8 +7,9 @@ */ import {CommonModule, ɵPLATFORM_WORKER_APP_ID as PLATFORM_WORKER_APP_ID} from '@angular/common'; -import {APP_INITIALIZER, ApplicationModule, ErrorHandler, NgModule, NgZone, PLATFORM_ID, PlatformRef, Provider, RendererFactory2, RootRenderer, createPlatformFactory, platformCore} from '@angular/core'; +import {APP_INITIALIZER, ApplicationModule, ErrorHandler, NgModule, NgZone, PLATFORM_ID, PlatformRef, RendererFactory2, RootRenderer, StaticProvider, createPlatformFactory, platformCore} from '@angular/core'; import {DOCUMENT, ɵBROWSER_SANITIZATION_PROVIDERS as BROWSER_SANITIZATION_PROVIDERS} from '@angular/platform-browser'; + import {ON_WEB_WORKER} from './web_workers/shared/api'; import {ClientMessageBrokerFactory, ClientMessageBrokerFactory_} from './web_workers/shared/client_message_broker'; import {MessageBus} from './web_workers/shared/message_bus'; diff --git a/packages/platform-webworker/src/worker_render.ts b/packages/platform-webworker/src/worker_render.ts index d3db75261c..71a6d16cc2 100644 --- a/packages/platform-webworker/src/worker_render.ts +++ b/packages/platform-webworker/src/worker_render.ts @@ -7,7 +7,7 @@ */ import {CommonModule, ɵPLATFORM_WORKER_UI_ID as PLATFORM_WORKER_UI_ID} from '@angular/common'; -import {ErrorHandler, Injectable, InjectionToken, Injector, NgZone, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, Testability, createPlatformFactory, isDevMode, platformCore, ɵAPP_ID_RANDOM_PROVIDER as APP_ID_RANDOM_PROVIDER} from '@angular/core'; +import {ErrorHandler, Injectable, InjectionToken, Injector, NgZone, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, StaticProvider, Testability, createPlatformFactory, isDevMode, platformCore, ɵAPP_ID_RANDOM_PROVIDER as APP_ID_RANDOM_PROVIDER} from '@angular/core'; import {DOCUMENT, EVENT_MANAGER_PLUGINS, EventManager, HAMMER_GESTURE_CONFIG, HammerGestureConfig, ɵBROWSER_SANITIZATION_PROVIDERS as BROWSER_SANITIZATION_PROVIDERS, ɵBrowserDomAdapter as BrowserDomAdapter, ɵBrowserGetTestability as BrowserGetTestability, ɵDomEventsPlugin as DomEventsPlugin, ɵDomRendererFactory2 as DomRendererFactory2, ɵDomSharedStylesHost as DomSharedStylesHost, ɵHammerGesturesPlugin as HammerGesturesPlugin, ɵKeyEventsPlugin as KeyEventsPlugin, ɵSharedStylesHost as SharedStylesHost, ɵgetDOM as getDOM} from '@angular/platform-browser'; import {ON_WEB_WORKER} from './web_workers/shared/api'; @@ -20,6 +20,7 @@ import {ServiceMessageBrokerFactory, ServiceMessageBrokerFactory_} from './web_w import {MessageBasedRenderer2} from './web_workers/ui/renderer'; + /** * Wrapper class that exposes the Worker * and underlying {@link MessageBus} for lower level message passing. @@ -52,32 +53,53 @@ export const WORKER_SCRIPT = new InjectionToken<string>('WebWorkerScript'); export const WORKER_UI_STARTABLE_MESSAGING_SERVICE = new InjectionToken<({start: () => void})[]>('WorkerRenderStartableMsgService'); -export const _WORKER_UI_PLATFORM_PROVIDERS: Provider[] = [ +export const _WORKER_UI_PLATFORM_PROVIDERS: StaticProvider[] = [ {provide: NgZone, useFactory: createNgZone, deps: []}, - MessageBasedRenderer2, + { + provide: MessageBasedRenderer2, + deps: [ServiceMessageBrokerFactory, MessageBus, Serializer, RenderStore, RendererFactory2] + }, {provide: WORKER_UI_STARTABLE_MESSAGING_SERVICE, useExisting: MessageBasedRenderer2, multi: true}, BROWSER_SANITIZATION_PROVIDERS, {provide: ErrorHandler, useFactory: _exceptionHandler, deps: []}, {provide: DOCUMENT, useFactory: _document, deps: []}, // TODO(jteplitz602): Investigate if we definitely need EVENT_MANAGER on the render thread // #5298 - {provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true}, - {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true}, - {provide: EVENT_MANAGER_PLUGINS, useClass: HammerGesturesPlugin, multi: true}, - {provide: HAMMER_GESTURE_CONFIG, useClass: HammerGestureConfig}, + { + provide: EVENT_MANAGER_PLUGINS, + useClass: DomEventsPlugin, + deps: [DOCUMENT, NgZone], + multi: true + }, + {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, deps: [DOCUMENT], multi: true}, + { + provide: EVENT_MANAGER_PLUGINS, + useClass: HammerGesturesPlugin, + deps: [DOCUMENT, HAMMER_GESTURE_CONFIG], + multi: true + }, + {provide: HAMMER_GESTURE_CONFIG, useClass: HammerGestureConfig, deps: []}, APP_ID_RANDOM_PROVIDER, - DomRendererFactory2, + {provide: DomRendererFactory2, deps: [EventManager, DomSharedStylesHost]}, {provide: RendererFactory2, useExisting: DomRendererFactory2}, {provide: SharedStylesHost, useExisting: DomSharedStylesHost}, - {provide: ServiceMessageBrokerFactory, useClass: ServiceMessageBrokerFactory_}, - {provide: ClientMessageBrokerFactory, useClass: ClientMessageBrokerFactory_}, - Serializer, + { + provide: ServiceMessageBrokerFactory, + useClass: ServiceMessageBrokerFactory_, + deps: [MessageBus, Serializer] + }, + { + provide: ClientMessageBrokerFactory, + useClass: ClientMessageBrokerFactory_, + deps: [MessageBus, Serializer] + }, + {provide: Serializer, deps: [RenderStore]}, {provide: ON_WEB_WORKER, useValue: false}, - RenderStore, - DomSharedStylesHost, - Testability, - EventManager, - WebWorkerInstance, + {provide: RenderStore, deps: []}, + {provide: DomSharedStylesHost, deps: [DOCUMENT]}, + {provide: Testability, deps: [NgZone]}, + {provide: EventManager, deps: [EVENT_MANAGER_PLUGINS, NgZone]}, + {provide: WebWorkerInstance, deps: []}, { provide: PLATFORM_INITIALIZER, useFactory: initWebWorkerRenderPlatform, diff --git a/packages/router/src/apply_redirects.ts b/packages/router/src/apply_redirects.ts index 8000df3368..0d8c386938 100644 --- a/packages/router/src/apply_redirects.ts +++ b/packages/router/src/apply_redirects.ts @@ -444,8 +444,11 @@ function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment if (!res) { return { - matched: false, consumedSegments: <any[]>[], lastChild: 0, positionalParamSegments: {}, - } + matched: false, + consumedSegments: <any[]>[], + lastChild: 0, + positionalParamSegments: {}, + }; } return { diff --git a/packages/router/src/directives/router_link.ts b/packages/router/src/directives/router_link.ts index 554fc2157c..87b805b305 100644 --- a/packages/router/src/directives/router_link.ts +++ b/packages/router/src/directives/router_link.ts @@ -70,7 +70,7 @@ import {UrlTree} from '../url_tree'; * * You can tell the directive to how to handle queryParams, available options are: * - 'merge' merge the queryParams into the current queryParams - * - 'preserve' prserve the current queryParams + * - 'preserve' preserve the current queryParams * - default / '' use the queryParams only * same options for {@link NavigationExtras#queryParamsHandling} * diff --git a/packages/router/src/events.ts b/packages/router/src/events.ts index 80564147d3..b1cc203dff 100644 --- a/packages/router/src/events.ts +++ b/packages/router/src/events.ts @@ -10,17 +10,66 @@ import {Route} from './config'; import {RouterStateSnapshot} from './router_state'; /** - * @whatItDoes Represents an event triggered when a navigation starts. + * @whatItDoes Base for events the Router goes through, as opposed to events tied to a specific + * Route. `RouterEvent`s will only be fired one time for any given navigation. * - * @stable + * Example: + * + * ``` + * class MyService { + * constructor(public router: Router, logger: Logger) { + * router.events.filter(e => e instanceof RouterEvent).subscribe(e => { + * logger.log(e.id, e.url); + * }); + * } + * } + * ``` + * + * @experimental */ -export class NavigationStart { +export class RouterEvent { constructor( /** @docsNotRequired */ public id: number, /** @docsNotRequired */ public url: string) {} +} +/** + * @whatItDoes Base for events tied to a specific `Route`, as opposed to events for the Router + * lifecycle. `RouteEvent`s may be fired multiple times during a single navigation and will + * always receive the `Route` they pertain to. + * + * Example: + * + * ``` + * class MyService { + * constructor(public router: Router, spinner: Spinner) { + * router.events.filter(e => e instanceof RouteEvent).subscribe(e => { + * if (e instanceof ChildActivationStart) { + * spinner.start(e.route); + * } else if (e instanceof ChildActivationEnd) { + * spinner.end(e.route); + * } + * }); + * } + * } + * ``` + * + * @experimental + */ +export class RouteEvent { + constructor( + /** @docsNotRequired */ + public route: Route) {} +} + +/** + * @whatItDoes Represents an event triggered when a navigation starts. + * + * @stable + */ +export class NavigationStart extends RouterEvent { /** @docsNotRequired */ toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; } } @@ -30,14 +79,16 @@ export class NavigationStart { * * @stable */ -export class NavigationEnd { +export class NavigationEnd extends RouterEvent { constructor( /** @docsNotRequired */ - public id: number, + id: number, /** @docsNotRequired */ - public url: string, + url: string, /** @docsNotRequired */ - public urlAfterRedirects: string) {} + public urlAfterRedirects: string) { + super(id, url); + } /** @docsNotRequired */ toString(): string { @@ -50,14 +101,16 @@ export class NavigationEnd { * * @stable */ -export class NavigationCancel { +export class NavigationCancel extends RouterEvent { constructor( /** @docsNotRequired */ - public id: number, + id: number, /** @docsNotRequired */ - public url: string, + url: string, /** @docsNotRequired */ - public reason: string) {} + public reason: string) { + super(id, url); + } /** @docsNotRequired */ toString(): string { return `NavigationCancel(id: ${this.id}, url: '${this.url}')`; } @@ -68,14 +121,16 @@ export class NavigationCancel { * * @stable */ -export class NavigationError { +export class NavigationError extends RouterEvent { constructor( /** @docsNotRequired */ - public id: number, + id: number, /** @docsNotRequired */ - public url: string, + url: string, /** @docsNotRequired */ - public error: any) {} + public error: any) { + super(id, url); + } /** @docsNotRequired */ toString(): string { @@ -88,16 +143,18 @@ export class NavigationError { * * @stable */ -export class RoutesRecognized { +export class RoutesRecognized extends RouterEvent { constructor( /** @docsNotRequired */ - public id: number, + id: number, /** @docsNotRequired */ - public url: string, + url: string, /** @docsNotRequired */ public urlAfterRedirects: string, /** @docsNotRequired */ - public state: RouterStateSnapshot) {} + public state: RouterStateSnapshot) { + super(id, url); + } /** @docsNotRequired */ toString(): string { @@ -105,43 +162,23 @@ export class RoutesRecognized { } } -/** - * @whatItDoes Represents an event triggered before lazy loading a route config. - * - * @experimental - */ -export class RouteConfigLoadStart { - constructor(public route: Route) {} - - toString(): string { return `RouteConfigLoadStart(path: ${this.route.path})`; } -} - -/** - * @whatItDoes Represents an event triggered when a route has been lazy loaded. - * - * @experimental - */ -export class RouteConfigLoadEnd { - constructor(public route: Route) {} - - toString(): string { return `RouteConfigLoadEnd(path: ${this.route.path})`; } -} - /** * @whatItDoes Represents the start of the Guard phase of routing. * * @experimental */ -export class GuardsCheckStart { +export class GuardsCheckStart extends RouterEvent { constructor( /** @docsNotRequired */ - public id: number, + id: number, /** @docsNotRequired */ - public url: string, + url: string, /** @docsNotRequired */ public urlAfterRedirects: string, /** @docsNotRequired */ - public state: RouterStateSnapshot) {} + public state: RouterStateSnapshot) { + super(id, url); + } toString(): string { return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; @@ -153,18 +190,20 @@ export class GuardsCheckStart { * * @experimental */ -export class GuardsCheckEnd { +export class GuardsCheckEnd extends RouterEvent { constructor( /** @docsNotRequired */ - public id: number, + id: number, /** @docsNotRequired */ - public url: string, + url: string, /** @docsNotRequired */ public urlAfterRedirects: string, /** @docsNotRequired */ public state: RouterStateSnapshot, /** @docsNotRequired */ - public shouldActivate: boolean) {} + public shouldActivate: boolean) { + super(id, url); + } toString(): string { return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`; @@ -179,16 +218,18 @@ export class GuardsCheckEnd { * * @experimental */ -export class ResolveStart { +export class ResolveStart extends RouterEvent { constructor( /** @docsNotRequired */ - public id: number, + id: number, /** @docsNotRequired */ - public url: string, + url: string, /** @docsNotRequired */ public urlAfterRedirects: string, /** @docsNotRequired */ - public state: RouterStateSnapshot) {} + public state: RouterStateSnapshot) { + super(id, url); + } toString(): string { return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; @@ -201,22 +242,62 @@ export class ResolveStart { * * @experimental */ -export class ResolveEnd { +export class ResolveEnd extends RouterEvent { constructor( /** @docsNotRequired */ - public id: number, + id: number, /** @docsNotRequired */ - public url: string, + url: string, /** @docsNotRequired */ public urlAfterRedirects: string, /** @docsNotRequired */ - public state: RouterStateSnapshot) {} + public state: RouterStateSnapshot) { + super(id, url); + } toString(): string { return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } +/** + * @whatItDoes Represents an event triggered before lazy loading a route config. + * + * @experimental + */ +export class RouteConfigLoadStart extends RouteEvent { + toString(): string { return `RouteConfigLoadStart(path: ${this.route.path})`; } +} + +/** + * @whatItDoes Represents an event triggered when a route has been lazy loaded. + * + * @experimental + */ +export class RouteConfigLoadEnd extends RouteEvent { + toString(): string { return `RouteConfigLoadEnd(path: ${this.route.path})`; } +} + +/** + * @whatItDoes Represents the start of end of the Resolve phase of routing. See note on + * {@link ChildActivationEnd} for use of this experimental API. + * + * @experimental + */ +export class ChildActivationStart extends RouteEvent { + toString(): string { return `ChildActivationStart(path: '${this.route.path}')`; } +} + +/** + * @whatItDoes Represents the start of end of the Resolve phase of routing. See note on + * {@link ChildActivationStart} for use of this experimental API. + * + * @experimental + */ +export class ChildActivationEnd extends RouteEvent { + toString(): string { return `ChildActivationEnd(path: '${this.route.path}')`; } +} + /** * @whatItDoes Represents a router event, allowing you to track the lifecycle of the router. * @@ -227,15 +308,15 @@ export class ResolveEnd { * - {@link RouteConfigLoadEnd}, * - {@link RoutesRecognized}, * - {@link GuardsCheckStart}, + * - {@link ChildActivationStart}, * - {@link GuardsCheckEnd}, * - {@link ResolveStart}, * - {@link ResolveEnd}, + * - {@link ChildActivationEnd} * - {@link NavigationEnd}, * - {@link NavigationCancel}, * - {@link NavigationError} * * @stable */ -export type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | - RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd | GuardsCheckStart | - GuardsCheckEnd | ResolveStart | ResolveEnd; +export type Event = RouterEvent | RouteEvent; diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 6d202fbc3d..75abe1211a 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -11,7 +11,7 @@ export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes, Ru export {RouterLink, RouterLinkWithHref} from './directives/router_link'; export {RouterLinkActive} from './directives/router_link_active'; export {RouterOutlet} from './directives/router_outlet'; -export {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; +export {ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteEvent, RoutesRecognized} from './events'; export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces'; export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy'; export {NavigationExtras, Router} from './router'; diff --git a/packages/router/src/pre_activation.ts b/packages/router/src/pre_activation.ts new file mode 100644 index 0000000000..e7d29466db --- /dev/null +++ b/packages/router/src/pre_activation.ts @@ -0,0 +1,347 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injector} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {from} from 'rxjs/observable/from'; +import {of } from 'rxjs/observable/of'; +import {concatMap} from 'rxjs/operator/concatMap'; +import {every} from 'rxjs/operator/every'; +import {first} from 'rxjs/operator/first'; +import {last} from 'rxjs/operator/last'; +import {map} from 'rxjs/operator/map'; +import {mergeMap} from 'rxjs/operator/mergeMap'; +import {reduce} from 'rxjs/operator/reduce'; + +import {LoadedRouterConfig, ResolveData, RunGuardsAndResolvers} from './config'; +import {ChildActivationStart, RouteEvent} from './events'; +import {ChildrenOutletContexts, OutletContext} from './router_outlet_context'; +import {ActivatedRouteSnapshot, RouterStateSnapshot, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state'; +import {andObservables, forEach, shallowEqual, wrapIntoObservable} from './utils/collection'; +import {TreeNode, nodeChildrenAsMap} from './utils/tree'; + +class CanActivate { + constructor(public path: ActivatedRouteSnapshot[]) {} + get route(): ActivatedRouteSnapshot { return this.path[this.path.length - 1]; } +} + +class CanDeactivate { + constructor(public component: Object|null, public route: ActivatedRouteSnapshot) {} +} + +/** + * This class bundles the actions involved in preactivation of a route. + */ +export class PreActivation { + private canActivateChecks: CanActivate[] = []; + private canDeactivateChecks: CanDeactivate[] = []; + + constructor( + private future: RouterStateSnapshot, private curr: RouterStateSnapshot, + private moduleInjector: Injector, private forwardEvent?: (evt: RouteEvent) => void) {} + + initalize(parentContexts: ChildrenOutletContexts): void { + const futureRoot = this.future._root; + const currRoot = this.curr ? this.curr._root : null; + this.setupChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]); + } + + checkGuards(): Observable<boolean> { + if (!this.isDeactivating() && !this.isActivating()) { + return of (true); + } + const canDeactivate$ = this.runCanDeactivateChecks(); + return mergeMap.call( + canDeactivate$, + (canDeactivate: boolean) => canDeactivate ? this.runCanActivateChecks() : of (false)); + } + + resolveData(): Observable<any> { + if (!this.isActivating()) return of (null); + const checks$ = from(this.canActivateChecks); + const runningChecks$ = + concatMap.call(checks$, (check: CanActivate) => this.runResolve(check.route)); + return reduce.call(runningChecks$, (_: any, __: any) => _); + } + + isDeactivating(): boolean { return this.canDeactivateChecks.length !== 0; } + + isActivating(): boolean { return this.canActivateChecks.length !== 0; } + + + /** + * Iterates over child routes and calls recursive `setupRouteGuards` to get `this` instance in + * proper state to run `checkGuards()` method. + */ + private setupChildRouteGuards( + futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>|null, + contexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[]): void { + const prevChildren = nodeChildrenAsMap(currNode); + + // Process the children of the future route + futureNode.children.forEach(c => { + this.setupRouteGuards( + c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value])); + delete prevChildren[c.value.outlet]; + }); + + // Process any children left from the current route (not active for the future route) + forEach( + prevChildren, (v: TreeNode<ActivatedRouteSnapshot>, k: string) => + this.deactivateRouteAndItsChildren(v, contexts !.getContext(k))); + } + + /** + * Iterates over child routes and calls recursive `setupRouteGuards` to get `this` instance in + * proper state to run `checkGuards()` method. + */ + private setupRouteGuards( + futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>, + parentContexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[]): void { + const future = futureNode.value; + const curr = currNode ? currNode.value : null; + const context = parentContexts ? parentContexts.getContext(futureNode.value.outlet) : null; + + // reusing the node + if (curr && future._routeConfig === curr._routeConfig) { + const shouldRunGuardsAndResolvers = this.shouldRunGuardsAndResolvers( + curr, future, future._routeConfig !.runGuardsAndResolvers); + if (shouldRunGuardsAndResolvers) { + this.canActivateChecks.push(new CanActivate(futurePath)); + } else { + // we need to set the data + future.data = curr.data; + future._resolvedData = curr._resolvedData; + } + + // If we have a component, we need to go through an outlet. + if (future.component) { + this.setupChildRouteGuards( + futureNode, currNode, context ? context.children : null, futurePath); + + // if we have a componentless route, we recurse but keep the same outlet map. + } else { + this.setupChildRouteGuards(futureNode, currNode, parentContexts, futurePath); + } + + if (shouldRunGuardsAndResolvers) { + const outlet = context !.outlet !; + this.canDeactivateChecks.push(new CanDeactivate(outlet.component, curr)); + } + } else { + if (curr) { + this.deactivateRouteAndItsChildren(currNode, context); + } + + this.canActivateChecks.push(new CanActivate(futurePath)); + // If we have a component, we need to go through an outlet. + if (future.component) { + this.setupChildRouteGuards(futureNode, null, context ? context.children : null, futurePath); + + // if we have a componentless route, we recurse but keep the same outlet map. + } else { + this.setupChildRouteGuards(futureNode, null, parentContexts, futurePath); + } + } + } + + private shouldRunGuardsAndResolvers( + curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot, + mode: RunGuardsAndResolvers|undefined): boolean { + switch (mode) { + case 'always': + return true; + + case 'paramsOrQueryParamsChange': + return !equalParamsAndUrlSegments(curr, future) || + !shallowEqual(curr.queryParams, future.queryParams); + + case 'paramsChange': + default: + return !equalParamsAndUrlSegments(curr, future); + } + } + + private deactivateRouteAndItsChildren( + route: TreeNode<ActivatedRouteSnapshot>, context: OutletContext|null): void { + const children = nodeChildrenAsMap(route); + const r = route.value; + + forEach(children, (node: TreeNode<ActivatedRouteSnapshot>, childName: string) => { + if (!r.component) { + this.deactivateRouteAndItsChildren(node, context); + } else if (context) { + this.deactivateRouteAndItsChildren(node, context.children.getContext(childName)); + } else { + this.deactivateRouteAndItsChildren(node, null); + } + }); + + if (!r.component) { + this.canDeactivateChecks.push(new CanDeactivate(null, r)); + } else if (context && context.outlet && context.outlet.isActivated) { + this.canDeactivateChecks.push(new CanDeactivate(context.outlet.component, r)); + } else { + this.canDeactivateChecks.push(new CanDeactivate(null, r)); + } + } + + private runCanDeactivateChecks(): Observable<boolean> { + const checks$ = from(this.canDeactivateChecks); + const runningChecks$ = mergeMap.call( + checks$, (check: CanDeactivate) => this.runCanDeactivate(check.component, check.route)); + return every.call(runningChecks$, (result: boolean) => result === true); + } + + private runCanActivateChecks(): Observable<boolean> { + const checks$ = from(this.canActivateChecks); + const runningChecks$ = concatMap.call( + checks$, (check: CanActivate) => andObservables(from([ + this.fireChildActivationStart(check.path), this.runCanActivateChild(check.path), + this.runCanActivate(check.route) + ]))); + return every.call(runningChecks$, (result: boolean) => result === true); + // this.fireChildActivationStart(check.path), + } + + /** + * This should fire off `ChildActivationStart` events for each route being activated at this + * level. + * In other words, if you're activating `a` and `b` below, `path` will contain the + * `ActivatedRouteSnapshot`s for both and we will fire `ChildActivationStart` for both. Always + * return + * `true` so checks continue to run. + */ + private fireChildActivationStart(path: ActivatedRouteSnapshot[]): Observable<boolean> { + if (!this.forwardEvent) return of (true); + const childActivations = path.slice(0, path.length - 1).reverse().filter(_ => _ !== null); + + return andObservables(map.call(from(childActivations), (snapshot: ActivatedRouteSnapshot) => { + if (this.forwardEvent && snapshot._routeConfig) { + this.forwardEvent(new ChildActivationStart(snapshot._routeConfig)); + } + return of (true); + })); + } + private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> { + const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; + if (!canActivate || canActivate.length === 0) return of (true); + const obs = map.call(from(canActivate), (c: any) => { + const guard = this.getToken(c, future); + let observable: Observable<boolean>; + if (guard.canActivate) { + observable = wrapIntoObservable(guard.canActivate(future, this.future)); + } else { + observable = wrapIntoObservable(guard(future, this.future)); + } + return first.call(observable); + }); + return andObservables(obs); + } + + private runCanActivateChild(path: ActivatedRouteSnapshot[]): Observable<boolean> { + const future = path[path.length - 1]; + + const canActivateChildGuards = path.slice(0, path.length - 1) + .reverse() + .map(p => this.extractCanActivateChild(p)) + .filter(_ => _ !== null); + + return andObservables(map.call(from(canActivateChildGuards), (d: any) => { + const obs = map.call(from(d.guards), (c: any) => { + const guard = this.getToken(c, d.node); + let observable: Observable<boolean>; + if (guard.canActivateChild) { + observable = wrapIntoObservable(guard.canActivateChild(future, this.future)); + } else { + observable = wrapIntoObservable(guard(future, this.future)); + } + return first.call(observable); + }); + return andObservables(obs); + })); + } + + private extractCanActivateChild(p: ActivatedRouteSnapshot): + {node: ActivatedRouteSnapshot, guards: any[]}|null { + const canActivateChild = p._routeConfig ? p._routeConfig.canActivateChild : null; + if (!canActivateChild || canActivateChild.length === 0) return null; + return {node: p, guards: canActivateChild}; + } + + private runCanDeactivate(component: Object|null, curr: ActivatedRouteSnapshot): + Observable<boolean> { + const canDeactivate = curr && curr._routeConfig ? curr._routeConfig.canDeactivate : null; + if (!canDeactivate || canDeactivate.length === 0) return of (true); + const canDeactivate$ = mergeMap.call(from(canDeactivate), (c: any) => { + const guard = this.getToken(c, curr); + let observable: Observable<boolean>; + if (guard.canDeactivate) { + observable = + wrapIntoObservable(guard.canDeactivate(component, curr, this.curr, this.future)); + } else { + observable = wrapIntoObservable(guard(component, curr, this.curr, this.future)); + } + return first.call(observable); + }); + return every.call(canDeactivate$, (result: any) => result === true); + } + + private runResolve(future: ActivatedRouteSnapshot): Observable<any> { + const resolve = future._resolve; + return map.call(this.resolveNode(resolve, future), (resolvedData: any): any => { + future._resolvedData = resolvedData; + future.data = {...future.data, ...inheritedParamsDataResolve(future).resolve}; + return null; + }); + } + + private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable<any> { + const keys = Object.keys(resolve); + if (keys.length === 0) { + return of ({}); + } + if (keys.length === 1) { + const key = keys[0]; + return map.call( + this.getResolver(resolve[key], future), (value: any) => { return {[key]: value}; }); + } + const data: {[k: string]: any} = {}; + const runningResolvers$ = mergeMap.call(from(keys), (key: string) => { + return map.call(this.getResolver(resolve[key], future), (value: any) => { + data[key] = value; + return value; + }); + }); + return map.call(last.call(runningResolvers$), () => data); + } + + private getResolver(injectionToken: any, future: ActivatedRouteSnapshot): Observable<any> { + const resolver = this.getToken(injectionToken, future); + return resolver.resolve ? wrapIntoObservable(resolver.resolve(future, this.future)) : + wrapIntoObservable(resolver(future, this.future)); + } + + private getToken(token: any, snapshot: ActivatedRouteSnapshot): any { + const config = closestLoadedConfig(snapshot); + const injector = config ? config.module.injector : this.moduleInjector; + return injector.get(token); + } +} + + +function closestLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig|null { + if (!snapshot) return null; + + for (let s = snapshot.parent; s; s = s.parent) { + const route = s._routeConfig; + if (route && route._loadedConfig) return route._loadedConfig; + } + + return null; +} diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index a5970a15a6..f9635c2e5b 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -12,31 +12,27 @@ import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; import {Subscription} from 'rxjs/Subscription'; -import {from} from 'rxjs/observable/from'; import {of } from 'rxjs/observable/of'; import {concatMap} from 'rxjs/operator/concatMap'; -import {every} from 'rxjs/operator/every'; -import {first} from 'rxjs/operator/first'; -import {last} from 'rxjs/operator/last'; import {map} from 'rxjs/operator/map'; import {mergeMap} from 'rxjs/operator/mergeMap'; -import {reduce} from 'rxjs/operator/reduce'; import {applyRedirects} from './apply_redirects'; -import {LoadedRouterConfig, QueryParamsHandling, ResolveData, Route, Routes, RunGuardsAndResolvers, validateConfig} from './config'; +import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, validateConfig} from './config'; import {createRouterState} from './create_router_state'; import {createUrlTree} from './create_url_tree'; -import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; +import {ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteEvent, RoutesRecognized} from './events'; +import {PreActivation} from './pre_activation'; import {recognize} from './recognize'; import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy'; import {RouterConfigLoader} from './router_config_loader'; -import {ChildrenOutletContexts, OutletContext} from './router_outlet_context'; -import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state'; +import {ChildrenOutletContexts} from './router_outlet_context'; +import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; import {Params, isNavigationCancelingError} from './shared'; import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy'; import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree'; -import {andObservables, forEach, shallowEqual, waitForMap, wrapIntoObservable} from './utils/collection'; -import {TreeNode} from './utils/tree'; +import {forEach} from './utils/collection'; +import {TreeNode, nodeChildrenAsMap} from './utils/tree'; declare let Zone: any; @@ -537,7 +533,7 @@ export class Router { let resolve: any = null; let reject: any = null; - const promise = new Promise((res, rej) => { + const promise = new Promise<boolean>((res, rej) => { resolve = res; reject = rej; }); @@ -626,18 +622,19 @@ export class Router { // run preactivation: guards and data resolvers let preActivation: PreActivation; - const preactivationTraverse$ = map.call( + const preactivationSetup$ = map.call( beforePreactivationDone$, ({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => { const moduleInjector = this.ngModule.injector; - preActivation = - new PreActivation(snapshot, this.currentRouterState.snapshot, moduleInjector); - preActivation.traverse(this.rootContexts); + preActivation = new PreActivation( + snapshot, this.currentRouterState.snapshot, moduleInjector, + (evt: RouteEvent) => this.triggerEvent(evt)); + preActivation.initalize(this.rootContexts); return {appliedUrl, snapshot}; }); const preactivationCheckGuards$ = mergeMap.call( - preactivationTraverse$, + preactivationSetup$, ({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => { if (this.navigationId !== id) return of (false); @@ -715,7 +712,8 @@ export class Router { } } - new ActivateRoutes(this.routeReuseStrategy, state, storedState) + new ActivateRoutes( + this.routeReuseStrategy, state, storedState, (evt: Event) => this.triggerEvent(evt)) .activate(this.rootContexts); navigationIsSuccessful = true; @@ -763,289 +761,10 @@ export class Router { } } - -class CanActivate { - constructor(public path: ActivatedRouteSnapshot[]) {} - get route(): ActivatedRouteSnapshot { return this.path[this.path.length - 1]; } -} - -class CanDeactivate { - constructor(public component: Object|null, public route: ActivatedRouteSnapshot) {} -} - -export class PreActivation { - private canActivateChecks: CanActivate[] = []; - private canDeactivateChecks: CanDeactivate[] = []; - - constructor( - private future: RouterStateSnapshot, private curr: RouterStateSnapshot, - private moduleInjector: Injector) {} - - traverse(parentContexts: ChildrenOutletContexts): void { - const futureRoot = this.future._root; - const currRoot = this.curr ? this.curr._root : null; - this.traverseChildRoutes(futureRoot, currRoot, parentContexts, [futureRoot.value]); - } - - // TODO(jasonaden): Refactor checkGuards and resolveData so they can collect the checks - // and guards before mapping into the observable. Likely remove the observable completely - // and make these pure functions so they are more predictable and don't rely on so much - // external state. - checkGuards(): Observable<boolean> { - if (!this.isDeactivating() && !this.isActivating()) { - return of (true); - } - const canDeactivate$ = this.runCanDeactivateChecks(); - return mergeMap.call( - canDeactivate$, - (canDeactivate: boolean) => canDeactivate ? this.runCanActivateChecks() : of (false)); - } - - resolveData(): Observable<any> { - if (!this.isActivating()) return of (null); - const checks$ = from(this.canActivateChecks); - const runningChecks$ = - concatMap.call(checks$, (check: CanActivate) => this.runResolve(check.route)); - return reduce.call(runningChecks$, (_: any, __: any) => _); - } - - isDeactivating(): boolean { return this.canDeactivateChecks.length !== 0; } - - isActivating(): boolean { return this.canActivateChecks.length !== 0; } - - private traverseChildRoutes( - futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>|null, - contexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[]): void { - const prevChildren = nodeChildrenAsMap(currNode); - - // Process the children of the future route - futureNode.children.forEach(c => { - this.traverseRoutes(c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value])); - delete prevChildren[c.value.outlet]; - }); - - // Process any children left from the current route (not active for the future route) - forEach( - prevChildren, (v: TreeNode<ActivatedRouteSnapshot>, k: string) => - this.deactivateRouteAndItsChildren(v, contexts !.getContext(k))); - } - - private traverseRoutes( - futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>, - parentContexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[]): void { - const future = futureNode.value; - const curr = currNode ? currNode.value : null; - const context = parentContexts ? parentContexts.getContext(futureNode.value.outlet) : null; - - // reusing the node - if (curr && future._routeConfig === curr._routeConfig) { - const shouldRunGuardsAndResolvers = this.shouldRunGuardsAndResolvers( - curr, future, future._routeConfig !.runGuardsAndResolvers); - if (shouldRunGuardsAndResolvers) { - this.canActivateChecks.push(new CanActivate(futurePath)); - } else { - // we need to set the data - future.data = curr.data; - future._resolvedData = curr._resolvedData; - } - - // If we have a component, we need to go through an outlet. - if (future.component) { - this.traverseChildRoutes( - futureNode, currNode, context ? context.children : null, futurePath); - - // if we have a componentless route, we recurse but keep the same outlet map. - } else { - this.traverseChildRoutes(futureNode, currNode, parentContexts, futurePath); - } - - if (shouldRunGuardsAndResolvers) { - const outlet = context !.outlet !; - this.canDeactivateChecks.push(new CanDeactivate(outlet.component, curr)); - } - } else { - if (curr) { - this.deactivateRouteAndItsChildren(currNode, context); - } - - this.canActivateChecks.push(new CanActivate(futurePath)); - // If we have a component, we need to go through an outlet. - if (future.component) { - this.traverseChildRoutes(futureNode, null, context ? context.children : null, futurePath); - - // if we have a componentless route, we recurse but keep the same outlet map. - } else { - this.traverseChildRoutes(futureNode, null, parentContexts, futurePath); - } - } - } - - private shouldRunGuardsAndResolvers( - curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot, - mode: RunGuardsAndResolvers|undefined): boolean { - switch (mode) { - case 'always': - return true; - - case 'paramsOrQueryParamsChange': - return !equalParamsAndUrlSegments(curr, future) || - !shallowEqual(curr.queryParams, future.queryParams); - - case 'paramsChange': - default: - return !equalParamsAndUrlSegments(curr, future); - } - } - - private deactivateRouteAndItsChildren( - route: TreeNode<ActivatedRouteSnapshot>, context: OutletContext|null): void { - const children = nodeChildrenAsMap(route); - const r = route.value; - - forEach(children, (node: TreeNode<ActivatedRouteSnapshot>, childName: string) => { - if (!r.component) { - this.deactivateRouteAndItsChildren(node, context); - } else if (context) { - this.deactivateRouteAndItsChildren(node, context.children.getContext(childName)); - } else { - this.deactivateRouteAndItsChildren(node, null); - } - }); - - if (!r.component) { - this.canDeactivateChecks.push(new CanDeactivate(null, r)); - } else if (context && context.outlet && context.outlet.isActivated) { - this.canDeactivateChecks.push(new CanDeactivate(context.outlet.component, r)); - } else { - this.canDeactivateChecks.push(new CanDeactivate(null, r)); - } - } - - private runCanDeactivateChecks(): Observable<boolean> { - const checks$ = from(this.canDeactivateChecks); - const runningChecks$ = mergeMap.call( - checks$, (check: CanDeactivate) => this.runCanDeactivate(check.component, check.route)); - return every.call(runningChecks$, (result: boolean) => result === true); - } - - private runCanActivateChecks(): Observable<boolean> { - const checks$ = from(this.canActivateChecks); - const runningChecks$ = concatMap.call( - checks$, (check: CanActivate) => andObservables(from( - [this.runCanActivateChild(check.path), this.runCanActivate(check.route)]))); - return every.call(runningChecks$, (result: boolean) => result === true); - } - - private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> { - const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; - if (!canActivate || canActivate.length === 0) return of (true); - const obs = map.call(from(canActivate), (c: any) => { - const guard = this.getToken(c, future); - let observable: Observable<boolean>; - if (guard.canActivate) { - observable = wrapIntoObservable(guard.canActivate(future, this.future)); - } else { - observable = wrapIntoObservable(guard(future, this.future)); - } - return first.call(observable); - }); - return andObservables(obs); - } - - private runCanActivateChild(path: ActivatedRouteSnapshot[]): Observable<boolean> { - const future = path[path.length - 1]; - - const canActivateChildGuards = path.slice(0, path.length - 1) - .reverse() - .map(p => this.extractCanActivateChild(p)) - .filter(_ => _ !== null); - - return andObservables(map.call(from(canActivateChildGuards), (d: any) => { - const obs = map.call(from(d.guards), (c: any) => { - const guard = this.getToken(c, d.node); - let observable: Observable<boolean>; - if (guard.canActivateChild) { - observable = wrapIntoObservable(guard.canActivateChild(future, this.future)); - } else { - observable = wrapIntoObservable(guard(future, this.future)); - } - return first.call(observable); - }); - return andObservables(obs); - })); - } - - private extractCanActivateChild(p: ActivatedRouteSnapshot): - {node: ActivatedRouteSnapshot, guards: any[]}|null { - const canActivateChild = p._routeConfig ? p._routeConfig.canActivateChild : null; - if (!canActivateChild || canActivateChild.length === 0) return null; - return {node: p, guards: canActivateChild}; - } - - private runCanDeactivate(component: Object|null, curr: ActivatedRouteSnapshot): - Observable<boolean> { - const canDeactivate = curr && curr._routeConfig ? curr._routeConfig.canDeactivate : null; - if (!canDeactivate || canDeactivate.length === 0) return of (true); - const canDeactivate$ = mergeMap.call(from(canDeactivate), (c: any) => { - const guard = this.getToken(c, curr); - let observable: Observable<boolean>; - if (guard.canDeactivate) { - observable = - wrapIntoObservable(guard.canDeactivate(component, curr, this.curr, this.future)); - } else { - observable = wrapIntoObservable(guard(component, curr, this.curr, this.future)); - } - return first.call(observable); - }); - return every.call(canDeactivate$, (result: any) => result === true); - } - - private runResolve(future: ActivatedRouteSnapshot): Observable<any> { - const resolve = future._resolve; - return map.call(this.resolveNode(resolve, future), (resolvedData: any): any => { - future._resolvedData = resolvedData; - future.data = {...future.data, ...inheritedParamsDataResolve(future).resolve}; - return null; - }); - } - - private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable<any> { - const keys = Object.keys(resolve); - if (keys.length === 0) { - return of ({}); - } - if (keys.length === 1) { - const key = keys[0]; - return map.call( - this.getResolver(resolve[key], future), (value: any) => { return {[key]: value}; }); - } - const data: {[k: string]: any} = {}; - const runningResolvers$ = mergeMap.call(from(keys), (key: string) => { - return map.call(this.getResolver(resolve[key], future), (value: any) => { - data[key] = value; - return value; - }); - }); - return map.call(last.call(runningResolvers$), () => data); - } - - private getResolver(injectionToken: any, future: ActivatedRouteSnapshot): Observable<any> { - const resolver = this.getToken(injectionToken, future); - return resolver.resolve ? wrapIntoObservable(resolver.resolve(future, this.future)) : - wrapIntoObservable(resolver(future, this.future)); - } - - private getToken(token: any, snapshot: ActivatedRouteSnapshot): any { - const config = closestLoadedConfig(snapshot); - const injector = config ? config.module.injector : this.moduleInjector; - return injector.get(token); - } -} - class ActivateRoutes { constructor( private routeReuseStrategy: RouteReuseStrategy, private futureState: RouterState, - private currState: RouterState) {} + private currState: RouterState, private forwardEvent: (evt: RouteEvent) => void) {} activate(parentContexts: ChildrenOutletContexts): void { const futureRoot = this.futureState._root; @@ -1128,7 +847,7 @@ class ActivateRoutes { const children: {[outletName: string]: any} = nodeChildrenAsMap(route); const contexts = route.value.component ? context.children : parentContexts; - forEach(children, (v: any, k: string) => {this.deactivateRouteAndItsChildren(v, contexts)}); + forEach(children, (v: any, k: string) => this.deactivateRouteAndItsChildren(v, contexts)); if (context.outlet) { // Destroy the component @@ -1145,6 +864,9 @@ class ActivateRoutes { const children: {[outlet: string]: any} = nodeChildrenAsMap(currNode); futureNode.children.forEach( c => { this.activateRoutes(c, children[c.value.outlet], contexts); }); + if (futureNode.children.length && futureNode.value.routeConfig) { + this.forwardEvent(new ChildActivationEnd(futureNode.value.routeConfig)); + } } private activateRoutes( @@ -1220,28 +942,6 @@ function parentLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfi return null; } -function closestLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig|null { - if (!snapshot) return null; - - for (let s = snapshot.parent; s; s = s.parent) { - const route = s._routeConfig; - if (route && route._loadedConfig) return route._loadedConfig; - } - - return null; -} - -// Return the list of T indexed by outlet name -function nodeChildrenAsMap<T extends{outlet: string}>(node: TreeNode<T>| null) { - const map: {[outlet: string]: TreeNode<T>} = {}; - - if (node) { - node.children.forEach(child => map[child.value.outlet] = child); - } - - return map; -} - function validateCommands(commands: string[]): void { for (let i = 0; i < commands.length; i++) { const cmd = commands[i]; diff --git a/packages/router/src/shared.ts b/packages/router/src/shared.ts index 56814d5edd..d8da4a68d2 100644 --- a/packages/router/src/shared.ts +++ b/packages/router/src/shared.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ - import {Route, UrlMatchResult} from './config'; import {UrlSegment, UrlSegmentGroup} from './url_tree'; diff --git a/packages/router/src/utils/collection.ts b/packages/router/src/utils/collection.ts index 36ffcca7d8..a72560ae9b 100644 --- a/packages/router/src/utils/collection.ts +++ b/packages/router/src/utils/collection.ts @@ -41,14 +41,23 @@ export function shallowEqual(a: {[x: string]: any}, b: {[x: string]: any}): bool return true; } +/** + * Flattens single-level nested arrays. + */ export function flatten<T>(arr: T[][]): T[] { return Array.prototype.concat.apply([], arr); } +/** + * Return the last element of an array. + */ export function last<T>(a: T[]): T|null { return a.length > 0 ? a[a.length - 1] : null; } +/** + * Verifys all booleans in an array are `true`. + */ export function and(bools: boolean[]): boolean { return !bools.some(v => !v); } @@ -64,7 +73,7 @@ export function forEach<K, V>(map: {[key: string]: V}, callback: (v: V, k: strin export function waitForMap<A, B>( obj: {[k: string]: A}, fn: (k: string, a: A) => Observable<B>): Observable<{[k: string]: B}> { if (Object.keys(obj).length === 0) { - return of ({}) + return of ({}); } const waitHead: Observable<B>[] = []; @@ -85,6 +94,10 @@ export function waitForMap<A, B>( return map.call(last$, () => res); } +/** + * ANDs Observables by merging all input observables, reducing to an Observable verifying all + * input Observables return `true`. + */ export function andObservables(observables: Observable<Observable<any>>): Observable<boolean> { const merged$ = mergeAll.call(observables); return every.call(merged$, (result: any) => result === true); @@ -103,5 +116,5 @@ export function wrapIntoObservable<T>(value: T | NgModuleFactory<T>| Promise<T>| return fromPromise(Promise.resolve(value)); } - return of (value); + return of (value as T); } diff --git a/packages/router/src/utils/tree.ts b/packages/router/src/utils/tree.ts index bcb6037e62..a811872a68 100644 --- a/packages/router/src/utils/tree.ts +++ b/packages/router/src/utils/tree.ts @@ -87,4 +87,15 @@ export class TreeNode<T> { constructor(public value: T, public children: TreeNode<T>[]) {} toString(): string { return `TreeNode(${this.value})`; } +} + +// Return the list of T indexed by outlet name +export function nodeChildrenAsMap<T extends{outlet: string}>(node: TreeNode<T>| null) { + const map: {[outlet: string]: TreeNode<T>} = {}; + + if (node) { + node.children.forEach(child => map[child.value.outlet] = child); + } + + return map; } \ No newline at end of file diff --git a/packages/router/test/config.spec.ts b/packages/router/test/config.spec.ts index ad643a8fae..9ef3037c69 100644 --- a/packages/router/test/config.spec.ts +++ b/packages/router/test/config.spec.ts @@ -45,7 +45,7 @@ describe('config', () => { expect(() => { validateConfig([ {path: 'a', component: ComponentA}, - [{path: 'b', component: ComponentB}, {path: 'c', component: ComponentC}] + [{path: 'b', component: ComponentB}, {path: 'c', component: ComponentC}] as any ]); }).toThrowError(`Invalid configuration of route '': Array cannot be specified`); }); diff --git a/packages/router/test/helpers.ts b/packages/router/test/helpers.ts new file mode 100644 index 0000000000..2ef3b7e141 --- /dev/null +++ b/packages/router/test/helpers.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Type} from '@angular/core'; + +import {Data, ResolveData, Route} from '../src/config'; +import {ActivatedRouteSnapshot} from '../src/router_state'; +import {PRIMARY_OUTLET, ParamMap, Params, convertToParamMap} from '../src/shared'; +import {UrlSegment, UrlSegmentGroup, UrlTree, equalSegments} from '../src/url_tree'; + +export class Logger { + logs: string[] = []; + add(thing: string) { this.logs.push(thing); } + empty() { this.logs.length = 0; } +} + +export function provideTokenLogger(token: string, returnValue = true) { + return { + provide: token, + useFactory: (logger: Logger) => () => (logger.add(token), returnValue), + deps: [Logger] + }; +}; + +export declare type ARSArgs = { + url?: UrlSegment[], + params?: Params, + queryParams?: Params, + fragment?: string, + data?: Data, + outlet?: string, + component: Type<any>| string | null, + routeConfig?: Route | null, + urlSegment?: UrlSegmentGroup, + lastPathIndex?: number, + resolve?: ResolveData +}; + +export function createActivatedRouteSnapshot(args: ARSArgs): ActivatedRouteSnapshot { + return new ActivatedRouteSnapshot( + args.url || <any>[], args.params || {}, args.queryParams || <any>null, + args.fragment || <any>null, args.data || <any>null, args.outlet || <any>null, + <any>args.component, args.routeConfig || <any>{}, args.urlSegment || <any>null, + args.lastPathIndex || -1, args.resolve || {}); +} diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index adeab68c68..5c42074f5e 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -11,7 +11,7 @@ import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactor import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '@angular/router'; +import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteEvent, RouteReuseStrategy, Router, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '@angular/router'; import {Observable} from 'rxjs/Observable'; import {Observer} from 'rxjs/Observer'; import {of } from 'rxjs/observable/of'; @@ -428,7 +428,7 @@ describe('Integration', () => { }]); const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); + router.events.forEach(e => e instanceof RouteEvent || recordedEvents.push(e)); router.navigateByUrl('/team/22/user/victor'); advance(fixture); @@ -986,7 +986,7 @@ describe('Integration', () => { [{path: 'simple', component: SimpleCmp, resolve: {error: 'resolveError'}}]); const recordedEvents: any[] = []; - router.events.subscribe(e => recordedEvents.push(e)); + router.events.subscribe(e => e instanceof RouteEvent || recordedEvents.push(e)); let e: any = null; router.navigateByUrl('/simple') !.catch(error => e = error); @@ -1070,7 +1070,7 @@ describe('Integration', () => { return map.call(of (null), () => { log.push('resolver2'); observer.next(null); - observer.complete() + observer.complete(); }); } }, @@ -1820,7 +1820,7 @@ describe('Integration', () => { function delayPromise(delay: number): Promise<boolean> { let resolve: (val: boolean) => void; - const promise = new Promise(res => resolve = res); + const promise = new Promise<boolean>(res => resolve = res); setTimeout(() => resolve(true), delay); return promise; } @@ -2388,9 +2388,11 @@ describe('Integration', () => { [RouteConfigLoadEnd], [RoutesRecognized, '/lazyTrue/loaded'], [GuardsCheckStart, '/lazyTrue/loaded'], + [ChildActivationStart], [GuardsCheckEnd, '/lazyTrue/loaded'], [ResolveStart, '/lazyTrue/loaded'], [ResolveEnd, '/lazyTrue/loaded'], + [ChildActivationEnd], [NavigationEnd, '/lazyTrue/loaded'], ]); }))); @@ -3342,7 +3344,7 @@ describe('Integration', () => { }]); const events: any[] = []; - router.events.subscribe(e => events.push(e)); + router.events.subscribe(e => e instanceof RouteEvent || events.push(e)); // supported URL router.navigateByUrl('/include/user/kate'); @@ -3406,7 +3408,7 @@ describe('Integration', () => { }]); const events: any[] = []; - router.events.subscribe(e => events.push(e)); + router.events.subscribe(e => e instanceof RouteEvent || events.push(e)); location.go('/include/user/kate(aux:excluded)'); advance(fixture); diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index 7ea6d6bac9..be1e469c6d 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -10,13 +10,16 @@ import {Location} from '@angular/common'; import {TestBed, inject} from '@angular/core/testing'; import {ResolveData} from '../src/config'; -import {PreActivation, Router} from '../src/router'; +import {PreActivation} from '../src/pre_activation'; +import {Router} from '../src/router'; import {ChildrenOutletContexts} from '../src/router_outlet_context'; import {ActivatedRouteSnapshot, RouterStateSnapshot, createEmptyStateSnapshot} from '../src/router_state'; import {DefaultUrlSerializer} from '../src/url_tree'; import {TreeNode} from '../src/utils/tree'; import {RouterTestingModule} from '../testing/src/router_testing_module'; +import {Logger, createActivatedRouteSnapshot, provideTokenLogger} from './helpers'; + describe('Router', () => { describe('resetRootComponentType', () => { class NewRootComponent {} @@ -56,51 +59,295 @@ describe('Router', () => { const serializer = new DefaultUrlSerializer(); const inj = {get: (token: any) => () => `${token}_value`}; let empty: RouterStateSnapshot; + let logger: Logger; - beforeEach(() => { empty = createEmptyStateSnapshot(serializer.parse('/'), null !); }); + const CA_CHILD = 'canActivate_child'; + const CA_CHILD_FALSE = 'canActivate_child_false'; + const CAC_CHILD = 'canActivateChild_child'; + const CAC_CHILD_FALSE = 'canActivateChild_child_false'; + const CA_GRANDCHILD = 'canActivate_grandchild'; + const CA_GRANDCHILD_FALSE = 'canActivate_grandchild_false'; + const CDA_CHILD = 'canDeactivate_child'; + const CDA_CHILD_FALSE = 'canDeactivate_child_false'; + const CDA_GRANDCHILD = 'canDeactivate_grandchild'; + const CDA_GRANDCHILD_FALSE = 'canDeactivate_grandchild_false'; - it('should resolve data', () => { - const r = {data: 'resolver'}; - const n = createActivatedRouteSnapshot('a', {resolve: r}); - const s = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n, [])])); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + Logger, provideTokenLogger(CA_CHILD), provideTokenLogger(CA_CHILD_FALSE, false), + provideTokenLogger(CAC_CHILD), provideTokenLogger(CAC_CHILD_FALSE, false), + provideTokenLogger(CA_GRANDCHILD), provideTokenLogger(CA_GRANDCHILD_FALSE, false), + provideTokenLogger(CDA_CHILD), provideTokenLogger(CDA_CHILD_FALSE, false), + provideTokenLogger(CDA_GRANDCHILD), provideTokenLogger(CDA_GRANDCHILD_FALSE, false) + ] + }); - checkResolveData(s, empty, inj, () => { - expect(s.root.firstChild !.data).toEqual({data: 'resolver_value'}); + }); + + beforeEach(inject([Logger], (_logger: Logger) => { + empty = createEmptyStateSnapshot(serializer.parse('/'), null !); + logger = _logger; + })); + + describe('guards', () => { + it('should run CanActivate checks', () => { + /** + * R --> R + * \ + * child (CA, CAC) + * \ + * grandchild (CA) + */ + + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: { + + canActivate: [CA_CHILD], + canActivateChild: [CAC_CHILD] + } + }); + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); + + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + + checkGuards(futureState, empty, TestBed, (result) => { + expect(result).toBe(true); + expect(logger.logs).toEqual([CA_CHILD, CAC_CHILD, CA_GRANDCHILD]); + }); + }); + + it('should not run grandchild guards if child fails', () => { + /** + * R --> R + * \ + * child (CA: x, CAC) + * \ + * grandchild (CA) + */ + + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {canActivate: [CA_CHILD_FALSE], canActivateChild: [CAC_CHILD]} + }); + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); + + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + + checkGuards(futureState, empty, TestBed, (result) => { + expect(result).toBe(false); + expect(logger.logs).toEqual([CA_CHILD_FALSE]); + }); + }); + + it('should not run grandchild guards if child canActivateChild fails', () => { + /** + * R --> R + * \ + * child (CA, CAC: x) + * \ + * grandchild (CA) + */ + + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD_FALSE]} + }); + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); + + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + + checkGuards(futureState, empty, TestBed, (result) => { + expect(result).toBe(false); + expect(logger.logs).toEqual([CA_CHILD, CAC_CHILD_FALSE]); + }); + }); + + it('should run deactivate guards before activate guards', () => { + /** + * R --> R + * / \ + * prev (CDA) child (CA) + * \ + * grandchild (CA) + */ + + const prevSnapshot = createActivatedRouteSnapshot( + {component: 'prev', routeConfig: {canDeactivate: [CDA_CHILD]}}); + + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]} + }); + + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); + + const currentState = new RouterStateSnapshot( + 'prev', new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])])); + + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + + checkGuards(futureState, currentState, TestBed, (result) => { + expect(logger.logs).toEqual([CDA_CHILD, CA_CHILD, CAC_CHILD, CA_GRANDCHILD]); + }); + }); + + it('should not run activate if deactivate fails guards', () => { + /** + * R --> R + * / \ + * prev (CDA) child (CA) + * \ + * grandchild (CA) + */ + + const prevSnapshot = createActivatedRouteSnapshot( + {component: 'prev', routeConfig: {canDeactivate: [CDA_CHILD_FALSE]}}); + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]} + }); + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); + + const currentState = new RouterStateSnapshot( + 'prev', new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])])); + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + + checkGuards(futureState, currentState, TestBed, (result) => { + expect(result).toBe(false); + expect(logger.logs).toEqual([CDA_CHILD_FALSE]); + }); + }); + it('should deactivate from bottom up, then activate top down', () => { + /** + * R --> R + * / \ + * prevChild (CDA) child (CA) + * / \ + * prevGrandchild(CDA) grandchild (CA) + */ + + const prevChildSnapshot = createActivatedRouteSnapshot( + {component: 'prev_child', routeConfig: {canDeactivate: [CDA_CHILD]}}); + const prevGrandchildSnapshot = createActivatedRouteSnapshot( + {component: 'prev_grandchild', routeConfig: {canDeactivate: [CDA_GRANDCHILD]}}); + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]} + }); + const grandchildSnapshot = createActivatedRouteSnapshot( + {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); + + const currentState = new RouterStateSnapshot( + 'prev', new TreeNode(empty.root, [ + new TreeNode(prevChildSnapshot, [new TreeNode(prevGrandchildSnapshot, [])]) + ])); + + const futureState = new RouterStateSnapshot( + 'url', + new TreeNode( + empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + + checkGuards(futureState, currentState, TestBed, (result) => { + expect(result).toBe(true); + expect(logger.logs).toEqual([ + CDA_GRANDCHILD, CDA_CHILD, CA_CHILD, CAC_CHILD, CA_GRANDCHILD + ]); + }); + + logger.empty(); + checkGuards(currentState, futureState, TestBed, (result) => { + expect(result).toBe(true); + expect(logger.logs).toEqual([]); + }); }); }); - it('should wait for the parent resolve to complete', () => { - const parentResolve = {data: 'resolver'}; - const childResolve = {}; + describe('resolve', () => { - const parent = createActivatedRouteSnapshot(null !, {resolve: parentResolve}); - const child = createActivatedRouteSnapshot('b', {resolve: childResolve}); + it('should resolve data', () => { + /** + * R --> R + * \ + * a + */ + const r = {data: 'resolver'}; + const n = createActivatedRouteSnapshot({component: 'a', resolve: r}); + const s = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n, [])])); - const s = new RouterStateSnapshot( - 'url', new TreeNode(empty.root, [new TreeNode(parent, [new TreeNode(child, [])])])); - - const inj = {get: (token: any) => () => Promise.resolve(`${token}_value`)}; - - checkResolveData(s, empty, inj, () => { - expect(s.root.firstChild !.firstChild !.data).toEqual({data: 'resolver_value'}); + checkResolveData(s, empty, inj, () => { + expect(s.root.firstChild !.data).toEqual({data: 'resolver_value'}); + }); }); - }); - it('should copy over data when creating a snapshot', () => { - const r1 = {data: 'resolver1'}; - const r2 = {data: 'resolver2'}; + it('should wait for the parent resolve to complete', () => { + /** + * R --> R + * \ + * null (resolve: parentResolve) + * \ + * b (resolve: childResolve) + */ + const parentResolve = {data: 'resolver'}; + const childResolve = {}; - const n1 = createActivatedRouteSnapshot('a', {resolve: r1}); - const s1 = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n1, [])])); - checkResolveData(s1, empty, inj, () => {}); + const parent = createActivatedRouteSnapshot({component: null !, resolve: parentResolve}); + const child = createActivatedRouteSnapshot({component: 'b', resolve: childResolve}); - const n21 = createActivatedRouteSnapshot('a', {resolve: r1}); - const n22 = createActivatedRouteSnapshot('b', {resolve: r2}); - const s2 = new RouterStateSnapshot( - 'url', new TreeNode(empty.root, [new TreeNode(n21, [new TreeNode(n22, [])])])); - checkResolveData(s2, s1, inj, () => { - expect(s2.root.firstChild !.data).toEqual({data: 'resolver1_value'}); - expect(s2.root.firstChild !.firstChild !.data).toEqual({data: 'resolver2_value'}); + const s = new RouterStateSnapshot( + 'url', new TreeNode(empty.root, [new TreeNode(parent, [new TreeNode(child, [])])])); + + const inj = {get: (token: any) => () => Promise.resolve(`${token}_value`)}; + + checkResolveData(s, empty, inj, () => { + expect(s.root.firstChild !.firstChild !.data).toEqual({data: 'resolver_value'}); + }); + }); + + it('should copy over data when creating a snapshot', () => { + /** + * R --> R --> R + * \ \ + * n1 (resolve: r1) n21 (resolve: r1) + * \ + * n22 (resolve: r2) + */ + const r1 = {data: 'resolver1'}; + const r2 = {data: 'resolver2'}; + + const n1 = createActivatedRouteSnapshot({component: 'a', resolve: r1}); + const s1 = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n1, [])])); + checkResolveData(s1, empty, inj, () => {}); + + const n21 = createActivatedRouteSnapshot({component: 'a', resolve: r1}); + const n22 = createActivatedRouteSnapshot({component: 'b', resolve: r2}); + const s2 = new RouterStateSnapshot( + 'url', new TreeNode(empty.root, [new TreeNode(n21, [new TreeNode(n22, [])])])); + checkResolveData(s2, s1, inj, () => { + expect(s2.root.firstChild !.data).toEqual({data: 'resolver1_value'}); + expect(s2.root.firstChild !.firstChild !.data).toEqual({data: 'resolver2_value'}); + }); }); }); }); @@ -109,12 +356,14 @@ describe('Router', () => { function checkResolveData( future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: any, check: any): void { const p = new PreActivation(future, curr, injector); - p.traverse(new ChildrenOutletContexts()); + p.initalize(new ChildrenOutletContexts()); p.resolveData().subscribe(check, (e) => { throw e; }); } -function createActivatedRouteSnapshot(cmp: string, extra: any = {}): ActivatedRouteSnapshot { - return new ActivatedRouteSnapshot( - <any>[], {}, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>{}, <any>null, -1, - extra.resolve); +function checkGuards( + future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: any, + check: (result: boolean) => void): void { + const p = new PreActivation(future, curr, injector); + p.initalize(new ChildrenOutletContexts()); + p.checkGuards().subscribe(check, (e) => { throw e; }); } diff --git a/packages/tsc-wrapped/src/collector.ts b/packages/tsc-wrapped/src/collector.ts index f6d117d200..3ad18800fc 100644 --- a/packages/tsc-wrapped/src/collector.ts +++ b/packages/tsc-wrapped/src/collector.ts @@ -26,7 +26,7 @@ const isStatic = (ts as any).ModifierFlags ? /** * A set of collector options to use when collecting metadata. */ -export class CollectorOptions { +export interface CollectorOptions { /** * Version of the metadata to collect. */ @@ -42,6 +42,11 @@ export class CollectorOptions { * Do not simplify invalid expressions. */ verboseInvalidExpression?: boolean; + + /** + * An expression substitution callback. + */ + substituteExpression?: (value: MetadataValue, node: ts.Node) => MetadataValue; } /** @@ -54,12 +59,25 @@ export class MetadataCollector { * Returns a JSON.stringify friendly form describing the decorators of the exported classes from * the source file that is expected to correspond to a module. */ - public getMetadata(sourceFile: ts.SourceFile, strict: boolean = false): ModuleMetadata|undefined { + public getMetadata( + sourceFile: ts.SourceFile, strict: boolean = false, + substituteExpression?: (value: MetadataValue, node: ts.Node) => MetadataValue): ModuleMetadata + |undefined { const locals = new Symbols(sourceFile); const nodeMap = new Map<MetadataValue|ClassMetadata|InterfaceMetadata|FunctionMetadata, ts.Node>(); - const evaluator = new Evaluator(locals, nodeMap, this.options); + const composedSubstituter = substituteExpression && this.options.substituteExpression ? + (value: MetadataValue, node: ts.Node) => + this.options.substituteExpression !(substituteExpression(value, node), node) : + substituteExpression; + const evaluatorOptions = substituteExpression ? + {...this.options, substituteExpression: composedSubstituter} : + this.options; let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined; + const evaluator = new Evaluator(locals, nodeMap, evaluatorOptions, (name, value) => { + if (!metadata) metadata = {}; + metadata[name] = value; + }); let exports: ModuleExportMetadata[]|undefined = undefined; function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { diff --git a/packages/tsc-wrapped/src/evaluator.ts b/packages/tsc-wrapped/src/evaluator.ts index b77f55c027..27e5afc9af 100644 --- a/packages/tsc-wrapped/src/evaluator.ts +++ b/packages/tsc-wrapped/src/evaluator.ts @@ -9,9 +9,10 @@ import * as ts from 'typescript'; import {CollectorOptions} from './collector'; -import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema'; +import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema'; import {Symbols} from './symbols'; + // In TypeScript 2.1 the spread element kind was renamed. const spreadElementSyntaxKind: ts.SyntaxKind = (ts.SyntaxKind as any).SpreadElement || (ts.SyntaxKind as any).SpreadElementExpression; @@ -104,7 +105,8 @@ export function errorSymbol( export class Evaluator { constructor( private symbols: Symbols, private nodeMap: Map<MetadataEntry, ts.Node>, - private options: CollectorOptions = {}) {} + private options: CollectorOptions = {}, + private recordExport?: (name: string, value: MetadataValue) => void) {} nameOf(node: ts.Node|undefined): string|MetadataError { if (node && node.kind == ts.SyntaxKind.Identifier) { @@ -232,7 +234,14 @@ export class Evaluator { const t = this; let error: MetadataError|undefined; - function recordEntry<T extends MetadataEntry>(entry: T, node: ts.Node): T { + function recordEntry(entry: MetadataValue, node: ts.Node): MetadataValue { + if (t.options.substituteExpression) { + const newEntry = t.options.substituteExpression(entry, node); + if (t.recordExport && newEntry != entry && isMetadataGlobalReferenceExpression(newEntry)) { + t.recordExport(newEntry.name, entry); + } + entry = newEntry; + } t.nodeMap.set(entry, node); return entry; } @@ -283,7 +292,7 @@ export class Evaluator { if (this.options.quotedNames && quoted.length) { obj['$quoted$'] = quoted; } - return obj; + return recordEntry(obj, node); case ts.SyntaxKind.ArrayLiteralExpression: let arr: MetadataValue[] = []; ts.forEachChild(node, child => { @@ -308,7 +317,7 @@ export class Evaluator { arr.push(value); }); if (error) return error; - return arr; + return recordEntry(arr, node); case spreadElementSyntaxKind: let spreadExpression = this.evaluateNode((node as any).expression); return recordEntry({__symbolic: 'spread', expression: spreadExpression}, node); diff --git a/packages/tsc-wrapped/test/collector.spec.ts b/packages/tsc-wrapped/test/collector_spec.ts similarity index 97% rename from packages/tsc-wrapped/test/collector.spec.ts rename to packages/tsc-wrapped/test/collector_spec.ts index b22f3d1f5b..7f95df423c 100644 --- a/packages/tsc-wrapped/test/collector.spec.ts +++ b/packages/tsc-wrapped/test/collector_spec.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {MetadataCollector} from '../src/collector'; -import {ClassMetadata, ConstructorMetadata, MetadataEntry, ModuleMetadata, isClassMetadata} from '../src/schema'; +import {ClassMetadata, ConstructorMetadata, MetadataEntry, ModuleMetadata, isClassMetadata, isMetadataGlobalReferenceExpression} from '../src/schema'; import {Directory, Host, expectValidSources} from './typescript.mocks'; @@ -939,6 +939,44 @@ describe('Collector', () => { }); }); + describe('substitutions', () => { + const lambdaTemp = 'lambdaTemp'; + + it('should be able to substitute a lambda', () => { + const source = createSource(` + const b = 1; + export const a = () => b; + `); + const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => { + if (node.kind === ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + }); + expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp}); + }); + + it('should compose substitution functions', () => { + const collector = new MetadataCollector({ + substituteExpression: (value, node) => isMetadataGlobalReferenceExpression(value) && + value.name == lambdaTemp ? + {__symbolic: 'reference', name: value.name + '2'} : + value + }); + const source = createSource(` + const b = 1; + export const a = () => b; + `); + const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => { + if (node.kind === ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + }); + expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp + '2'}); + }); + }); + function override(fileName: string, content: string) { host.overrideFile(fileName, content); host.addFile(fileName); diff --git a/packages/tsc-wrapped/test/evaluator.spec.ts b/packages/tsc-wrapped/test/evaluator_spec.ts similarity index 91% rename from packages/tsc-wrapped/test/evaluator.spec.ts rename to packages/tsc-wrapped/test/evaluator_spec.ts index 2205a27120..b8aec84a62 100644 --- a/packages/tsc-wrapped/test/evaluator.spec.ts +++ b/packages/tsc-wrapped/test/evaluator_spec.ts @@ -223,6 +223,45 @@ describe('Evaluator', () => { expect(evaluator.evaluateNode(expr.initializer !)) .toEqual({__symbolic: 'new', expression: {__symbolic: 'reference', name: 'f'}}); }); + + describe('with substitution', () => { + let evaluator: Evaluator; + const lambdaTemp = 'lambdaTemp'; + + beforeEach(() => { + evaluator = new Evaluator(symbols, new Map(), { + substituteExpression: (value, node) => { + if (node.kind == ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + } + }); + }); + + it('should be able to substitute a lambda with a reference', () => { + const source = sourceFileOf(` + var b = 1; + export var a = () => b; + `); + const expr = findVar(source, 'a'); + expect(evaluator.evaluateNode(expr !.initializer !)) + .toEqual({__symbolic: 'reference', name: lambdaTemp}); + }); + + it('should be able to substitute a lambda in an expression', () => { + const source = sourceFileOf(` + var b = 1; + export var a = [ + { provide: 'someValue': useFactory: () => b } + ]; + `); + const expr = findVar(source, 'a'); + expect(evaluator.evaluateNode(expr !.initializer !)).toEqual([ + {provide: 'someValue', useFactory: {__symbolic: 'reference', name: lambdaTemp}} + ]); + }); + }); }); function sourceFileOf(text: string): ts.SourceFile { diff --git a/packages/tsc-wrapped/test/main.spec.ts b/packages/tsc-wrapped/test/main_spec.ts similarity index 100% rename from packages/tsc-wrapped/test/main.spec.ts rename to packages/tsc-wrapped/test/main_spec.ts diff --git a/packages/tsc-wrapped/test/symbols.spec.ts b/packages/tsc-wrapped/test/symbols_spec.ts similarity index 100% rename from packages/tsc-wrapped/test/symbols.spec.ts rename to packages/tsc-wrapped/test/symbols_spec.ts diff --git a/packages/tsc-wrapped/test/tsc.spec.ts b/packages/tsc-wrapped/test/tsc_spec.ts similarity index 100% rename from packages/tsc-wrapped/test/tsc.spec.ts rename to packages/tsc-wrapped/test/tsc_spec.ts diff --git a/packages/tsc-wrapped/test/typescript.mocks.ts b/packages/tsc-wrapped/test/typescript.mocks.ts index fbd35bdd5f..aae1a1dd8a 100644 --- a/packages/tsc-wrapped/test/typescript.mocks.ts +++ b/packages/tsc-wrapped/test/typescript.mocks.ts @@ -1,3 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + import * as fs from 'fs'; import * as ts from 'typescript'; @@ -70,7 +78,7 @@ export class MockNode implements ts.Node { public kind: ts.SyntaxKind = ts.SyntaxKind.Identifier, public flags: ts.NodeFlags = 0, public pos: number = 0, public end: number = 0) {} getSourceFile(): ts.SourceFile { return null as any as ts.SourceFile; } - getChildCount(sourceFile?: ts.SourceFile): number { return 0 } + getChildCount(sourceFile?: ts.SourceFile): number { return 0; } getChildAt(index: number, sourceFile?: ts.SourceFile): ts.Node { return null as any as ts.Node; } getChildren(sourceFile?: ts.SourceFile): ts.Node[] { return []; } getStart(sourceFile?: ts.SourceFile): number { return 0; } @@ -90,12 +98,14 @@ export class MockNode implements ts.Node { export class MockIdentifier extends MockNode implements ts.Identifier { public text: string; + // tslint:disable public _primaryExpressionBrand: any; public _memberExpressionBrand: any; public _leftHandSideExpressionBrand: any; public _incrementExpressionBrand: any; public _unaryExpressionBrand: any; public _expressionBrand: any; + // tslint:enable constructor( public name: string, public kind: ts.SyntaxKind.Identifier = ts.SyntaxKind.Identifier, @@ -106,6 +116,7 @@ export class MockIdentifier extends MockNode implements ts.Identifier { } export class MockVariableDeclaration extends MockNode implements ts.VariableDeclaration { + // tslint:disable-next-line public _declarationBrand: any; constructor( @@ -130,7 +141,7 @@ export class MockSymbol implements ts.Symbol { getDeclarations(): ts.Declaration[] { return [this.node]; } getDocumentationComment(): ts.SymbolDisplayPart[] { return []; } // TODO(vicb): removed in TS 2.2 - getJsDocTags(): any[]{return []}; + getJsDocTags(): any[] { return []; } static of (name: string): MockSymbol { return new MockSymbol(name); } } @@ -139,6 +150,7 @@ export function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) { for (const diagnostic of diagnostics) { const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + // tslint:disable-next-line:no-console console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); } expect(diagnostics.length).toBe(0); @@ -159,7 +171,7 @@ export function allChildren<T>(node: ts.Node, cb: (node: ts.Node) => T): T { return result; } return allChildren(child, cb); - }) + }); } export function findClass(sourceFile: ts.SourceFile, name: string): ts.ClassDeclaration|undefined { diff --git a/packages/upgrade/src/common/downgrade_component_adapter.ts b/packages/upgrade/src/common/downgrade_component_adapter.ts index 06ead26b3b..e00605c3ab 100644 --- a/packages/upgrade/src/common/downgrade_component_adapter.ts +++ b/packages/upgrade/src/common/downgrade_component_adapter.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core'; +import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core'; import * as angular from './angular1'; import {PropertyBinding} from './component_info'; @@ -57,8 +57,8 @@ export class DowngradeComponentAdapter { } createComponent(projectableNodes: Node[][]) { - const childInjector = ReflectiveInjector.resolveAndCreate( - [{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector); + const childInjector = + Injector.create([{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector); this.componentRef = this.componentFactory.create(childInjector, projectableNodes, this.element[0]); diff --git a/packages/upgrade/src/dynamic/upgrade_adapter.ts b/packages/upgrade/src/dynamic/upgrade_adapter.ts index fe437cd1b4..f4e744007b 100644 --- a/packages/upgrade/src/dynamic/upgrade_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_adapter.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Compiler, CompilerOptions, Directive, Injector, NgModule, NgModuleRef, NgZone, Provider, Testability, Type} from '@angular/core'; +import {Compiler, CompilerOptions, Directive, Injector, NgModule, NgModuleRef, NgZone, StaticProvider, Testability, Type} from '@angular/core'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import * as angular from '../common/angular1'; @@ -110,7 +110,7 @@ export class UpgradeAdapter { * @internal */ private ng1ComponentsToBeUpgraded: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {}; - private upgradedProviders: Provider[] = []; + private upgradedProviders: StaticProvider[] = []; private ngZone: NgZone; private ng1Module: angular.IModule; private moduleRef: NgModuleRef<any>|null = null; diff --git a/packages/upgrade/src/static/angular1_providers.ts b/packages/upgrade/src/static/angular1_providers.ts index 36c8187cae..3481f96e8a 100644 --- a/packages/upgrade/src/static/angular1_providers.ts +++ b/packages/upgrade/src/static/angular1_providers.ts @@ -43,7 +43,7 @@ export const angular1Providers = [ // > Metadata collected contains an error that will be reported at runtime: // > Function calls are not supported. // > Consider replacing the function or lambda with a reference to an exported function - {provide: '$injector', useFactory: injectorFactory}, + {provide: '$injector', useFactory: injectorFactory, deps: []}, {provide: '$rootScope', useFactory: rootScopeFactory, deps: ['$injector']}, {provide: '$compile', useFactory: compileFactory, deps: ['$injector']}, {provide: '$parse', useFactory: parseFactory, deps: ['$injector']} diff --git a/packages/upgrade/src/static/downgrade_module.ts b/packages/upgrade/src/static/downgrade_module.ts index 8a82075384..dcd0d5fb79 100644 --- a/packages/upgrade/src/static/downgrade_module.ts +++ b/packages/upgrade/src/static/downgrade_module.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, NgModuleFactory, NgModuleRef, Provider} from '@angular/core'; +import {Injector, NgModuleFactory, NgModuleRef, StaticProvider} from '@angular/core'; import {platformBrowser} from '@angular/platform-browser'; import * as angular from '../common/angular1'; @@ -20,11 +20,11 @@ import {NgAdapterInjector} from './util'; /** @experimental */ export function downgradeModule<T>( moduleFactoryOrBootstrapFn: NgModuleFactory<T>| - ((extraProviders: Provider[]) => Promise<NgModuleRef<T>>)): string { + ((extraProviders: StaticProvider[]) => Promise<NgModuleRef<T>>)): string { const LAZY_MODULE_NAME = UPGRADE_MODULE_NAME + '.lazy'; const bootstrapFn = isFunction(moduleFactoryOrBootstrapFn) ? moduleFactoryOrBootstrapFn : - (extraProviders: Provider[]) => + (extraProviders: StaticProvider[]) => platformBrowser(extraProviders).bootstrapModuleFactory(moduleFactoryOrBootstrapFn); let injector: Injector; diff --git a/packages/upgrade/test/static/integration/downgrade_module_spec.ts b/packages/upgrade/test/static/integration/downgrade_module_spec.ts index 589d3b759e..4544e5c7b7 100644 --- a/packages/upgrade/test/static/integration/downgrade_module_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_module_spec.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Inject, Injector, Input, NgModule, NgZone, OnChanges, Provider, destroyPlatform} from '@angular/core'; +import {Component, Inject, Injector, Input, NgModule, NgZone, OnChanges, StaticProvider, destroyPlatform} from '@angular/core'; import {async, fakeAsync, tick} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import * as angular from '@angular/upgrade/src/common/angular1'; import {$ROOT_SCOPE, INJECTOR_KEY, LAZY_MODULE_REF} from '@angular/upgrade/src/common/constants'; import {LazyModuleRef} from '@angular/upgrade/src/common/util'; @@ -45,7 +46,7 @@ export function main() { ngDoBootstrap() {} } - const bootstrapFn = (extraProviders: Provider[]) => + const bootstrapFn = (extraProviders: StaticProvider[]) => platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const ng1Module = @@ -107,7 +108,7 @@ export function main() { ngDoBootstrap() {} } - const bootstrapFn = (extraProviders: Provider[]) => + const bootstrapFn = (extraProviders: StaticProvider[]) => platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const ng1Module = @@ -151,7 +152,7 @@ export function main() { ngDoBootstrap() {} } - const bootstrapFn = (extraProviders: Provider[]) => + const bootstrapFn = (extraProviders: StaticProvider[]) => platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const ng1Module = @@ -190,7 +191,7 @@ export function main() { ngDoBootstrap() {} } - const bootstrapFn = (extraProviders: Provider[]) => + const bootstrapFn = (extraProviders: StaticProvider[]) => platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const ng1Module = @@ -244,7 +245,7 @@ export function main() { ngDoBootstrap() {} } - const bootstrapFn = (extraProviders: Provider[]) => + const bootstrapFn = (extraProviders: StaticProvider[]) => platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const ng1Module = @@ -298,7 +299,8 @@ export function main() { ngDoBootstrap() {} } - const bootstrapFn = (extraProviders: Provider[]) => + const tickDelay = browserDetection.isIE ? 100 : 0; + const bootstrapFn = (extraProviders: StaticProvider[]) => platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const ng1Module = @@ -311,8 +313,8 @@ export function main() { const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService; $rootScope.$apply('showNg2 = true'); - tick(); // Wait for the module to be bootstrapped and `$evalAsync()` to propagate - // inputs. + tick(tickDelay); // Wait for the module to be bootstrapped and `$evalAsync()` to + // propagate inputs. const injector = ($injector.get(LAZY_MODULE_REF) as LazyModuleRef).injector !; const injectorGet = injector.get; @@ -327,7 +329,7 @@ export function main() { expect(element.textContent).toBe(''); $rootScope.$apply('showNg2 = true'); - tick(); // Wait for `$evalAsync()` to propagate inputs. + tick(tickDelay); // Wait for `$evalAsync()` to propagate inputs. expect(element.textContent).toBe('Count: 2 | In the zone: true'); $rootScope.$destroy(); @@ -353,7 +355,7 @@ export function main() { ngDoBootstrap() {} } - const bootstrapFn = (extraProviders: Provider[]) => + const bootstrapFn = (extraProviders: StaticProvider[]) => platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const ng1Module = diff --git a/packages/upgrade/test/static/integration/upgrade_component_spec.ts b/packages/upgrade/test/static/integration/upgrade_component_spec.ts index a2d31286a3..33f3a8906c 100644 --- a/packages/upgrade/test/static/integration/upgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/upgrade_component_spec.ts @@ -1028,7 +1028,10 @@ export function main() { // Define `ng1Component` const ng1ComponentA: angular.IComponent = {template: 'ng1A(<ng1-b></ng1-b>)'}; const ng1DirectiveB: angular.IDirective = { - compile: tElem => grandParentNodeName = tElem.parent !().parent !()[0].nodeName + compile: tElem => { + grandParentNodeName = tElem.parent !().parent !()[0].nodeName; + return {}; + } }; // Define `Ng1ComponentAFacade` diff --git a/scripts/ci/deploy.sh b/scripts/ci/deploy.sh index c5a85f576b..e88bac203a 100755 --- a/scripts/ci/deploy.sh +++ b/scripts/ci/deploy.sh @@ -28,6 +28,7 @@ fi case ${CI_MODE} in + e2e) # Don't deploy if this is a PR build if [[ ${TRAVIS_PULL_REQUEST} != "false" ]]; then @@ -39,34 +40,13 @@ case ${CI_MODE} in ${thisDir}/publish-build-artifacts.sh travisFoldEnd "deploy.packages" ;; + aio) - # Only deploy if this not a PR. PRs are deployed early in `build.sh`. - if [[ $TRAVIS_PULL_REQUEST == "false" ]]; then - - # Don't deploy if this build is not for master or the stable branch. - if [[ $TRAVIS_BRANCH != "master" ]] && [[ $TRAVIS_BRANCH != $STABLE_BRANCH ]]; then - echo "Skipping deploy because this build is not for master or the stable branch ($STABLE_BRANCH)." - exit 0 - fi - - travisFoldStart "deploy.aio" - ( - cd ${TRAVIS_BUILD_DIR}/aio - - if [[ $TRAVIS_BRANCH == $STABLE_BRANCH ]]; then - # This is upstream <stable-branch>: Deploy to production. - travisFoldStart "deploy.aio.production" - yarn deploy-production - travisFoldEnd "deploy.aio.production" - else - # This is upstream master: Deploy to staging. - travisFoldStart "deploy.aio.staging" - yarn deploy-staging - travisFoldEnd "deploy.aio.staging" - fi - ) - travisFoldEnd "deploy.aio" - - fi + travisFoldStart "deploy.aio" + ( + cd ${TRAVIS_BUILD_DIR}/aio + yarn deploy-production + ) + travisFoldEnd "deploy.aio" ;; esac diff --git a/scripts/ci/install.sh b/scripts/ci/install.sh index f335e28be9..45dce806e2 100755 --- a/scripts/ci/install.sh +++ b/scripts/ci/install.sh @@ -42,7 +42,7 @@ if [[ ${CI_MODE} != "aio" && ${CI_MODE} != 'docs_test' ]]; then fi -if [[ ${TRAVIS} && (${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e" || ${CI_MODE} == "docs_test") ]]; then +if [[ ${TRAVIS} && (${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e" || ${CI_MODE} == "aio_tools_test") ]]; then # Install version of yarn that we are locked against travisFoldStart "install-yarn" curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "${YARN_VERSION}" @@ -50,7 +50,7 @@ if [[ ${TRAVIS} && (${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} = fi -if [[ ${TRAVIS} && (${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e" || ${CI_MODE} == "docs_test") ]]; then +if [[ ${TRAVIS} && (${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e" || ${CI_MODE} == "aio_tools_test") ]]; then # angular.io: Install all yarn dependencies according to angular.io/yarn.lock travisFoldStart "yarn-install.aio" ( @@ -75,18 +75,11 @@ if [[ ${TRAVIS} && ${CI_MODE} == "bazel" ]]; then travisFoldEnd "bazel-install" fi -# Install Chromium -if [[ ${CI_MODE} == "js" || ${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e" ]]; then - travisFoldStart "install-chromium" - ( - # Start xvfb for local Chrome used for testing - if [[ ${TRAVIS} ]]; then - travisFoldStart "install-chromium.xvfb-start" - sh -e /etc/init.d/xvfb start - travisFoldEnd "install-chromium.xvfb-start" - fi - ) - travisFoldEnd "install-chromium" +# Start xvfb for local Chrome testing +if [[ ${TRAVIS} && (${CI_MODE} == "js" || ${CI_MODE} == "e2e" || ${CI_MODE} == "e2e_2" || ${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e") ]]; then + travisFoldStart "xvfb-start" + sh -e /etc/init.d/xvfb start + travisFoldEnd "xvfb-start" fi diff --git a/scripts/ci/test-docs.sh b/scripts/ci/test-aio-tools.sh similarity index 92% rename from scripts/ci/test-docs.sh rename to scripts/ci/test-aio-tools.sh index 74d8a90593..cf608c4312 100755 --- a/scripts/ci/test-docs.sh +++ b/scripts/ci/test-aio-tools.sh @@ -10,6 +10,6 @@ source ${thisDir}/_travis-fold.sh travisFoldStart "test.docs" ( cd ${PROJECT_ROOT}/aio - yarn docs-test + yarn tools-test ) travisFoldEnd "test.docs" diff --git a/scripts/ci/test.sh b/scripts/ci/test.sh index d8f08932b3..a72da1aca7 100755 --- a/scripts/ci/test.sh +++ b/scripts/ci/test.sh @@ -37,8 +37,8 @@ case ${CI_MODE} in browserstack_optional) ${thisDir}/test-browserstack.sh ;; - docs_test) - ${thisDir}/test-docs.sh + aio_tools_test) + ${thisDir}/test-aio-tools.sh ;; aio) ${thisDir}/test-aio.sh diff --git a/tools/gulp-tasks/lint.js b/tools/gulp-tasks/lint.js index cacf7e0d00..6a1496327b 100644 --- a/tools/gulp-tasks/lint.js +++ b/tools/gulp-tasks/lint.js @@ -9,9 +9,13 @@ module.exports = (gulp) => () => { // todo(vicb): add .js files when supported // see https://github.com/palantir/tslint/pull/1515 './modules/**/*.ts', + './packages/**/*.ts', './tools/**/*.ts', './*.ts', + // Ignore node_modules directories + '!**/node_modules/**', + // Ignore TypeScript mocks because it's not managed by us '!./tools/@angular/tsc-wrapped/test/typescript.mocks.ts', diff --git a/tools/public_api_guard/animations/animations.d.ts b/tools/public_api_guard/animations/animations.d.ts index d395e86ebf..45dd78afa3 100644 --- a/tools/public_api_guard/animations/animations.d.ts +++ b/tools/public_api_guard/animations/animations.d.ts @@ -151,6 +151,11 @@ export interface AnimationStaggerMetadata extends AnimationMetadata { /** @experimental */ export interface AnimationStateMetadata extends AnimationMetadata { name: string; + options?: { + params: { + [name: string]: any; + }; + }; styles: AnimationStyleMetadata; } @@ -221,7 +226,11 @@ export declare function sequence(steps: AnimationMetadata[], options?: Animation export declare function stagger(timings: string | number, animation: AnimationMetadata | AnimationMetadata[]): AnimationStaggerMetadata; /** @experimental */ -export declare function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata; +export declare function state(name: string, styles: AnimationStyleMetadata, options?: { + params: { + [name: string]: any; + }; +}): AnimationStateMetadata; /** @experimental */ export declare function style(tokens: '*' | { diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 609b804790..2487babe7c 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -209,7 +209,7 @@ export declare type CompilerOptions = { /** @deprecated */ useDebug?: boolean; useJit?: boolean; defaultEncapsulation?: ViewEncapsulation; - providers?: any[]; + providers?: StaticProvider[]; missingTranslation?: MissingTranslationStrategy; enableLegacyTemplate?: boolean; }; @@ -289,7 +289,7 @@ export interface ContentChildrenDecorator { export declare function createPlatform(injector: Injector): PlatformRef; /** @experimental */ -export declare function createPlatformFactory(parentPlatformFactory: ((extraProviders?: Provider[]) => PlatformRef) | null, name: string, providers?: Provider[]): (extraProviders?: Provider[]) => PlatformRef; +export declare function createPlatformFactory(parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef) | null, name: string, providers?: StaticProvider[]): (extraProviders?: StaticProvider[]) => PlatformRef; /** @stable */ export declare const CUSTOM_ELEMENTS_SCHEMA: SchemaMetadata; @@ -491,6 +491,7 @@ export declare abstract class Injector { /** @deprecated */ abstract get(token: any, notFoundValue?: any): any; static NULL: Injector; static THROW_IF_NOT_FOUND: Object; + static create(providers: StaticProvider[], parent?: Injector): Injector; } /** @stable */ @@ -536,7 +537,7 @@ export declare class IterableDiffers { constructor(factories: IterableDifferFactory[]); find(iterable: any): IterableDifferFactory; static create(factories: IterableDifferFactory[], parent?: IterableDiffers): IterableDiffers; - static extend(factories: IterableDifferFactory[]): Provider; + static extend(factories: IterableDifferFactory[]): StaticProvider; } /** @deprecated */ @@ -579,7 +580,7 @@ export declare class KeyValueDiffers { constructor(factories: KeyValueDifferFactory[]); find(kv: any): KeyValueDifferFactory; static create<S>(factories: KeyValueDifferFactory[], parent?: KeyValueDiffers): KeyValueDiffers; - static extend<S>(factories: KeyValueDifferFactory[]): Provider; + static extend<S>(factories: KeyValueDifferFactory[]): StaticProvider; } /** @experimental */ @@ -714,7 +715,7 @@ export declare const PLATFORM_ID: InjectionToken<Object>; export declare const PLATFORM_INITIALIZER: InjectionToken<(() => void)[]>; /** @experimental */ -export declare const platformCore: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformCore: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; /** @stable */ export declare abstract class PlatformRef { @@ -771,7 +772,7 @@ export declare abstract class ReflectiveInjector implements Injector { static resolveAndCreate(providers: Provider[], parent?: Injector): ReflectiveInjector; } -/** @experimental */ +/** @deprecated */ export declare class ReflectiveKey { readonly displayName: string; id: number; @@ -951,6 +952,9 @@ export interface SkipSelfDecorator { /** @deprecated */ export declare function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata; +/** @stable */ +export declare type StaticProvider = ValueProvider | ExistingProvider | StaticClassProvider | ConstructorProvider | FactoryProvider | any[]; + /** @deprecated */ export declare function style(tokens: { [key: string]: string | number; diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index fcd570bc0a..e82c9caf13 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -144,9 +144,6 @@ export declare class DefaultValueAccessor implements ControlValueAccessor { onChange: (_: any) => void; onTouched: () => void; constructor(_renderer: Renderer2, _elementRef: ElementRef, _compositionMode: boolean); - _compositionEnd(value: any): void; - _compositionStart(): void; - _handleInput(value: any): void; registerOnChange(fn: (_: any) => void): void; registerOnTouched(fn: () => void): void; setDisabledState(isDisabled: boolean): void; @@ -175,7 +172,7 @@ export interface Form { export declare class FormArray extends AbstractControl { controls: AbstractControl[]; readonly length: number; - constructor(controls: AbstractControl[], validator?: ValidatorFn | null, asyncValidator?: AsyncValidatorFn | null); + constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); at(index: number): AbstractControl; getRawValue(): any[]; insert(index: number, control: AbstractControl): void; @@ -222,7 +219,7 @@ export declare class FormBuilder { /** @stable */ export declare class FormControl extends AbstractControl { - constructor(formState?: any, validator?: ValidatorFn | ValidatorFn[] | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); patchValue(value: any, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -283,7 +280,7 @@ export declare class FormGroup extends AbstractControl { }; constructor(controls: { [key: string]: AbstractControl; - }, validator?: ValidatorFn | null, asyncValidator?: AsyncValidatorFn | null); + }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); addControl(name: string, control: AbstractControl): void; contains(controlName: string): boolean; getRawValue(): any; diff --git a/tools/public_api_guard/platform-browser-dynamic/platform-browser-dynamic.d.ts b/tools/public_api_guard/platform-browser-dynamic/platform-browser-dynamic.d.ts index 9dda5c4bc9..414918714b 100644 --- a/tools/public_api_guard/platform-browser-dynamic/platform-browser-dynamic.d.ts +++ b/tools/public_api_guard/platform-browser-dynamic/platform-browser-dynamic.d.ts @@ -1,5 +1,5 @@ /** @stable */ -export declare const platformBrowserDynamic: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformBrowserDynamic: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; /** @experimental */ export declare const RESOURCE_CACHE_PROVIDER: Provider[]; diff --git a/tools/public_api_guard/platform-browser-dynamic/testing.d.ts b/tools/public_api_guard/platform-browser-dynamic/testing.d.ts index ba6af4425c..86bfad1dfe 100644 --- a/tools/public_api_guard/platform-browser-dynamic/testing.d.ts +++ b/tools/public_api_guard/platform-browser-dynamic/testing.d.ts @@ -3,4 +3,4 @@ export declare class BrowserDynamicTestingModule { } /** @stable */ -export declare const platformBrowserDynamicTesting: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformBrowserDynamicTesting: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; diff --git a/tools/public_api_guard/platform-browser/platform-browser.d.ts b/tools/public_api_guard/platform-browser/platform-browser.d.ts index c109d8feaf..45d0d2ef66 100644 --- a/tools/public_api_guard/platform-browser/platform-browser.d.ts +++ b/tools/public_api_guard/platform-browser/platform-browser.d.ts @@ -90,7 +90,7 @@ export declare class NgProbeToken { } /** @stable */ -export declare const platformBrowser: (extraProviders?: Provider[]) => PlatformRef; +export declare const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef; /** @stable */ export interface SafeHtml extends SafeValue { diff --git a/tools/public_api_guard/platform-browser/testing.d.ts b/tools/public_api_guard/platform-browser/testing.d.ts index c37f0f4adf..102040b559 100644 --- a/tools/public_api_guard/platform-browser/testing.d.ts +++ b/tools/public_api_guard/platform-browser/testing.d.ts @@ -3,4 +3,4 @@ export declare class BrowserTestingModule { } /** @stable */ -export declare const platformBrowserTesting: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformBrowserTesting: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; diff --git a/tools/public_api_guard/platform-server/platform-server.d.ts b/tools/public_api_guard/platform-server/platform-server.d.ts index 5a7c35f860..4a0495b6da 100644 --- a/tools/public_api_guard/platform-server/platform-server.d.ts +++ b/tools/public_api_guard/platform-server/platform-server.d.ts @@ -8,10 +8,10 @@ export interface PlatformConfig { } /** @experimental */ -export declare const platformDynamicServer: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformDynamicServer: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; /** @experimental */ -export declare const platformServer: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformServer: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; /** @experimental */ export declare class PlatformState { @@ -24,14 +24,14 @@ export declare class PlatformState { export declare function renderModule<T>(module: Type<T>, options: { document?: string; url?: string; - extraProviders?: Provider[]; + extraProviders?: StaticProvider[]; }): Promise<string>; /** @experimental */ export declare function renderModuleFactory<T>(moduleFactory: NgModuleFactory<T>, options: { document?: string; url?: string; - extraProviders?: Provider[]; + extraProviders?: StaticProvider[]; }): Promise<string>; /** @experimental */ diff --git a/tools/public_api_guard/platform-server/testing.d.ts b/tools/public_api_guard/platform-server/testing.d.ts index 56b00cdb39..faafa085ea 100644 --- a/tools/public_api_guard/platform-server/testing.d.ts +++ b/tools/public_api_guard/platform-server/testing.d.ts @@ -1,5 +1,5 @@ /** @experimental */ -export declare const platformServerTesting: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformServerTesting: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; /** @experimental */ export declare class ServerTestingModule { diff --git a/tools/public_api_guard/platform-webworker-dynamic/platform-webworker-dynamic.d.ts b/tools/public_api_guard/platform-webworker-dynamic/platform-webworker-dynamic.d.ts index 763602dc79..a51be8b680 100644 --- a/tools/public_api_guard/platform-webworker-dynamic/platform-webworker-dynamic.d.ts +++ b/tools/public_api_guard/platform-webworker-dynamic/platform-webworker-dynamic.d.ts @@ -1,5 +1,5 @@ /** @experimental */ -export declare const platformWorkerAppDynamic: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformWorkerAppDynamic: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; /** @stable */ export declare const VERSION: Version; diff --git a/tools/public_api_guard/platform-webworker/platform-webworker.d.ts b/tools/public_api_guard/platform-webworker/platform-webworker.d.ts index 9516b81312..47f56568c5 100644 --- a/tools/public_api_guard/platform-webworker/platform-webworker.d.ts +++ b/tools/public_api_guard/platform-webworker/platform-webworker.d.ts @@ -1,5 +1,5 @@ /** @experimental */ -export declare function bootstrapWorkerUi(workerScriptUri: string, customProviders?: Provider[]): Promise<PlatformRef>; +export declare function bootstrapWorkerUi(workerScriptUri: string, customProviders?: StaticProvider[]): Promise<PlatformRef>; /** @experimental */ export declare abstract class ClientMessageBroker { @@ -41,10 +41,10 @@ export interface MessageBusSource { } /** @experimental */ -export declare const platformWorkerApp: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformWorkerApp: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; /** @experimental */ -export declare const platformWorkerUi: (extraProviders?: Provider[] | undefined) => PlatformRef; +export declare const platformWorkerUi: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; /** @experimental */ export declare const PRIMITIVE: SerializerTypes; @@ -100,7 +100,7 @@ export declare const WORKER_APP_LOCATION_PROVIDERS: ({ })[]; /** @experimental */ -export declare const WORKER_UI_LOCATION_PROVIDERS: Provider[]; +export declare const WORKER_UI_LOCATION_PROVIDERS: StaticProvider[]; /** @experimental */ export declare class WorkerAppModule { diff --git a/tools/public_api_guard/router/router.d.ts b/tools/public_api_guard/router/router.d.ts index d234e646c9..471c6e849e 100644 --- a/tools/public_api_guard/router/router.d.ts +++ b/tools/public_api_guard/router/router.d.ts @@ -59,6 +59,16 @@ export interface CanLoad { canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean; } +/** @experimental */ +export declare class ChildActivationEnd extends RouteEvent { + toString(): string; +} + +/** @experimental */ +export declare class ChildActivationStart extends RouteEvent { + toString(): string; +} + /** @stable */ export declare class ChildrenOutletContexts { getContext(childName: string): OutletContext | null; @@ -87,7 +97,7 @@ export declare class DefaultUrlSerializer implements UrlSerializer { export declare type DetachedRouteHandle = {}; /** @stable */ -export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd | GuardsCheckStart | GuardsCheckEnd | ResolveStart | ResolveEnd; +export declare type Event = RouterEvent | RouteEvent; /** @stable */ export interface ExtraOptions { @@ -99,11 +109,9 @@ export interface ExtraOptions { } /** @experimental */ -export declare class GuardsCheckEnd { - id: number; +export declare class GuardsCheckEnd extends RouterEvent { shouldActivate: boolean; state: RouterStateSnapshot; - url: string; urlAfterRedirects: string; constructor( id: number, @@ -115,10 +123,8 @@ export declare class GuardsCheckEnd { } /** @experimental */ -export declare class GuardsCheckStart { - id: number; +export declare class GuardsCheckStart extends RouterEvent { state: RouterStateSnapshot; - url: string; urlAfterRedirects: string; constructor( id: number, @@ -135,10 +141,8 @@ export declare type LoadChildren = string | LoadChildrenCallback; export declare type LoadChildrenCallback = () => Type<any> | NgModuleFactory<any> | Promise<Type<any>> | Observable<Type<any>>; /** @stable */ -export declare class NavigationCancel { - id: number; +export declare class NavigationCancel extends RouterEvent { reason: string; - url: string; constructor( id: number, url: string, @@ -147,9 +151,7 @@ export declare class NavigationCancel { } /** @stable */ -export declare class NavigationEnd { - id: number; - url: string; +export declare class NavigationEnd extends RouterEvent { urlAfterRedirects: string; constructor( id: number, @@ -159,10 +161,8 @@ export declare class NavigationEnd { } /** @stable */ -export declare class NavigationError { +export declare class NavigationError extends RouterEvent { error: any; - id: number; - url: string; constructor( id: number, url: string, @@ -183,12 +183,7 @@ export interface NavigationExtras { } /** @stable */ -export declare class NavigationStart { - id: number; - url: string; - constructor( - id: number, - url: string); +export declare class NavigationStart extends RouterEvent { toString(): string; } @@ -246,10 +241,8 @@ export declare type ResolveData = { }; /** @experimental */ -export declare class ResolveEnd { - id: number; +export declare class ResolveEnd extends RouterEvent { state: RouterStateSnapshot; - url: string; urlAfterRedirects: string; constructor( id: number, @@ -260,10 +253,8 @@ export declare class ResolveEnd { } /** @experimental */ -export declare class ResolveStart { - id: number; +export declare class ResolveStart extends RouterEvent { state: RouterStateSnapshot; - url: string; urlAfterRedirects: string; constructor( id: number, @@ -293,19 +284,22 @@ export interface Route { } /** @experimental */ -export declare class RouteConfigLoadEnd { - route: Route; - constructor(route: Route); +export declare class RouteConfigLoadEnd extends RouteEvent { toString(): string; } /** @experimental */ -export declare class RouteConfigLoadStart { - route: Route; - constructor(route: Route); +export declare class RouteConfigLoadStart extends RouteEvent { toString(): string; } +/** @experimental */ +export declare class RouteEvent { + route: Route; + constructor( + route: Route); +} + /** @stable */ export declare class Router { config: Routes; @@ -453,10 +447,8 @@ export declare type Routes = Route[]; export declare const ROUTES: InjectionToken<Route[][]>; /** @stable */ -export declare class RoutesRecognized { - id: number; +export declare class RoutesRecognized extends RouterEvent { state: RouterStateSnapshot; - url: string; urlAfterRedirects: string; constructor( id: number, diff --git a/tools/public_api_guard/upgrade/static.d.ts b/tools/public_api_guard/upgrade/static.d.ts index 75b541cad7..8af9735a03 100644 --- a/tools/public_api_guard/upgrade/static.d.ts +++ b/tools/public_api_guard/upgrade/static.d.ts @@ -11,7 +11,7 @@ export declare function downgradeComponent(info: { export declare function downgradeInjectable(token: any): Function; /** @experimental */ -export declare function downgradeModule<T>(moduleFactoryOrBootstrapFn: NgModuleFactory<T> | ((extraProviders: Provider[]) => Promise<NgModuleRef<T>>)): string; +export declare function downgradeModule<T>(moduleFactoryOrBootstrapFn: NgModuleFactory<T> | ((extraProviders: StaticProvider[]) => Promise<NgModuleRef<T>>)): string; /** @stable */ export declare function getAngularLib(): any;