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:
commit
a6df16f891
@ -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
|
||||
|
27
CHANGELOG.md
27
CHANGELOG.md
@ -1,3 +1,30 @@
|
||||
<a name="5.0.0-beta.2"></a>
|
||||
# [5.0.0-beta.2](https://github.com/angular/angular/compare/5.0.0-beta.1...5.0.0-beta.2) (2017-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** do not consider arguments when determining recursion ([e64b54b](https://github.com/angular/angular/commit/e64b54b))
|
||||
* **compiler:** fix for element needing implicit parent placed in top-level ng-container ([381471d](https://github.com/angular/angular/commit/381471d)), closes [#18314](https://github.com/angular/angular/issues/18314)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **forms:** add options arg to abstract controls ([ebef5e6](https://github.com/angular/angular/commit/ebef5e6))
|
||||
* **router:** add events tracking activation of individual routes ([49cd851](https://github.com/angular/angular/commit/49cd851))
|
||||
|
||||
|
||||
|
||||
<a name="4.3.3"></a>
|
||||
## [4.3.3](https://github.com/angular/angular/compare/4.3.2...4.3.3) (2017-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** fix for element needing implicit parent placed in top-level ng-container ([f5cbc2e](https://github.com/angular/angular/commit/f5cbc2e)), closes [#18314](https://github.com/angular/angular/issues/18314)
|
||||
|
||||
|
||||
|
||||
<a name="5.0.0-beta.1"></a>
|
||||
# [5.0.0-beta.1](https://github.com/angular/angular/compare/5.0.0-beta.0...5.0.0-beta.1) (2017-07-27)
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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 { }
|
||||
|
@ -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 { }
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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 { }
|
@ -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
|
||||
|
@ -1,9 +0,0 @@
|
||||
// #docregion
|
||||
export class Hero {
|
||||
constructor(
|
||||
public id: number,
|
||||
public name: string,
|
||||
public power: string,
|
||||
public alterEgo?: string
|
||||
) { }
|
||||
}
|
@ -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 { }
|
@ -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); }
|
||||
}
|
@ -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>
|
@ -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]};
|
||||
|
||||
}
|
@ -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 { }
|
@ -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>
|
@ -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
|
@ -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>
|
@ -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
|
@ -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
@ -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源码中**不需要`@@`前缀**。
|
||||
|
@ -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"
|
||||
},
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
<!--FULL HEADER BLOCK-->
|
||||
<!-- FULL HEADER BLOCK -->
|
||||
<header>
|
||||
|
||||
<!--BACKGROUND IMAGE-->
|
||||
<!-- BACKGROUND IMAGE -->
|
||||
<div class="background-sky hero"></div>
|
||||
|
||||
<!--INTRO SECTION -->
|
||||
<!-- INTRO SECTION -->
|
||||
<section id="intro">
|
||||
|
||||
<!-- LOGO -->
|
||||
@ -12,31 +12,32 @@
|
||||
<img src="assets/images/logos/angular/angular.svg"/>
|
||||
</div>
|
||||
|
||||
<!-- CONTAINER -->
|
||||
<!-- CONTAINER -->
|
||||
<div class="homepage-container">
|
||||
<!-- container content starts -->
|
||||
|
||||
<div class="hero-headline no-toc">一套框架,多种平台。<br>移动端 & 桌面端</div>
|
||||
<div class="hero-headline no-toc">一套框架,多种平台<br>移动端 & 桌面端</div>
|
||||
<a class="button hero-cta" href="guide/quickstart">快速上手</a>
|
||||
</div><!-- CONTAINER END -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</header>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<article>
|
||||
|
||||
<h1 class="no-toc" style="display: none"></h1>
|
||||
|
||||
<div class="home-rows">
|
||||
|
||||
<!--Announcement Bar-->
|
||||
<!-- Announcement Bar -->
|
||||
<div class="homepage-container">
|
||||
<div class="announcement-bar">
|
||||
<img src="generated/images/marketing/home/angular-mix.png" height="40" width="151">
|
||||
<p>2017年十月 加入我们的最新活动</p>
|
||||
<a class="button" href="https://angularmix.com/">了解更多</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 1-->
|
||||
<!-- Group 1 -->
|
||||
<div layout="row" layout-xs="column" class="home-row homepage-container">
|
||||
<div class="promo-img-container promo-1">
|
||||
<div>
|
||||
@ -52,7 +53,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<!-- Group 2-->
|
||||
|
||||
<!-- Group 2 -->
|
||||
<div layout="row" layout-xs="column" class="home-row">
|
||||
<div class="text-container">
|
||||
<div class="text-block">
|
||||
@ -70,7 +72,7 @@
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Group 3-->
|
||||
<!-- Group 3 -->
|
||||
<div layout="row" layout-xs="column" class="home-row">
|
||||
<div class="promo-img-container promo-3">
|
||||
<div><img src="generated/images/marketing/home/joyful-development.svg" alt="IDE example"></div>
|
||||
@ -86,9 +88,8 @@
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Group 4-->
|
||||
<!-- Group 4 -->
|
||||
<div layout="row" layout-xs="column" class="home-row">
|
||||
|
||||
<div class="text-container">
|
||||
<div class="text-block l-pad-top-2">
|
||||
<div class="text-headline">百万粉丝热捧</div>
|
||||
@ -103,20 +104,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA CARDS -->
|
||||
<div layout="row" layout-xs="column" class="home-row">
|
||||
|
||||
<a href="guide/quickstart">
|
||||
<div class="card">
|
||||
<!-- CTA CARDS -->
|
||||
<div layout="row" layout-xs="column" class="home-row">
|
||||
<a href="guide/quickstart">
|
||||
<div class="card">
|
||||
<img src="generated/images/marketing/home/code-icon.svg" height="70px">
|
||||
<div class="card-text-container">
|
||||
<div class="text-headline">立即开始</div>
|
||||
<p>开始构建你的 Angular 应用</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div> <!-- end of home rows -->
|
||||
</div><!-- end of home rows -->
|
||||
|
||||
</article>
|
||||
|
@ -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 官方博客"
|
||||
}
|
||||
|
@ -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"},
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
158
aio/scripts/deploy-to-firebase.test.sh
Executable file
158
aio/scripts/deploy-to-firebase.test.sh
Executable 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"
|
||||
)
|
@ -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}}"
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
16
aio/src/app/layout/mode-banner/mode-banner.component.ts
Normal file
16
aio/src/app/layout/mode-banner/mode-banner.component.ts
Normal 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;
|
||||
}
|
32
aio/src/app/shared/deployment.service.spec.ts
Normal file
32
aio/src/app/shared/deployment.service.spec.ts
Normal 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('') }
|
||||
]);
|
||||
}
|
17
aio/src/app/shared/deployment.service.ts
Normal file
17
aio/src/app/shared/deployment.service.ts
Normal 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) {}
|
||||
};
|
@ -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');
|
||||
}
|
||||
|
6
aio/src/environments/environment.archive.ts
Normal file
6
aio/src/environments/environment.archive.ts
Normal 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'
|
||||
};
|
6
aio/src/environments/environment.next.ts
Normal file
6
aio/src/environments/environment.next.ts
Normal 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'
|
||||
};
|
@ -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'
|
||||
};
|
@ -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
|
||||
};
|
@ -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'
|
||||
};
|
||||
|
9
aio/src/extra-files/README.md
Normal file
9
aio/src/extra-files/README.md
Normal 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.
|
2
aio/src/extra-files/archive/robots.txt
Normal file
2
aio/src/extra-files/archive/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
2
aio/src/extra-files/next/robots.txt
Normal file
2
aio/src/extra-files/next/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
50
aio/src/styles/2-modules/_deploy-theme.scss
Normal file
50
aio/src/styles/2-modules/_deploy-theme.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -29,3 +29,4 @@
|
||||
@import 'subsection';
|
||||
@import 'toc';
|
||||
@import 'select-menu';
|
||||
@import 'deploy-theme';
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'
|
@ -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);
|
||||
}
|
@ -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 },
|
@ -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,
|
@ -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
|
||||
})
|
@ -37,5 +37,5 @@ function getText(h1) {
|
||||
(node.properties.ariaHidden === 'true' || node.properties['aria-hidden'] === 'true')
|
||||
));
|
||||
|
||||
return toString(cleaned);
|
||||
}
|
||||
return cleaned ? toString(cleaned) : '';
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
552
aio/yarn.lock
552
aio/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
```
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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/);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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'}
|
||||
];
|
||||
|
||||
|
@ -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(); }
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);');
|
||||
}
|
||||
|
||||
|
@ -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(); }
|
||||
|
||||
|
@ -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(); }
|
||||
|
@ -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)
|
||||
])
|
||||
|
@ -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', () => {
|
||||
|
@ -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', () => {
|
||||
|
@ -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', () => {
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
])
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)}
|
||||
])
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user