Merge branch 'master' into aio

# Conflicts:
#	aio/content/guide/form-validation.md
#	aio/content/guide/i18n.md
#	aio/content/guide/reactive-forms.md
#	aio/content/marketing/index.html
#	aio/content/navigation.json
#	aio/src/environments/environment.stable.ts
This commit is contained in:
Zhicheng Wang 2017-08-08 13:10:17 +08:00
commit a6df16f891
270 changed files with 6748 additions and 3230 deletions

View File

@ -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

View File

@ -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)

View File

@ -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"
}
}
],

View File

@ -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');
}

View File

@ -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 { }

View File

@ -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 { }

View File

@ -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>

View File

@ -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

View File

@ -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 { }

View File

@ -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

View File

@ -1,9 +0,0 @@
// #docregion
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}

View File

@ -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 { }

View File

@ -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); }
}

View File

@ -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>

View File

@ -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]};
}

View File

@ -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 { }

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -1,3 +1,4 @@
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}

File diff suppressed because it is too large Load Diff

View File

@ -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源码中**不需要`@@`前缀**。

View File

@ -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"
},

View File

@ -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>移动端 &amp; 桌面端</div>
<div class="hero-headline no-toc">一套框架,多种平台<br>移动端 &amp; 桌面端</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>

View File

@ -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 官方博客"
}

View File

@ -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"},

View File

@ -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
});
};

View File

@ -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",

View File

@ -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

View File

@ -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"
)

View File

@ -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}}"

View File

@ -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>

View File

@ -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) {

View File

@ -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

View File

@ -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,

View File

@ -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;
}

View File

@ -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('') }
]);
}

View File

@ -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) {}
};

View File

@ -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');
}

View File

@ -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'
};

View File

@ -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'
};

View File

@ -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'
};

View File

@ -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
};

View File

@ -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'
};

View File

@ -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.

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -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;
}
}

View File

@ -29,3 +29,4 @@
@import 'subsection';
@import 'toc';
@import 'select-menu';
@import 'deploy-theme';

View File

@ -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

View File

@ -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'

View File

@ -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);
}

View File

@ -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 },

View File

@ -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,

View File

@ -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
})

View File

@ -37,5 +37,5 @@ function getText(h1) {
(node.properties.ariaHidden === 'true' || node.properties['aria-hidden'] === 'true')
));
return toString(cleaned);
}
return cleaned ? toString(cleaned) : '';
}

View File

@ -70,4 +70,14 @@ describe('h1Checker postprocessor', () => {
processor.$process([doc]);
expect(doc.vFile.title).toEqual('What is Angular?');
});
});
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();
});
});

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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,

View File

@ -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
```

View File

@ -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",

View File

@ -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,

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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(

View File

@ -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;
}

View File

@ -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();

View File

@ -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/);
});
});

View File

@ -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;
}

View File

@ -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};
}
/**

View File

@ -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';

View File

@ -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;
});

View File

@ -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},

View File

@ -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

View File

@ -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,

View File

@ -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}

View File

@ -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,

View File

@ -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'}
];

View File

@ -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(); }

View File

@ -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;

View File

@ -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);');
}

View File

@ -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(); }

View File

@ -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(); }

View File

@ -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)
])

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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',

View File

@ -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)
])

View File

@ -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();
},

View File

@ -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',

View File

@ -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}

View File

@ -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);
}

View File

@ -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)

View File

@ -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)

View File

@ -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)}
])

View File

@ -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;
}
/**

Some files were not shown because too many files have changed in this diff Show More