Compare commits

..

52 Commits
4.3.6 ... 4.4.2

Author SHA1 Message Date
49d122e560 release: cut the 4.4.2 release 2017-09-18 21:14:03 -07:00
ff8729423e docs(changelog): fix the changelog link for comparing 4.4.1 to 4.3.6 2017-09-15 16:00:56 -07:00
0c72f7d358 docs: update combined changelog for 4.4 stable 2017-09-15 15:53:54 -07:00
818f4a751e release: cut the 4.4.1 release 2017-09-15 15:35:25 -07:00
4e7d2bd5bf docs: add changelog for 4.4.0 2017-09-15 14:58:32 -07:00
395ac510f7 release: cut the 4.4.0 release 2017-09-15 14:51:21 -07:00
b20c5d2c37 fix(upgrade): remove code setting id attribute. (#19182)
The id was leftover from previous iterations of ngUpgrade and is
no longer needed. Moreover, setting it can clash with CSS usage of id.

Fixes #18446

PR Close #19182
2017-09-15 17:19:50 -04:00
ea02b1ccfa fix(upgrade): remove code setting id attribute.
The id was leftover from previous iterations of ngUpgrade and is
no longer needed. Moreover, setting it can clash with CSS usage of id.

Fixes #18446
2017-09-15 17:19:50 -04:00
0bafd03e85 revert: test(packaging): added test for source map correctness
This reverts commit 86f7b4170c.
2017-09-15 11:55:29 -07:00
e8d1858c64 revert: build(platform-browser): fix typo
This reverts commit 991a802a8e.
2017-09-15 13:47:07 -04:00
d1efc5ae90 revert: fix(upgrade): remove code setting id attribute
This reverts commit 1302e54947.
2017-09-13 13:19:19 -07:00
86f7b4170c test(packaging): added test for source map correctness 2017-09-13 13:48:36 -04:00
9d93c859d7 build(aio): auto-link more code items
We now parse all code blocks, after they have been rendered by dgeni
and insert links to API docs that match "words" in the code.
2017-09-12 14:06:26 -04:00
5baa069b16 docs(aio): fix typo 2017-09-12 14:06:14 -04:00
12b7d00747 fix(aio): relax search on titles further
This change will now match `ControlValueAccessor` for the query `accessor`.

Closes #18872
2017-09-12 14:06:02 -04:00
d777d79c61 build(aio): do not render annotations block for directives 2017-09-12 14:05:46 -04:00
062a772e48 build(aio): improve rendering of directive selectors 2017-09-12 14:05:41 -04:00
3618cc6d34 build(aio): do not render comments in decorators
Closes #18873
2017-09-12 14:05:35 -04:00
9413ca8a2e build(aio): update karma & systemjs config for HttpClient
While adding the references to the `HttpClient` packages it also crucially
adds ref to new “tslib” library required by `HttpClient`.
2017-09-12 13:46:12 -04:00
1302e54947 fix(upgrade): remove code setting id attribute.
The id was leftover from previous iterations of ngUpgrade and is
no longer needed. Moreover, setting it can clash with CSS usage of id.

Fixes #18446
2017-09-08 16:30:18 -07:00
edf423af3d build(aio): update package.json for angular-in-mem-web-api 0.4.0 2017-09-08 18:25:20 -04:00
37086748bf docs(aio): provide link text for AbstractControl references in reactive directives for forms
Closes #17484
2017-09-08 18:25:14 -04:00
6c3f1f70ba build(aio): render metadata members from decorator ancestors 2017-09-08 18:25:08 -04:00
8a8c4d37aa build(aio): render ancestor members in directives
See `CheckBoxRequiredValidator` for an example.
2017-09-08 18:25:00 -04:00
f6a7183c52 fix(tsc-wrapped): fix metadata symbol reference 2017-09-08 17:26:28 -04:00
c86e16db5f feat(aio): include more API results in search
By adding a more relaxed search on the title
of docs, we are more likely to catch API docs.

The additional search terms match anything
with a word in the title that starts with the
characters of the first term in the search.

E.g. if the search is "ngCont guide" then
search for "ngCont guide titleWords:ngCont*"
2017-09-08 17:01:39 -04:00
c3907893c1 build(aio): don't show constructor detail if there is none 2017-09-08 17:01:39 -04:00
d61c6f996a build(aio): support rendering of constructor overloads in API docs
Closes #18258
2017-09-08 17:01:39 -04:00
bd04cd61f8 docs(aio): change Stack Overflow link 2017-09-08 16:29:17 -04:00
96dcfafe45 docs(aio): Updated jqwidgets resource desc and url
Updated the description and url of the jqwidgets item in the resources page
2017-09-07 16:07:52 -04:00
991a802a8e build(platform-browser): fix typo
fixes #19033
2017-09-07 16:07:27 -04:00
48ae1a6574 fix(tsc-wrapped): deduplicate metadata for re-exported modules 2017-09-07 16:06:12 -04:00
dd2d1be006 fix(aio): align footer background image repeat (#19035)
Refs #17465
PR Close #19035
2017-09-05 23:25:06 -05:00
5369de80d6 docs(aio): add changelog, as hidden, to navigation (#19028)
This ensures that the changelog page is formatted correctly.

Closes #17604

PR Close #19028
2017-09-05 23:25:06 -05:00
552dbfc2f1 docs(aio): add Nir Kaufman to GDE resources (#19012)
PR Close #19012
2017-09-05 23:25:06 -05:00
0aa4cbdbc8 docs(forms): clarify ControlValueAccessor docs (#19008)
Closes #18174

PR Close #19008
2017-09-05 23:25:06 -05:00
9f16c2620c docs(aio): updating about page for team changes (#19003)
PR Close #19003
2017-09-05 23:25:06 -05:00
9b256a9144 build(aio): render the extends ancestors of classes in API docs (#18927)
PR Close #18927
2017-09-05 23:25:06 -05:00
0f1476be33 build(aio): ensure dgeni can load all angular Typescript modules (#18927)
PR Close #18927
2017-09-05 23:25:06 -05:00
769b2aada2 docs(aio): add new lib to Data Libraries (#18656)
add AngularCommerse, set of components to build e-commerce solutions with Angular + Firebase

PR Close #18656
2017-09-05 23:25:06 -05:00
301236e1a5 docs(aio): Updated usage of Observables in router guide. Added section for advanced redirects (#18197)
PR Close #18197
2017-09-05 23:25:06 -05:00
aeb98dbcdf docs: add changelog for 4.4.0-RC.0 2017-09-01 22:01:41 -07:00
8036d05412 release: cut the 4.4.0-RC.0 release 2017-09-01 23:49:51 -05:00
7d137d7f88 fix(core): complete EventEmitter in QueryList on component destroy (#18902)
Fixes #18741

PR Close #18902
2017-09-01 22:52:03 -05:00
b8b551cf2b perf(core): add option to remove blank text nodes from compiled templates (#18823)
PR Close #18823
2017-09-01 13:30:04 -05:00
7ec28fe9af feat(compiler): allow multiple exportAs names (#18723)
This change allows users to specify multiple exportAs names for a
directive by giving a comma-delimited list inside the string.

The primary motivation for this change is to allow these names to be
changed in a backwards compatible way.

PR Close #18723
2017-09-01 13:26:10 -05:00
1cc3fe21b6 fix(animations): do not leak DOM nodes/styling for host triggered animations (#18853)
Closes #18606

PR Close #18853
2017-09-01 10:24:14 -07:00
ba7d70e5e0 build: fix changelog to remove AIO line items (#18956)
fixes 18740

PR Close #18956
2017-08-30 18:10:56 -07:00
497e0178cc fix(compiler): normalize the locale name (#18963)
PR Close #18963
2017-08-30 17:48:08 -07:00
8821723526 fix(common): fix XSSI prefix stripping by using JSON.parse always (#18466)
Currently HttpClient sends requests for JSON data with the
XMLHttpRequest.responseType set to 'json'. With this flag, the browser
will attempt to parse the response as JSON, but will return 'null' on
any errors. If the JSON response contains an XSSI-prevention prefix,
this will cause the browser's parsing to fail, which is unrecoverable.

The only compelling reason to use the responseType 'json' is for
performance (especially if the browser offloads JSON parsing to a
separate thread). I'm not aware of any browser which does this currently,
nor of any plans to do so. JSON.parse and responseType 'json' both
end up using the same V8 code path in Chrome to implement the parse.

Thus, this change switches all JSON parsing in HttpClient to use
JSON.parse directly.

Fixes #18396, #18453.

PR Close #18466
2017-08-29 17:19:02 -07:00
a203a959ae fix(common): fix improper packaging for @angular/common/http (#18613)
PR Close #18613
2017-08-29 17:16:56 -07:00
dfe2bad663 build: Add GitHub scripts for rebasing PRs (#18359)
PR Close #18359
2017-08-28 18:33:11 -05:00
114 changed files with 2253 additions and 614 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`** &mdash; An `Observable` that contains the required and [optional parameters](#optional-route-parameters) specific to the route. Use `paramMap` instead.
**`queryParams`** &mdash; 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`** &mdash; An `Observable` that contains the required and [optional parameters](#optional-route-parameters) specific to the route. Use `paramMap` instead.
**`queryParams`** &mdash; 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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,3 +9,5 @@
{$ directiveHelper.renderBindings(doc.outputs, 'outputs', 'output', 'Outputs') $}
{% include "includes/export-as.html" %}
{% endblock %}
{% block annotations %}{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -388,4 +388,4 @@ function createProgramWithStubsHost(
fileExists = (fileName: string) =>
this.generatedFiles.has(fileName) || originalHost.fileExists(fileName);
};
}
}

View File

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

View File

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

View File

@ -15,4 +15,5 @@ export interface AotCompilerOptions {
missingTranslation?: MissingTranslationStrategy;
enableLegacyTemplate?: boolean;
enableSummariesForJit?: boolean;
preserveWhitespaces?: boolean;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -71,6 +71,7 @@ export function mergeNsAndName(prefix: string, localName: string): string {
// This list is not exhaustive to keep the compiler footprint low.
// The `&#123;` / `&#x1ab;` 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -97,4 +97,4 @@ const serializerVisitor = new _SerializerVisitor();
export function serializeNodes(nodes: html.Node[]): string[] {
return nodes.map(node => node.visit(serializerVisitor, null));
}
}

View 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],
]);
});
});
}

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {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({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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