Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
35
CHANGELOG.md
35
CHANGELOG.md
@ -1,3 +1,30 @@
|
||||
<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)
|
||||
|
||||
@ -26,17 +53,10 @@
|
||||
|
||||
### 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)
|
||||
|
||||
@ -1143,7 +1163,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)
|
||||
|
@ -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 },
|
||||
|
@ -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}
|
||||
|
||||
|
||||
|
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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -320,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",
|
||||
|
@ -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!
|
||||
|
@ -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",
|
||||
|
@ -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'];
|
||||
});
|
||||
|
@ -3,34 +3,69 @@ 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, ancestors) => {
|
||||
if (is(node, 'code') && ancestors.every(ancestor => !is(ancestor, 'a'))) {
|
||||
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', () => {
|
||||
@ -43,4 +43,35 @@ describe('autoLinkCode post-processor', () => {
|
||||
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"
|
||||
|
||||
|
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?
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "4.3.6",
|
||||
"version": "4.4.2",
|
||||
"private": true,
|
||||
"branchPattern": "2.0.*",
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
|
@ -837,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;
|
||||
@ -845,7 +845,7 @@ export class TransitionAnimationEngine {
|
||||
addClass(element, LEAVE_CLASSNAME);
|
||||
allLeaveNodes.push(element);
|
||||
if (!details.hasAnimation) {
|
||||
leaveNodesWithoutAnimations.push(element);
|
||||
leaveNodesWithoutAnimations.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -937,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1435,9 +1437,11 @@ function cloakElement(element: any, value?: string) {
|
||||
}
|
||||
|
||||
function cloakAndComputeStyles(
|
||||
driver: AnimationDriver, elements: any[], elementPropsMap: Map<any, Set<string>>,
|
||||
driver: AnimationDriver, elements: Set<any>, elementPropsMap: Map<any, Set<string>>,
|
||||
defaultStyle: string): [Map<any, ɵStyleData>, any[]] {
|
||||
const cloakVals = elements.map(element => cloakElement(element));
|
||||
const cloakVals: string[] = [];
|
||||
elements.forEach(element => cloakVals.push(cloakElement(element)));
|
||||
|
||||
const valuesMap = new Map<any, ɵStyleData>();
|
||||
const failedElements: any[] = [];
|
||||
|
||||
@ -1456,7 +1460,10 @@ function cloakAndComputeStyles(
|
||||
valuesMap.set(element, styles);
|
||||
});
|
||||
|
||||
elements.forEach((element, i) => cloakElement(element, cloakVals[i]));
|
||||
// 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];
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,7 +9,7 @@
|
||||
"ng-xi18n": "./src/extract_i18n.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/tsc-wrapped": "4.3.6",
|
||||
"@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;
|
||||
|
@ -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[] {
|
||||
|
@ -821,6 +821,7 @@ const LIBRARY: MockDirectory = {
|
||||
'public-api.ts': `
|
||||
export * from './src/bolder.component';
|
||||
export * from './src/bolder.module';
|
||||
export {BolderModule as ReExportedModule} from './src/bolder.module';
|
||||
`,
|
||||
src: {
|
||||
'bolder.component.ts': `
|
||||
|
@ -6,8 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {CompileAnimationEntryMetadata} from '@angular/compiler';
|
||||
import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata';
|
||||
import {CompilerConfig} from '@angular/compiler/src/config';
|
||||
import {CompileStylesheetMetadata, CompileTemplateMetadata} from '@angular/compiler/src/compile_metadata';
|
||||
import {CompilerConfig, preserveWhitespacesDefault} from '@angular/compiler/src/config';
|
||||
import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer';
|
||||
import {ResourceLoader} from '@angular/compiler/src/resource_loader';
|
||||
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
|
||||
@ -31,6 +31,7 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
|
||||
interpolation?: [string, string] | null;
|
||||
encapsulation?: ViewEncapsulation | null;
|
||||
animations?: CompileAnimationEntryMetadata[];
|
||||
preserveWhitespaces?: boolean | null;
|
||||
}) {
|
||||
return normalizer.normalizeTemplate({
|
||||
ngModuleType: noUndefined(o.ngModuleType),
|
||||
@ -42,7 +43,8 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
|
||||
styleUrls: noUndefined(o.styleUrls),
|
||||
interpolation: noUndefined(o.interpolation),
|
||||
encapsulation: noUndefined(o.encapsulation),
|
||||
animations: noUndefined(o.animations)
|
||||
animations: noUndefined(o.animations),
|
||||
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
|
||||
});
|
||||
}
|
||||
|
||||
@ -54,6 +56,7 @@ function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: {
|
||||
interpolation?: [string, string] | null;
|
||||
encapsulation?: ViewEncapsulation | null;
|
||||
animations?: CompileAnimationEntryMetadata[];
|
||||
preserveWhitespaces?: boolean | null;
|
||||
}) {
|
||||
return normalizer.normalizeTemplateOnly({
|
||||
ngModuleType: noUndefined(o.ngModuleType),
|
||||
@ -65,13 +68,14 @@ function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: {
|
||||
styleUrls: noUndefined(o.styleUrls),
|
||||
interpolation: noUndefined(o.interpolation),
|
||||
encapsulation: noUndefined(o.encapsulation),
|
||||
animations: noUndefined(o.animations)
|
||||
animations: noUndefined(o.animations),
|
||||
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
|
||||
});
|
||||
}
|
||||
|
||||
function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls,
|
||||
externalStylesheets, animations, ngContentSelectors,
|
||||
interpolation, isInline}: {
|
||||
interpolation, isInline, preserveWhitespaces}: {
|
||||
encapsulation?: ViewEncapsulation | null,
|
||||
template?: string | null,
|
||||
templateUrl?: string | null,
|
||||
@ -81,7 +85,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
|
||||
ngContentSelectors?: string[],
|
||||
animations?: any[],
|
||||
interpolation?: [string, string] | null,
|
||||
isInline?: boolean
|
||||
isInline?: boolean,
|
||||
preserveWhitespaces?: boolean | null
|
||||
}): CompileTemplateMetadata {
|
||||
return new CompileTemplateMetadata({
|
||||
encapsulation: encapsulation || null,
|
||||
@ -94,6 +99,7 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
|
||||
animations: animations || [],
|
||||
interpolation: interpolation || null,
|
||||
isInline: !!isInline,
|
||||
preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)),
|
||||
});
|
||||
}
|
||||
|
||||
@ -106,6 +112,7 @@ function normalizeLoadedTemplate(
|
||||
interpolation?: [string, string] | null;
|
||||
encapsulation?: ViewEncapsulation | null;
|
||||
animations?: CompileAnimationEntryMetadata[];
|
||||
preserveWhitespaces?: boolean;
|
||||
},
|
||||
template: string, templateAbsUrl: string) {
|
||||
return normalizer.normalizeLoadedTemplate(
|
||||
@ -120,6 +127,7 @@ function normalizeLoadedTemplate(
|
||||
interpolation: o.interpolation || null,
|
||||
encapsulation: o.encapsulation || null,
|
||||
animations: o.animations || [],
|
||||
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
|
||||
},
|
||||
template, templateAbsUrl);
|
||||
}
|
||||
@ -169,6 +177,18 @@ export function main() {
|
||||
}))
|
||||
.toThrowError(`'SomeComp' component cannot define both template and templateUrl`);
|
||||
}));
|
||||
it('should throw if preserveWhitespaces is not a boolean',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
expect(() => normalizeTemplate(normalizer, {
|
||||
ngModuleType: null,
|
||||
componentType: SomeComp,
|
||||
moduleUrl: SOME_MODULE_URL,
|
||||
template: '',
|
||||
preserveWhitespaces: <any>'WRONG',
|
||||
}))
|
||||
.toThrowError(
|
||||
'The preserveWhitespaces option for component SomeComp must be a boolean');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('normalizeTemplateOnly sync', () => {
|
||||
@ -434,6 +454,28 @@ export function main() {
|
||||
expect(template.encapsulation).toBe(viewEncapsulation);
|
||||
}));
|
||||
|
||||
it('should use preserveWhitespaces setting from compiler config if none provided',
|
||||
inject(
|
||||
[DirectiveNormalizer, CompilerConfig],
|
||||
(normalizer: DirectiveNormalizer, config: CompilerConfig) => {
|
||||
const template = normalizeLoadedTemplate(normalizer, {}, '', '');
|
||||
expect(template.preserveWhitespaces).toBe(config.preserveWhitespaces);
|
||||
}));
|
||||
|
||||
it('should store the preserveWhitespaces=false in the result',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template =
|
||||
normalizeLoadedTemplate(normalizer, {preserveWhitespaces: false}, '', '');
|
||||
expect(template.preserveWhitespaces).toBe(false);
|
||||
}));
|
||||
|
||||
it('should store the preserveWhitespaces=true in the result',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template =
|
||||
normalizeLoadedTemplate(normalizer, {preserveWhitespaces: true}, '', '');
|
||||
expect(template.preserveWhitespaces).toBe(true);
|
||||
}));
|
||||
|
||||
it('should keep the template as html',
|
||||
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
|
||||
const template = normalizeLoadedTemplate(
|
||||
|
@ -79,7 +79,12 @@ class SomeDirectiveWithViewChild {
|
||||
c: any;
|
||||
}
|
||||
|
||||
@Component({selector: 'sample', template: 'some template', styles: ['some styles']})
|
||||
@Component({
|
||||
selector: 'sample',
|
||||
template: 'some template',
|
||||
styles: ['some styles'],
|
||||
preserveWhitespaces: true
|
||||
})
|
||||
class ComponentWithTemplate {
|
||||
}
|
||||
|
||||
@ -439,6 +444,7 @@ export function main() {
|
||||
const compMetadata: Component = resolver.resolve(ComponentWithTemplate);
|
||||
expect(compMetadata.template).toEqual('some template');
|
||||
expect(compMetadata.styles).toEqual(['some styles']);
|
||||
expect(compMetadata.preserveWhitespaces).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,7 +9,6 @@
|
||||
import {Component, Directive, Input} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
|
@ -97,4 +97,4 @@ const serializerVisitor = new _SerializerVisitor();
|
||||
|
||||
export function serializeNodes(nodes: html.Node[]): string[] {
|
||||
return nodes.map(node => node.visit(serializerVisitor, null));
|
||||
}
|
||||
}
|
||||
|
118
packages/compiler/test/ml_parser/html_whitespaces_spec.ts
Normal file
118
packages/compiler/test/ml_parser/html_whitespaces_spec.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @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 '../../src/ml_parser/ast';
|
||||
import {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||
import {PRESERVE_WS_ATTR_NAME, removeWhitespaces} from '../../src/ml_parser/html_whitespaces';
|
||||
|
||||
import {humanizeDom} from './ast_spec_utils';
|
||||
|
||||
export function main() {
|
||||
describe('removeWhitespaces', () => {
|
||||
|
||||
function parseAndRemoveWS(template: string): any[] {
|
||||
return humanizeDom(removeWhitespaces(new HtmlParser().parse(template, 'TestComp')));
|
||||
}
|
||||
|
||||
it('should remove blank text nodes', () => {
|
||||
expect(parseAndRemoveWS(' ')).toEqual([]);
|
||||
expect(parseAndRemoveWS('\n')).toEqual([]);
|
||||
expect(parseAndRemoveWS('\t')).toEqual([]);
|
||||
expect(parseAndRemoveWS(' \t \n ')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should remove whitespaces (space, tab, new line) between elements', () => {
|
||||
expect(parseAndRemoveWS('<br> <br>\t<br>\n<br>')).toEqual([
|
||||
[html.Element, 'br', 0],
|
||||
[html.Element, 'br', 0],
|
||||
[html.Element, 'br', 0],
|
||||
[html.Element, 'br', 0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove whitespaces from child text nodes', () => {
|
||||
expect(parseAndRemoveWS('<div><span> </span></div>')).toEqual([
|
||||
[html.Element, 'div', 0],
|
||||
[html.Element, 'span', 1],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove whitespaces from the beginning and end of a template', () => {
|
||||
expect(parseAndRemoveWS(` <br>\t`)).toEqual([
|
||||
[html.Element, 'br', 0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert &ngsp; to a space and preserve it', () => {
|
||||
expect(parseAndRemoveWS('<div><span>foo</span>&ngsp;<span>bar</span></div>')).toEqual([
|
||||
[html.Element, 'div', 0],
|
||||
[html.Element, 'span', 1],
|
||||
[html.Text, 'foo', 2],
|
||||
[html.Text, ' ', 1],
|
||||
[html.Element, 'span', 1],
|
||||
[html.Text, 'bar', 2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should replace multiple whitespaces with one space', () => {
|
||||
expect(parseAndRemoveWS('\n\n\nfoo\t\t\t')).toEqual([[html.Text, ' foo ', 0]]);
|
||||
expect(parseAndRemoveWS(' \n foo \t ')).toEqual([[html.Text, ' foo ', 0]]);
|
||||
});
|
||||
|
||||
it('should not replace single tab and newline with spaces', () => {
|
||||
expect(parseAndRemoveWS('\nfoo')).toEqual([[html.Text, '\nfoo', 0]]);
|
||||
expect(parseAndRemoveWS('\tfoo')).toEqual([[html.Text, '\tfoo', 0]]);
|
||||
});
|
||||
|
||||
it('should preserve single whitespaces between interpolations', () => {
|
||||
expect(parseAndRemoveWS(`{{fooExp}} {{barExp}}`)).toEqual([
|
||||
[html.Text, '{{fooExp}} {{barExp}}', 0],
|
||||
]);
|
||||
expect(parseAndRemoveWS(`{{fooExp}}\t{{barExp}}`)).toEqual([
|
||||
[html.Text, '{{fooExp}}\t{{barExp}}', 0],
|
||||
]);
|
||||
expect(parseAndRemoveWS(`{{fooExp}}\n{{barExp}}`)).toEqual([
|
||||
[html.Text, '{{fooExp}}\n{{barExp}}', 0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should preserve whitespaces around interpolations', () => {
|
||||
expect(parseAndRemoveWS(` {{exp}} `)).toEqual([
|
||||
[html.Text, ' {{exp}} ', 0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should preserve whitespaces inside <pre> elements', () => {
|
||||
expect(parseAndRemoveWS(`<pre><strong>foo</strong>\n<strong>bar</strong></pre>`)).toEqual([
|
||||
[html.Element, 'pre', 0],
|
||||
[html.Element, 'strong', 1],
|
||||
[html.Text, 'foo', 2],
|
||||
[html.Text, '\n', 1],
|
||||
[html.Element, 'strong', 1],
|
||||
[html.Text, 'bar', 2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip whitespace trimming in <textarea>', () => {
|
||||
expect(parseAndRemoveWS(`<textarea>foo\n\n bar</textarea>`)).toEqual([
|
||||
[html.Element, 'textarea', 0],
|
||||
[html.Text, 'foo\n\n bar', 1],
|
||||
]);
|
||||
});
|
||||
|
||||
it(`should preserve whitespaces inside elements annotated with ${PRESERVE_WS_ATTR_NAME}`,
|
||||
() => {
|
||||
expect(parseAndRemoveWS(`<div ${PRESERVE_WS_ATTR_NAME}><img> <img></div>`)).toEqual([
|
||||
[html.Element, 'div', 0],
|
||||
[html.Element, 'img', 1],
|
||||
[html.Text, ' ', 1],
|
||||
[html.Element, 'img', 1],
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {CompileQueryMetadata, CompilerConfig, JitReflector, ProxyClass, StaticSymbol} from '@angular/compiler';
|
||||
import {CompileQueryMetadata, CompilerConfig, JitReflector, ProxyClass, StaticSymbol, preserveWhitespacesDefault} from '@angular/compiler';
|
||||
import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata';
|
||||
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
|
||||
import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry';
|
||||
@ -84,7 +84,7 @@ function compileDirectiveMetadataCreate(
|
||||
|
||||
function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls,
|
||||
externalStylesheets, animations, ngContentSelectors,
|
||||
interpolation, isInline}: {
|
||||
interpolation, isInline, preserveWhitespaces}: {
|
||||
encapsulation?: ViewEncapsulation | null,
|
||||
template?: string | null,
|
||||
templateUrl?: string | null,
|
||||
@ -94,7 +94,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
|
||||
ngContentSelectors?: string[],
|
||||
animations?: any[],
|
||||
interpolation?: [string, string] | null,
|
||||
isInline?: boolean
|
||||
isInline?: boolean,
|
||||
preserveWhitespaces?: boolean | null,
|
||||
}): CompileTemplateMetadata {
|
||||
return new CompileTemplateMetadata({
|
||||
encapsulation: noUndefined(encapsulation),
|
||||
@ -106,7 +107,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
|
||||
animations: animations || [],
|
||||
ngContentSelectors: ngContentSelectors || [],
|
||||
interpolation: noUndefined(interpolation),
|
||||
isInline: !!isInline
|
||||
isInline: !!isInline,
|
||||
preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)),
|
||||
});
|
||||
}
|
||||
|
||||
@ -116,7 +118,7 @@ export function main() {
|
||||
let ngIf: CompileDirectiveSummary;
|
||||
let parse: (
|
||||
template: string, directives: CompileDirectiveSummary[], pipes?: CompilePipeSummary[],
|
||||
schemas?: SchemaMetadata[]) => TemplateAst[];
|
||||
schemas?: SchemaMetadata[], preserveWhitespaces?: boolean) => TemplateAst[];
|
||||
let console: ArrayConsole;
|
||||
|
||||
function commonBeforeEach() {
|
||||
@ -148,12 +150,15 @@ export function main() {
|
||||
|
||||
parse =
|
||||
(template: string, directives: CompileDirectiveSummary[],
|
||||
pipes: CompilePipeSummary[] | null = null,
|
||||
schemas: SchemaMetadata[] = []): TemplateAst[] => {
|
||||
pipes: CompilePipeSummary[] | null = null, schemas: SchemaMetadata[] = [],
|
||||
preserveWhitespaces = true): TemplateAst[] => {
|
||||
if (pipes === null) {
|
||||
pipes = [];
|
||||
}
|
||||
return parser.parse(component, template, directives, pipes, schemas, 'TestComp')
|
||||
return parser
|
||||
.parse(
|
||||
component, template, directives, pipes, schemas, 'TestComp',
|
||||
preserveWhitespaces)
|
||||
.template;
|
||||
};
|
||||
}));
|
||||
@ -398,7 +403,8 @@ export function main() {
|
||||
externalStylesheets: [],
|
||||
styleUrls: [],
|
||||
styles: [],
|
||||
encapsulation: null
|
||||
encapsulation: null,
|
||||
preserveWhitespaces: preserveWhitespacesDefault(null),
|
||||
}),
|
||||
isHost: false,
|
||||
exportAs: null,
|
||||
@ -417,7 +423,7 @@ export function main() {
|
||||
|
||||
});
|
||||
expect(humanizeTplAst(
|
||||
parser.parse(component, '{%a%}', [], [], [], 'TestComp').template,
|
||||
parser.parse(component, '{%a%}', [], [], [], 'TestComp', true).template,
|
||||
{start: '{%', end: '%}'}))
|
||||
.toEqual([[BoundTextAst, '{% a %}']]);
|
||||
}));
|
||||
@ -1203,6 +1209,24 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
|
||||
]);
|
||||
});
|
||||
|
||||
it('should assign references to directives via exportAs with multiple names', () => {
|
||||
const pizzaTestDirective =
|
||||
compileDirectiveMetadataCreate({
|
||||
selector: 'pizza-test',
|
||||
type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Pizza'}}),
|
||||
exportAs: 'pizza, cheeseSauceBread'
|
||||
}).toSummary();
|
||||
|
||||
const template = '<pizza-test #food="pizza" #yum="cheeseSauceBread"></pizza-test>';
|
||||
|
||||
expect(humanizeTplAst(parse(template, [pizzaTestDirective]))).toEqual([
|
||||
[ElementAst, 'pizza-test'],
|
||||
[ReferenceAst, 'food', createTokenForReference(pizzaTestDirective.type.reference)],
|
||||
[ReferenceAst, 'yum', createTokenForReference(pizzaTestDirective.type.reference)],
|
||||
[DirectiveAst, pizzaTestDirective],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report references with values that dont match a directive as errors', () => {
|
||||
expect(() => parse('<div #a="dirA"></div>', [])).toThrowError(`Template parse errors:
|
||||
There is no directive with "exportAs" set to "dirA" ("<div [ERROR ->]#a="dirA"></div>"): TestComp@0:5`);
|
||||
@ -1225,6 +1249,31 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
|
||||
|
||||
});
|
||||
|
||||
it('should report duplicate reference names when using mutliple exportAs names', () => {
|
||||
const pizzaDirective =
|
||||
compileDirectiveMetadataCreate({
|
||||
selector: '[dessert-pizza]',
|
||||
type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Pizza'}}),
|
||||
exportAs: 'dessertPizza, chocolate'
|
||||
}).toSummary();
|
||||
|
||||
const chocolateDirective =
|
||||
compileDirectiveMetadataCreate({
|
||||
selector: '[chocolate]',
|
||||
type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Chocolate'}}),
|
||||
exportAs: 'chocolate'
|
||||
}).toSummary();
|
||||
|
||||
const template = '<div dessert-pizza chocolate #snack="chocolate"></div>';
|
||||
const compileTemplate = () => parse(template, [pizzaDirective, chocolateDirective]);
|
||||
const duplicateReferenceError = 'Template parse errors:\n' +
|
||||
'Reference "#snack" is defined several times ' +
|
||||
'("<div dessert-pizza chocolate [ERROR ->]#snack="chocolate"></div>")' +
|
||||
': TestComp@0:29';
|
||||
|
||||
expect(compileTemplate).toThrowError(duplicateReferenceError);
|
||||
});
|
||||
|
||||
it('should not throw error when there is same reference name in different templates',
|
||||
() => {
|
||||
expect(() => parse('<div #a><template #a><span>OK</span></template></div>', []))
|
||||
@ -2052,6 +2101,66 @@ The pipe 'test' could not be found ("{{[ERROR ->]a | test}}"): TestComp@0:2`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitespaces removal', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureCompiler({providers: [TEST_COMPILER_PROVIDERS, MOCK_SCHEMA_REGISTRY]});
|
||||
});
|
||||
|
||||
commonBeforeEach();
|
||||
|
||||
it('should not remove whitespaces by default', () => {
|
||||
expect(humanizeTplAst(parse(' <br> <br>\t<br>\n<br> ', []))).toEqual([
|
||||
[TextAst, ' '],
|
||||
[ElementAst, 'br'],
|
||||
[TextAst, ' '],
|
||||
[ElementAst, 'br'],
|
||||
[TextAst, '\t'],
|
||||
[ElementAst, 'br'],
|
||||
[TextAst, '\n'],
|
||||
[ElementAst, 'br'],
|
||||
[TextAst, ' '],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should replace each &ngsp; with a space when preserveWhitespaces is true', () => {
|
||||
expect(humanizeTplAst(parse('foo&ngsp;&ngsp;&ngsp;bar', [], [], [], true))).toEqual([
|
||||
[TextAst, 'foo bar'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should replace every &ngsp; with a single space when preserveWhitespaces is false', () => {
|
||||
expect(humanizeTplAst(parse('foo&ngsp;&ngsp;&ngsp;bar', [], [], [], false))).toEqual([
|
||||
[TextAst, 'foo bar'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove whitespaces when explicitly requested', () => {
|
||||
expect(humanizeTplAst(parse(' <br> <br>\t<br>\n<br> ', [], [], [], false))).toEqual([
|
||||
[ElementAst, 'br'],
|
||||
[ElementAst, 'br'],
|
||||
[ElementAst, 'br'],
|
||||
[ElementAst, 'br'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove whitespace between ICU expansions when not preserving whitespaces', () => {
|
||||
const shortForm = '{ count, plural, =0 {small} many {big} }';
|
||||
const expandedForm = '<ng-container [ngPlural]="count">' +
|
||||
'<ng-template ngPluralCase="=0">small</ng-template>' +
|
||||
'<ng-template ngPluralCase="many">big</ng-template>' +
|
||||
'</ng-container>';
|
||||
const humanizedExpandedForm = humanizeTplAst(parse(expandedForm, []));
|
||||
|
||||
// ICU expansions are converted to `<ng-container>` tags and all blank text nodes are reomved
|
||||
// so any whitespace between ICU exansions are removed as well
|
||||
expect(humanizeTplAst(parse(`${shortForm} ${shortForm}`, [], [], [], false))).toEqual([
|
||||
...humanizedExpandedForm, ...humanizedExpandedForm
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Template Parser - opt-out `<template>` support', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureCompiler({
|
||||
|
@ -85,7 +85,8 @@ export class MockDirectiveResolver extends DirectiveResolver {
|
||||
styles: view.styles,
|
||||
styleUrls: view.styleUrls,
|
||||
encapsulation: view.encapsulation,
|
||||
interpolation: view.interpolation
|
||||
interpolation: view.interpolation,
|
||||
preserveWhitespaces: view.preserveWhitespaces,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -106,6 +106,7 @@ export type CompilerOptions = {
|
||||
// Whether to support the `<template>` tag and the `template` attribute to define angular
|
||||
// templates. They have been deprecated in 4.x, `<ng-template>` should be used instead.
|
||||
enableLegacyTemplate?: boolean,
|
||||
preserveWhitespaces?: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -108,6 +108,12 @@ export class QueryList<T>/* implements Iterable<T> */ {
|
||||
|
||||
/** internal */
|
||||
get dirty() { return this._dirty; }
|
||||
|
||||
/** internal */
|
||||
destroy(): void {
|
||||
this._emitter.complete();
|
||||
this._emitter.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
function flatten<T>(list: Array<T|T[]>): T[] {
|
||||
|
@ -54,7 +54,8 @@ export interface DirectiveDecorator {
|
||||
*
|
||||
* **Metadata Properties:**
|
||||
*
|
||||
* * **exportAs** - name under which the component instance is exported in a template
|
||||
* * **exportAs** - name under which the component instance is exported in a template. Can be
|
||||
* given a single name or a comma-delimited list of names.
|
||||
* * **host** - map of class property to host element bindings for events, properties and
|
||||
* attributes
|
||||
* * **inputs** - list of class property names to data-bind as component inputs
|
||||
@ -675,6 +676,74 @@ export interface Component extends Directive {
|
||||
* {@link ComponentFactoryResolver}.
|
||||
*/
|
||||
entryComponents?: Array<Type<any>|any[]>;
|
||||
|
||||
/**
|
||||
* If {@link Component#preserveWhitespaces Component.preserveWhitespaces} is set to `false`
|
||||
* potentially superfluous whitespace characters (ones matching the `\s` character class in
|
||||
* JavaScript regular expressions) will be removed from a compiled template. This can greatly
|
||||
* reduce AOT-generated code size as well as speed up view creation.
|
||||
*
|
||||
* Current implementation works according to the following rules:
|
||||
* - all whitespaces at the beginning and the end of a template are removed (trimmed);
|
||||
* - text nodes consisting of whitespaces only are removed (ex.:
|
||||
* `<button>Action 1</button> <button>Action 2</button>` will be converted to
|
||||
* `<button>Action 1</button><button>Action 2</button>` (no whitespaces between buttons);
|
||||
* - series of whitespaces in text nodes are replaced with one space (ex.:
|
||||
* `<span>\n some text\n</span>` will be converted to `<span> some text </span>`);
|
||||
* - text nodes are left as-is inside HTML tags where whitespaces are significant (ex. `<pre>`,
|
||||
* `<textarea>`).
|
||||
*
|
||||
* Described transformations can (potentially) influence DOM nodes layout so the
|
||||
* `preserveWhitespaces` option is `true` be default (no whitespace removal).
|
||||
* In Angular 5 you need to opt-in for whitespace removal (but we might revisit the default
|
||||
* setting in Angular 6 or later). If you want to change the default setting for all components
|
||||
* in your application you can use the `preserveWhitespaces` option of the AOT compiler.
|
||||
*
|
||||
* Even if you decide to opt-in for whitespace removal there are ways of preserving whitespaces
|
||||
* in certain fragments of a template. You can either exclude entire DOM sub-tree by using the
|
||||
* `ngPreserveWhitespaces` attribute, ex.:
|
||||
*
|
||||
* ```html
|
||||
* <div ngPreserveWhitespaces>
|
||||
* whitespaces are preserved here
|
||||
* <span> and here </span>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* Alternatively you can force a space to be preserved in a text node by using the `&ngsp;`
|
||||
* pseudo-entity. `&ngsp;` will be replaced with a space character by Angular's template
|
||||
* compiler, ex.:
|
||||
*
|
||||
* ```html
|
||||
* <a>Spaces</a>&ngsp;<a>between</a>&ngsp;<a>links.</a>
|
||||
* ```
|
||||
*
|
||||
* will be compiled to the equivalent of:
|
||||
*
|
||||
* ```html
|
||||
* <a>Spaces</a> <a>between</a> <a>links.</a>
|
||||
* ```
|
||||
*
|
||||
* Please note that sequences of `&ngsp;` are still collapsed to just one space character when
|
||||
* the `preserveWhitespaces` option is set to `false`. Ex.:
|
||||
*
|
||||
* ```html
|
||||
* <a>before</a>&ngsp;&ngsp;&ngsp;<a>after</a>
|
||||
* ```
|
||||
*
|
||||
* would be equivalent to:
|
||||
*
|
||||
* ```html
|
||||
* <a>before</a> <a>after</a>
|
||||
* ```
|
||||
*
|
||||
* The `&ngsp;` pseudo-entity is useful for forcing presence of
|
||||
* one space (a text node having `&ngsp;` pseudo-entities will never be removed), but it is not
|
||||
* meant to mark sequences of whitespace characters. The previously described
|
||||
* `ngPreserveWhitespaces` attribute is more useful for preserving sequences of whitespace
|
||||
* characters.
|
||||
*/
|
||||
preserveWhitespaces?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -530,6 +530,8 @@ function destroyViewNodes(view: ViewData) {
|
||||
view.renderer.destroyNode !(asElementData(view, i).renderElement);
|
||||
} else if (def.flags & NodeFlags.TypeText) {
|
||||
view.renderer.destroyNode !(asTextData(view, i).renderText);
|
||||
} else if (def.flags & NodeFlags.TypeContentQuery || def.flags & NodeFlags.TypeViewQuery) {
|
||||
asQueryList(view, i).destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -733,6 +733,46 @@ export function main() {
|
||||
flushMicrotasks();
|
||||
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
|
||||
}));
|
||||
|
||||
it('should properly evaluate pre/auto-style values when components are inserted/removed which contain host animations',
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'parent-cmp',
|
||||
template: `
|
||||
<child-cmp *ngFor="let item of items"></child-cmp>
|
||||
`
|
||||
})
|
||||
class ParentCmp {
|
||||
items: any[] = [1, 2, 3, 4, 5];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'child-cmp',
|
||||
template: '... child ...',
|
||||
animations:
|
||||
[trigger('host', [transition(':leave', [animate(1000, style({opacity: 0}))])])]
|
||||
})
|
||||
class ChildCmp {
|
||||
@HostBinding('@host') public hostAnimation = 'a';
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(ParentCmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
const element = fixture.nativeElement;
|
||||
fixture.detectChanges();
|
||||
|
||||
cmp.items = [0, 2, 4, 6]; // 1,3,5 get removed
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = element.querySelectorAll('child-cmp');
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
expect(item.style['display']).toBeFalsy();
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('should cancel and merge in mid-animation styles into the follow-up animation, but only for animation keyframes that start right away',
|
||||
|
@ -469,6 +469,20 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
.toBeAnInstanceOf(ExportDir);
|
||||
});
|
||||
|
||||
it('should assign a directive to a ref when it has multiple exportAs names', () => {
|
||||
TestBed.configureTestingModule(
|
||||
{declarations: [MyComp, DirectiveWithMultipleExportAsNames]});
|
||||
|
||||
const template = '<div multiple-export-as #x="dirX" #y="dirY"></div>';
|
||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
expect(fixture.debugElement.children[0].references !['x'])
|
||||
.toBeAnInstanceOf(DirectiveWithMultipleExportAsNames);
|
||||
expect(fixture.debugElement.children[0].references !['y'])
|
||||
.toBeAnInstanceOf(DirectiveWithMultipleExportAsNames);
|
||||
});
|
||||
|
||||
it('should make the assigned component accessible in property bindings, even if they were declared before the component',
|
||||
() => {
|
||||
TestBed.configureTestingModule({declarations: [MyComp, ChildComp]});
|
||||
@ -1758,6 +1772,51 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitespaces in templates', () => {
|
||||
it('should not remove whitespaces by default', async(() => {
|
||||
@Component({
|
||||
selector: 'comp',
|
||||
template: '<span>foo</span> <span>bar</span>',
|
||||
})
|
||||
class MyCmp {
|
||||
}
|
||||
|
||||
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
|
||||
f.detectChanges();
|
||||
|
||||
expect(f.nativeElement.childNodes.length).toBe(3);
|
||||
}));
|
||||
|
||||
it('should not remove whitespaces when explicitly requested not to do so', async(() => {
|
||||
@Component({
|
||||
selector: 'comp',
|
||||
template: '<span>foo</span> <span>bar</span>',
|
||||
preserveWhitespaces: true,
|
||||
})
|
||||
class MyCmp {
|
||||
}
|
||||
|
||||
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
|
||||
f.detectChanges();
|
||||
|
||||
expect(f.nativeElement.childNodes.length).toBe(3);
|
||||
}));
|
||||
|
||||
it('should remove whitespaces when explicitly requested to do so', async(() => {
|
||||
@Component({
|
||||
selector: 'comp',
|
||||
template: '<span>foo</span> <span>bar</span>',
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
class MyCmp {
|
||||
}
|
||||
|
||||
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
|
||||
f.detectChanges();
|
||||
|
||||
expect(f.nativeElement.childNodes.length).toBe(2);
|
||||
}));
|
||||
});
|
||||
|
||||
if (getDOM().supportsDOMEvents()) {
|
||||
describe('svg', () => {
|
||||
@ -2398,6 +2457,10 @@ class SomeImperativeViewport {
|
||||
class ExportDir {
|
||||
}
|
||||
|
||||
@Directive({selector: '[multiple-export-as]', exportAs: 'dirX, dirY'})
|
||||
export class DirectiveWithMultipleExportAsNames {
|
||||
}
|
||||
|
||||
@Component({selector: 'comp'})
|
||||
class ComponentWithoutView {
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit,
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
import {stringify} from '../../src/util';
|
||||
|
||||
export function main() {
|
||||
@ -348,16 +350,26 @@ export function main() {
|
||||
view.componentInstance.shouldShow = true;
|
||||
view.detectChanges();
|
||||
|
||||
let isQueryListCompleted = false;
|
||||
|
||||
const q: NeedsQuery = view.debugElement.children[0].references !['q'];
|
||||
const changes = <Subject<any>>q.query.changes;
|
||||
expect(q.query.length).toEqual(1);
|
||||
expect(changes.closed).toBeFalsy();
|
||||
changes.subscribe(() => {}, () => {}, () => { isQueryListCompleted = true; });
|
||||
|
||||
view.componentInstance.shouldShow = false;
|
||||
view.detectChanges();
|
||||
expect(changes.closed).toBeTruthy();
|
||||
expect(isQueryListCompleted).toBeTruthy();
|
||||
|
||||
view.componentInstance.shouldShow = true;
|
||||
view.detectChanges();
|
||||
const q2: NeedsQuery = view.debugElement.children[0].references !['q'];
|
||||
|
||||
expect(q2.query.length).toEqual(1);
|
||||
expect(changes.closed).toBeTruthy();
|
||||
expect((<Subject<any>>q2.query.changes).closed).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -9,34 +9,100 @@
|
||||
import {InjectionToken} from '@angular/core';
|
||||
|
||||
/**
|
||||
* A bridge between a control and a native element.
|
||||
* A `ControlValueAccessor` acts as a bridge between the Angular forms API and a
|
||||
* native element in the DOM.
|
||||
*
|
||||
* A `ControlValueAccessor` abstracts the operations of writing a new value to a
|
||||
* DOM element representing an input control.
|
||||
*
|
||||
* Please see {@link DefaultValueAccessor} for more information.
|
||||
* Implement this interface if you want to create a custom form control directive
|
||||
* that integrates with Angular forms.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface ControlValueAccessor {
|
||||
/**
|
||||
* Write a new value to the element.
|
||||
* Writes a new value to the element.
|
||||
*
|
||||
* This method will be called by the forms API to write to the view when programmatic
|
||||
* (model -> view) changes are requested.
|
||||
*
|
||||
* Example implementation of `writeValue`:
|
||||
*
|
||||
* ```ts
|
||||
* writeValue(value: any): void {
|
||||
* this._renderer.setProperty(this._elementRef.nativeElement, 'value', value);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
writeValue(obj: any): void;
|
||||
|
||||
/**
|
||||
* Set the function to be called when the control receives a change event.
|
||||
* Registers a callback function that should be called when the control's value
|
||||
* changes in the UI.
|
||||
*
|
||||
* This is called by the forms API on initialization so it can update the form
|
||||
* model when values propagate from the view (view -> model).
|
||||
*
|
||||
* If you are implementing `registerOnChange` in your own value accessor, you
|
||||
* will typically want to save the given function so your class can call it
|
||||
* at the appropriate time.
|
||||
*
|
||||
* ```ts
|
||||
* registerOnChange(fn: (_: any) => void): void {
|
||||
* this._onChange = fn;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* When the value changes in the UI, your class should call the registered
|
||||
* function to allow the forms API to update itself:
|
||||
*
|
||||
* ```ts
|
||||
* host: {
|
||||
* (change): '_onChange($event.target.value)'
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
registerOnChange(fn: any): void;
|
||||
|
||||
/**
|
||||
* Set the function to be called when the control receives a touch event.
|
||||
* Registers a callback function that should be called when the control receives
|
||||
* a blur event.
|
||||
*
|
||||
* This is called by the forms API on initialization so it can update the form model
|
||||
* on blur.
|
||||
*
|
||||
* If you are implementing `registerOnTouched` in your own value accessor, you
|
||||
* will typically want to save the given function so your class can call it
|
||||
* when the control should be considered blurred (a.k.a. "touched").
|
||||
*
|
||||
* ```ts
|
||||
* registerOnTouched(fn: any): void {
|
||||
* this._onTouched = fn;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* On blur (or equivalent), your class should call the registered function to allow
|
||||
* the forms API to update itself:
|
||||
*
|
||||
* ```ts
|
||||
* host: {
|
||||
* '(blur)': '_onTouched()'
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
registerOnTouched(fn: any): void;
|
||||
|
||||
/**
|
||||
* This function is called when the control status changes to or from "DISABLED".
|
||||
* Depending on the value, it will enable or disable the appropriate DOM element.
|
||||
* This function is called by the forms API when the control status changes to
|
||||
* or from "DISABLED". Depending on the value, it should enable or disable the
|
||||
* appropriate DOM element.
|
||||
*
|
||||
* Example implementation of `setDisabledState`:
|
||||
*
|
||||
* ```ts
|
||||
* setDisabledState(isDisabled: boolean): void {
|
||||
* this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param isDisabled
|
||||
*/
|
||||
|
@ -45,12 +45,12 @@ export const formControlBinding: any = {
|
||||
* {@link AbstractControl}.
|
||||
*
|
||||
* **Set the value**: You can pass in an initial value when instantiating the {@link FormControl},
|
||||
* or you can set it programmatically later using {@link AbstractControl#setValue} or
|
||||
* {@link AbstractControl#patchValue}.
|
||||
* or you can set it programmatically later using {@link AbstractControl#setValue setValue} or
|
||||
* {@link AbstractControl#patchValue patchValue}.
|
||||
*
|
||||
* **Listen to value**: If you want to listen to changes in the value of the control, you can
|
||||
* subscribe to the {@link AbstractControl#valueChanges} event. You can also listen to
|
||||
* {@link AbstractControl#statusChanges} to be notified when the validation status is
|
||||
* subscribe to the {@link AbstractControl#valueChanges valueChanges} event. You can also listen to
|
||||
* {@link AbstractControl#statusChanges statusChanges} to be notified when the validation status is
|
||||
* re-calculated.
|
||||
*
|
||||
* ### Example
|
||||
|
@ -45,7 +45,7 @@ export const controlNameBinding: any = {
|
||||
* closest {@link FormGroup} or {@link FormArray} above it.
|
||||
*
|
||||
* **Access the control**: You can access the {@link FormControl} associated with
|
||||
* this directive by using the {@link AbstractControl#get} method.
|
||||
* this directive by using the {@link AbstractControl#get get} method.
|
||||
* Ex: `this.form.get('first');`
|
||||
*
|
||||
* **Get value**: the `value` property is always synced and available on the {@link FormControl}.
|
||||
@ -53,11 +53,11 @@ export const controlNameBinding: any = {
|
||||
*
|
||||
* **Set value**: You can set an initial value for the control when instantiating the
|
||||
* {@link FormControl}, or you can set it programmatically later using
|
||||
* {@link AbstractControl#setValue} or {@link AbstractControl#patchValue}.
|
||||
* {@link AbstractControl#setValue setValue} or {@link AbstractControl#patchValue patchValue}.
|
||||
*
|
||||
* **Listen to value**: If you want to listen to changes in the value of the control, you can
|
||||
* subscribe to the {@link AbstractControl#valueChanges} event. You can also listen to
|
||||
* {@link AbstractControl#statusChanges} to be notified when the validation status is
|
||||
* subscribe to the {@link AbstractControl#valueChanges valueChanges} event. You can also listen to
|
||||
* {@link AbstractControl#statusChanges statusChanges} to be notified when the validation status is
|
||||
* re-calculated.
|
||||
*
|
||||
* ### Example
|
||||
|
@ -34,12 +34,13 @@ export const formDirectiveProvider: any = {
|
||||
*
|
||||
* **Set value**: You can set the form's initial value when instantiating the
|
||||
* {@link FormGroup}, or you can set it programmatically later using the {@link FormGroup}'s
|
||||
* {@link AbstractControl#setValue} or {@link AbstractControl#patchValue} methods.
|
||||
* {@link AbstractControl#setValue setValue} or {@link AbstractControl#patchValue patchValue}
|
||||
* methods.
|
||||
*
|
||||
* **Listen to value**: If you want to listen to changes in the value of the form, you can subscribe
|
||||
* to the {@link FormGroup}'s {@link AbstractControl#valueChanges} event. You can also listen to
|
||||
* its {@link AbstractControl#statusChanges} event to be notified when the validation status is
|
||||
* re-calculated.
|
||||
* to the {@link FormGroup}'s {@link AbstractControl#valueChanges valueChanges} event. You can also
|
||||
* listen to its {@link AbstractControl#statusChanges statusChanges} event to be notified when the
|
||||
* validation status is re-calculated.
|
||||
*
|
||||
* Furthermore, you can listen to the directive's `ngSubmit` event to be notified when the user has
|
||||
* triggered a form submission. The `ngSubmit` event will be emitted with the original form
|
||||
|
@ -50,11 +50,11 @@ export const formGroupNameProvider: any = {
|
||||
*
|
||||
* **Set the value**: You can set an initial value for each child control when instantiating
|
||||
* the {@link FormGroup}, or you can set it programmatically later using
|
||||
* {@link AbstractControl#setValue} or {@link AbstractControl#patchValue}.
|
||||
* {@link AbstractControl#setValue setValue} or {@link AbstractControl#patchValue patchValue}.
|
||||
*
|
||||
* **Listen to value**: If you want to listen to changes in the value of the group, you can
|
||||
* subscribe to the {@link AbstractControl#valueChanges} event. You can also listen to
|
||||
* {@link AbstractControl#statusChanges} to be notified when the validation status is
|
||||
* subscribe to the {@link AbstractControl#valueChanges valueChanges} event. You can also listen to
|
||||
* {@link AbstractControl#statusChanges statusChanges} to be notified when the validation status is
|
||||
* re-calculated.
|
||||
*
|
||||
* ### Example
|
||||
|
@ -13,8 +13,6 @@ import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, REQUIRE_INJECTOR, REQUIRE_NG_
|
||||
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
|
||||
import {controllerKey, getComponentName} from './util';
|
||||
|
||||
let downgradeCount = 0;
|
||||
|
||||
/**
|
||||
* @whatItDoes
|
||||
*
|
||||
@ -57,9 +55,6 @@ export function downgradeComponent(info: {
|
||||
/** @deprecated since v4. This parameter is no longer used */
|
||||
selectors?: string[];
|
||||
}): any /* angular.IInjectable */ {
|
||||
const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`;
|
||||
let idCount = 0;
|
||||
|
||||
const directiveFactory:
|
||||
angular.IAnnotatedFunction = function(
|
||||
$compile: angular.ICompileService,
|
||||
@ -90,10 +85,9 @@ export function downgradeComponent(info: {
|
||||
throw new Error('Expecting ComponentFactory for: ' + getComponentName(info.component));
|
||||
}
|
||||
|
||||
const id = idPrefix + (idCount++);
|
||||
const injectorPromise = new ParentInjectorPromise(element);
|
||||
const facade = new DowngradeComponentAdapter(
|
||||
id, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
|
||||
element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
|
||||
componentFactory);
|
||||
|
||||
const projectableNodes = facade.compileContents();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user