Compare commits
81 Commits
Author | SHA1 | Date | |
---|---|---|---|
e2e8ba6ffa | |||
60cdf9dc38 | |||
0371538d10 | |||
351331a81b | |||
0a54713f8a | |||
49d122e560 | |||
ff8729423e | |||
0c72f7d358 | |||
818f4a751e | |||
4e7d2bd5bf | |||
395ac510f7 | |||
b20c5d2c37 | |||
ea02b1ccfa | |||
0bafd03e85 | |||
e8d1858c64 | |||
d1efc5ae90 | |||
86f7b4170c | |||
9d93c859d7 | |||
5baa069b16 | |||
12b7d00747 | |||
d777d79c61 | |||
062a772e48 | |||
3618cc6d34 | |||
9413ca8a2e | |||
1302e54947 | |||
edf423af3d | |||
37086748bf | |||
6c3f1f70ba | |||
8a8c4d37aa | |||
f6a7183c52 | |||
c86e16db5f | |||
c3907893c1 | |||
d61c6f996a | |||
bd04cd61f8 | |||
96dcfafe45 | |||
991a802a8e | |||
48ae1a6574 | |||
dd2d1be006 | |||
5369de80d6 | |||
552dbfc2f1 | |||
0aa4cbdbc8 | |||
9f16c2620c | |||
9b256a9144 | |||
0f1476be33 | |||
769b2aada2 | |||
301236e1a5 | |||
aeb98dbcdf | |||
8036d05412 | |||
7d137d7f88 | |||
b8b551cf2b | |||
7ec28fe9af | |||
1cc3fe21b6 | |||
ba7d70e5e0 | |||
497e0178cc | |||
8821723526 | |||
a203a959ae | |||
dfe2bad663 | |||
f09a266e01 | |||
3853fff795 | |||
641be64544 | |||
bcf211bdb3 | |||
ee5591d583 | |||
1f43713506 | |||
325b9b4562 | |||
88abdbd50b | |||
14d34c9bdf | |||
e1f45a33b7 | |||
9a754f9f0f | |||
c3dcbf9cb3 | |||
5d68c830d2 | |||
ac58914b97 | |||
77ebd2b020 | |||
fec3b1a0e9 | |||
3b571a4f3d | |||
efee81eb57 | |||
a7a698c36f | |||
b5f1dc32d1 | |||
eef28144ce | |||
f9b290570e | |||
4852f55875 | |||
793f31b9b3 |
75
CHANGELOG.md
75
CHANGELOG.md
@ -1,20 +1,80 @@
|
||||
<a name="4.4.3"></a>
|
||||
## [4.4.3](https://github.com/angular/angular/compare/4.4.2...4.4.3) (2017-09-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tsc-wrapped:** deduplicate metadata only when the module is the same ([#19261](https://github.com/angular/angular/issues/19261)) ([0371538](https://github.com/angular/angular/commit/0371538)), closes [#19219](https://github.com/angular/angular/issues/19219)
|
||||
|
||||
|
||||
<a name="4.4.2"></a>
|
||||
## [4.4.2](https://github.com/angular/angular/compare/4.4.1...4.4.2) (2017-09-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **platform-server**: fix for packaging issues [#19250](https://github.com/angular/angular/issues/19250)
|
||||
|
||||
|
||||
<a name="4.4.1"></a>
|
||||
# [4.4.1](https://github.com/angular/angular/compare/4.3.6...4.4.1) (2017-09-15)
|
||||
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** do not leak DOM nodes/styling for host triggered animations ([#18853](https://github.com/angular/angular/issues/18853)) ([1cc3fe2](https://github.com/angular/angular/commit/1cc3fe2)), closes [#18606](https://github.com/angular/angular/issues/18606)
|
||||
* **common:** fix improper packaging for [@angular](https://github.com/angular)/common/http ([#18613](https://github.com/angular/angular/issues/18613)) ([a203a95](https://github.com/angular/angular/commit/a203a95))
|
||||
* **common:** fix XSSI prefix stripping by using JSON.parse always ([#18466](https://github.com/angular/angular/issues/18466)) ([8821723](https://github.com/angular/angular/commit/8821723)), closes [#18396](https://github.com/angular/angular/issues/18396) [#18453](https://github.com/angular/angular/issues/18453)
|
||||
* **compiler:** normalize the locale name ([#18963](https://github.com/angular/angular/issues/18963)) ([497e017](https://github.com/angular/angular/commit/497e017))
|
||||
* **core:** complete EventEmitter in QueryList on component destroy ([#18902](https://github.com/angular/angular/issues/18902)) ([7d137d7](https://github.com/angular/angular/commit/7d137d7)), closes [#18741](https://github.com/angular/angular/issues/18741)
|
||||
* **tsc-wrapped:** deduplicate metadata for re-exported modules ([48ae1a6](https://github.com/angular/angular/commit/48ae1a6))
|
||||
* **tsc-wrapped:** fix metadata symbol reference ([f6a7183](https://github.com/angular/angular/commit/f6a7183))
|
||||
* **upgrade:** remove code setting id attribute. ([#19182](https://github.com/angular/angular/issues/19182)) ([b20c5d2](https://github.com/angular/angular/commit/b20c5d2)), closes [#18446](https://github.com/angular/angular/issues/18446)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** allow multiple exportAs names ([#18723](https://github.com/angular/angular/issues/18723)) ([7ec28fe](https://github.com/angular/angular/commit/7ec28fe))
|
||||
* **core:** add option to remove blank text nodes from compiled templates ([#18823](https://github.com/angular/angular/issues/18823)) ([b8b551c](https://github.com/angular/angular/commit/b8b551c))
|
||||
|
||||
|
||||
Note: the 4.4.0 release on npm accidentally glitched-out midway, so we cut 4.4.1 instead. oops :-)
|
||||
|
||||
|
||||
|
||||
<a name="4.3.6"></a>
|
||||
## [4.3.6](https://github.com/angular/angular/compare/4.3.5...4.3.6) (2017-08-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** ensure animations are disabled on the element containing the @.disabled flag ([#18714](https://github.com/angular/angular/issues/18714)) ([5d68c83](https://github.com/angular/angular/commit/5d68c83))
|
||||
* **animations:** make sure @.disabled respects disabled parent/sub animation sequences ([#18715](https://github.com/angular/angular/issues/18715)) ([c3dcbf9](https://github.com/angular/angular/commit/c3dcbf9))
|
||||
* **animations:** make sure animation cancellations respect AUTO style values ([#18787](https://github.com/angular/angular/issues/18787)) ([9a754f9](https://github.com/angular/angular/commit/9a754f9)), closes [#17450](https://github.com/angular/angular/issues/17450)
|
||||
* **animations:** resolve error when using AnimationBuilder with platform-server ([#18642](https://github.com/angular/angular/issues/18642)) ([f9b2905](https://github.com/angular/angular/commit/f9b2905)), closes [#18635](https://github.com/angular/angular/issues/18635)
|
||||
* **animations:** restore auto-style support for removed DOM nodes ([#18787](https://github.com/angular/angular/issues/18787)) ([e1f45a3](https://github.com/angular/angular/commit/e1f45a3))
|
||||
* **core:** correct order in ContentChildren query result ([#18326](https://github.com/angular/angular/issues/18326)) ([fec3b1a](https://github.com/angular/angular/commit/fec3b1a)), closes [#16568](https://github.com/angular/angular/issues/16568)
|
||||
* **core:** make sure onStable runs in the right zone ([#18706](https://github.com/angular/angular/issues/18706)) ([ee5591d](https://github.com/angular/angular/commit/ee5591d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **animations:** allow @.disabled property to work without an expression ([#18713](https://github.com/angular/angular/issues/18713)) ([ac58914](https://github.com/angular/angular/commit/ac58914))
|
||||
* **common:** add an empty DeprecatedI18NPipesModule module ([793f31b](https://github.com/angular/angular/commit/793f31b))
|
||||
|
||||
|
||||
|
||||
<a name="4.3.5"></a>
|
||||
## [4.3.5](https://github.com/angular/angular/compare/4.3.4...4.3.5) (2017-08-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **aio:** skip PWA test when redeploying non-public commit ([b9c1c91](https://github.com/angular/angular/commit/b9c1c91))
|
||||
* **core:** forbid destroyed views to be inserted or moved in VC ([972538b](https://github.com/angular/angular/commit/972538b)), closes [#18615](https://github.com/angular/angular/issues/18615)
|
||||
* **forms:** re-assigning options should not clear select ([a1624f2](https://github.com/angular/angular/commit/a1624f2)), closes [#18330](https://github.com/angular/angular/issues/18330)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **aio:** update to new version of build-optimizer ([d7be4f1](https://github.com/angular/angular/commit/d7be4f1))
|
||||
|
||||
|
||||
|
||||
<a name="4.3.4"></a>
|
||||
## [4.3.4](https://github.com/angular/angular/compare/4.3.3...4.3.4) (2017-08-10)
|
||||
|
||||
@ -1121,7 +1181,6 @@ templates is unaffected. We expect no or little impact on apps from this change,
|
||||
|
||||
### Features
|
||||
|
||||
* **aio:** add initial angular-cli scaffold ([#14118](https://github.com/angular/angular/issues/14118)) ([e130bc1](https://github.com/angular/angular/commit/e130bc1))
|
||||
* **common:** rename underlying `NgFor` class and add a type parameter ([#14104](https://github.com/angular/angular/issues/14104)) ([86b2b25](https://github.com/angular/angular/commit/86b2b25))
|
||||
* **compiler:** allow missing translations ([#14113](https://github.com/angular/angular/issues/14113)) ([8775ab9](https://github.com/angular/angular/commit/8775ab9)), closes [#13861](https://github.com/angular/angular/issues/13861)
|
||||
* **compiler:** do not parse xtb messages not needed by angular ([#14111](https://github.com/angular/angular/issues/14111)) ([f7fba74](https://github.com/angular/angular/commit/f7fba74)), closes [#14046](https://github.com/angular/angular/issues/14046)
|
||||
|
@ -26,7 +26,7 @@ Here are the most important tasks you might need to use:
|
||||
* `yarn docs-lint` - check that the doc gen code follows our style rules.
|
||||
* `yarn docs-test` - run the unit tests for the doc generation code.
|
||||
|
||||
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally.
|
||||
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally. Add the option `-- --local` to use your local version of Angular contained in the "dist" folder.
|
||||
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
|
||||
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
|
||||
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
|
||||
@ -34,6 +34,7 @@ Here are the most important tasks you might need to use:
|
||||
* `yarn example-e2e` - run all e2e tests for examples
|
||||
- `yarn example-e2e -- --setup` - force webdriver update & other setup, then run tests
|
||||
- `yarn example-e2e -- --filter=foo` - limit e2e tests to those containing the word "foo"
|
||||
- `yarn example-e2e -- --setup --local` - run e2e tests with the local version of Angular contained in the "dist" folder
|
||||
|
||||
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
|
||||
|
||||
|
@ -12,13 +12,13 @@ describe('Router', () => {
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
function getPageStruct() {
|
||||
const hrefEles = element.all(by.css('my-app a'));
|
||||
const hrefEles = element.all(by.css('my-app > nav a'));
|
||||
const crisisDetail = element.all(by.css('my-app > ng-component > ng-component > ng-component > div')).first();
|
||||
const heroDetail = element(by.css('my-app > ng-component > div'));
|
||||
|
||||
return {
|
||||
hrefs: hrefEles,
|
||||
activeHref: element(by.css('my-app a.active')),
|
||||
activeHref: element(by.css('my-app > nav a.active')),
|
||||
|
||||
crisisHref: hrefEles.get(0),
|
||||
crisisList: element.all(by.css('my-app > ng-component > ng-component li')),
|
||||
|
@ -15,6 +15,10 @@
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.items li a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
.items li:hover {
|
||||
color: #607D8B;
|
||||
background-color: #DDD;
|
||||
|
@ -28,7 +28,7 @@ const appRoutes: Routes = [
|
||||
data: { preload: true }
|
||||
},
|
||||
// #enddocregion preload-v2
|
||||
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
|
||||
{ path: '', redirectTo: '/superheroes', pathMatch: 'full' },
|
||||
{ path: '**', component: PageNotFoundComponent }
|
||||
];
|
||||
|
||||
|
23
aio/content/examples/router/src/app/app.component.6.ts
Normal file
23
aio/content/examples/router/src/app/app.component.6.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
// #docregion template
|
||||
template: `
|
||||
<h1 class="title">Angular Router</h1>
|
||||
<nav>
|
||||
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
|
||||
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
|
||||
<a routerLink="/admin" routerLinkActive="active">Admin</a>
|
||||
<a routerLink="/login" routerLinkActive="active">Login</a>
|
||||
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet name="popup"></router-outlet>
|
||||
`
|
||||
// #enddocregion template
|
||||
})
|
||||
export class AppComponent {
|
||||
}
|
@ -9,7 +9,7 @@ import { Component } from '@angular/core';
|
||||
<h1 class="title">Angular Router</h1>
|
||||
<nav>
|
||||
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
|
||||
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
|
||||
<a routerLink="/superheroes" routerLinkActive="active">Heroes</a>
|
||||
<a routerLink="/admin" routerLinkActive="active">Admin</a>
|
||||
<a routerLink="/login" routerLinkActive="active">Login</a>
|
||||
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
|
||||
|
@ -1,5 +1,6 @@
|
||||
// #docregion
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { CanDeactivate,
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot } from '@angular/router';
|
||||
@ -13,7 +14,7 @@ export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent>
|
||||
component: CrisisDetailComponent,
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Promise<boolean> | boolean {
|
||||
): Observable<boolean> | boolean {
|
||||
// Get the Crisis Center ID
|
||||
console.log(route.paramMap.get('id'));
|
||||
|
||||
@ -25,7 +26,7 @@ export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent>
|
||||
return true;
|
||||
}
|
||||
// Otherwise ask the user with the dialog service and return its
|
||||
// promise which resolves to true or false when the user decides
|
||||
// observable which resolves to true or false when the user decides
|
||||
return component.dialogService.confirm('Discard changes?');
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,21 @@
|
||||
// #docregion
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/take';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Router, Resolve, RouterStateSnapshot,
|
||||
ActivatedRouteSnapshot } from '@angular/router';
|
||||
|
||||
import { Crisis, CrisisService } from './crisis.service';
|
||||
import { Crisis, CrisisService } from './crisis.service';
|
||||
|
||||
@Injectable()
|
||||
export class CrisisDetailResolver implements Resolve<Crisis> {
|
||||
constructor(private cs: CrisisService, private router: Router) {}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Crisis> {
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> {
|
||||
let id = route.paramMap.get('id');
|
||||
|
||||
return this.cs.getCrisis(id).then(crisis => {
|
||||
return this.cs.getCrisis(id).take(1).map(crisis => {
|
||||
if (crisis) {
|
||||
return crisis;
|
||||
} else { // id not found
|
||||
|
@ -1,8 +1,9 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
import { Component, OnInit, HostBinding } from '@angular/core';
|
||||
import { Component, OnInit, HostBinding } from '@angular/core';
|
||||
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { slideInDownAnimation } from '../animations';
|
||||
import { Crisis, CrisisService } from './crisis.service';
|
||||
@ -45,7 +46,8 @@ export class CrisisDetailComponent implements OnInit {
|
||||
// #docregion ngOnInit
|
||||
ngOnInit() {
|
||||
this.route.paramMap
|
||||
.switchMap((params: ParamMap) => this.service.getCrisis(params.get('id')))
|
||||
.switchMap((params: ParamMap) =>
|
||||
this.service.getCrisis(params.get('id')))
|
||||
.subscribe((crisis: Crisis) => {
|
||||
if (crisis) {
|
||||
this.editName = crisis.name;
|
||||
@ -66,13 +68,13 @@ export class CrisisDetailComponent implements OnInit {
|
||||
this.gotoCrises();
|
||||
}
|
||||
|
||||
canDeactivate(): Promise<boolean> | boolean {
|
||||
canDeactivate(): Observable<boolean> | boolean {
|
||||
// Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
|
||||
if (!this.crisis || this.crisis.name === this.editName) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise ask the user with the dialog service and return its
|
||||
// promise which resolves to true or false when the user decides
|
||||
// observable which resolves to true or false when the user decides
|
||||
return this.dialogService.confirm('Discard changes?');
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// #docregion
|
||||
import { Component, OnInit, HostBinding } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { slideInDownAnimation } from '../animations';
|
||||
import { Crisis } from './crisis.service';
|
||||
@ -62,13 +63,13 @@ export class CrisisDetailComponent implements OnInit {
|
||||
// #enddocregion cancel-save
|
||||
|
||||
// #docregion canDeactivate
|
||||
canDeactivate(): Promise<boolean> | boolean {
|
||||
canDeactivate(): Observable<boolean> | boolean {
|
||||
// Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
|
||||
if (!this.crisis || this.crisis.name === this.editName) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise ask the user with the dialog service and return its
|
||||
// promise which resolves to true or false when the user decides
|
||||
// observable which resolves to true or false when the user decides
|
||||
return this.dialogService.confirm('Discard changes?');
|
||||
}
|
||||
// #enddocregion canDeactivate
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'rxjs/add/operator/do';
|
||||
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
|
||||
import { Crisis, CrisisService } from './crisis.service';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
@ -10,35 +10,34 @@ import { Observable } from 'rxjs/Observable';
|
||||
// #docregion relative-navigation-router-link
|
||||
template: `
|
||||
<ul class="items">
|
||||
<li *ngFor="let crisis of crises | async">
|
||||
<a [routerLink]="[crisis.id]"
|
||||
[class.selected]="isSelected(crisis)">
|
||||
<span class="badge">{{ crisis.id }}</span>
|
||||
{{ crisis.name }}
|
||||
<li *ngFor="let crisis of crises$ | async"
|
||||
[class.selected]="crisis.id === selectedId">
|
||||
<a [routerLink]="[crisis.id]">
|
||||
<span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>`
|
||||
</ul>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
// #enddocregion relative-navigation-router-link
|
||||
})
|
||||
export class CrisisListComponent implements OnInit {
|
||||
crises: Observable<Crisis[]>;
|
||||
crises$: Observable<Crisis[]>;
|
||||
selectedId: number;
|
||||
|
||||
|
||||
constructor(
|
||||
private service: CrisisService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.crises = this.route.paramMap
|
||||
this.crises$ = this.route.paramMap
|
||||
.switchMap((params: ParamMap) => {
|
||||
this.selectedId = +params.get('id');
|
||||
return this.service.getCrises();
|
||||
});
|
||||
}
|
||||
|
||||
isSelected(crisis: Crisis) {
|
||||
return crisis.id === this.selectedId;
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,19 @@
|
||||
// #docregion
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
|
||||
import { Crisis, CrisisService } from './crisis.service';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ul class="items">
|
||||
<li *ngFor="let crisis of crises | async"
|
||||
(click)="onSelect(crisis)"
|
||||
[class.selected]="isSelected(crisis)">
|
||||
<span class="badge">{{ crisis.id }}</span>
|
||||
{{ crisis.name }}
|
||||
<li *ngFor="let crisis of crises$ | async"
|
||||
[class.selected]="crisis.id === selectedId">
|
||||
<a [routerLink]="[crisis.id]">
|
||||
<span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -22,35 +21,21 @@ import { Crisis, CrisisService } from './crisis.service';
|
||||
`
|
||||
})
|
||||
export class CrisisListComponent implements OnInit {
|
||||
crises: Observable<Crisis[]>;
|
||||
crises$: Observable<Crisis[]>;
|
||||
selectedId: number;
|
||||
|
||||
// #docregion ctor
|
||||
constructor(
|
||||
private service: CrisisService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
// #enddocregion ctor
|
||||
|
||||
isSelected(crisis: Crisis) {
|
||||
return crisis.id === this.selectedId;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.crises = this.route.paramMap
|
||||
this.crises$ = this.route.paramMap
|
||||
.switchMap((params: ParamMap) => {
|
||||
this.selectedId = +params.get('id');
|
||||
return this.service.getCrises();
|
||||
});
|
||||
}
|
||||
|
||||
// #docregion onSelect
|
||||
onSelect(crisis: Crisis) {
|
||||
this.selectedId = crisis.id;
|
||||
|
||||
// Navigate with relative link
|
||||
this.router.navigate([crisis.id], { relativeTo: this.route });
|
||||
}
|
||||
// #enddocregion onSelect
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
// #docplaster
|
||||
// #docregion , mock-crises
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
export class Crisis {
|
||||
constructor(public id: number, public name: string) { }
|
||||
}
|
||||
@ -12,20 +16,18 @@ const CRISES = [
|
||||
];
|
||||
// #enddocregion mock-crises
|
||||
|
||||
let crisesPromise = Promise.resolve(CRISES);
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class CrisisService {
|
||||
|
||||
static nextCrisisId = 100;
|
||||
private crises$: BehaviorSubject<Crisis[]> = new BehaviorSubject<Crisis[]>(CRISES);
|
||||
|
||||
getCrises() { return crisesPromise; }
|
||||
getCrises() { return this.crises$; }
|
||||
|
||||
getCrisis(id: number | string) {
|
||||
return crisesPromise
|
||||
.then(crises => crises.find(crisis => crisis.id === +id));
|
||||
return this.getCrises()
|
||||
.map(crises => crises.find(crisis => crisis.id === +id));
|
||||
}
|
||||
|
||||
// #enddocregion
|
||||
@ -33,7 +35,8 @@ export class CrisisService {
|
||||
name = name.trim();
|
||||
if (name) {
|
||||
let crisis = new Crisis(CrisisService.nextCrisisId++, name);
|
||||
crisesPromise.then(crises => crises.push(crisis));
|
||||
CRISES.push(crisis);
|
||||
this.crises$.next(CRISES);
|
||||
}
|
||||
}
|
||||
// #docregion
|
||||
|
@ -1,5 +1,8 @@
|
||||
// #docregion
|
||||
import 'rxjs/add/observable/of';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
/**
|
||||
* Async modal dialog service
|
||||
* DialogService makes this app easier to test by faking this service.
|
||||
@ -9,11 +12,11 @@ import { Injectable } from '@angular/core';
|
||||
export class DialogService {
|
||||
/**
|
||||
* Ask user to confirm an action. `message` explains the action and choices.
|
||||
* Returns promise resolving to `true`=confirm or `false`=cancel
|
||||
* Returns observable resolving to `true`=confirm or `false`=cancel
|
||||
*/
|
||||
confirm(message?: string) {
|
||||
return new Promise<boolean>(resolve => {
|
||||
return resolve(window.confirm(message || 'Is it OK?'));
|
||||
});
|
||||
confirm(message?: string): Observable<boolean> {
|
||||
const confirmation = window.confirm(message || 'Is it OK?');
|
||||
|
||||
return Observable.of(confirmation);
|
||||
};
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
// #enddocregion rxjs-operator-import
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
// #docregion imports
|
||||
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
||||
// #enddocregion imports
|
||||
@ -13,7 +14,7 @@ import { Hero, HeroService } from './hero.service';
|
||||
@Component({
|
||||
template: `
|
||||
<h2>HEROES</h2>
|
||||
<div *ngIf="hero">
|
||||
<div *ngIf="hero$ | async as hero">
|
||||
<h3>"{{ hero.name }}"</h3>
|
||||
<div>
|
||||
<label>Id: </label>{{ hero.id }}</div>
|
||||
@ -28,7 +29,7 @@ import { Hero, HeroService } from './hero.service';
|
||||
`
|
||||
})
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
hero: Hero;
|
||||
hero$: Observable<Hero>;
|
||||
|
||||
// #docregion ctor
|
||||
constructor(
|
||||
@ -40,10 +41,9 @@ export class HeroDetailComponent implements OnInit {
|
||||
|
||||
// #docregion ngOnInit
|
||||
ngOnInit() {
|
||||
this.route.paramMap
|
||||
this.hero$ = this.route.paramMap
|
||||
.switchMap((params: ParamMap) =>
|
||||
this.service.getHero(params.get('id')))
|
||||
.subscribe((hero: Hero) => this.hero = hero);
|
||||
this.service.getHero(params.get('id')));
|
||||
}
|
||||
// #enddocregion ngOnInit
|
||||
|
||||
|
@ -2,13 +2,14 @@
|
||||
// #docregion
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Hero, HeroService } from './hero.service';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<h2>HEROES</h2>
|
||||
<div *ngIf="hero">
|
||||
<div *ngIf="hero$ | async as hero">
|
||||
<h3>"{{ hero.name }}"</h3>
|
||||
<div>
|
||||
<label>Id: </label>{{ hero.id }}</div>
|
||||
@ -23,7 +24,7 @@ import { Hero, HeroService } from './hero.service';
|
||||
`
|
||||
})
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
hero: Hero;
|
||||
hero$: Observable<Hero>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@ -35,8 +36,7 @@ export class HeroDetailComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
let id = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
this.service.getHero(id)
|
||||
.then((hero: Hero) => this.hero = hero);
|
||||
this.hero$ = this.service.getHero(id);
|
||||
}
|
||||
// #enddocregion snapshot
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
// #enddocregion rxjs-operator-import
|
||||
import { Component, OnInit, HostBinding } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
||||
|
||||
import { slideInDownAnimation } from '../animations';
|
||||
@ -13,7 +14,7 @@ import { Hero, HeroService } from './hero.service';
|
||||
@Component({
|
||||
template: `
|
||||
<h2>HEROES</h2>
|
||||
<div *ngIf="hero">
|
||||
<div *ngIf="hero$ | async as hero">
|
||||
<h3>"{{ hero.name }}"</h3>
|
||||
<div>
|
||||
<label>Id: </label>{{ hero.id }}</div>
|
||||
@ -22,7 +23,7 @@ import { Hero, HeroService } from './hero.service';
|
||||
<input [(ngModel)]="hero.name" placeholder="name"/>
|
||||
</div>
|
||||
<p>
|
||||
<button (click)="gotoHeroes()">Back</button>
|
||||
<button (click)="gotoHeroes(hero)">Back</button>
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
@ -35,7 +36,7 @@ export class HeroDetailComponent implements OnInit {
|
||||
@HostBinding('style.position') position = 'absolute';
|
||||
// #enddocregion host-bindings
|
||||
|
||||
hero: Hero;
|
||||
hero$: Observable<Hero>;
|
||||
|
||||
// #docregion ctor
|
||||
constructor(
|
||||
@ -47,16 +48,15 @@ export class HeroDetailComponent implements OnInit {
|
||||
|
||||
// #docregion ngOnInit
|
||||
ngOnInit() {
|
||||
this.route.paramMap
|
||||
this.hero$ = this.route.paramMap
|
||||
.switchMap((params: ParamMap) =>
|
||||
this.service.getHero(params.get('id')))
|
||||
.subscribe((hero: Hero) => this.hero = hero);
|
||||
this.service.getHero(params.get('id')));
|
||||
}
|
||||
// #enddocregion ngOnInit
|
||||
|
||||
// #docregion gotoHeroes
|
||||
gotoHeroes() {
|
||||
let heroId = this.hero ? this.hero.id : null;
|
||||
gotoHeroes(hero: Hero) {
|
||||
let heroId = hero ? hero.id : null;
|
||||
// Pass along the hero id if available
|
||||
// so that the HeroList component can select that hero.
|
||||
// Include a junk 'foo' property for fun.
|
||||
|
@ -3,6 +3,7 @@
|
||||
// TODO SOMEDAY: Feature Componetized like HeroCenter
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Hero, HeroService } from './hero.service';
|
||||
|
||||
@ -11,9 +12,12 @@ import { Hero, HeroService } from './hero.service';
|
||||
template: `
|
||||
<h2>HEROES</h2>
|
||||
<ul class="items">
|
||||
<li *ngFor="let hero of heroes | async"
|
||||
(click)="onSelect(hero)">
|
||||
<span class="badge">{{ hero.id }}</span> {{ hero.name }}
|
||||
<li *ngFor="let hero of heroes$ | async">
|
||||
// #docregion nav-to-detail
|
||||
<a [routerLink]="['/hero', hero.id]">
|
||||
<span class="badge">{{ hero.id }}</span>{{ hero.name }}
|
||||
</a>
|
||||
// #enddocregion nav-to-detail
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -22,7 +26,7 @@ import { Hero, HeroService } from './hero.service';
|
||||
// #enddocregion template
|
||||
})
|
||||
export class HeroListComponent implements OnInit {
|
||||
heroes: Promise<Hero[]>;
|
||||
heroes$: Observable<Hero[]>;
|
||||
|
||||
// #docregion ctor
|
||||
constructor(
|
||||
@ -32,16 +36,8 @@ export class HeroListComponent implements OnInit {
|
||||
// #enddocregion ctor
|
||||
|
||||
ngOnInit() {
|
||||
this.heroes = this.service.getHeroes();
|
||||
this.heroes$ = this.service.getHeroes();
|
||||
}
|
||||
|
||||
// #docregion select
|
||||
onSelect(hero: Hero) {
|
||||
// #docregion nav-to-detail
|
||||
this.router.navigate(['/hero', hero.id]);
|
||||
// #enddocregion nav-to-detail
|
||||
}
|
||||
// #enddocregion select
|
||||
}
|
||||
// #enddocregion
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { Observable } from 'rxjs/Observable';
|
||||
// #enddocregion rxjs-imports
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
// #docregion import-router
|
||||
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
// #enddocregion import-router
|
||||
|
||||
import { Hero, HeroService } from './hero.service';
|
||||
@ -17,10 +17,11 @@ import { Hero, HeroService } from './hero.service';
|
||||
template: `
|
||||
<h2>HEROES</h2>
|
||||
<ul class="items">
|
||||
<li *ngFor="let hero of heroes | async"
|
||||
[class.selected]="isSelected(hero)"
|
||||
(click)="onSelect(hero)">
|
||||
<span class="badge">{{ hero.id }}</span> {{ hero.name }}
|
||||
<li *ngFor="let hero of heroes$ | async"
|
||||
[class.selected]="hero.id === selectedId">
|
||||
<a [routerLink]="['/hero', hero.id]">
|
||||
<span class="badge">{{ hero.id }}</span>{{ hero.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -30,18 +31,17 @@ import { Hero, HeroService } from './hero.service';
|
||||
})
|
||||
// #docregion ctor
|
||||
export class HeroListComponent implements OnInit {
|
||||
heroes: Observable<Hero[]>;
|
||||
heroes$: Observable<Hero[]>;
|
||||
|
||||
private selectedId: number;
|
||||
|
||||
constructor(
|
||||
private service: HeroService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.heroes = this.route.paramMap
|
||||
this.heroes$ = this.route.paramMap
|
||||
.switchMap((params: ParamMap) => {
|
||||
// (+) before `params.get()` turns the string into a number
|
||||
this.selectedId = +params.get('id');
|
||||
@ -49,16 +49,6 @@ export class HeroListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
// #enddocregion ctor
|
||||
|
||||
// #docregion isSelected
|
||||
isSelected(hero: Hero) { return hero.id === this.selectedId; }
|
||||
// #enddocregion isSelected
|
||||
|
||||
// #docregion select
|
||||
onSelect(hero: Hero) {
|
||||
this.router.navigate(['/hero', hero.id]);
|
||||
}
|
||||
// #enddocregion select
|
||||
// #docregion ctor
|
||||
}
|
||||
// #enddocregion
|
||||
|
@ -1,11 +1,14 @@
|
||||
// #docregion
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
export class Hero {
|
||||
constructor(public id: number, public name: string) { }
|
||||
}
|
||||
|
||||
let HEROES = [
|
||||
const HEROES = [
|
||||
new Hero(11, 'Mr. Nice'),
|
||||
new Hero(12, 'Narco'),
|
||||
new Hero(13, 'Bombasto'),
|
||||
@ -14,15 +17,13 @@ let HEROES = [
|
||||
new Hero(16, 'RubberMan')
|
||||
];
|
||||
|
||||
let heroesPromise = Promise.resolve(HEROES);
|
||||
|
||||
@Injectable()
|
||||
export class HeroService {
|
||||
getHeroes() { return heroesPromise; }
|
||||
getHeroes() { return Observable.of(HEROES); }
|
||||
|
||||
getHero(id: number | string) {
|
||||
return heroesPromise
|
||||
return this.getHeroes()
|
||||
// (+) before `id` turns the string into a number
|
||||
.then(heroes => heroes.find(hero => hero.id === +id));
|
||||
.map(heroes => heroes.find(hero => hero.id === +id));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
// #docregion
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { HeroListComponent } from './hero-list.component';
|
||||
import { HeroDetailComponent } from './hero-detail.component';
|
||||
|
||||
const heroesRoutes: Routes = [
|
||||
{ path: 'heroes', component: HeroListComponent },
|
||||
// #docregion hero-detail-route
|
||||
{ path: 'hero/:id', component: HeroDetailComponent }
|
||||
// #enddocregion hero-detail-route
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(heroesRoutes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class HeroRoutingModule { }
|
||||
// #enddocregion
|
@ -6,10 +6,10 @@ import { HeroListComponent } from './hero-list.component';
|
||||
import { HeroDetailComponent } from './hero-detail.component';
|
||||
|
||||
const heroesRoutes: Routes = [
|
||||
{ path: 'heroes', component: HeroListComponent },
|
||||
// #docregion hero-detail-route
|
||||
{ path: 'hero/:id', component: HeroDetailComponent }
|
||||
// #enddocregion hero-detail-route
|
||||
{ path: 'heroes', redirectTo: '/superheroes' },
|
||||
{ path: 'hero/:id', redirectTo: '/superhero/:id' },
|
||||
{ path: 'superheroes', component: HeroListComponent },
|
||||
{ path: 'superhero/:id', component: HeroDetailComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -35,7 +35,7 @@ export class HeroDetailComponent {
|
||||
@Input() prefix = '';
|
||||
|
||||
// #docregion deleteRequest
|
||||
// This component make a request but it can't actually delete a hero.
|
||||
// This component makes a request but it can't actually delete a hero.
|
||||
deleteRequest = new EventEmitter<Hero>();
|
||||
|
||||
delete() {
|
||||
|
@ -44,6 +44,7 @@ System.config({
|
||||
map: {
|
||||
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
|
||||
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
|
||||
'@angular/common/http/testing': 'npm:@angular/common/bundles/common-http-testing.umd.js',
|
||||
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
|
||||
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
|
||||
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
|
||||
|
@ -52,6 +52,10 @@ module.exports = function(config) {
|
||||
{ pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false },
|
||||
{ pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false },
|
||||
|
||||
// tslib (TS helper fns such as `__extends`)
|
||||
{ pattern: 'node_modules/tslib/**/*.js', included: false, watched: false },
|
||||
{ pattern: 'node_modules/tslib/**/*.js.map', included: false, watched: false },
|
||||
|
||||
// Paths loaded via module imports:
|
||||
// Angular itself
|
||||
{ pattern: 'node_modules/@angular/**/*.js', included: false, watched: false },
|
||||
|
@ -363,6 +363,10 @@ Those _npm_ commands are long and difficult to remember.
|
||||
|
||||
Add the following _npm_ convenience script to the `package.json` so you can compile and rollup in one command.
|
||||
|
||||
<code-example language="json">
|
||||
"build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js",
|
||||
</code-example>
|
||||
|
||||
Open a terminal window and try it.
|
||||
|
||||
<code-example language="none" class="code-shell">
|
||||
@ -526,10 +530,10 @@ Compiling with AOT presupposes certain supporting files, most of them discussed
|
||||
</code-pane>
|
||||
</code-tabs>
|
||||
|
||||
Extend the `scripts` section of the `package.json` with these npm scripts:
|
||||
With the following npm script in the `scripts` section of the `package.json`, you can easily serve
|
||||
the AOT-compiled application:
|
||||
|
||||
<code-example language="json">
|
||||
"build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js",
|
||||
"serve:aot": "lite-server -c bs-config.aot.json",
|
||||
</code-example>
|
||||
|
||||
@ -545,7 +549,7 @@ Copy the AOT distribution files into the `/aot` folder with the node script:
|
||||
|
||||
</div>
|
||||
|
||||
Now AOT-compile the app and launch it with the `lite-server`:
|
||||
Now AOT-compile the app and launch:
|
||||
|
||||
<code-example language="none" class="code-shell">
|
||||
npm run build:aot && npm run serve:aot
|
||||
|
@ -126,7 +126,7 @@ http
|
||||
err => {
|
||||
console.log('Something went wrong!');
|
||||
}
|
||||
});
|
||||
);
|
||||
```
|
||||
|
||||
#### Getting error details
|
||||
@ -141,7 +141,7 @@ In both cases, you can look at the `HttpErrorResponse` to figure out what happen
|
||||
http
|
||||
.get<ItemsResponse>('/api/items')
|
||||
.subscribe(
|
||||
data => {...},
|
||||
data => {...},
|
||||
(err: HttpErrorResponse) => {
|
||||
if (err.error instanceof Error) {
|
||||
// A client-side or network error occurred. Handle it accordingly.
|
||||
@ -152,7 +152,7 @@ http
|
||||
console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
```
|
||||
|
||||
#### `.retry()`
|
||||
|
@ -204,6 +204,149 @@ application using the `Router` service and the `routerState` property.
|
||||
Each `ActivatedRoute` in the `RouterState` provides methods to traverse up and down the route tree
|
||||
to get information from parent, child and sibling routes.
|
||||
|
||||
{@a activated-route}
|
||||
|
||||
|
||||
### Activated route
|
||||
|
||||
The route path and parameters are available through an injected router service called the
|
||||
[ActivatedRoute](api/router/ActivatedRoute).
|
||||
It has a great deal of useful information including:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
Property
|
||||
</th>
|
||||
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>url</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` of the route path(s), represented as an array of strings for each part of the route path.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>data</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` that contains the `data` object provided for the route. Also contains any resolved values from the [resolve guard](#resolve-guard).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>paramMap</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` that contains a [map](api/router/ParamMap) of the required and [optional parameters](#optional-route-parameters) specific to the route. The map supports retrieving single and multiple values from the same parameter.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>queryParamMap</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` that contains a [map](api/router/ParamMap) of the [query parameters](#query-parameters) available to all routes.
|
||||
The map supports retrieving single and multiple values from the query parameter.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>fragment</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` of the URL [fragment](#fragment) available to all routes.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>outlet</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
The name of the `RouterOutlet` used to render the route. For an unnamed outlet, the outlet name is _primary_.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>routeConfig</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
The route configuration used for the route that contains the origin path.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>parent</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
The route's parent `ActivatedRoute` when this route is a [child route](#child-routing-component).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>firstChild</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Contains the first `ActivatedRoute` in the list of this route's child routes.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>children</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Contains all the [child routes](#child-routing-component) activated under the current route.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
Two older properties are still available. They are less capable than their replacements, discouraged, and may be deprecated in a future Angular version.
|
||||
|
||||
**`params`** — An `Observable` that contains the required and [optional parameters](#optional-route-parameters) specific to the route. Use `paramMap` instead.
|
||||
|
||||
**`queryParams`** — An `Observable` that contains the [query parameters](#query-parameters) available to all routes.
|
||||
Use `queryParamMap` instead.
|
||||
|
||||
</div>
|
||||
|
||||
### Router events
|
||||
|
||||
During each navigation, the `Router` emits navigation events through the `Router.events` property. These events range from when the navigation starts and ends to many points in between. The full list of navigation events is displayed in the table below.
|
||||
@ -247,7 +390,7 @@ During each navigation, the `Router` emits navigation events through the `Router
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An [event](api/router/RouteConfigLoadStart) triggered before the `Router`
|
||||
An [event](api/router/RouteConfigLoadStart) triggered before the `Router`
|
||||
[lazy loads](#asynchronous-routing) a route configuration.
|
||||
|
||||
</td>
|
||||
@ -281,7 +424,7 @@ During each navigation, the `Router` emits navigation events through the `Router
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An [event](api/router/NavigationCancel) triggered when navigation is canceled.
|
||||
An [event](api/router/NavigationCancel) triggered when navigation is canceled.
|
||||
This is due to a [Route Guard](#guards) returning false during navigation.
|
||||
|
||||
</td>
|
||||
@ -924,7 +1067,7 @@ When the application launches, the initial URL in the browser bar is something l
|
||||
localhost:3000
|
||||
</code-example>
|
||||
|
||||
That doesn't match any of the concrete configured routes which means
|
||||
That doesn't match any of the concrete configured routes which means
|
||||
the router falls through to the wildcard route and displays the `PageNotFoundComponent`.
|
||||
|
||||
The application needs a **default route** to a valid page.
|
||||
@ -1333,7 +1476,7 @@ Create a new `heroes-routing.module.ts` in the `heroes` folder
|
||||
using the same techniques you learned while creating the `AppRoutingModule`.
|
||||
|
||||
|
||||
<code-example path="router/src/app/heroes/heroes-routing.module.ts" title="src/app/heroes/heroes-routing.module.ts">
|
||||
<code-example path="router/src/app/heroes/heroes-routing.module.1.ts" title="src/app/heroes/heroes-routing.module.ts">
|
||||
|
||||
</code-example>
|
||||
|
||||
@ -1503,7 +1646,7 @@ Return to the `HeroesRoutingModule` and look at the route definitions again.
|
||||
The route to `HeroDetailComponent` has a twist.
|
||||
|
||||
|
||||
<code-example path="router/src/app/heroes/heroes-routing.module.ts" linenums="false" title="src/app/heroes/heroes-routing.module.ts (excerpt)" region="hero-detail-route">
|
||||
<code-example path="router/src/app/heroes/heroes-routing.module.1.ts" linenums="false" title="src/app/heroes/heroes-routing.module.ts (excerpt)" region="hero-detail-route">
|
||||
|
||||
</code-example>
|
||||
|
||||
@ -1547,52 +1690,6 @@ a route for some other hero.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{@a navigate}
|
||||
|
||||
|
||||
### Navigate to hero detail imperatively
|
||||
|
||||
Users *will not* navigate to the detail component by clicking a link
|
||||
so you won't add a new `RouterLink` anchor tag to the shell.
|
||||
|
||||
Instead, when the user *clicks* a hero in the list, you'll ask the router
|
||||
to navigate to the hero detail view for the selected hero.
|
||||
|
||||
Start in the `HeroListComponent`.
|
||||
Revise its constructor so that it acquires the `Router` and the `HeroService` by dependency injection:
|
||||
|
||||
|
||||
<code-example path="router/src/app/heroes/hero-list.component.1.ts" linenums="false" title="src/app/heroes/hero-list.component.ts (constructor)" region="ctor">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
Make the following few changes to the component's template:
|
||||
|
||||
|
||||
<code-example path="router/src/app/heroes/hero-list.component.1.ts" linenums="false" title="src/app/heroes/hero-list.component.ts (template)" region="template">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
The template defines an `*ngFor` repeater such as [you've seen before](guide/displaying-data#ngFor).
|
||||
There's a `(click)` [event binding](guide/template-syntax#event-binding) to the component's
|
||||
`onSelect` method which you implement as follows:
|
||||
|
||||
|
||||
<code-example path="router/src/app/heroes/hero-list.component.1.ts" linenums="false" title="src/app/heroes/hero-list.component.ts (select)" region="select">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
The component's `onSelect` calls the router's **`navigate`** method with a _link parameters array_.
|
||||
You can use this same syntax in a `RouterLink` if you decide later to navigate in HTML template rather than in component code.
|
||||
|
||||
|
||||
{@a route-parameters}
|
||||
|
||||
|
||||
@ -1629,152 +1726,9 @@ the `HeroDetailComponent` via the `ActivatedRoute` service.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{@a activated-route}
|
||||
|
||||
|
||||
### ActivatedRoute: the one-stop-shop for route information
|
||||
|
||||
The route path and parameters are available through an injected router service called the
|
||||
[ActivatedRoute](api/router/ActivatedRoute).
|
||||
It has a great deal of useful information including:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
Property
|
||||
</th>
|
||||
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>url</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` of the route path(s), represented as an array of strings for each part of the route path.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>data</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` that contains the `data` object provided for the route. Also contains any resolved values from the [resolve guard](#resolve-guard).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>paramMap</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` that contains a [map](api/router/ParamMap) of the required and [optional parameters](#optional-route-parameters) specific to the route. The map supports retrieving single and multiple values from the same parameter.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>queryParamMap</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` that contains a [map](api/router/ParamMap) of the [query parameters](#query-parameters) available to all routes.
|
||||
The map supports retrieving single and multiple values from the query parameter.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>fragment</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
An `Observable` of the URL [fragment](#fragment) available to all routes.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>outlet</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
The name of the `RouterOutlet` used to render the route. For an unnamed outlet, the outlet name is _primary_.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>routeConfig</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
The route configuration used for the route that contains the origin path.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>parent</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
The route's parent `ActivatedRoute` when this route is a [child route](#child-routing-component).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>firstChild</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Contains the first `ActivatedRoute` in the list of this route's child routes.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<code>children</code>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Contains all the [child routes](#child-routing-component) activated under the current route.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
Two older properties are still available. They are less capable than their replacements, discouraged, and may be deprecated in a future Angular version.
|
||||
|
||||
**`params`** — An `Observable` that contains the required and [optional parameters](#optional-route-parameters) specific to the route. Use `paramMap` instead.
|
||||
|
||||
**`queryParams`** — An `Observable` that contains the [query parameters](#query-parameters) available to all routes.
|
||||
Use `queryParamMap` instead.
|
||||
|
||||
</div>
|
||||
|
||||
#### _Activated Route_ in action
|
||||
### _Activated Route_ in action
|
||||
|
||||
Import the `Router`, `ActivatedRoute`, and `ParamMap` tokens from the router package.
|
||||
|
||||
@ -1813,20 +1767,19 @@ pull the hero `id` from the parameters and retrieve the hero to display.
|
||||
|
||||
</code-example>
|
||||
|
||||
The `paramMap` processing is a bit tricky. When the map changes, you `get()`
|
||||
The `paramMap` processing is a bit tricky. When the map changes, you `get()`
|
||||
the `id` parameter from the changed parameters.
|
||||
|
||||
Then you tell the `HeroService` to fetch the hero with that `id` and return the result of the `HeroService` request.
|
||||
Then you tell the `HeroService` to fetch the hero with that `id` and return the result of the `HeroService` request.
|
||||
|
||||
You might think to use the RxJS `map` operator.
|
||||
But the `HeroService` returns an `Observable<Hero>`.
|
||||
Your subscription wants the `Hero`, not an `Observable<Hero>`.
|
||||
So you flatten the `Observable` with the `switchMap` operator instead.
|
||||
|
||||
The `switchMap` operator also cancels previous in-flight requests. If the user re-navigates to this route
|
||||
with a new `id` while the `HeroService` is still retrieving the old `id`, `switchMap` discards that old request and returns the hero for the new `id`.
|
||||
|
||||
Finally, you activate the observable with `subscribe` method and (re)set the component's `hero` property with the retrieved hero.
|
||||
The observable `Subscription` will be handled by the `AsyncPipe` and the component's `hero` property will be (re)set with the retrieved hero.
|
||||
|
||||
#### _ParamMap_ API
|
||||
|
||||
@ -1861,7 +1814,7 @@ to handle parameter access for both route parameters (`paramMap`) and query para
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Returns the parameter name value (a `string`) if present, or `null` if the parameter name is not in the map. Returns the _first_ element if the parameter value is actually an array of values.
|
||||
Returns the parameter name value (a `string`) if present, or `null` if the parameter name is not in the map. Returns the _first_ element if the parameter value is actually an array of values.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@ -2054,7 +2007,7 @@ The router embedded the `id` value in the navigation URL because you had defined
|
||||
as a route parameter with an `:id` placeholder token in the route `path`:
|
||||
|
||||
|
||||
<code-example path="router/src/app/heroes/heroes-routing.module.ts" linenums="false" title="src/app/heroes/heroes-routing.module.ts (hero-detail-route)" region="hero-detail-route">
|
||||
<code-example path="router/src/app/heroes/heroes-routing.module.1.ts" linenums="false" title="src/app/heroes/heroes-routing.module.ts (hero-detail-route)" region="hero-detail-route">
|
||||
|
||||
</code-example>
|
||||
|
||||
@ -2190,17 +2143,9 @@ Then you inject the `ActivatedRoute` in the `HeroListComponent` constructor.
|
||||
The `ActivatedRoute.paramMap` property is an `Observable` map of route parameters. The `paramMap` emits a new map of values that includes `id`
|
||||
when the user navigates to the component. In `ngOnInit` you subscribe to those values, set the `selectedId`, and get the heroes.
|
||||
|
||||
Add an `isSelected` method that returns `true` when a hero's `id` matches the selected `id`.
|
||||
|
||||
|
||||
<code-example path="router/src/app/heroes/hero-list.component.ts" linenums="false" title="src/app/heroes/hero-list.component.ts (isSelected)" region="isSelected">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
Finally, update the template with a [class binding](guide/template-syntax#class-binding) to that `isSelected` method.
|
||||
The binding adds the `selected` CSS class when the method returns `true` and removes it when `false`.
|
||||
Update the template with a [class binding](guide/template-syntax#class-binding).
|
||||
The binding adds the `selected` CSS class when the comparison returns `true` and removes it when `false`.
|
||||
Look for it within the repeated `<li>` tag as shown here:
|
||||
|
||||
|
||||
@ -2439,7 +2384,7 @@ Here are the relevant files for this version of the sample application.
|
||||
|
||||
</code-pane>
|
||||
|
||||
<code-pane title="heroes-routing.module.ts" path="router/src/app/heroes/heroes-routing.module.ts">
|
||||
<code-pane title="heroes-routing.module.ts" path="router/src/app/heroes/heroes-routing.module.1.ts">
|
||||
|
||||
</code-pane>
|
||||
|
||||
@ -2693,40 +2638,15 @@ The router then calculates the target URL based on the active route's location.
|
||||
{@a nav-to-crisis}
|
||||
|
||||
|
||||
### Navigate to crisis detail with a relative URL
|
||||
|
||||
Update the *Crisis List* `onSelect` method to use relative navigation so you don't have
|
||||
to start from the top of the route configuration.
|
||||
### Navigate to crisis list with a relative URL
|
||||
|
||||
You've already injected the `ActivatedRoute` that you need to compose the relative navigation path.
|
||||
|
||||
<code-example path="router/src/app/crisis-center/crisis-list.component.ts" linenums="false" title="src/app/crisis-center/crisis-list.component.ts (constructor)" region="ctor">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
When you visit the *Crisis Center*, the ancestor path is `/crisis-center`,
|
||||
so you only need to add the `id` of the *Crisis Center* to the existing path.
|
||||
|
||||
|
||||
<code-example path="router/src/app/crisis-center/crisis-list.component.ts" linenums="false" title="src/app/crisis-center/crisis-list.component.ts (relative navigation)" region="onSelect">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
If you were using a `RouterLink` to navigate instead of the `Router` service, you'd use the _same_
|
||||
When using a `RouterLink` to navigate instead of the `Router` service, you'd use the _same_
|
||||
link parameters array, but you wouldn't provide the object with the `relativeTo` property.
|
||||
The `ActivatedRoute` is implicit in a `RouterLink` directive.
|
||||
|
||||
|
||||
<code-example path="router/src/app/crisis-center/crisis-list.component.1.ts" linenums="false" title="src/app/crisis-center/crisis-list.component.ts (relative routerLink)" region="relative-navigation-router-link">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
Update the `gotoCrises` method of the `CrisisDetailComponent` to navigate back to the *Crisis Center* list using relative path navigation.
|
||||
|
||||
|
||||
@ -2735,7 +2655,6 @@ Update the `gotoCrises` method of the `CrisisDetailComponent` to navigate back t
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
Notice that the path goes up a level using the `../` syntax.
|
||||
If the current crisis `id` is `3`, the resulting path back to the crisis list is `/crisis-center/;id=3;foo=foo`.
|
||||
|
||||
@ -3415,8 +3334,7 @@ is like waiting for the server asynchronously.
|
||||
|
||||
The `DialogService`, provided in the `AppModule` for app-wide use, does the asking.
|
||||
|
||||
It returns a [promise](http://exploringjs.com/es6/ch_promises.html) that
|
||||
*resolves* when the user eventually decides what to do: either
|
||||
It returns an `Observable` that *resolves* when the user eventually decides what to do: either
|
||||
to discard changes and navigate away (`true`) or to preserve the pending changes and stay in the crisis editor (`false`).
|
||||
|
||||
|
||||
@ -3541,8 +3459,12 @@ Be explicit. Implement the `Resolve` interface with a type of `Crisis`.
|
||||
Inject the `CrisisService` and `Router` and implement the `resolve()` method.
|
||||
That method could return a `Promise`, an `Observable`, or a synchronous return value.
|
||||
|
||||
The `CrisisService.getCrisis` method returns a promise.
|
||||
Return that promise to prevent the route from loading until the data is fetched.
|
||||
The `CrisisService.getCrisis` method returns an Observable.
|
||||
Return that observable to prevent the route from loading until the data is fetched.
|
||||
The `Router` guards require an Observable to `complete`, meaning it has emitted all
|
||||
of its values. You use the `take` operator with an argument of `1` to ensure that the
|
||||
Observable completes after retrieving the first value from the Observable returned by the
|
||||
`getCrisis` method.
|
||||
If it doesn't return a valid `Crisis`, navigate the user back to the `CrisisListComponent`,
|
||||
canceling the previous in-flight navigation to the `CrisisDetailComponent`.
|
||||
|
||||
@ -3580,12 +3502,15 @@ The router looks for that method and calls it if found.
|
||||
Don't worry about all the ways that the user could navigate away.
|
||||
That's the router's job. Write this class and let the router take it from there.
|
||||
|
||||
1. The Observable provided to the Router _must_ complete.
|
||||
If the Observable does not complete, the navigation will not continue.
|
||||
|
||||
The relevant *Crisis Center* code for this milestone follows.
|
||||
|
||||
|
||||
<code-tabs>
|
||||
|
||||
<code-pane title="app.component.ts" path="router/src/app/app.component.ts">
|
||||
<code-pane title="app.component.ts" path="router/src/app/app.component.6.ts">
|
||||
|
||||
</code-pane>
|
||||
|
||||
@ -4041,6 +3966,52 @@ Verify this by logging in to the `Admin` feature area and noting that the `crisi
|
||||
It's also logged to the browser's console.
|
||||
|
||||
|
||||
{@a redirect-advanced}
|
||||
|
||||
## Migrating URLs with Redirects
|
||||
|
||||
You've setup the routes for navigating around your application. You've used navigation imperatively and declaratively to many different routes. But like any application, requirements change over time. You've setup links and navigation to `/heroes` and `/hero/:id` from the `HeroListComponent` and `HeroDetailComponent` components. If there was a requirement that links to `heroes` become `superheroes`, you still want the previous URLs to navigate correctly. You also don't want to go and update every link in your application, so redirects makes refactoring routes trivial.
|
||||
|
||||
|
||||
{@a url-refactor}
|
||||
|
||||
### Changing /heroes to /superheroes
|
||||
|
||||
Let's take the `Hero` routes and migrate them to new URLs. The `Router` checks for redirects in your configuration before navigating, so each redirect is triggered when needed. To support this change, you'll add redirects from the old routes to the new routes in the `heroes-routing.module`.
|
||||
|
||||
<code-example path="router/src/app/heroes/heroes-routing.module.ts" linenums="false" title="src/app/heroes/heroes-routing.module.ts (heroes redirects)">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
You'll notice two different types of redirects. The first change is from `/heroes` to `/superheroes` without any parameters. This is a straightforward redirect, unlike the change from `/hero/:id` to `/superhero/:id`, which includes the `:id` route parameter. Router redirects also use powerful pattern matching, so the `Router` inspects the URL and replaces route parameters in the `path` with their appropriate destination. Previously, you navigated to a URL such as `/hero/15` with a route parameter `id` of `15`.
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
The `Router` also supports [query parameters](#query-parameters) and the [fragment](#fragment) when using redirects.
|
||||
|
||||
* When using absolute redirects, the `Router` will use the query parameters and the fragment from the redirectTo in the route config.
|
||||
* When using relative redirects, the `Router` use the query params and the fragment from the source URL.
|
||||
|
||||
</div>
|
||||
|
||||
Before updating the `app-routing.module.ts`, you'll need to consider an important rule. Currently, our empty path route redirects to `/heroes`, which redirects to `/superheroes`. This _won't_ work and is by design as the `Router` handles redirects once at each level of routing configuration. This prevents chaining of redirects, which can lead to endless redirect loops.
|
||||
|
||||
So instead, you'll update the empty path route in `app-routing.module.ts` to redirect to `/superheroes`.
|
||||
|
||||
<code-example path="router/src/app/app-routing.module.ts" linenums="false" title="src/app/app-routing.module.ts (superheroes redirect)">
|
||||
|
||||
</code-example>
|
||||
|
||||
Since `RouterLink`s aren't tied to route configuration, you'll need to update the associated router links so they remain active when the new route is active. You'll update the `app.component.ts` template for the `/heroes` routerLink.
|
||||
|
||||
<code-example path="router/src/app/app.component.ts" linenums="false" title="src/app/app.component.ts (superheroes active routerLink)">
|
||||
|
||||
</code-example>
|
||||
|
||||
With the redirects setup, all previous routes now point to their new destinations and both URLs still function as intended.
|
||||
|
||||
|
||||
{@a inspect-config}
|
||||
|
||||
|
||||
|
@ -1155,7 +1155,7 @@ other HTML elements, attributes, properties, and components.
|
||||
They are usually applied to elements as if they were HTML attributes, hence the name.
|
||||
|
||||
Many details are covered in the [_Attribute Directives_](guide/attribute-directives) guide.
|
||||
Many NgMdules such as the [`RouterModule`](guide/router "Routing and Navigation")
|
||||
Many NgModules such as the [`RouterModule`](guide/router "Routing and Navigation")
|
||||
and the [`FormsModule`](guide/forms "Forms") define their own attribute directives.
|
||||
This section is an introduction to the most commonly used attribute directives:
|
||||
|
||||
|
BIN
aio/content/images/bios/nirkaufman.jpg
Normal file
BIN
aio/content/images/bios/nirkaufman.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
@ -80,15 +80,6 @@
|
||||
"group": "Angular"
|
||||
},
|
||||
|
||||
"aaronzhang": {
|
||||
"name": "Aaron Zhang (章小飞)",
|
||||
"picture": "xiaofei.jpg",
|
||||
"twitter": "",
|
||||
"website": "http://github.com/damoqiongqiu",
|
||||
"bio": "Aaron is Angular's developer PM in China. He is the lead for angular.cn and social channels in China, and helps developers in China's enterprise and open source communities to be successful with Angular. One of the earliest Angular developers in China since Angular 2012, he translated the first books on Angular into Chinese. Aaron joined the Google team in 2016.",
|
||||
"group": "Angular"
|
||||
},
|
||||
|
||||
"tobias": {
|
||||
"name": "Tobias Bosch",
|
||||
"picture": "tobias.jpg",
|
||||
@ -582,5 +573,23 @@
|
||||
"website": "https://medium.com/@gerard.sans",
|
||||
"bio": "Gerard is very excited about the future of the Web and JavaScript. Always happy Computer Science Engineer and humble Google Developer Expert. He loves to share his learnings by giving talks, trainings and writing about cool technologies. He loves running AngularZone and GraphQL London, mentoring students and giving back to the community.",
|
||||
"group": "GDE"
|
||||
}
|
||||
},
|
||||
|
||||
"amcdnl": {
|
||||
"name": "Austin McDaniel",
|
||||
"picture": "amcdnl.jpg",
|
||||
"twitter": "amcdnl",
|
||||
"website": "https://amcdnl.com",
|
||||
"bio": "Austin is an software architect with a passion for JavaScript and Angular. Austin loves to share his experiences with other like-minded developers by giving talks, blogging, podcasting and open-sourcing.",
|
||||
"group": "Angular"
|
||||
},
|
||||
|
||||
"nirkaufman": {
|
||||
"name": "Nir Kaufman",
|
||||
"picture": "nirkaufman.jpg",
|
||||
"twitter": "nirkaufman",
|
||||
"website": "http://ngnir.life/",
|
||||
"bio": "Nir is a Principal Frontend Consultant & Head of the Angular department at 500Tech, Google Developer Expert and community leader. He organizes the largest Angular meetup group in Israel (Angular-IL), talks and teaches about front-end technologies around the world. He is also the author of two books about Angular and the founder of the 'Frontend Band'.",
|
||||
"group": "GDE"
|
||||
}
|
||||
}
|
||||
|
@ -10,13 +10,11 @@ Angular is a platform that makes it easy to build applications with the web. Ang
|
||||
<p class="card-footer">Angular in Action</p>
|
||||
</a>
|
||||
|
||||
<div class="docs-card">
|
||||
<a href="guide/quickstart" class="docs-card" title="Angular Quickstart">
|
||||
<section>Get Going with Angular</section>
|
||||
<p>Get going on your own environment with the Quickstart.</p>
|
||||
<p class="card-footer" >
|
||||
<a href="guide/quickstart" title="Angular Quickstart">Quickstart</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="card-footer">Quickstart</p>
|
||||
</a>
|
||||
|
||||
<a href="guide/architecture" class="docs-card" title="Angular Architecture">
|
||||
<section>Fundamentals</section>
|
||||
|
@ -37,6 +37,12 @@
|
||||
<td>London, United Kingdom</td>
|
||||
<td>November 07, 2017</td>
|
||||
</tr>
|
||||
<!-- ngAtlanta-->
|
||||
<tr>
|
||||
<th><a href="http://ng-atl.org/" title="ngAtlanta">ngAtlanta</a></th>
|
||||
<td>Atlanta, Georgia</td>
|
||||
<td>January 30, 2018</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
|
@ -130,6 +130,13 @@
|
||||
"rev": true,
|
||||
"title": "Apollo",
|
||||
"url": "http://docs.apollostack.com/apollo-client/angular2.html"
|
||||
},
|
||||
"ab4": {
|
||||
"desc": "Angular Commerce is a solution for building modern e-commerce applications with power of Google Firebase. Set of components is design agnostic and allows to easily extend functionality.",
|
||||
"logo": "",
|
||||
"rev": true,
|
||||
"title": "AngularCommerce",
|
||||
"url": "https://github.com/NodeArt/angular-commerce"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -202,7 +209,7 @@
|
||||
"url": "https://cli.angular.io"
|
||||
},
|
||||
"d1": {
|
||||
"desc": "A set of tslint rules for static code analysis of Angular TypeScript projects.",
|
||||
"desc": "Static analysis for Angular projects.",
|
||||
"logo": "",
|
||||
"rev": true,
|
||||
"title": "Codelyzer",
|
||||
@ -221,6 +228,13 @@
|
||||
"rev": true,
|
||||
"title": "Compodoc",
|
||||
"url": "https://github.com/compodoc/compodoc"
|
||||
},
|
||||
"ncg": {
|
||||
"desc": "Generate several types of CRUD apps complete with e2e testing using template-sets for Angular, Material Design, Bootstrap, Kendo UI, Ionic, ...",
|
||||
"logo": "https://avatars3.githubusercontent.com/u/27976684",
|
||||
"rev": true,
|
||||
"title": "NinjaCodeGen - Angular CRUD Generator",
|
||||
"url": "https://ninjaCodeGen.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -313,10 +327,10 @@
|
||||
"url": "https://www.ag-grid.com/best-angular-2-data-grid/"
|
||||
},
|
||||
"jqwidgets": {
|
||||
"desc": "Angular UI Components including Grids, Charts, Scheduling and more.",
|
||||
"desc": "Angular UI Components including data grid, tree grid, pivot grid, scheduler, charts, editors and other multi-purpose components",
|
||||
"rev": true,
|
||||
"title": "jQWidgets",
|
||||
"url": "http://www.jqwidgets.com/angular/"
|
||||
"url": "https://www.jqwidgets.com/angular/"
|
||||
},
|
||||
"amexio": {
|
||||
"desc": "Amexio (Angular MetaMagic EXtensions for Inputs and Outputs) is a rich set of Angular components powered by Bootstrap for Responsive Design. UI Components include Standard Form Components, Data Grids, Tree Grids, Tabs etc. Open Source (Apache 2 License) & Free and backed by MetaMagic Global Inc",
|
||||
@ -324,7 +338,7 @@
|
||||
"title": "Amexio - Angular Extensions",
|
||||
"url": "http://www.amexio.tech/",
|
||||
"logo": "http://www.amexio.org/amexio-logo.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -380,6 +380,12 @@
|
||||
"title": "API",
|
||||
"tooltip": "Details of the Angular classes and values.",
|
||||
"url": "api"
|
||||
},
|
||||
{
|
||||
"url": "guide/change-log",
|
||||
"title": "Change Log",
|
||||
"tooltip": "Angular Documentation Change Log",
|
||||
"hidden": true
|
||||
}
|
||||
],
|
||||
|
||||
@ -413,7 +419,7 @@
|
||||
"title": "Help",
|
||||
"children": [
|
||||
{
|
||||
"url": "http://stackoverflow.com/questions/tagged/angular2",
|
||||
"url": "https://stackoverflow.com/questions/tagged/angular",
|
||||
"title": "Stack Overflow",
|
||||
"tooltip": "Stack Overflow: where the community answers your technical Angular questions."
|
||||
},
|
||||
|
@ -92,4 +92,12 @@ describe('site App', function() {
|
||||
// Todo: add test to confirm tracking URL when navigate.
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should find pages when searching by a partial word in the title', () => {
|
||||
page.enterSearch('ngCont');
|
||||
expect(page.getSearchResults().map(link => link.getText())).toContain('NgControl');
|
||||
page.enterSearch('accessor');
|
||||
expect(page.getSearchResults().map(link => link.getText())).toContain('ControlValueAccessor');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { browser, element, by, promise, ElementFinder } from 'protractor';
|
||||
import { browser, element, by, promise, ElementFinder, ExpectedConditions } from 'protractor';
|
||||
|
||||
const githubRegex = /https:\/\/github.com\/angular\/angular\//;
|
||||
|
||||
@ -50,6 +50,18 @@ export class SitePage {
|
||||
return browser.executeScript('window.scrollTo(0, document.body.scrollHeight)');
|
||||
}
|
||||
|
||||
enterSearch(query: string) {
|
||||
const input = element(by.css('.search-container input[type=search]'));
|
||||
input.clear();
|
||||
input.sendKeys(query);
|
||||
}
|
||||
|
||||
getSearchResults() {
|
||||
const results = element.all(by.css('.search-results li'));
|
||||
browser.wait(ExpectedConditions.presenceOf(results.first()), 8000);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the ambient Google Analytics tracker with homebrew spy
|
||||
* don't send commands to GA during e2e testing!
|
||||
|
@ -45,7 +45,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.5 <7.0.0",
|
||||
"yarn": ">=0.21.3 <1.0.0"
|
||||
"yarn": ">=1.0.2 <2.0.0"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@ -82,7 +82,7 @@
|
||||
"concurrently": "^3.4.0",
|
||||
"cross-spawn": "^5.1.0",
|
||||
"dgeni": "^0.4.7",
|
||||
"dgeni-packages": "^0.20.0",
|
||||
"dgeni-packages": "^0.21.2",
|
||||
"entities": "^1.1.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-plugin-jasmine": "^2.2.0",
|
||||
@ -124,6 +124,7 @@
|
||||
"unist-util-filter": "^0.2.1",
|
||||
"unist-util-source": "^1.0.1",
|
||||
"unist-util-visit": "^1.1.1",
|
||||
"unist-util-visit-parents": "^1.1.1",
|
||||
"vrsource-tslint-rules": "^4.0.1",
|
||||
"watchr": "^3.0.1",
|
||||
"yargs": "^7.0.2"
|
||||
|
@ -310,13 +310,11 @@ describe('AppComponent', () => {
|
||||
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 a url', () => {
|
||||
setupSelectorForTesting();
|
||||
const versionWithoutUrlIndex = component.docVersions.findIndex(v => !v.url);
|
||||
const versionWithoutUrl = component.docVersions[versionWithoutUrlIndex];
|
||||
selectElement.triggerEventHandler('change', { option: versionWithoutUrl, index: versionWithoutUrlIndex});
|
||||
const versionWithoutUrlIndex = component.docVersions.length;
|
||||
const versionWithoutUrl = component.docVersions[versionWithoutUrlIndex] = { title: 'foo', url: null };
|
||||
selectElement.triggerEventHandler('change', { option: versionWithoutUrl, index: versionWithoutUrlIndex });
|
||||
expect(locationService.go).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -794,6 +792,10 @@ describe('AppComponent', () => {
|
||||
createTestingModule('api', 'archive');
|
||||
initializeTest();
|
||||
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
|
||||
|
||||
createTestingModule('api/core/getPlatform', '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', () => {
|
||||
@ -824,6 +826,10 @@ describe('AppComponent', () => {
|
||||
createTestingModule('api', 'next');
|
||||
initializeTest();
|
||||
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
|
||||
|
||||
createTestingModule('api/core/getPlatform', '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', () => {
|
||||
@ -854,6 +860,10 @@ describe('AppComponent', () => {
|
||||
createTestingModule('api', 'stable');
|
||||
initializeTest();
|
||||
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
|
||||
|
||||
createTestingModule('api/core/getPlatform', 'stable');
|
||||
initializeTest();
|
||||
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -131,7 +131,7 @@ 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)) {
|
||||
if (this.deployment.mode !== 'stable' && !/^(docs$|api|guide|tutorial)/.test(path)) {
|
||||
this.locationService.replace('docs');
|
||||
}
|
||||
if (path === this.currentPath) {
|
||||
|
@ -9,7 +9,18 @@ var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
|
||||
importScripts('/assets/js/lunr.min.js');
|
||||
|
||||
var index;
|
||||
var pages = {};
|
||||
var pages /* : SearchInfo */ = {};
|
||||
|
||||
// interface SearchInfo {
|
||||
// [key: string]: PageInfo;
|
||||
// }
|
||||
|
||||
// interface PageInfo {
|
||||
// path: string;
|
||||
// type: string,
|
||||
// titleWords: string;
|
||||
// keyWords: string;
|
||||
// }
|
||||
|
||||
self.onmessage = handleMessage;
|
||||
|
||||
@ -49,15 +60,7 @@ function handleMessage(message) {
|
||||
// Use XHR to make a request to the server
|
||||
function makeRequest(url, callback) {
|
||||
|
||||
// The JSON file that is loaded should be an array of SearchTerms:
|
||||
//
|
||||
// export interface SearchTerms {
|
||||
// path: string;
|
||||
// type: string,
|
||||
// titleWords: string;
|
||||
// keyWords: string;
|
||||
// }
|
||||
|
||||
// The JSON file that is loaded should be an array of PageInfo:
|
||||
var searchDataRequest = new XMLHttpRequest();
|
||||
searchDataRequest.onload = function() {
|
||||
callback(JSON.parse(this.responseText));
|
||||
@ -68,11 +71,11 @@ function makeRequest(url, callback) {
|
||||
|
||||
|
||||
// Create the search index from the searchInfo which contains the information about each page to be indexed
|
||||
function loadIndex(searchInfo) {
|
||||
function loadIndex(searchInfo /*: SearchInfo */) {
|
||||
return function(index) {
|
||||
// Store the pages data to be used in mapping query results back to pages
|
||||
// Add search terms from each page to the search index
|
||||
searchInfo.forEach(function(page) {
|
||||
searchInfo.forEach(function(page /*: PageInfo */) {
|
||||
index.add(page);
|
||||
pages[page.path] = page;
|
||||
});
|
||||
@ -81,7 +84,19 @@ function loadIndex(searchInfo) {
|
||||
|
||||
// Query the index and return the processed results
|
||||
function queryIndex(query) {
|
||||
var results = index.search(query);
|
||||
// Only return the array of paths to pages
|
||||
return results.map(function(hit) { return pages[hit.ref]; });
|
||||
try {
|
||||
if (query.length) {
|
||||
// Add a relaxed search in the title for the first word in the query
|
||||
// E.g. if the search is "ngCont guide" then we search for "ngCont guide titleWords:ngCont*"
|
||||
var titleQuery = 'titleWords:*' + query.split(' ', 1)[0] + '*';
|
||||
var results = index.search(query + ' ' + titleQuery);
|
||||
// Map the hits into info about each page to be returned as results
|
||||
return results.map(function(hit) { return pages[hit.ref]; });
|
||||
}
|
||||
} catch(e) {
|
||||
// If the search query cannot be parsed the index throws an error
|
||||
// Log it and recover
|
||||
console.log(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ footer::after {
|
||||
right: 0;
|
||||
background:
|
||||
url('../src/assets/images/logos/angular/angular_whiteTransparent_withMargin.png') top 0 left 0 repeat,
|
||||
url('../src/assets/images/logos/angular/angular_whiteTransparent_withMargin.png') top 80px left 168px repeat;
|
||||
url('../src/assets/images/logos/angular/angular_whiteTransparent_withMargin.png') top 80px left 160px repeat;
|
||||
opacity: 0.05;
|
||||
background-size: 320px auto;
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
'@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
|
||||
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
|
||||
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
|
||||
'@angular/common/http': 'npm:@angular/common/bundles/common-http.umd.js',
|
||||
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
|
||||
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
|
||||
'@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
|
||||
@ -31,6 +32,7 @@
|
||||
|
||||
// other libraries
|
||||
'rxjs': 'npm:rxjs',
|
||||
'tslib': 'npm:tslib/tslib.js',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
|
||||
},
|
||||
// packages tells the System loader how to load when no filename and/or no extension
|
||||
|
@ -42,6 +42,7 @@
|
||||
'@angular/animations/browser': 'ng:animations-builds/master/bundles/animations-browser.umd.js',
|
||||
'@angular/core': 'ng:core-builds/master/bundles/core.umd.js',
|
||||
'@angular/common': 'ng:common-builds/master/bundles/common.umd.js',
|
||||
'@angular/common/http': 'ng:common-builds/master/bundles/common-http.umd.js',
|
||||
'@angular/compiler': 'ng:compiler-builds/master/bundles/compiler.umd.js',
|
||||
'@angular/platform-browser': 'ng:platform-browser-builds/master/bundles/platform-browser.umd.js',
|
||||
'@angular/platform-browser/animations': 'ng:animations-builds/master/bundles/platform-browser-animations.umd.js',
|
||||
@ -56,6 +57,7 @@
|
||||
// angular testing umd bundles (overwrite the shim mappings)
|
||||
'@angular/core/testing': 'ng:core-builds/master/bundles/core-testing.umd.js',
|
||||
'@angular/common/testing': 'ng:common-builds/master/bundles/common-testing.umd.js',
|
||||
'@angular/common/http/testing': 'ng:common-builds/master/bundles/common-http-testing.umd.js',
|
||||
'@angular/compiler/testing': 'ng:compiler-builds/master/bundles/compiler-testing.umd.js',
|
||||
'@angular/platform-browser/testing': 'ng:platform-browser-builds/master/bundles/platform-browser-testing.umd.js',
|
||||
'@angular/platform-browser-dynamic/testing': 'ng:platform-browser-dynamic-builds/master/bundles/platform-browser-dynamic-testing.umd.js',
|
||||
@ -65,6 +67,7 @@
|
||||
|
||||
// other libraries
|
||||
'rxjs': 'npm:rxjs@5.0.1',
|
||||
'tslib': 'npm:tslib/tslib.js',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
|
||||
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
|
||||
'typescript': 'npm:typescript@2.3.2/lib/typescript.js',
|
||||
|
@ -39,6 +39,7 @@
|
||||
'@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
|
||||
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
|
||||
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
|
||||
'@angular/common/http': 'npm:@angular/common/bundles/common-http.umd.js',
|
||||
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
|
||||
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
|
||||
'@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
|
||||
@ -52,6 +53,7 @@
|
||||
|
||||
// other libraries
|
||||
'rxjs': 'npm:rxjs@5.0.1',
|
||||
'tslib': 'npm:tslib/tslib.js',
|
||||
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
|
||||
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
|
||||
'typescript': 'npm:typescript@2.3.2/lib/typescript.js',
|
||||
|
@ -25,7 +25,7 @@
|
||||
"@angular/router": "~4.3.1",
|
||||
"@angular/tsc-wrapped": "~4.3.1",
|
||||
"@angular/upgrade": "~4.3.1",
|
||||
"angular-in-memory-web-api": "~0.3.2",
|
||||
"angular-in-memory-web-api": "~0.4.0",
|
||||
"core-js": "^2.4.1",
|
||||
"rxjs": "^5.1.0",
|
||||
"systemjs": "0.19.39",
|
||||
|
@ -325,9 +325,9 @@ amdefine@>=0.0.4:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
||||
|
||||
angular-in-memory-web-api@~0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/angular-in-memory-web-api/-/angular-in-memory-web-api-0.3.2.tgz#8836d9e2534d37b728f3cb5a1caf6fe1e7fbbecd"
|
||||
angular-in-memory-web-api@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/angular-in-memory-web-api/-/angular-in-memory-web-api-0.4.0.tgz#996715f37d8a4e659e154fedf76c4726470cb8d8"
|
||||
|
||||
angular2-template-loader@^0.6.0:
|
||||
version "0.6.2"
|
||||
@ -5163,18 +5163,18 @@ request-progress@~2.0.1:
|
||||
dependencies:
|
||||
throttleit "^1.0.0"
|
||||
|
||||
request@2, request@^2.72.0, request@^2.79.0, request@^2.81.0:
|
||||
version "2.81.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
|
||||
request@2, request@^2.72.0, request@^2.78.0, request@^2.79.0, request@~2.79.0:
|
||||
version "2.79.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
|
||||
dependencies:
|
||||
aws-sign2 "~0.6.0"
|
||||
aws4 "^1.2.1"
|
||||
caseless "~0.12.0"
|
||||
caseless "~0.11.0"
|
||||
combined-stream "~1.0.5"
|
||||
extend "~3.0.0"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.1.1"
|
||||
har-validator "~4.2.1"
|
||||
har-validator "~2.0.6"
|
||||
hawk "~3.1.3"
|
||||
http-signature "~1.1.0"
|
||||
is-typedarray "~1.0.0"
|
||||
@ -5182,12 +5182,10 @@ request@2, request@^2.72.0, request@^2.79.0, request@^2.81.0:
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.7"
|
||||
oauth-sign "~0.8.1"
|
||||
performance-now "^0.2.0"
|
||||
qs "~6.4.0"
|
||||
safe-buffer "^5.0.1"
|
||||
qs "~6.3.0"
|
||||
stringstream "~0.0.4"
|
||||
tough-cookie "~2.3.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
tunnel-agent "~0.4.1"
|
||||
uuid "^3.0.0"
|
||||
|
||||
request@2.78.0:
|
||||
@ -5215,18 +5213,18 @@ request@2.78.0:
|
||||
tough-cookie "~2.3.0"
|
||||
tunnel-agent "~0.4.1"
|
||||
|
||||
request@^2.78.0, request@~2.79.0:
|
||||
version "2.79.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
|
||||
request@^2.81.0:
|
||||
version "2.81.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
|
||||
dependencies:
|
||||
aws-sign2 "~0.6.0"
|
||||
aws4 "^1.2.1"
|
||||
caseless "~0.11.0"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.5"
|
||||
extend "~3.0.0"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.1.1"
|
||||
har-validator "~2.0.6"
|
||||
har-validator "~4.2.1"
|
||||
hawk "~3.1.3"
|
||||
http-signature "~1.1.0"
|
||||
is-typedarray "~1.0.0"
|
||||
@ -5234,10 +5232,12 @@ request@^2.78.0, request@~2.79.0:
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.7"
|
||||
oauth-sign "~0.8.1"
|
||||
qs "~6.3.0"
|
||||
performance-now "^0.2.0"
|
||||
qs "~6.4.0"
|
||||
safe-buffer "^5.0.1"
|
||||
stringstream "~0.0.4"
|
||||
tough-cookie "~2.3.0"
|
||||
tunnel-agent "~0.4.1"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.0.0"
|
||||
|
||||
require-directory@^2.1.1:
|
||||
|
@ -27,7 +27,11 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
|
||||
.processor(require('./processors/simplifyMemberAnchors'))
|
||||
|
||||
// Where do we get the source files?
|
||||
.config(function(readTypeScriptModules, readFilesProcessor, collectExamples) {
|
||||
.config(function(readTypeScriptModules, readFilesProcessor, collectExamples, tsParser) {
|
||||
|
||||
// Tell TypeScript how to load modules that start with with `@angular`
|
||||
tsParser.options.paths = { '@angular/*': [API_SOURCE_PATH + '/*'] };
|
||||
tsParser.options.baseUrl = '.';
|
||||
|
||||
// API files are typescript
|
||||
readTypeScriptModules.basePath = API_SOURCE_PATH;
|
||||
@ -124,4 +128,5 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
|
||||
convertToJsonProcessor.docTypes = convertToJsonProcessor.docTypes.concat(DOCS_TO_CONVERT);
|
||||
postProcessHtml.docTypes = convertToJsonProcessor.docTypes.concat(DOCS_TO_CONVERT);
|
||||
autoLinkCode.docTypes = DOCS_TO_CONVERT;
|
||||
autoLinkCode.codeElements = ['code', 'code-example', 'code-pane'];
|
||||
});
|
||||
|
@ -1,36 +1,71 @@
|
||||
const visit = require('unist-util-visit');
|
||||
const visit = require('unist-util-visit-parents');
|
||||
const is = require('hast-util-is-element');
|
||||
const textContent = require('hast-util-to-string');
|
||||
|
||||
/**
|
||||
* Automatically add in a link to the relevant document for simple
|
||||
* code blocks, e.g. `<code>MyClass</code>` becomes
|
||||
* `<code><a href="path/to/myclass">MyClass</a></code>`
|
||||
* Automatically add in a link to the relevant document for code blocks.
|
||||
* E.g. `<code>MyClass</code>` becomes `<code><a href="path/to/myclass">MyClass</a></code>`
|
||||
*
|
||||
* @property docTypes an array of strings. Only docs that have one of these docTypes
|
||||
* will be linked to.
|
||||
* @property docTypes an array of strings.
|
||||
* Only docs that have one of these docTypes will be linked to.
|
||||
* Usually set to the API exported docTypes, e.g. "class", "function", "directive", etc.
|
||||
*
|
||||
* @property codeElements an array of strings.
|
||||
* Only text contained in these elements will be linked to.
|
||||
* Usually set to "code" but also "code-example" for angular.io.
|
||||
*/
|
||||
module.exports = function autoLinkCode(getDocFromAlias) {
|
||||
autoLinkCodeImpl.docTypes = [];
|
||||
autoLinkCodeImpl.codeElements = ['code'];
|
||||
return autoLinkCodeImpl;
|
||||
|
||||
function autoLinkCodeImpl() {
|
||||
return (ast) => {
|
||||
visit(ast, node => {
|
||||
if (is(node, 'code')) {
|
||||
const docs = getDocFromAlias(textContent(node));
|
||||
if (docs.length === 1 && autoLinkCodeImpl.docTypes.indexOf(docs[0].docType) !== -1) {
|
||||
const link = {
|
||||
type: 'element',
|
||||
tagName: 'a',
|
||||
properties: { href: docs[0].path },
|
||||
children: node.children
|
||||
};
|
||||
node.children = [link];
|
||||
}
|
||||
visit(ast, 'element', (node, ancestors) => {
|
||||
// Only interested in code elements that are not inside links
|
||||
if (autoLinkCodeImpl.codeElements.some(elementType => is(node, elementType)) &&
|
||||
ancestors.every(ancestor => !is(ancestor, 'a'))) {
|
||||
visit(node, 'text', (node, ancestors) => {
|
||||
// Only interested in text nodes that are not inside links
|
||||
if (ancestors.every(ancestor => !is(ancestor, 'a'))) {
|
||||
|
||||
const parent = ancestors[ancestors.length-1];
|
||||
const index = parent.children.indexOf(node);
|
||||
|
||||
// Can we convert the whole text node into a doc link?
|
||||
const docs = getDocFromAlias(node.value);
|
||||
if (foundValidDoc(docs)) {
|
||||
parent.children.splice(index, 1, createLinkNode(docs[0], node.value));
|
||||
} else {
|
||||
// Parse the text for words that we can convert to links
|
||||
const nodes = textContent(node).split(/([A-Za-z0-9_]+)/)
|
||||
.filter(word => word.length)
|
||||
.map(word => {
|
||||
const docs = getDocFromAlias(word);
|
||||
return foundValidDoc(docs) ?
|
||||
createLinkNode(docs[0], word) : // Create a link wrapping the text node.
|
||||
{ type: 'text', value: word }; // this is just text so push a new text node
|
||||
});
|
||||
|
||||
// Replace the text node with the links and leftover text nodes
|
||||
Array.prototype.splice.apply(parent.children, [index, 1].concat(nodes));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
function foundValidDoc(docs) {
|
||||
return docs.length === 1 && autoLinkCodeImpl.docTypes.indexOf(docs[0].docType) !== -1;
|
||||
}
|
||||
|
||||
function createLinkNode(doc, text) {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'a',
|
||||
properties: { href: doc.path, class: 'code-anchor' },
|
||||
children: [{ type: 'text', value: text }]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ describe('autoLinkCode post-processor', () => {
|
||||
const dgeni = new Dgeni([testPackage]);
|
||||
const injector = dgeni.configureInjector();
|
||||
autoLinkCode = injector.get('autoLinkCode');
|
||||
autoLinkCode.docTypes = ['class', 'pipe'];
|
||||
autoLinkCode.docTypes = ['class', 'pipe', 'function', 'const'];
|
||||
aliasMap = injector.get('aliasMap');
|
||||
processor = injector.get('postProcessHtml');
|
||||
processor.docTypes = ['test-doc'];
|
||||
@ -20,14 +20,14 @@ describe('autoLinkCode post-processor', () => {
|
||||
aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' });
|
||||
const doc = { docType: 'test-doc', renderedContent: '<code>MyClass</code>' };
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code><a href="a/b/myclass">MyClass</a></code>');
|
||||
expect(doc.renderedContent).toEqual('<code><a href="a/b/myclass" class="code-anchor">MyClass</a></code>');
|
||||
});
|
||||
|
||||
it('should insert an anchor into every code item that matches an alias of an API doc', () => {
|
||||
aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass', 'foo.MyClass'], path: 'a/b/myclass' });
|
||||
const doc = { docType: 'test-doc', renderedContent: '<code>foo.MyClass</code>' };
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code><a href="a/b/myclass">foo.MyClass</a></code>');
|
||||
expect(doc.renderedContent).toEqual('<code><a href="a/b/myclass" class="code-anchor">foo.MyClass</a></code>');
|
||||
});
|
||||
|
||||
it('should ignore code items that do not match a link to an API doc', () => {
|
||||
@ -36,4 +36,42 @@ describe('autoLinkCode post-processor', () => {
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code>MyClass</code>');
|
||||
});
|
||||
|
||||
it('should ignore code items that are already inside a link', () => {
|
||||
aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' });
|
||||
const doc = { docType: 'test-doc', renderedContent: '<a href="..."><div><code>MyClass</code></div></a>' };
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<a href="..."><div><code>MyClass</code></div></a>');
|
||||
});
|
||||
|
||||
it('should ignore code items match an API doc but are not in the list of acceptable docTypes', () => {
|
||||
aliasMap.addDoc({ docType: 'directive', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' });
|
||||
const doc = { docType: 'test-doc', renderedContent: '<code>MyClass</code>' };
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code>MyClass</code>');
|
||||
});
|
||||
|
||||
it('should insert anchors for individual text nodes within a code block', () => {
|
||||
aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' });
|
||||
const doc = { docType: 'test-doc', renderedContent: '<code><span>MyClass</span><span>MyClass</span></code>' };
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code><span><a href="a/b/myclass" class="code-anchor">MyClass</a></span><span><a href="a/b/myclass" class="code-anchor">MyClass</a></span></code>');
|
||||
});
|
||||
|
||||
it('should insert anchors for words that match within text nodes in a code block', () => {
|
||||
aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' });
|
||||
aliasMap.addDoc({ docType: 'function', id: 'myFunc', aliases: ['myFunc'], path: 'ng/myfunc' });
|
||||
aliasMap.addDoc({ docType: 'const', id: 'MY_CONST', aliases: ['MY_CONST'], path: 'ng/my_const' });
|
||||
const doc = { docType: 'test-doc', renderedContent: '<code>myFunc() {\n return new MyClass(MY_CONST);\n}</code>' };
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code><a href="ng/myfunc" class="code-anchor">myFunc</a>() {\n return new <a href="a/b/myclass" class="code-anchor">MyClass</a>(<a href="ng/my_const" class="code-anchor">MY_CONST</a>);\n}</code>');
|
||||
});
|
||||
|
||||
it('should work with custom elements', () => {
|
||||
autoLinkCode.codeElements = ['code-example'];
|
||||
aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' });
|
||||
const doc = { docType: 'test-doc', renderedContent: '<code-example>MyClass</code-example>' };
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code-example><a href="a/b/myclass" class="code-anchor">MyClass</a></code-example>');
|
||||
});
|
||||
});
|
||||
|
@ -7,8 +7,8 @@
|
||||
{% block additional %}{% endblock %}
|
||||
{% include "includes/description.html" %}
|
||||
{$ memberHelpers.renderMemberDetails(doc.statics, 'static-members', 'static-member', 'Static Members') $}
|
||||
{% include "includes/constructor.html" %}
|
||||
{% if doc.constructorDoc %}{$ memberHelpers.renderMemberDetails([doc.constructorDoc], 'constructors', 'constructor', 'Constructor') $}{% endif %}
|
||||
{$ memberHelpers.renderMemberDetails(doc.members, 'instance-members', 'instance-member', 'Members') $}
|
||||
{% include "includes/annotations.html" %}
|
||||
{% block annotations %}{% include "includes/annotations.html" %}{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -9,3 +9,5 @@
|
||||
{$ directiveHelper.renderBindings(doc.outputs, 'outputs', 'output', 'Outputs') $}
|
||||
{% include "includes/export-as.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block annotations %}{% endblock %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<section class="annotations">
|
||||
<h2>Annotations</h2>
|
||||
{%- for decorator in doc.decorators %}
|
||||
<code-example hideCopy="true" class="no-box api-heading">@{$ decorator.name $}{$ params.paramList(decorator.arguments) $}</code-example>
|
||||
<code-example hideCopy="true" class="no-box api-heading">@{$ decorator.name $}({$ decorator.arguments $})</code-example>
|
||||
{% if not decorator.notYetDocumented %}{$ decorator.description | marked $}{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
@ -4,10 +4,11 @@
|
||||
<h2>Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">
|
||||
{$ doc.docType $} {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} {
|
||||
{%- if doc.constructorDoc %}{% if not doc.constructorDoc.internal %}
|
||||
<a class="code-anchor" href="#{$ doc.constructorDoc.anchor $}">{$ memberHelper.renderMember(doc.constructorDoc, 1) $}</a>{% endif %}{% endif -%}
|
||||
{%- if doc.statics.length %}{% for member in doc.statics %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
{%- if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif -%}
|
||||
{$ memberHelper.renderMembers(doc) $}
|
||||
}
|
||||
</code-example>
|
||||
</section>
|
||||
|
@ -1,8 +0,0 @@
|
||||
{%- if doc.constructorDoc and not doc.constructorDoc.internal %}
|
||||
<section class="constructor">
|
||||
<a id="{$ doc.constructorDoc.name $}"></a>
|
||||
<h2>Constructor</h2>
|
||||
<code-example hideCopy="true" class="no-box api-heading">{$ doc.constructorDoc.name $}{$ params.paramList(doc.constructorDoc.parameters) $}</code-example>
|
||||
{% if not doc.constructorDoc.notYetDocumented %}{$ doc.constructorDoc.description | marked $}{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
@ -3,8 +3,7 @@
|
||||
<section class="decorator-overview">
|
||||
<h2>Metadata Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">
|
||||
@{$ doc.name $}{$ doc.typeParams | escape $}({ {% if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
@{$ doc.name $}{$ doc.typeParams | escape $}({ {$ memberHelper.renderMembers(doc) $}
|
||||
})
|
||||
</code-example>
|
||||
</section>
|
@ -3,12 +3,11 @@
|
||||
<section class="{$ doc.docType $}-overview">
|
||||
<h2>Overview</h2>
|
||||
<code-example language="ts" hideCopy="true">{% for decorator in doc.decorators %}
|
||||
<a href="#annotations">@{$ decorator.name $}{$ params.paramList(decorator.arguments) $}</a>{% endfor %}
|
||||
@{$ decorator.name $}({$ decorator.arguments $}){% endfor %}
|
||||
class {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} {
|
||||
{%- if doc.statics.length %}{% for member in doc.statics %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
{%- if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif -%}
|
||||
{$ memberHelper.renderMembers(doc) $}
|
||||
}
|
||||
</code-example>
|
||||
</section>
|
||||
|
@ -1,10 +1,9 @@
|
||||
{%- if doc.selector %}
|
||||
<section class="selectors">
|
||||
<h2>Selectors</h2>
|
||||
{% for selector in doc.selector.split(',') %}
|
||||
<div class="selector">
|
||||
<code>{$ selector $}</code>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<code-example hideCopy="true" class="no-box api-heading selector">
|
||||
{%- for selector in doc.selector.split(',') %}
|
||||
{$ selector $}{% endfor %}
|
||||
</code-example>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
@ -2,13 +2,20 @@
|
||||
|
||||
{%- macro renderHeritage(exportDoc) -%}
|
||||
{%- if exportDoc.extendsClauses.length %} extends {% for clause in exportDoc.extendsClauses -%}
|
||||
{$ clause $}{% if not loop.last %}, {% endif -%}
|
||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
||||
{% endfor %}{% endif %}
|
||||
{%- if exportDoc.implementsClauses.length %} implements {% for clause in exportDoc.implementsClauses -%}
|
||||
{$ clause $}{% if not loop.last %}, {% endif -%}
|
||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
||||
{% endfor %}{% endif %}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro renderMembers(doc) -%}
|
||||
{%- if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||
<a class="code-anchor" href="{$ doc.path $}#{$ member.anchor $}">{$ renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||
{%- for ancestor in doc.extendsClauses %}{% if ancestor.doc %}
|
||||
// inherited from <a class="code-anchor" href="{$ ancestor.doc.path $}">{$ ancestor.doc.id $}</a>{$ renderMembers(ancestor.doc) $}{% endif %}{% endfor %}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro renderMember(member, truncateLines) -%}
|
||||
{%- if member.accessibility !== 'public' %}{$ member.accessibility $} {% endif -%}
|
||||
{%- if member.isGetAccessor %}get {% endif -%}
|
||||
|
@ -2002,9 +2002,9 @@ devtools-timeline-model@1.1.6:
|
||||
chrome-devtools-frontend "1.0.401423"
|
||||
resolve "1.1.7"
|
||||
|
||||
dgeni-packages@^0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.20.0.tgz#e7da99b0a119ee2eb584202d054a5aa01f23e208"
|
||||
dgeni-packages@^0.21.2:
|
||||
version "0.21.2"
|
||||
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.21.2.tgz#b031194176507b7c7d1c9735ea14664970763866"
|
||||
dependencies:
|
||||
canonical-path "0.0.2"
|
||||
catharsis "^0.8.1"
|
||||
@ -2026,7 +2026,7 @@ dgeni-packages@^0.20.0:
|
||||
source-map-support "^0.4.15"
|
||||
spdx-license-list "^2.1.0"
|
||||
stringmap "^0.2.2"
|
||||
typescript "^2.3.4"
|
||||
typescript "2.4"
|
||||
|
||||
dgeni@^0.4.7:
|
||||
version "0.4.7"
|
||||
@ -7619,7 +7619,7 @@ typescript@2.3.2, "typescript@>=2.0.0 <2.5.0":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.2.tgz#f0f045e196f69a72f06b25fd3bd39d01c3ce9984"
|
||||
|
||||
typescript@^2.3.3, typescript@^2.3.4:
|
||||
typescript@2.4, typescript@^2.3.3:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc"
|
||||
|
||||
@ -7789,6 +7789,10 @@ unist-util-stringify-position@^1.0.0:
|
||||
dependencies:
|
||||
has "^1.0.1"
|
||||
|
||||
unist-util-visit-parents@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-1.1.1.tgz#7d3f56b5b039a3c6e2d16e51cc093f10e4755342"
|
||||
|
||||
unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.1.1.tgz#e917a3b137658b335cb4420c7da2e74d928e4e94"
|
||||
|
4
build.sh
4
build.sh
@ -347,6 +347,10 @@ if [[ ${BUILD_TOOLS} == true ]]; then
|
||||
$(npm bin)/tsc -p ${TSCONFIG}
|
||||
|
||||
cp ./tools/@angular/tsc-wrapped/package.json ./dist/tools/@angular/tsc-wrapped
|
||||
|
||||
echo "====== VERSION: Updating version references"
|
||||
echo "====== EXECUTE: perl -p -i -e \"s/0\.0\.0\-PLACEHOLDER/${VERSION}/g\" $""(grep -ril 0\.0\.0\-PLACEHOLDER ./dist/tools/@angular/tsc-wrapped/package.json)"
|
||||
perl -p -i -e "s/0\.0\.0\-PLACEHOLDER/${VERSION}/g" $(grep -ril 0\.0\.0\-PLACEHOLDER ./dist/tools/@angular/tsc-wrapped/package.json) < /dev/null 2> /dev/null
|
||||
travisFoldEnd "build tools"
|
||||
fi
|
||||
|
||||
|
92
docs/CARETAKER.md
Normal file
92
docs/CARETAKER.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Caretaker
|
||||
|
||||
Caretaker is responsible for merging PRs into the individual branches and internally at Google.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Draining the queue of PRs ready to be merged. (PRs with [`PR action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22PR+action%3A+merge%22) label)
|
||||
- Assigining [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
|
||||
|
||||
## Setup
|
||||
|
||||
### Set `upstream` to fetch PRs into your local repo
|
||||
|
||||
Use this conmmands to configure your `git` to fetch PRs into your local repo.
|
||||
|
||||
```
|
||||
git remote add upstream git@github.com:angular/angular.git
|
||||
git config --add remote.upstream.fetch +refs/pull/*/head:refs/remotes/upstream/pr/*
|
||||
```
|
||||
|
||||
|
||||
## Merging the PR
|
||||
|
||||
A PR needs to have `PR action: merge` and `PR target: *` labels to be considered
|
||||
ready to merge. Merging is performed by running `merge-pr` with a PR number to merge.
|
||||
|
||||
NOTE: before running `merge-pr` ensure that you have synced all of the PRs
|
||||
locally by running:
|
||||
|
||||
```
|
||||
$ git fetch upstream
|
||||
```
|
||||
|
||||
To merge a PR run:
|
||||
|
||||
```
|
||||
$ ./scripts/github/merge-pr 1234
|
||||
```
|
||||
|
||||
The `merge-pr` script will:
|
||||
- Ensure that all approriate labels are on the PR.
|
||||
- That the current branch (`master` or `?.?.x` patch) mathches the `PR target: *` label.
|
||||
- It will `cherry-pick` all of the SHAs from the PR into the current branch.
|
||||
- It will rewrite commit history by automatically adding `Close #1234` and `(#1234)` into the commit message.
|
||||
|
||||
|
||||
### Recovering from failed `merge-pr` due to conflicts
|
||||
|
||||
When running `merge-pr` the script will output the commands which it is about to run.
|
||||
|
||||
```
|
||||
$ ./scripts/github/merge-pr 1234
|
||||
======================
|
||||
GitHub Merge PR Steps
|
||||
======================
|
||||
git cherry-pick upstream/pr/1234~1..upstream/pr/1234
|
||||
git filter-branch -f --msg-filter "/usr/local/google/home/misko/angular-pr/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
|
||||
```
|
||||
|
||||
If the `cherry-pick` command fails than resolve conflicts and use `git cherry-pick --continue` once ready. After the `cherry-pick` is done cut&paste and run the `filter-branch` command to properly rewrite the messages
|
||||
|
||||
## Cherry-picking PRs into patch branch
|
||||
|
||||
In addition to merging PRs into the master branch, many PRs need to be also merged into a patch branch.
|
||||
Follow these steps to get path brach up to date.
|
||||
|
||||
1. Check out the most recent patch branch: `git checkout 4.3.x`
|
||||
2. Get a list of PRs merged into master: `git log master --oneline -n10`
|
||||
3. For each PR number in the commit message run: `././scripts/github/merge-pr 1234`
|
||||
- The PR will only merge if the `PR target:` matches the branch.
|
||||
|
||||
Once all of the PRs are in patch branch, push the all branches and tags to github using `push-upstream` script.
|
||||
|
||||
|
||||
## Pushing merged PRs into github
|
||||
|
||||
Use `push-upstream` script to push all of the branch and tags to github.
|
||||
|
||||
```
|
||||
$ ./scripts/github/push-upstream
|
||||
git push git@github.com:angular/angular.git master:master 4.3.x:4.3.x
|
||||
Counting objects: 25, done.
|
||||
Delta compression using up to 6 threads.
|
||||
Compressing objects: 100% (17/17), done.
|
||||
Writing objects: 100% (25/25), 2.22 KiB | 284.00 KiB/s, done.
|
||||
Total 25 (delta 22), reused 8 (delta 7)
|
||||
remote: Resolving deltas: 100% (22/22), completed with 18 local objects.
|
||||
To github.com:angular/angular.git
|
||||
079d884b6..d1c4a94bb master -> master
|
||||
git push --tags -f git@github.com:angular/angular.git patch_sync:patch_sync
|
||||
Everything up-to-date
|
||||
```
|
@ -13,8 +13,8 @@ Change approvals in our monorepo are managed via [pullapprove.com](https://about
|
||||
|
||||
# Merging
|
||||
|
||||
Once a change has all the approvals either the last approver or the PR author (if PR author has the project collaborator status) should mark the PR with "PR: merge" label.
|
||||
This signals to the caretaker that the PR should be merged.
|
||||
Once a change has all the approvals either the last approver or the PR author (if PR author has the project collaborator status) should mark the PR with `PR: merge` as well as `PR target: *` labels.
|
||||
This signals to the caretaker that the PR should be merged. See [merge instructions](../CARETAKER.md).
|
||||
|
||||
# Who is the Caretaker?
|
||||
|
||||
|
@ -6,12 +6,12 @@
|
||||
"@angular/animations": "file:../../dist/packages-dist/animations",
|
||||
"@angular/common": "file:../../dist/packages-dist/common",
|
||||
"@angular/compiler": "file:../../dist/packages-dist/compiler",
|
||||
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
|
||||
"@angular/core": "file:../../dist/packages-dist/core",
|
||||
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
|
||||
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
|
||||
"@angular/tsc-wrapped": "file:../../dist/tools/@angular/tsc-wrapped",
|
||||
"google-closure-compiler": "20170409.0.0",
|
||||
"reflect-metadata": "^0.1.2",
|
||||
"rxjs": "5.3.1",
|
||||
"typescript": "2.1.6",
|
||||
"zone.js": "0.8.6"
|
||||
@ -29,4 +29,4 @@
|
||||
"preprotractor": "tsc -p e2e",
|
||||
"protractor": "protractor e2e/protractor.config.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
"@angular/animations": "file:../../dist/packages-dist/animations",
|
||||
"@angular/common": "file:../../dist/packages-dist/common",
|
||||
"@angular/compiler": "file:../../dist/packages-dist/compiler",
|
||||
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
|
||||
"@angular/core": "file:../../dist/packages-dist/core",
|
||||
"@angular/language-service": "file:../../dist/packages-dist/language-service",
|
||||
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
|
||||
|
@ -24,6 +24,15 @@ for testDir in $(ls | grep -v node_modules) ; do
|
||||
# Workaround for https://github.com/yarnpkg/yarn/issues/2256
|
||||
rm -f yarn.lock
|
||||
yarn install --cache-folder ../$cache
|
||||
|
||||
# this is a workaround for resolving tsc-wrapped as the local version not available on npm
|
||||
# tsc-wrapped is installed as a transitive dependency of compiler-cli
|
||||
if [[ $testDir == "hello_world__closure" || $$testDir == "language_service_plugin" ]]; then
|
||||
# compiler-cli needs to be installed while offline so that it doesn't try to look up tsc-wrapped version on npm
|
||||
# it should instead use the version that is already installed in the project via package.json
|
||||
yarn add --offline ../../dist/packages-dist/compiler-cli --cache-folder ../$cache
|
||||
fi
|
||||
|
||||
yarn test || exit 1
|
||||
)
|
||||
done
|
||||
|
@ -7,7 +7,6 @@
|
||||
"@angular/animations": "file:../../dist/packages-dist/animations",
|
||||
"@angular/common": "file:../../dist/packages-dist/common",
|
||||
"@angular/compiler": "file:../../dist/packages-dist/compiler",
|
||||
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
|
||||
"@angular/core": "file:../../dist/packages-dist/core",
|
||||
"@angular/forms": "file:../../dist/packages-dist/forms",
|
||||
"@angular/http": "file:../../dist/packages-dist/http",
|
||||
|
@ -7,7 +7,6 @@
|
||||
"@angular/animations": "file:../../dist/packages-dist/animations",
|
||||
"@angular/common": "file:../../dist/packages-dist/common",
|
||||
"@angular/compiler": "file:../../dist/packages-dist/compiler",
|
||||
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
|
||||
"@angular/core": "file:../../dist/packages-dist/core",
|
||||
"@angular/forms": "file:../../dist/packages-dist/forms",
|
||||
"@angular/http": "file:../../dist/packages-dist/http",
|
||||
|
@ -7,7 +7,6 @@
|
||||
"@angular/animations": "file:../../dist/packages-dist/animations",
|
||||
"@angular/common": "file:../../dist/packages-dist/common",
|
||||
"@angular/compiler": "file:../../dist/packages-dist/compiler",
|
||||
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
|
||||
"@angular/core": "file:../../dist/packages-dist/core",
|
||||
"@angular/forms": "file:../../dist/packages-dist/forms",
|
||||
"@angular/http": "file:../../dist/packages-dist/http",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "4.3.5",
|
||||
"version": "4.4.3",
|
||||
"private": true,
|
||||
"branchPattern": "2.0.*",
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
@ -10,7 +10,7 @@
|
||||
"engines": {
|
||||
"node": ">=6.9.5 <7.0.0",
|
||||
"npm": ">=3.10.7 <4.0.0",
|
||||
"yarn": ">=0.21.3 <1.0.0"
|
||||
"yarn": ">=1.0.2 <2.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -817,6 +817,7 @@ export class TransitionAnimationEngine {
|
||||
|
||||
const disabledElementsSet = new Set<any>();
|
||||
this.disabledNodes.forEach(node => {
|
||||
disabledElementsSet.add(node);
|
||||
const nodesThatAreDisabled = this.driver.query(node, QUEUED_SELECTOR, true);
|
||||
for (let i = 0; i < nodesThatAreDisabled.length; i++) {
|
||||
disabledElementsSet.add(nodesThatAreDisabled[i]);
|
||||
@ -836,7 +837,7 @@ export class TransitionAnimationEngine {
|
||||
}
|
||||
|
||||
const allLeaveNodes: any[] = [];
|
||||
const leaveNodesWithoutAnimations: any[] = [];
|
||||
const leaveNodesWithoutAnimations = new Set<any>();
|
||||
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
|
||||
const element = this.collectedLeaveElements[i];
|
||||
const details = element[REMOVAL_FLAG] as ElementAnimationState;
|
||||
@ -844,7 +845,7 @@ export class TransitionAnimationEngine {
|
||||
addClass(element, LEAVE_CLASSNAME);
|
||||
allLeaveNodes.push(element);
|
||||
if (!details.hasAnimation) {
|
||||
leaveNodesWithoutAnimations.push(element);
|
||||
leaveNodesWithoutAnimations.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -936,12 +937,14 @@ export class TransitionAnimationEngine {
|
||||
}
|
||||
|
||||
// these can only be detected here since we have a map of all the elements
|
||||
// that have animations attached to them...
|
||||
const enterNodesWithoutAnimations: any[] = [];
|
||||
// that have animations attached to them... We use a set here in the event
|
||||
// multiple enter captures on the same element were caught in different
|
||||
// renderer namespaces (e.g. when a @trigger was on a host binding that had *ngIf)
|
||||
const enterNodesWithoutAnimations = new Set<any>();
|
||||
for (let i = 0; i < allEnterNodes.length; i++) {
|
||||
const element = allEnterNodes[i];
|
||||
if (!subTimelines.has(element)) {
|
||||
enterNodesWithoutAnimations.push(element);
|
||||
enterNodesWithoutAnimations.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
@ -960,22 +963,45 @@ export class TransitionAnimationEngine {
|
||||
const element = player.element;
|
||||
const previousPlayers =
|
||||
this._getPreviousPlayers(element, false, player.namespaceId, player.triggerName, null);
|
||||
previousPlayers.forEach(
|
||||
prevPlayer => { getOrSetAsInMap(allPreviousPlayersMap, element, []).push(prevPlayer); });
|
||||
previousPlayers.forEach(prevPlayer => {
|
||||
getOrSetAsInMap(allPreviousPlayersMap, element, []).push(prevPlayer);
|
||||
prevPlayer.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy()));
|
||||
|
||||
// PRE STAGE: fill the ! styles
|
||||
const preStylesMap = allPreStyleElements.size ?
|
||||
cloakAndComputeStyles(
|
||||
this.driver, enterNodesWithoutAnimations, allPreStyleElements, PRE_STYLE) :
|
||||
new Map<any, ɵStyleData>();
|
||||
// this is a special case for nodes that will be removed (either by)
|
||||
// having their own leave animations or by being queried in a container
|
||||
// that will be removed once a parent animation is complete. The idea
|
||||
// here is that * styles must be identical to ! styles because of
|
||||
// backwards compatibility (* is also filled in by default in many places).
|
||||
// Otherwise * styles will return an empty value or auto since the element
|
||||
// that is being getComputedStyle'd will not be visible (since * = destination)
|
||||
const replaceNodes = allLeaveNodes.filter(node => {
|
||||
return replacePostStylesAsPre(node, allPreStyleElements, allPostStyleElements);
|
||||
});
|
||||
|
||||
// POST STAGE: fill the * styles
|
||||
const postStylesMap = cloakAndComputeStyles(
|
||||
const [postStylesMap, allLeaveQueriedNodes] = cloakAndComputeStyles(
|
||||
this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
|
||||
|
||||
allLeaveQueriedNodes.forEach(node => {
|
||||
if (replacePostStylesAsPre(node, allPreStyleElements, allPostStyleElements)) {
|
||||
replaceNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
// PRE STAGE: fill the ! styles
|
||||
const [preStylesMap] = allPreStyleElements.size ?
|
||||
cloakAndComputeStyles(
|
||||
this.driver, enterNodesWithoutAnimations, allPreStyleElements, PRE_STYLE) :
|
||||
[new Map<any, ɵStyleData>()];
|
||||
|
||||
replaceNodes.forEach(node => {
|
||||
const post = postStylesMap.get(node);
|
||||
const pre = preStylesMap.get(node);
|
||||
postStylesMap.set(node, { ...post, ...pre } as any);
|
||||
});
|
||||
|
||||
const rootPlayers: TransitionAnimationPlayer[] = [];
|
||||
const subPlayers: TransitionAnimationPlayer[] = [];
|
||||
queuedInstructions.forEach(entry => {
|
||||
@ -1015,11 +1041,20 @@ export class TransitionAnimationEngine {
|
||||
} else {
|
||||
eraseStyles(element, instruction.fromStyles);
|
||||
player.onDestroy(() => setStyles(element, instruction.toStyles));
|
||||
// there still might be a ancestor player animating this
|
||||
// element therefore we will still add it as a sub player
|
||||
// even if its animation may be disabled
|
||||
subPlayers.push(player);
|
||||
if (disabledElementsSet.has(element)) {
|
||||
skippedPlayers.push(player);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// find all of the sub players' corresponding inner animation player
|
||||
subPlayers.forEach(player => {
|
||||
// even if any players are not found for a sub animation then it
|
||||
// will still complete itself after the next tick since it's Noop
|
||||
const playersForElement = skippedPlayersMap.get(player.element);
|
||||
if (playersForElement && playersForElement.length) {
|
||||
const innerPlayer = optimizeGroupPlayer(playersForElement);
|
||||
@ -1051,7 +1086,7 @@ export class TransitionAnimationEngine {
|
||||
// until that animation is over (or the parent queried animation)
|
||||
if (details && details.hasAnimation) continue;
|
||||
|
||||
let players: AnimationPlayer[] = [];
|
||||
let players: TransitionAnimationPlayer[] = [];
|
||||
|
||||
// if this element is queried or if it contains queried children
|
||||
// then we want for the element not to be removed from the page
|
||||
@ -1070,8 +1105,10 @@ export class TransitionAnimationEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (players.length) {
|
||||
removeNodesAfterAnimationDone(this, element, players);
|
||||
|
||||
const activePlayers = players.filter(p => !p.destroyed);
|
||||
if (activePlayers.length) {
|
||||
removeNodesAfterAnimationDone(this, element, activePlayers);
|
||||
} else {
|
||||
this.processLeaveNode(element);
|
||||
}
|
||||
@ -1141,10 +1178,6 @@ export class TransitionAnimationEngine {
|
||||
private _beforeAnimationBuild(
|
||||
namespaceId: string, instruction: AnimationTransitionInstruction,
|
||||
allPreviousPlayersMap: Map<any, TransitionAnimationPlayer[]>) {
|
||||
// it's important to do this step before destroying the players
|
||||
// so that the onDone callback below won't fire before this
|
||||
eraseStyles(instruction.element, instruction.fromStyles);
|
||||
|
||||
const triggerName = instruction.triggerName;
|
||||
const rootElement = instruction.element;
|
||||
|
||||
@ -1166,9 +1199,14 @@ export class TransitionAnimationEngine {
|
||||
if (realPlayer.beforeDestroy) {
|
||||
realPlayer.beforeDestroy();
|
||||
}
|
||||
player.destroy();
|
||||
players.push(player);
|
||||
});
|
||||
});
|
||||
|
||||
// this needs to be done so that the PRE/POST styles can be
|
||||
// computed properly without interfering with the previous animation
|
||||
eraseStyles(rootElement, instruction.fromStyles);
|
||||
}
|
||||
|
||||
private _buildAnimation(
|
||||
@ -1399,10 +1437,13 @@ function cloakElement(element: any, value?: string) {
|
||||
}
|
||||
|
||||
function cloakAndComputeStyles(
|
||||
driver: AnimationDriver, elements: any[], elementPropsMap: Map<any, Set<string>>,
|
||||
defaultStyle: string): Map<any, ɵStyleData> {
|
||||
const cloakVals = elements.map(element => cloakElement(element));
|
||||
driver: AnimationDriver, elements: Set<any>, elementPropsMap: Map<any, Set<string>>,
|
||||
defaultStyle: string): [Map<any, ɵStyleData>, any[]] {
|
||||
const cloakVals: string[] = [];
|
||||
elements.forEach(element => cloakVals.push(cloakElement(element)));
|
||||
|
||||
const valuesMap = new Map<any, ɵStyleData>();
|
||||
const failedElements: any[] = [];
|
||||
|
||||
elementPropsMap.forEach((props: Set<string>, element: any) => {
|
||||
const styles: ɵStyleData = {};
|
||||
@ -1413,13 +1454,17 @@ function cloakAndComputeStyles(
|
||||
// by a parent animation element being detached.
|
||||
if (!value || value.length == 0) {
|
||||
element[REMOVAL_FLAG] = NULL_REMOVED_QUERIED_STATE;
|
||||
failedElements.push(element);
|
||||
}
|
||||
});
|
||||
valuesMap.set(element, styles);
|
||||
});
|
||||
|
||||
elements.forEach((element, i) => cloakElement(element, cloakVals[i]));
|
||||
return valuesMap;
|
||||
// we use a index variable here since Set.forEach(a, i) does not return
|
||||
// an index value for the closure (but instead just the value)
|
||||
let i = 0;
|
||||
elements.forEach(element => cloakElement(element, cloakVals[i++]));
|
||||
return [valuesMap, failedElements];
|
||||
}
|
||||
|
||||
/*
|
||||
@ -1526,3 +1571,20 @@ function objEquals(a: {[key: string]: any}, b: {[key: string]: any}): boolean {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function replacePostStylesAsPre(
|
||||
element: any, allPreStyleElements: Map<any, Set<string>>,
|
||||
allPostStyleElements: Map<any, Set<string>>): boolean {
|
||||
const postEntry = allPostStyleElements.get(element);
|
||||
if (!postEntry) return false;
|
||||
|
||||
let preEntry = allPreStyleElements.get(element);
|
||||
if (preEntry) {
|
||||
postEntry.forEach(data => preEntry !.add(data));
|
||||
} else {
|
||||
allPreStyleElements.set(element, postEntry);
|
||||
}
|
||||
|
||||
allPostStyleElements.delete(element);
|
||||
return true;
|
||||
}
|
||||
|
@ -158,9 +158,9 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
|
||||
destroy(): void {
|
||||
if (!this._destroyed) {
|
||||
this._destroyed = true;
|
||||
this._resetDomPlayerState();
|
||||
this._onFinish();
|
||||
this._destroyed = true;
|
||||
this._onDestroyFns.forEach(fn => fn());
|
||||
this._onDestroyFns = [];
|
||||
}
|
||||
|
@ -411,8 +411,10 @@ export function main() {
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = trigger('something', [
|
||||
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
|
||||
state('z', style({opacity: 1})), transition('* => *', animate(1000))
|
||||
state('x', style({opacity: 0})),
|
||||
state('y', style({opacity: .5})),
|
||||
state('z', style({opacity: 1})),
|
||||
transition('* => *', animate(1000)),
|
||||
]);
|
||||
|
||||
registerTrigger(element, engine, trig);
|
||||
@ -428,7 +430,7 @@ export function main() {
|
||||
|
||||
const player2 = engine.players[0];
|
||||
|
||||
expect(parseFloat(element.style.opacity)).toEqual(.5);
|
||||
expect(parseFloat(element.style.opacity)).not.toEqual(.5);
|
||||
|
||||
player2.finish();
|
||||
expect(parseFloat(element.style.opacity)).toEqual(1);
|
||||
|
@ -282,11 +282,12 @@ export interface AnimationStaggerMetadata extends AnimationMetadata {
|
||||
* <div [@myAnimationTrigger]="myStatusExp">...</div>
|
||||
* ```
|
||||
*
|
||||
* ## Disable Child Animations
|
||||
* ## Disable Animations
|
||||
* A special animation control binding called `@.disabled` can be placed on an element which will
|
||||
then disable animations for any inner animation triggers situated within the element.
|
||||
then disable animations for any inner animation triggers situated within the element as well as
|
||||
any animations on the element itself.
|
||||
*
|
||||
* When true, the `@.disabled` binding will prevent inner animations from rendering. The example
|
||||
* When true, the `@.disabled` binding will prevent all animations from rendering. The example
|
||||
below shows how to use this feature:
|
||||
*
|
||||
* ```ts
|
||||
@ -312,8 +313,8 @@ export interface AnimationStaggerMetadata extends AnimationMetadata {
|
||||
* The `@childAnimation` trigger will not animate because `@.disabled` prevents it from happening
|
||||
(when true).
|
||||
*
|
||||
* Note that `@.disbled` will only disable inner animations (any animations running on the same
|
||||
element will not be disabled).
|
||||
* Note that `@.disbled` will only disable all animations (this means any animations running on
|
||||
* the same element will also be disabled).
|
||||
*
|
||||
* ### Disabling Animations Application-wide
|
||||
* When an area of the template is set to have animations disabled, **all** inner components will
|
||||
|
@ -6,16 +6,25 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
const globals = {
|
||||
'@angular/core': 'ng.core',
|
||||
'@angular/platform-browser': 'ng.platformBrowser',
|
||||
'rxjs/Observable': 'Rx',
|
||||
'rxjs/Subject': 'Rx',
|
||||
|
||||
'rxjs/observable/of': 'Rx.Observable.prototype',
|
||||
|
||||
'rxjs/operator/concatMap': 'Rx.Observable.prototype',
|
||||
'rxjs/operator/filter': 'Rx.Observable.prototype',
|
||||
'rxjs/operator/map': 'Rx.Observable.prototype',
|
||||
};
|
||||
|
||||
export default {
|
||||
entry: '../../../dist/packages-dist/common/@angular/common/http.es5.js',
|
||||
dest: '../../../dist/packages-dist/common/bundles/common-http.umd.js',
|
||||
format: 'umd',
|
||||
exports: 'named',
|
||||
moduleName: 'ng.commmon.http',
|
||||
globals: {
|
||||
'@angular/core': 'ng.core',
|
||||
'@angular/platform-browser': 'ng.platformBrowser',
|
||||
'rxjs/Observable': 'Rx',
|
||||
'rxjs/Subject': 'Rx'
|
||||
}
|
||||
moduleName: 'ng.common.http',
|
||||
external: Object.keys(globals),
|
||||
globals: globals
|
||||
};
|
||||
|
@ -107,7 +107,14 @@ export class HttpXhrBackend implements HttpBackend {
|
||||
|
||||
// Set the responseType if one was requested.
|
||||
if (req.responseType) {
|
||||
xhr.responseType = req.responseType.toLowerCase() as any;
|
||||
const responseType = req.responseType.toLowerCase();
|
||||
|
||||
// JSON responses need to be processed as text. This is because if the server
|
||||
// returns an XSSI-prefixed JSON response, the browser will fail to parse it,
|
||||
// xhr.response will be null, and xhr.responseText cannot be accessed to
|
||||
// retrieve the prefixed JSON data in order to strip the prefix. Thus, all JSON
|
||||
// is parsed by first requesting text and then applying JSON.parse.
|
||||
xhr.responseType = ((responseType !== 'json') ? responseType : 'text') as any;
|
||||
}
|
||||
|
||||
// Serialize the request body if one is present. If not, this will be set to null.
|
||||
@ -158,12 +165,6 @@ export class HttpXhrBackend implements HttpBackend {
|
||||
if (status !== 204) {
|
||||
// Use XMLHttpRequest.response if set, responseText otherwise.
|
||||
body = (typeof xhr.response === 'undefined') ? xhr.responseText : xhr.response;
|
||||
|
||||
// Strip a common XSSI prefix from string responses.
|
||||
// TODO: determine if this behavior should be optional and moved to an interceptor.
|
||||
if (typeof body === 'string') {
|
||||
body = body.replace(XSSI_PREFIX, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize another potential bug (this one comes from CORS).
|
||||
@ -179,8 +180,9 @@ export class HttpXhrBackend implements HttpBackend {
|
||||
|
||||
// Check whether the body needs to be parsed as JSON (in many cases the browser
|
||||
// will have done that already).
|
||||
if (ok && typeof body === 'string' && req.responseType === 'json') {
|
||||
if (ok && req.responseType === 'json' && typeof body === 'string') {
|
||||
// Attempt the parse. If it fails, a parse error should be delivered to the user.
|
||||
body = body.replace(XSSI_PREFIX, '');
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch (error) {
|
||||
|
@ -79,8 +79,8 @@ export class MockXMLHttpRequest {
|
||||
return new HttpHeaders(this.mockResponseHeaders).get(header);
|
||||
}
|
||||
|
||||
mockFlush(status: number, statusText: string, body: any|null) {
|
||||
if (this.responseType === 'text') {
|
||||
mockFlush(status: number, statusText: string, body?: string) {
|
||||
if (typeof body === 'string') {
|
||||
this.responseText = body;
|
||||
} else {
|
||||
this.response = body;
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ddescribe, describe, it} from '@angular/core/testing/src/testing_internal';
|
||||
import {ddescribe, describe, iit, it} from '@angular/core/testing/src/testing_internal';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
import {HttpRequest} from '../src/request';
|
||||
@ -87,14 +87,22 @@ export function main() {
|
||||
});
|
||||
it('handles a json response', () => {
|
||||
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
||||
factory.mock.mockFlush(200, 'OK', {data: 'some data'});
|
||||
factory.mock.mockFlush(200, 'OK', JSON.stringify({data: 'some data'}));
|
||||
expect(events.length).toBe(2);
|
||||
const res = events[1] as HttpResponse<{data: string}>;
|
||||
expect(res.body !.data).toBe('some data');
|
||||
});
|
||||
it('handles a json response that comes via responseText', () => {
|
||||
it('handles a json string response', () => {
|
||||
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
||||
factory.mock.mockFlush(200, 'OK', JSON.stringify({data: 'some data'}));
|
||||
expect(factory.mock.responseType).toEqual('text');
|
||||
factory.mock.mockFlush(200, 'OK', JSON.stringify('this is a string'));
|
||||
expect(events.length).toBe(2);
|
||||
const res = events[1] as HttpResponse<string>;
|
||||
expect(res.body).toEqual('this is a string');
|
||||
});
|
||||
it('handles a json response with an XSSI prefix', () => {
|
||||
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
||||
factory.mock.mockFlush(200, 'OK', ')]}\'\n' + JSON.stringify({data: 'some data'}));
|
||||
expect(events.length).toBe(2);
|
||||
const res = events[1] as HttpResponse<{data: string}>;
|
||||
expect(res.body !.data).toBe('some data');
|
||||
@ -299,7 +307,7 @@ export function main() {
|
||||
expect(error.status).toBe(0);
|
||||
done();
|
||||
});
|
||||
factory.mock.mockFlush(0, 'CORS 0 status', null);
|
||||
factory.mock.mockFlush(0, 'CORS 0 status');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -14,7 +14,7 @@
|
||||
export * from './location/index';
|
||||
export {NgLocaleLocalization, NgLocalization} from './localization';
|
||||
export {parseCookieValue as ɵparseCookieValue} from './cookie';
|
||||
export {CommonModule} from './common_module';
|
||||
export {CommonModule, DeprecatedI18NPipesModule} from './common_module';
|
||||
export {NgClass, NgFor, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index';
|
||||
export {DOCUMENT} from './dom_tokens';
|
||||
export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index';
|
||||
|
@ -29,3 +29,19 @@ import {COMMON_PIPES} from './pipes/index';
|
||||
})
|
||||
export class CommonModule {
|
||||
}
|
||||
|
||||
/**
|
||||
* I18N pipes are being changed to move away from using the JS Intl API.
|
||||
*
|
||||
* The former pipes relying on the Intl API will be moved to this module while the `CommonModule`
|
||||
* will contain the new pipes that do not rely on Intl.
|
||||
*
|
||||
* As a first step this module is created empty to ease the migration.
|
||||
*
|
||||
* see https://github.com/angular/angular/pull/18284
|
||||
*
|
||||
* @deprecated from v5
|
||||
*/
|
||||
@NgModule({declarations: [], exports: []})
|
||||
export class DeprecatedI18NPipesModule {
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
"ng-xi18n": "./src/extract_i18n.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/tsc-wrapped": "4.3.5",
|
||||
"@angular/tsc-wrapped": "0.0.0-PLACEHOLDER",
|
||||
"reflect-metadata": "^0.1.2",
|
||||
"minimist": "^1.2.0"
|
||||
},
|
||||
|
@ -104,6 +104,7 @@ export class CodeGenerator {
|
||||
locale: cliOptions.locale, missingTranslation,
|
||||
enableLegacyTemplate: options.enableLegacyTemplate !== false,
|
||||
enableSummariesForJit: options.enableSummariesForJit !== false,
|
||||
preserveWhitespaces: options.preserveWhitespaces,
|
||||
});
|
||||
return new CodeGenerator(options, program, tsCompilerHost, aotCompiler, ngCompilerHost);
|
||||
}
|
||||
|
@ -91,6 +91,10 @@ export interface CompilerOptions extends ts.CompilerOptions {
|
||||
|
||||
// Whether to enable support for <template> and the template attribute (true by default)
|
||||
enableLegacyTemplate?: boolean;
|
||||
|
||||
// Whether to remove blank text nodes from compiled templates. It is `true` by default
|
||||
// in Angular 4 and will be re-visited post Angular 5.
|
||||
preserveWhitespaces?: boolean;
|
||||
}
|
||||
|
||||
export interface ModuleFilenameResolver {
|
||||
|
@ -388,4 +388,4 @@ function createProgramWithStubsHost(
|
||||
fileExists = (fileName: string) =>
|
||||
this.generatedFiles.has(fileName) || originalHost.fileExists(fileName);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -257,9 +257,10 @@ export class AotCompiler {
|
||||
const providers: CompileProviderMetadata[] = [];
|
||||
|
||||
if (this._localeId) {
|
||||
const normalizedLocale = this._localeId.replace(/_/g, '-');
|
||||
providers.push({
|
||||
token: createTokenForExternalReference(this._reflector, Identifiers.LOCALE_ID),
|
||||
useValue: this._localeId,
|
||||
useValue: normalizedLocale,
|
||||
});
|
||||
}
|
||||
|
||||
@ -322,9 +323,10 @@ export class AotCompiler {
|
||||
const pipes = ngModule.transitiveModule.pipes.map(
|
||||
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
|
||||
|
||||
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
|
||||
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
|
||||
compMeta, compMeta.template !.template !, directives, pipes, ngModule.schemas,
|
||||
templateSourceUrl(ngModule.type, compMeta, compMeta.template !));
|
||||
templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces);
|
||||
const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]);
|
||||
const viewResult = this._viewCompiler.compileComponent(
|
||||
outputCtx, compMeta, parsedTemplate, stylesExpr, usedPipes);
|
||||
|
@ -54,6 +54,7 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
|
||||
useJit: false,
|
||||
enableLegacyTemplate: options.enableLegacyTemplate !== false,
|
||||
missingTranslation: options.missingTranslation,
|
||||
preserveWhitespaces: options.preserveWhitespaces,
|
||||
});
|
||||
const normalizer = new DirectiveNormalizer(
|
||||
{get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config);
|
||||
|
@ -15,4 +15,5 @@ export interface AotCompilerOptions {
|
||||
missingTranslation?: MissingTranslationStrategy;
|
||||
enableLegacyTemplate?: boolean;
|
||||
enableSummariesForJit?: boolean;
|
||||
preserveWhitespaces?: boolean;
|
||||
}
|
||||
|
@ -252,8 +252,9 @@ export class CompileTemplateMetadata {
|
||||
animations: any[];
|
||||
ngContentSelectors: string[];
|
||||
interpolation: [string, string]|null;
|
||||
preserveWhitespaces: boolean;
|
||||
constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets,
|
||||
animations, ngContentSelectors, interpolation, isInline}: {
|
||||
animations, ngContentSelectors, interpolation, isInline, preserveWhitespaces}: {
|
||||
encapsulation: ViewEncapsulation | null,
|
||||
template: string|null,
|
||||
templateUrl: string|null,
|
||||
@ -263,7 +264,8 @@ export class CompileTemplateMetadata {
|
||||
ngContentSelectors: string[],
|
||||
animations: any[],
|
||||
interpolation: [string, string]|null,
|
||||
isInline: boolean
|
||||
isInline: boolean,
|
||||
preserveWhitespaces: boolean
|
||||
}) {
|
||||
this.encapsulation = encapsulation;
|
||||
this.template = template;
|
||||
@ -278,6 +280,7 @@ export class CompileTemplateMetadata {
|
||||
}
|
||||
this.interpolation = interpolation;
|
||||
this.isInline = isInline;
|
||||
this.preserveWhitespaces = preserveWhitespaces;
|
||||
}
|
||||
|
||||
toSummary(): CompileTemplateSummary {
|
||||
@ -516,7 +519,8 @@ export function createHostComponentMeta(
|
||||
animations: [],
|
||||
isInline: true,
|
||||
externalStylesheets: [],
|
||||
interpolation: null
|
||||
interpolation: null,
|
||||
preserveWhitespaces: false,
|
||||
}),
|
||||
exportAs: null,
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
|
@ -24,7 +24,7 @@
|
||||
export {VERSION} from './version';
|
||||
export * from './template_parser/template_ast';
|
||||
export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser';
|
||||
export {CompilerConfig} from './config';
|
||||
export {CompilerConfig, preserveWhitespacesDefault} from './config';
|
||||
export * from './compile_metadata';
|
||||
export * from './aot/compiler_factory';
|
||||
export * from './aot/compiler';
|
||||
|
@ -6,11 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {InjectionToken, MissingTranslationStrategy, ViewEncapsulation, isDevMode} from '@angular/core';
|
||||
|
||||
import {CompileIdentifierMetadata} from './compile_metadata';
|
||||
import {Identifiers} from './identifiers';
|
||||
|
||||
import {MissingTranslationStrategy, ViewEncapsulation} from '@angular/core';
|
||||
import {noUndefined} from './util';
|
||||
|
||||
export class CompilerConfig {
|
||||
public defaultEncapsulation: ViewEncapsulation|null;
|
||||
@ -19,18 +16,26 @@ export class CompilerConfig {
|
||||
public enableLegacyTemplate: boolean;
|
||||
public useJit: boolean;
|
||||
public missingTranslation: MissingTranslationStrategy|null;
|
||||
public preserveWhitespaces: boolean;
|
||||
|
||||
constructor(
|
||||
{defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation,
|
||||
enableLegacyTemplate}: {
|
||||
enableLegacyTemplate, preserveWhitespaces}: {
|
||||
defaultEncapsulation?: ViewEncapsulation,
|
||||
useJit?: boolean,
|
||||
missingTranslation?: MissingTranslationStrategy,
|
||||
enableLegacyTemplate?: boolean,
|
||||
preserveWhitespaces?: boolean
|
||||
} = {}) {
|
||||
this.defaultEncapsulation = defaultEncapsulation;
|
||||
this.useJit = !!useJit;
|
||||
this.missingTranslation = missingTranslation || null;
|
||||
this.enableLegacyTemplate = enableLegacyTemplate !== false;
|
||||
this.preserveWhitespaces = preserveWhitespacesDefault(noUndefined(preserveWhitespaces));
|
||||
}
|
||||
}
|
||||
|
||||
export function preserveWhitespacesDefault(
|
||||
preserveWhitespacesOption: boolean | null, defaultSetting = true): boolean {
|
||||
return preserveWhitespacesOption === null ? defaultSetting : preserveWhitespacesOption;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
|
||||
|
||||
import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, templateSourceUrl} from './compile_metadata';
|
||||
import {CompilerConfig} from './config';
|
||||
import {CompilerConfig, preserveWhitespacesDefault} from './config';
|
||||
import {CompilerInjectable} from './injectable';
|
||||
import * as html from './ml_parser/ast';
|
||||
import {HtmlParser} from './ml_parser/html_parser';
|
||||
@ -31,6 +31,7 @@ export interface PrenormalizedTemplateMetadata {
|
||||
interpolation: [string, string]|null;
|
||||
encapsulation: ViewEncapsulation|null;
|
||||
animations: CompileAnimationEntryMetadata[];
|
||||
preserveWhitespaces: boolean|null;
|
||||
}
|
||||
|
||||
@CompilerInjectable()
|
||||
@ -82,6 +83,13 @@ export class DirectiveNormalizer {
|
||||
throw syntaxError(
|
||||
`No template specified for component ${stringify(prenormData.componentType)}`);
|
||||
}
|
||||
|
||||
if (isDefined(prenormData.preserveWhitespaces) &&
|
||||
typeof prenormData.preserveWhitespaces !== 'boolean') {
|
||||
throw syntaxError(
|
||||
`The preserveWhitespaces option for component ${stringify(prenormData.componentType)} must be a boolean`);
|
||||
}
|
||||
|
||||
return SyncAsync.then(
|
||||
this.normalizeTemplateOnly(prenormData),
|
||||
(result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result));
|
||||
@ -149,7 +157,9 @@ export class DirectiveNormalizer {
|
||||
ngContentSelectors: visitor.ngContentSelectors,
|
||||
animations: prenormData.animations,
|
||||
interpolation: prenormData.interpolation, isInline,
|
||||
externalStylesheets: []
|
||||
externalStylesheets: [],
|
||||
preserveWhitespaces: preserveWhitespacesDefault(
|
||||
prenormData.preserveWhitespaces, this._config.preserveWhitespaces),
|
||||
});
|
||||
}
|
||||
|
||||
@ -168,6 +178,7 @@ export class DirectiveNormalizer {
|
||||
animations: templateMeta.animations,
|
||||
interpolation: templateMeta.interpolation,
|
||||
isInline: templateMeta.isInline,
|
||||
preserveWhitespaces: templateMeta.preserveWhitespaces,
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -152,7 +152,8 @@ export class DirectiveResolver {
|
||||
styleUrls: directive.styleUrls,
|
||||
encapsulation: directive.encapsulation,
|
||||
animations: directive.animations,
|
||||
interpolation: directive.interpolation
|
||||
interpolation: directive.interpolation,
|
||||
preserveWhitespaces: directive.preserveWhitespaces,
|
||||
});
|
||||
} else {
|
||||
return new Directive({
|
||||
|
@ -262,6 +262,7 @@ export class JitCompiler implements Compiler {
|
||||
const externalStylesheetsByModuleUrl = new Map<string, CompiledStylesheet>();
|
||||
const outputContext = createOutputContext();
|
||||
const componentStylesheet = this._styleCompiler.compileComponent(outputContext, compMeta);
|
||||
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
|
||||
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
|
||||
const compiledStylesheet =
|
||||
this._styleCompiler.compileStyles(createOutputContext(), compMeta, stylesheetMeta);
|
||||
@ -274,7 +275,8 @@ export class JitCompiler implements Compiler {
|
||||
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
|
||||
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
|
||||
compMeta, compMeta.template !.template !, directives, pipes, template.ngModule.schemas,
|
||||
templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !));
|
||||
templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !),
|
||||
preserveWhitespaces);
|
||||
const compileResult = this._viewCompiler.compileComponent(
|
||||
outputContext, compMeta, parsedTemplate, ir.variable(componentStylesheet.stylesVar),
|
||||
usedPipes);
|
||||
|
@ -106,6 +106,7 @@ export class JitCompilerFactory implements CompilerFactory {
|
||||
defaultEncapsulation: ViewEncapsulation.Emulated,
|
||||
missingTranslation: MissingTranslationStrategy.Warning,
|
||||
enableLegacyTemplate: true,
|
||||
preserveWhitespaces: true,
|
||||
};
|
||||
|
||||
this._defaultOptions = [compilerOptions, ...defaultOptions];
|
||||
@ -125,6 +126,7 @@ export class JitCompilerFactory implements CompilerFactory {
|
||||
defaultEncapsulation: opts.defaultEncapsulation,
|
||||
missingTranslation: opts.missingTranslation,
|
||||
enableLegacyTemplate: opts.enableLegacyTemplate,
|
||||
preserveWhitespaces: opts.preserveWhitespaces,
|
||||
});
|
||||
},
|
||||
deps: []
|
||||
@ -152,6 +154,7 @@ function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions {
|
||||
providers: _mergeArrays(optionsArr.map(options => options.providers !)),
|
||||
missingTranslation: _lastDefined(optionsArr.map(options => options.missingTranslation)),
|
||||
enableLegacyTemplate: _lastDefined(optionsArr.map(options => options.enableLegacyTemplate)),
|
||||
preserveWhitespaces: _lastDefined(optionsArr.map(options => options.preserveWhitespaces)),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -219,7 +219,8 @@ export class CompileMetadataResolver {
|
||||
styles: template.styles,
|
||||
styleUrls: template.styleUrls,
|
||||
animations: template.animations,
|
||||
interpolation: template.interpolation
|
||||
interpolation: template.interpolation,
|
||||
preserveWhitespaces: template.preserveWhitespaces
|
||||
});
|
||||
if (isPromise(templateMeta) && isSync) {
|
||||
this._reportError(componentStillLoadingError(directiveType), directiveType);
|
||||
@ -267,7 +268,8 @@ export class CompileMetadataResolver {
|
||||
interpolation: noUndefined(dirMeta.interpolation),
|
||||
isInline: !!dirMeta.template,
|
||||
externalStylesheets: [],
|
||||
ngContentSelectors: []
|
||||
ngContentSelectors: [],
|
||||
preserveWhitespaces: noUndefined(dirMeta.preserveWhitespaces),
|
||||
});
|
||||
}
|
||||
|
||||
|
86
packages/compiler/src/ml_parser/html_whitespaces.ts
Normal file
86
packages/compiler/src/ml_parser/html_whitespaces.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as html from './ast';
|
||||
import {ParseTreeResult} from './parser';
|
||||
import {NGSP_UNICODE} from './tags';
|
||||
|
||||
export const PRESERVE_WS_ATTR_NAME = 'ngPreserveWhitespaces';
|
||||
|
||||
const SKIP_WS_TRIM_TAGS = new Set(['pre', 'template', 'textarea', 'script', 'style']);
|
||||
|
||||
function hasPreserveWhitespacesAttr(attrs: html.Attribute[]): boolean {
|
||||
return attrs.some((attr: html.Attribute) => attr.name === PRESERVE_WS_ATTR_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Angular Dart introduced &ngsp; as a placeholder for non-removable space, see:
|
||||
* https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart#L25-L32
|
||||
* In Angular Dart &ngsp; is converted to the 0xE500 PUA (Private Use Areas) unicode character
|
||||
* and later on replaced by a space. We are re-implementing the same idea here.
|
||||
*/
|
||||
export function replaceNgsp(value: string): string {
|
||||
// lexer is replacing the &ngsp; pseudo-entity with NGSP_UNICODE
|
||||
return value.replace(new RegExp(NGSP_UNICODE, 'g'), ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* This visitor can walk HTML parse tree and remove / trim text nodes using the following rules:
|
||||
* - consider spaces, tabs and new lines as whitespace characters;
|
||||
* - drop text nodes consisting of whitespace characters only;
|
||||
* - for all other text nodes replace consecutive whitespace characters with one space;
|
||||
* - convert &ngsp; pseudo-entity to a single space;
|
||||
*
|
||||
* Removal and trimming of whitespaces have positive performance impact (less code to generate
|
||||
* while compiling templates, faster view creation). At the same time it can be "destructive"
|
||||
* in some cases (whitespaces can influence layout). Because of the potential of breaking layout
|
||||
* this visitor is not activated by default in Angular 4 and people need to explicitly opt-in for
|
||||
* whitespace removal. The default option for whitespace removal will be revisited post Angular 5
|
||||
* and might be changed to "on" by default.
|
||||
*/
|
||||
class WhitespaceVisitor implements html.Visitor {
|
||||
visitElement(element: html.Element, context: any): any {
|
||||
if (SKIP_WS_TRIM_TAGS.has(element.name) || hasPreserveWhitespacesAttr(element.attrs)) {
|
||||
// don't descent into elements where we need to preserve whitespaces
|
||||
// but still visit all attributes to eliminate one used as a market to preserve WS
|
||||
return new html.Element(
|
||||
element.name, html.visitAll(this, element.attrs), element.children, element.sourceSpan,
|
||||
element.startSourceSpan, element.endSourceSpan);
|
||||
}
|
||||
|
||||
return new html.Element(
|
||||
element.name, element.attrs, html.visitAll(this, element.children), element.sourceSpan,
|
||||
element.startSourceSpan, element.endSourceSpan);
|
||||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: any): any {
|
||||
return attribute.name !== PRESERVE_WS_ATTR_NAME ? attribute : null;
|
||||
}
|
||||
|
||||
visitText(text: html.Text, context: any): any {
|
||||
const isBlank = text.value.trim().length === 0;
|
||||
|
||||
if (!isBlank) {
|
||||
return new html.Text(replaceNgsp(text.value).replace(/\s\s+/g, ' '), text.sourceSpan);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
visitComment(comment: html.Comment, context: any): any { return comment; }
|
||||
|
||||
visitExpansion(expansion: html.Expansion, context: any): any { return expansion; }
|
||||
|
||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
|
||||
}
|
||||
|
||||
export function removeWhitespaces(htmlAstWithErrors: ParseTreeResult): ParseTreeResult {
|
||||
return new ParseTreeResult(
|
||||
html.visitAll(new WhitespaceVisitor(), htmlAstWithErrors.rootNodes),
|
||||
htmlAstWithErrors.errors);
|
||||
}
|
@ -71,6 +71,7 @@ export function mergeNsAndName(prefix: string, localName: string): string {
|
||||
// This list is not exhaustive to keep the compiler footprint low.
|
||||
// The `{` / `ƫ` syntax should be used when the named character reference does not
|
||||
// exist.
|
||||
|
||||
export const NAMED_ENTITIES: {[k: string]: string} = {
|
||||
'Aacute': '\u00C1',
|
||||
'aacute': '\u00E1',
|
||||
@ -325,3 +326,9 @@ export const NAMED_ENTITIES: {[k: string]: string} = {
|
||||
'zwj': '\u200D',
|
||||
'zwnj': '\u200C',
|
||||
};
|
||||
|
||||
// The &ngsp; pseudo-entity is denoting a space. see:
|
||||
// https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart
|
||||
export const NGSP_UNICODE = '\uE500';
|
||||
|
||||
NAMED_ENTITIES['ngsp'] = NGSP_UNICODE;
|
||||
|
@ -219,7 +219,7 @@ export class BindingParser {
|
||||
// This will occur when a @trigger is not paired with an expression.
|
||||
// For animations it is valid to not have an expression since */void
|
||||
// states will be applied by angular when the element is attached/detached
|
||||
const ast = this._parseBinding(expression || 'null', false, sourceSpan);
|
||||
const ast = this._parseBinding(expression || 'undefined', false, sourceSpan);
|
||||
targetMatchableAttrs.push([name, ast.source !]);
|
||||
targetProps.push(new BoundProperty(name, ast, BoundPropertyType.ANIMATION, sourceSpan));
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import {Identifiers, createTokenForExternalReference, createTokenForReference} f
|
||||
import {CompilerInjectable} from '../injectable';
|
||||
import * as html from '../ml_parser/ast';
|
||||
import {ParseTreeResult} from '../ml_parser/html_parser';
|
||||
import {removeWhitespaces, replaceNgsp} from '../ml_parser/html_whitespaces';
|
||||
import {expandNodes} from '../ml_parser/icu_ast_expander';
|
||||
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||
import {isNgTemplate, splitNsName} from '../ml_parser/tags';
|
||||
@ -113,9 +114,10 @@ export class TemplateParser {
|
||||
|
||||
parse(
|
||||
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
|
||||
pipes: CompilePipeSummary[], schemas: SchemaMetadata[],
|
||||
templateUrl: string): {template: TemplateAst[], pipes: CompilePipeSummary[]} {
|
||||
const result = this.tryParse(component, template, directives, pipes, schemas, templateUrl);
|
||||
pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string,
|
||||
preserveWhitespaces: boolean): {template: TemplateAst[], pipes: CompilePipeSummary[]} {
|
||||
const result = this.tryParse(
|
||||
component, template, directives, pipes, schemas, templateUrl, preserveWhitespaces);
|
||||
const warnings =
|
||||
result.errors !.filter(error => error.level === ParseErrorLevel.WARNING)
|
||||
.filter(warnOnlyOnce(
|
||||
@ -137,12 +139,17 @@ export class TemplateParser {
|
||||
|
||||
tryParse(
|
||||
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
|
||||
pipes: CompilePipeSummary[], schemas: SchemaMetadata[],
|
||||
templateUrl: string): TemplateParseResult {
|
||||
pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string,
|
||||
preserveWhitespaces: boolean): TemplateParseResult {
|
||||
let htmlParseResult = this._htmlParser !.parse(
|
||||
template, templateUrl, true, this.getInterpolationConfig(component));
|
||||
|
||||
if (!preserveWhitespaces) {
|
||||
htmlParseResult = removeWhitespaces(htmlParseResult);
|
||||
}
|
||||
|
||||
return this.tryParseHtml(
|
||||
this.expandHtml(this._htmlParser !.parse(
|
||||
template, templateUrl, true, this.getInterpolationConfig(component))),
|
||||
component, directives, pipes, schemas);
|
||||
this.expandHtml(htmlParseResult), component, directives, pipes, schemas);
|
||||
}
|
||||
|
||||
tryParseHtml(
|
||||
@ -253,9 +260,10 @@ class TemplateParseVisitor implements html.Visitor {
|
||||
|
||||
visitText(text: html.Text, parent: ElementContext): any {
|
||||
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR) !;
|
||||
const expr = this._bindingParser.parseInterpolation(text.value, text.sourceSpan !);
|
||||
const valueNoNgsp = replaceNgsp(text.value);
|
||||
const expr = this._bindingParser.parseInterpolation(valueNoNgsp, text.sourceSpan !);
|
||||
return expr ? new BoundTextAst(expr, ngContentIndex, text.sourceSpan !) :
|
||||
new TextAst(text.value, ngContentIndex, text.sourceSpan !);
|
||||
new TextAst(valueNoNgsp, ngContentIndex, text.sourceSpan !);
|
||||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: any): any {
|
||||
@ -573,7 +581,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||
directive.inputs, props, directiveProperties, targetBoundDirectivePropNames);
|
||||
elementOrDirectiveRefs.forEach((elOrDirRef) => {
|
||||
if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
|
||||
(directive.exportAs == elOrDirRef.value)) {
|
||||
(elOrDirRef.isReferenceToDirective(directive))) {
|
||||
targetReferences.push(new ReferenceAst(
|
||||
elOrDirRef.name, createTokenForReference(directive.type.reference),
|
||||
elOrDirRef.sourceSpan));
|
||||
@ -798,8 +806,25 @@ class NonBindableVisitor implements html.Visitor {
|
||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to an element or directive in a template. E.g., the reference in this template:
|
||||
*
|
||||
* <div #myMenu="coolMenu">
|
||||
*
|
||||
* would be {name: 'myMenu', value: 'coolMenu', sourceSpan: ...}
|
||||
*/
|
||||
class ElementOrDirectiveRef {
|
||||
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
|
||||
/** Gets whether this is a reference to the given directive. */
|
||||
isReferenceToDirective(directive: CompileDirectiveSummary) {
|
||||
return splitExportAs(directive.exportAs).indexOf(this.value) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Splits a raw, potentially comma-delimted `exportAs` value into an array of names. */
|
||||
function splitExportAs(exportAs: string | null): string[] {
|
||||
return exportAs ? exportAs.split(',').map(e => e.trim()) : [];
|
||||
}
|
||||
|
||||
export function splitClasses(classAttrValue: string): string[] {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user