Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
5298b2bda3 | |||
25ae886cad | |||
fc6dfc2e08 | |||
c0670ef52d | |||
fe96cafd03 | |||
ad674dad37 | |||
86517f2ad5 | |||
6d9a4f8aea | |||
a1efc27ff2 | |||
311232004c |
@ -15,11 +15,16 @@ build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:764
|
||||
# Prevent unstable environment variables from tainting cache keys
|
||||
build --experimental_strict_action_env
|
||||
|
||||
# Save downloaded repositories such as the go toolchain
|
||||
# This directory can then be included in the CircleCI cache
|
||||
# It should save time running the first build
|
||||
build --experimental_repository_cache=/home/circleci/bazel_repository_cache
|
||||
|
||||
# Workaround https://github.com/bazelbuild/bazel/issues/3645
|
||||
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
|
||||
# Limit Bazel to consuming resources that fit in CircleCI "medium" class which is the default:
|
||||
# Limit Bazel to consuming resources that fit in CircleCI "xlarge" class
|
||||
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
|
||||
build --local_resources=3072,2.0,1.0
|
||||
build --local_resources=14336,8.0,1.0
|
||||
|
||||
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
||||
test --flaky_test_attempts=2
|
||||
|
@ -13,7 +13,7 @@
|
||||
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
||||
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
||||
var_1: &docker_image angular/ngcontainer:0.1.0
|
||||
var_2: &cache_key angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.1.0
|
||||
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.1.0
|
||||
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
var_3: &setup-bazel-remote-cache
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
|
||||
build:
|
||||
<<: *job_defaults
|
||||
resource_class: large
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
@ -71,6 +71,7 @@ jobs:
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
|
||||
- run: ls /home/circleci/bazel_repository_cache || true
|
||||
- run: bazel info release
|
||||
- run: bazel run @yarn//:yarn
|
||||
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
|
||||
@ -82,6 +83,7 @@ jobs:
|
||||
key: *cache_key
|
||||
paths:
|
||||
- "node_modules"
|
||||
- "~/bazel_repository_cache"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,3 +1,14 @@
|
||||
<a name="5.2.9"></a>
|
||||
## [5.2.9](https://github.com/angular/angular/compare/5.2.8...5.2.9) (2018-03-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **platform-server:** add styles to elements correctly ([#22527](https://github.com/angular/angular/issues/22527)) ([fc6dfc2](https://github.com/angular/angular/commit/fc6dfc2))
|
||||
* **router:** correct over-encoding of URL fragment ([#22687](https://github.com/angular/angular/issues/22687)) ([86517f2](https://github.com/angular/angular/commit/86517f2))
|
||||
|
||||
|
||||
|
||||
<a name="5.2.8"></a>
|
||||
## [5.2.8](https://github.com/angular/angular/compare/5.2.7...5.2.8) (2018-03-07)
|
||||
|
||||
|
@ -6,32 +6,38 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ReactiveFormsModule } from '@angular/forms'; // <-- #1 import module
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { HeroDetailComponent } from './hero-detail/hero-detail.component'; // <-- #1 import component
|
||||
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
|
||||
// #enddocregion v1
|
||||
// #docregion hero-service-list
|
||||
// add JavaScript imports
|
||||
import { HeroListComponent } from './hero-list/hero-list.component';
|
||||
|
||||
import { HeroService } from './hero.service'; // <-- #1 import service
|
||||
import { HeroService } from './hero.service';
|
||||
// #docregion v1
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
ReactiveFormsModule // <-- #2 add to @NgModule imports
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
HeroDetailComponent,
|
||||
// #enddocregion v1
|
||||
HeroListComponent
|
||||
HeroListComponent // <--declare HeroListComponent
|
||||
// #docregion v1
|
||||
],
|
||||
// #enddocregion v1
|
||||
exports: [ // export for the DemoModule
|
||||
// #enddocregion hero-service-list
|
||||
imports: [
|
||||
BrowserModule,
|
||||
ReactiveFormsModule // <-- #2 add to @NgModule imports
|
||||
],
|
||||
// #enddocregion v1
|
||||
// export for the DemoModule
|
||||
// #docregion hero-service-list
|
||||
// ...
|
||||
exports: [
|
||||
AppComponent,
|
||||
HeroDetailComponent,
|
||||
HeroListComponent
|
||||
HeroListComponent // <-- export HeroListComponent
|
||||
],
|
||||
providers: [ HeroService ], // <-- #4 provide HeroService
|
||||
providers: [ HeroService ], // <-- provide HeroService
|
||||
// #enddocregion hero-service-list
|
||||
// #docregion v1
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!-- #docregion basic-form-->
|
||||
<h2>Hero Detail</h2>
|
||||
<h3><i>FormControl in a FormGroup</i></h3>
|
||||
<form [formGroup]="heroForm" novalidate>
|
||||
<form [formGroup]="heroForm">
|
||||
<div class="form-group">
|
||||
<label class="center-block">Name:
|
||||
<input class="form-control" formControlName="name">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!-- #docregion basic-form-->
|
||||
<h2>Hero Detail</h2>
|
||||
<h3><i>A FormGroup with a single FormControl using FormBuilder</i></h3>
|
||||
<form [formGroup]="heroForm" novalidate>
|
||||
<form [formGroup]="heroForm">
|
||||
<div class="form-group">
|
||||
<label class="center-block">Name:
|
||||
<input class="form-control" formControlName="name">
|
||||
@ -13,4 +13,4 @@
|
||||
<!-- #docregion form-value-json -->
|
||||
<p>Form value: {{ heroForm.value | json }}</p>
|
||||
<p>Form status: {{ heroForm.status | json }}</p>
|
||||
<!-- #enddocregion form-value-json -->
|
||||
<!-- #enddocregion form-value-json -->
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!-- #docregion -->
|
||||
<h2>Hero Detail</h2>
|
||||
<h3><i>A FormGroup with multiple FormControls</i></h3>
|
||||
<form [formGroup]="heroForm" novalidate>
|
||||
<form [formGroup]="heroForm">
|
||||
<div class="form-group">
|
||||
<label class="center-block">Name:
|
||||
<input class="form-control" formControlName="name">
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
<form [formGroup]="heroForm" novalidate>
|
||||
<form [formGroup]="heroForm">
|
||||
<div class="form-group">
|
||||
<label class="center-block">Name:
|
||||
<input class="form-control" formControlName="name">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!-- #docregion -->
|
||||
<h2>Hero Detail</h2>
|
||||
<h3><i>PatchValue to initialize a value</i></h3>
|
||||
<form [formGroup]="heroForm" novalidate>
|
||||
<form [formGroup]="heroForm">
|
||||
<div class="form-group">
|
||||
<label class="center-block">Name:
|
||||
<input class="form-control" formControlName="name">
|
||||
|
@ -44,7 +44,13 @@ export class HeroDetailComponent6 implements OnChanges {
|
||||
}
|
||||
|
||||
// #docregion patch-value-on-changes
|
||||
ngOnChanges() { // <-- wrap patchValue in ngOnChanges
|
||||
ngOnChanges() { // <-- call rebuildForm in ngOnChanges
|
||||
this.rebuildForm();
|
||||
}
|
||||
// #enddocregion patch-value-on-changes
|
||||
|
||||
// #docregion patch-value-rebuildform
|
||||
rebuildForm() { // <-- wrap patchValue in rebuildForm
|
||||
this.heroForm.reset();
|
||||
// #docregion patch-value
|
||||
this.heroForm.patchValue({
|
||||
@ -52,7 +58,9 @@ export class HeroDetailComponent6 implements OnChanges {
|
||||
});
|
||||
// #enddocregion patch-value
|
||||
}
|
||||
// #enddocregion patch-value-on-changes
|
||||
// #enddocregion patch-value-rebuildform
|
||||
}
|
||||
|
||||
|
||||
|
||||
// #enddocregion v6
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!-- #docregion -->
|
||||
<h2>Hero Detail</h2>
|
||||
<h3><i>A FormGroup with multiple FormControls</i></h3>
|
||||
<form [formGroup]="heroForm" novalidate>
|
||||
<form [formGroup]="heroForm">
|
||||
<div class="form-group">
|
||||
<label class="center-block">Name:
|
||||
<input class="form-control" formControlName="name">
|
||||
|
@ -38,32 +38,31 @@ export class HeroDetailComponent7 implements OnChanges {
|
||||
|
||||
// #docregion ngOnChanges
|
||||
ngOnChanges() {
|
||||
this.rebuildForm();
|
||||
}
|
||||
// #enddocregion ngOnChanges
|
||||
|
||||
// #docregion rebuildForm
|
||||
rebuildForm() {
|
||||
this.heroForm.reset({
|
||||
name: this.hero.name,
|
||||
address: this.hero.addresses[0] || new Address()
|
||||
});
|
||||
}
|
||||
// #enddocregion ngOnChanges
|
||||
|
||||
/* First version of ngOnChanges
|
||||
// #docregion ngOnChanges-1
|
||||
ngOnChanges()
|
||||
// #enddocregion ngOnChanges-1
|
||||
*/
|
||||
ngOnChanges1() {
|
||||
// #docregion reset
|
||||
this.heroForm.reset();
|
||||
// #enddocregion reset
|
||||
// #docregion ngOnChanges-1
|
||||
// #docregion set-value
|
||||
this.heroForm.setValue({
|
||||
name: this.hero.name,
|
||||
// #docregion set-value-address
|
||||
address: this.hero.addresses[0] || new Address()
|
||||
// #enddocregion set-value-address
|
||||
});
|
||||
}
|
||||
// #enddocregion rebuildForm
|
||||
|
||||
/* First version of rebuildForm */
|
||||
rebuildForm1() {
|
||||
// #docregion reset
|
||||
this.heroForm.reset();
|
||||
// #enddocregion reset
|
||||
// #docregion set-value
|
||||
this.heroForm.setValue({
|
||||
name: this.hero.name,
|
||||
address: this.hero.addresses[0] || new Address()
|
||||
});
|
||||
// #enddocregion set-value
|
||||
}
|
||||
// #enddocregion ngOnChanges-1
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!-- #docplaster-->
|
||||
<h3><i>Using FormArray to add groups</i></h3>
|
||||
|
||||
<form [formGroup]="heroForm" novalidate>
|
||||
<form [formGroup]="heroForm">
|
||||
<p>Form Changed: {{ heroForm.dirty }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
@ -11,7 +11,9 @@
|
||||
</div>
|
||||
<!-- #docregion form-array-->
|
||||
<!-- #docregion form-array-skeleton -->
|
||||
<!-- #docregion form-array-name -->
|
||||
<div formArrayName="secretLairs" class="well well-lg">
|
||||
<!-- #enddocregion form-array-name -->
|
||||
<div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
|
||||
<!-- The repeated address template -->
|
||||
<!-- #enddocregion form-array-skeleton -->
|
||||
|
@ -39,12 +39,18 @@ export class HeroDetailComponent8 implements OnChanges {
|
||||
|
||||
// #docregion onchanges
|
||||
ngOnChanges() {
|
||||
this.rebuildForm();
|
||||
}
|
||||
// #enddocregion onchanges
|
||||
|
||||
// #docregion rebuildform
|
||||
rebuildForm() {
|
||||
this.heroForm.reset({
|
||||
name: this.hero.name
|
||||
});
|
||||
this.setAddresses(this.hero.addresses);
|
||||
}
|
||||
// #enddocregion onchanges
|
||||
// #enddocregion rebuildform
|
||||
|
||||
// #docregion get-secret-lairs
|
||||
get secretLairs(): FormArray {
|
||||
|
@ -1,11 +1,11 @@
|
||||
<!-- #docplaster -->
|
||||
<!-- #docregion -->
|
||||
<!-- #docregion buttons -->
|
||||
<form [formGroup]="heroForm" (ngSubmit)="onSubmit()" novalidate>
|
||||
<form [formGroup]="heroForm" (ngSubmit)="onSubmit()">
|
||||
<div style="margin-bottom: 1em">
|
||||
<button type="submit"
|
||||
[disabled]="heroForm.pristine" class="btn btn-success">Save</button>
|
||||
<button type="reset" (click)="revert()"
|
||||
<button type="button" (click)="revert()"
|
||||
[disabled]="heroForm.pristine" class="btn btn-danger">Revert</button>
|
||||
</div>
|
||||
|
||||
|
@ -13,7 +13,10 @@ import { HeroService } from '../hero.service';
|
||||
templateUrl: './hero-detail.component.html',
|
||||
styleUrls: ['./hero-detail.component.css']
|
||||
})
|
||||
|
||||
// #docregion onchanges-implementation
|
||||
export class HeroDetailComponent implements OnChanges {
|
||||
// #enddocregion onchanges-implementation
|
||||
@Input() hero: Hero;
|
||||
|
||||
heroForm: FormGroup;
|
||||
@ -42,6 +45,10 @@ export class HeroDetailComponent implements OnChanges {
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.rebuildForm();
|
||||
}
|
||||
|
||||
rebuildForm() {
|
||||
this.heroForm.reset({
|
||||
name: this.hero.name
|
||||
});
|
||||
@ -66,7 +73,7 @@ export class HeroDetailComponent implements OnChanges {
|
||||
onSubmit() {
|
||||
this.hero = this.prepareSaveHero();
|
||||
this.heroService.updateHero(this.hero).subscribe(/* error handling */);
|
||||
this.ngOnChanges();
|
||||
this.rebuildForm();
|
||||
}
|
||||
// #enddocregion on-submit
|
||||
|
||||
@ -92,7 +99,7 @@ export class HeroDetailComponent implements OnChanges {
|
||||
// #enddocregion prepare-save-hero
|
||||
|
||||
// #docregion revert
|
||||
revert() { this.ngOnChanges(); }
|
||||
revert() { this.rebuildForm(); }
|
||||
// #enddocregion revert
|
||||
|
||||
// #docregion log-name-change
|
||||
|
@ -197,20 +197,21 @@ function heroModuleSetup() {
|
||||
|
||||
// #docregion title-case-pipe
|
||||
it('should convert hero name to Title Case', () => {
|
||||
const inputName = 'quick BROWN fox';
|
||||
const titleCaseName = 'Quick Brown Fox';
|
||||
const { nameInput, nameDisplay } = page;
|
||||
// get the name's input and display elements from the DOM
|
||||
const hostElement = fixture.nativeElement;
|
||||
const nameInput: HTMLInputElement = hostElement.querySelector('input');
|
||||
const nameDisplay: HTMLElement = hostElement.querySelector('span');
|
||||
|
||||
// simulate user entering new name into the input box
|
||||
nameInput.value = inputName;
|
||||
// simulate user entering a new name into the input box
|
||||
nameInput.value = 'quick BROWN fOx';
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
nameInput.dispatchEvent(newEvent('input'));
|
||||
|
||||
// Tell Angular to update the output span through the title pipe
|
||||
// Tell Angular to update the display binding through the title pipe
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(nameDisplay.textContent).toBe(titleCaseName);
|
||||
expect(nameDisplay.textContent).toBe('Quick Brown Fox');
|
||||
});
|
||||
// #enddocregion title-case-pipe
|
||||
// #enddocregion selected-tests
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -785,6 +785,25 @@ There is no harm in calling `detectChanges()` more often than is strictly necess
|
||||
|
||||
<hr>
|
||||
|
||||
{@a dispatch-event}
|
||||
|
||||
#### Change an input value with _dispatchEvent()_
|
||||
|
||||
To simulate user input, you can find the input element and set its `value` property.
|
||||
|
||||
You will call `fixture.detectChanges()` to trigger Angular's change detection.
|
||||
But there is an essential, intermediate step.
|
||||
|
||||
Angular doesn't know that you set the input element's `value` property.
|
||||
It won't read that property until you raise the element's `input` event by calling `dispatchEvent()`.
|
||||
_Then_ you call `detectChanges()`.
|
||||
|
||||
The following example demonstrates the proper sequence.
|
||||
|
||||
<code-example path="testing/src/app/hero/hero-detail.component.spec.ts" region="title-case-pipe" title="app/hero/hero-detail.component.spec.ts (pipe test)"></code-example>
|
||||
|
||||
<hr>
|
||||
|
||||
### Component with external files
|
||||
|
||||
The `BannerComponent` above is defined with an _inline template_ and _inline css_, specified in the `@Component.template` and `@Component.styles` properties respectively.
|
||||
|
@ -49,6 +49,12 @@
|
||||
<td>Vienna</td>
|
||||
<td>May 16-18, 2018</td>
|
||||
</tr>
|
||||
<!-- ngJapan-->
|
||||
<tr>
|
||||
<th><a href="https://ngjapan.org/en.html" title="ng-japan">ng-japan</a></th>
|
||||
<td>Tokyo, Japan</td>
|
||||
<td>Jun 16, 2018</td>
|
||||
</tr>
|
||||
<!-- AngularConnect-->
|
||||
<tr>
|
||||
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
|
||||
|
@ -15,7 +15,7 @@ When you’re done, users will be able to navigate the app like this:
|
||||
|
||||
</figure>
|
||||
|
||||
## Add the _AppRoutingModule_
|
||||
## Add the `AppRoutingModule`
|
||||
|
||||
An Angular best practice is to load and configure the router in a separate, top-level module
|
||||
that is dedicated to routing and imported by the root `AppModule`.
|
||||
@ -138,7 +138,7 @@ You should see the familiar heroes master/detail view.
|
||||
|
||||
{@a routerlink}
|
||||
|
||||
## Add a navigation link (_routerLink_)
|
||||
## Add a navigation link (`routerLink`)
|
||||
|
||||
Users shouldn't have to paste a route URL into the address bar.
|
||||
They should be able to click a link to navigate.
|
||||
@ -283,7 +283,7 @@ The user should be able to get to these details in three ways.
|
||||
In this section, you'll enable navigation to the `HeroDetailsComponent`
|
||||
and liberate it from the `HeroesComponent`.
|
||||
|
||||
### Delete _hero details_ from _HeroesComponent_
|
||||
### Delete _hero details_ from `HeroesComponent`
|
||||
|
||||
When the user clicks a hero item in the `HeroesComponent`,
|
||||
the app should navigate to the `HeroDetailComponent`,
|
||||
@ -325,7 +325,7 @@ At this point, all application routes are in place.
|
||||
title="src/app/app-routing.module.ts (all routes)">
|
||||
</code-example>
|
||||
|
||||
### _DashboardComponent_ hero links
|
||||
### `DashboardComponent` hero links
|
||||
|
||||
The `DashboardComponent` hero links do nothing at the moment.
|
||||
|
||||
@ -343,7 +343,7 @@ to insert the current interation's `hero.id` into each
|
||||
[`routerLink`](#routerlink).
|
||||
|
||||
{@a heroes-component-links}
|
||||
### _HeroesComponent_ hero links
|
||||
### `HeroesComponent` hero links
|
||||
|
||||
The hero items in the `HeroesComponent` are `<li>` elements whose click events
|
||||
are bound to the component's `onSelect()` method.
|
||||
@ -446,7 +446,7 @@ The browser refreshes and the app crashes with a compiler error.
|
||||
`HeroService` doesn't have a `getHero()` method.
|
||||
Add it now.
|
||||
|
||||
### Add *HeroService.getHero()*
|
||||
### Add `HeroService.getHero()`
|
||||
|
||||
Open `HeroService` and add this `getHero()` method
|
||||
|
||||
@ -518,7 +518,7 @@ Here are the code files discussed on this page and your app should look like thi
|
||||
|
||||
{@a approutingmodule}
|
||||
{@a appmodule}
|
||||
#### _AppRoutingModule_ and _AppModule_
|
||||
#### _AppRoutingModule_, _AppModule_, and _HeroService_
|
||||
|
||||
<code-tabs>
|
||||
<code-pane
|
||||
@ -529,6 +529,10 @@ Here are the code files discussed on this page and your app should look like thi
|
||||
title="src/app/app.module.ts"
|
||||
path="toh-pt5/src/app/app.module.ts">
|
||||
</code-pane>
|
||||
<code-pane
|
||||
title="src/app/hero.service.ts"
|
||||
path="toh-pt5/src/app/hero.service.ts">
|
||||
</code-pane>
|
||||
</code-tabs>
|
||||
|
||||
{@a appcomponent}
|
||||
@ -565,6 +569,7 @@ Here are the code files discussed on this page and your app should look like thi
|
||||
|
||||
{@a heroescomponent}
|
||||
#### _HeroesComponent_
|
||||
|
||||
<code-tabs>
|
||||
<code-pane
|
||||
title="src/app/heroes/heroes.component.html" path="toh-pt5/src/app/heroes/heroes.component.html">
|
||||
|
@ -69,15 +69,20 @@ describe('DocumentService', () => {
|
||||
it('should emit the not-found document if the document is not found on the server', () => {
|
||||
let currentDocument: DocumentContents|undefined;
|
||||
const notFoundDoc = { id: FILE_NOT_FOUND_ID, contents: '<h1>Page Not Found</h1>' };
|
||||
const { docService } = getServices('missing/doc');
|
||||
const { docService, logger } = getServices('missing/doc');
|
||||
docService.currentDocument.subscribe(doc => currentDocument = doc);
|
||||
|
||||
// Initial request return 404.
|
||||
httpMock.expectOne({}).flush(null, {status: 404, statusText: 'NOT FOUND'});
|
||||
expect(logger.output.error).toEqual([
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(logger.output.error[0][0].message).toEqual(`Document file not found at 'missing/doc'`);
|
||||
|
||||
// Subsequent request for not-found document.
|
||||
logger.output.error = [];
|
||||
httpMock.expectOne(CONTENT_URL_PREFIX + 'file-not-found.json').flush(notFoundDoc);
|
||||
|
||||
expect(logger.output.error).toEqual([]); // does not report repeate errors
|
||||
expect(currentDocument).toEqual(notFoundDoc);
|
||||
});
|
||||
|
||||
@ -102,12 +107,17 @@ describe('DocumentService', () => {
|
||||
let latestDocument: DocumentContents|undefined;
|
||||
const doc1 = { contents: 'doc 1' };
|
||||
const doc2 = { contents: 'doc 2' };
|
||||
const { docService, locationService } = getServices('initial/doc');
|
||||
const { docService, locationService, logger } = getServices('initial/doc');
|
||||
|
||||
docService.currentDocument.subscribe(doc => latestDocument = doc);
|
||||
|
||||
httpMock.expectOne({}).flush(null, {status: 500, statusText: 'Server Error'});
|
||||
expect(latestDocument!.id).toEqual(FETCHING_ERROR_ID);
|
||||
expect(logger.output.error).toEqual([
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(logger.output.error[0][0].message)
|
||||
.toEqual(`Error fetching document 'initial/doc': (Http failure response for generated/docs/initial/doc.json: 500 Server Error)`);
|
||||
|
||||
locationService.go('new/doc');
|
||||
httpMock.expectOne({}).flush(doc1);
|
||||
|
@ -78,7 +78,7 @@ export class DocumentService {
|
||||
|
||||
private getFileNotFoundDoc(id: string): Observable<DocumentContents> {
|
||||
if (id !== FILE_NOT_FOUND_ID) {
|
||||
this.logger.error(`Document file not found at '${id}'`);
|
||||
this.logger.error(new Error(`Document file not found at '${id}'`));
|
||||
// using `getDocument` means that we can fetch the 404 doc contents from the server and cache it
|
||||
return this.getDocument(FILE_NOT_FOUND_ID);
|
||||
} else {
|
||||
@ -90,7 +90,7 @@ export class DocumentService {
|
||||
}
|
||||
|
||||
private getErrorDoc(id: string, error: HttpErrorResponse): Observable<DocumentContents> {
|
||||
this.logger.error('Error fetching document', error);
|
||||
this.logger.error(new Error(`Error fetching document '${id}': (${error.message})`));
|
||||
this.cache.delete(id);
|
||||
return Observable.of({
|
||||
id: FETCHING_ERROR_ID,
|
||||
|
@ -66,7 +66,10 @@ describe('AnnouncementBarComponent', () => {
|
||||
const request = httpMock.expectOne('generated/announcements.json');
|
||||
request.flush('some random response');
|
||||
expect(component.announcement).toBeUndefined();
|
||||
expect(mockLogger.output.error[0][0]).toContain('generated/announcements.json contains invalid data:');
|
||||
expect(mockLogger.output.error).toEqual([
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json contains invalid data:/);
|
||||
});
|
||||
|
||||
it('should handle a failed request for `announcements.json`', () => {
|
||||
@ -74,7 +77,10 @@ describe('AnnouncementBarComponent', () => {
|
||||
const request = httpMock.expectOne('generated/announcements.json');
|
||||
request.error(new ErrorEvent('404'));
|
||||
expect(component.announcement).toBeUndefined();
|
||||
expect(mockLogger.output.error[0][0]).toContain('generated/announcements.json request failed:');
|
||||
expect(mockLogger.output.error).toEqual([
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json request failed:/);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -59,12 +59,12 @@ export class AnnouncementBarComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.http.get<Announcement[]>(announcementsPath)
|
||||
.catch(error => {
|
||||
this.logger.error(`${announcementsPath} request failed: ${error.message}`);
|
||||
this.logger.error(new Error(`${announcementsPath} request failed: ${error.message}`));
|
||||
return [];
|
||||
})
|
||||
.map(announcements => this.findCurrentAnnouncement(announcements))
|
||||
.catch(error => {
|
||||
this.logger.error(`${announcementsPath} contains invalid data: ${error.message}`);
|
||||
this.logger.error(new Error(`${announcementsPath} contains invalid data: ${error.message}`));
|
||||
return [];
|
||||
})
|
||||
.subscribe(announcement => this.announcement = announcement);
|
||||
|
@ -254,10 +254,14 @@ describe('CodeComponent', () => {
|
||||
it('should display an error when copy fails', () => {
|
||||
const snackBar: MatSnackBar = TestBed.get(MatSnackBar);
|
||||
const copierService: CopierService = TestBed.get(CopierService);
|
||||
const logger: TestLogger = TestBed.get(Logger);
|
||||
spyOn(snackBar, 'open');
|
||||
spyOn(copierService, 'copyText').and.returnValue(false);
|
||||
getButton().click();
|
||||
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 });
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(jasmine.any(Error));
|
||||
expect(logger.error.calls.mostRecent().args[0].message).toMatch(/^ERROR copying code to clipboard:/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -148,7 +148,7 @@ export class CodeComponent implements OnChanges {
|
||||
duration: 800,
|
||||
});
|
||||
} else {
|
||||
this.logger.error('ERROR copying code to clipboard:', code);
|
||||
this.logger.error(new Error(`ERROR copying code to clipboard: "${code}"`));
|
||||
// failure snackbar alert
|
||||
this.snackbar.open('Copy failed. Please try again!', '', {
|
||||
duration: 800,
|
||||
|
@ -33,8 +33,8 @@ export class PrettyPrinter {
|
||||
.then(
|
||||
() => (window as any)['prettyPrintOne'],
|
||||
err => {
|
||||
const msg = 'Cannot get prettify.js from server';
|
||||
this.logger.error(msg, err);
|
||||
const msg = `Cannot get prettify.js from server: ${err.message}`;
|
||||
this.logger.error(new Error(msg));
|
||||
// return a pretty print fn that always fails.
|
||||
return () => { throw new Error(msg); };
|
||||
});
|
||||
|
@ -555,8 +555,9 @@ describe('DocViewerComponent', () => {
|
||||
expect(swapViewsSpy).not.toHaveBeenCalled();
|
||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
||||
expect(logger.output.error).toEqual([
|
||||
[`[DocViewer] Error preparing document 'foo': ${error.stack}`],
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'foo': ${error.stack}`);
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
||||
});
|
||||
@ -576,8 +577,9 @@ describe('DocViewerComponent', () => {
|
||||
expect(swapViewsSpy).not.toHaveBeenCalled();
|
||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
||||
expect(logger.output.error).toEqual([
|
||||
[`[DocViewer] Error preparing document 'bar': ${error.stack}`],
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'bar': ${error.stack}`);
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
||||
});
|
||||
@ -597,8 +599,9 @@ describe('DocViewerComponent', () => {
|
||||
expect(swapViewsSpy).not.toHaveBeenCalled();
|
||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
||||
expect(logger.output.error).toEqual([
|
||||
[`[DocViewer] Error preparing document 'baz': ${error.stack}`],
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'baz': ${error.stack}`);
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
||||
});
|
||||
@ -618,8 +621,9 @@ describe('DocViewerComponent', () => {
|
||||
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
||||
expect(logger.output.error).toEqual([
|
||||
[`[DocViewer] Error preparing document 'qux': ${error.stack}`],
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'qux': ${error.stack}`);
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
||||
});
|
||||
@ -636,8 +640,9 @@ describe('DocViewerComponent', () => {
|
||||
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
||||
expect(logger.output.error).toEqual([
|
||||
[`[DocViewer] Error preparing document 'qux': ${error}`],
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'qux': ${error}`);
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
|
||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
||||
});
|
||||
|
@ -157,7 +157,7 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
||||
.do(() => this.docRendered.emit())
|
||||
.catch(err => {
|
||||
const errorMessage = (err instanceof Error) ? err.stack : err;
|
||||
this.logger.error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`);
|
||||
this.logger.error(new Error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`));
|
||||
this.nextViewContainer.innerHTML = '';
|
||||
this.setNoIndex(true);
|
||||
return this.void$;
|
||||
|
@ -34,8 +34,9 @@ describe('logger service', () => {
|
||||
|
||||
describe('error', () => {
|
||||
it('should delegate to ErrorHandler', () => {
|
||||
logger.error('param1', 'param2', 'param3');
|
||||
expect(errorHandler.handleError).toHaveBeenCalledWith('param1 param2 param3');
|
||||
const err = new Error('some error message');
|
||||
logger.error(err);
|
||||
expect(errorHandler.handleError).toHaveBeenCalledWith(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -13,9 +13,8 @@ export class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
error(value: any, ...rest: any[]) {
|
||||
const message = [value, ...rest].join(' ');
|
||||
this.errorHandler.handleError(message);
|
||||
error(error: Error) {
|
||||
this.errorHandler.handleError(error);
|
||||
}
|
||||
|
||||
warn(value: any, ...rest: any[]) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "5.2.8",
|
||||
"version": "5.2.9",
|
||||
"private": true,
|
||||
"branchPattern": "2.0.*",
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
|
@ -74,6 +74,15 @@ import {el, stringifyElement} from '@angular/platform-browser/testing/src/browse
|
||||
expect(getDOM().getStyle(d, 'background-url')).toBe('url(http://test.com/bg.jpg)');
|
||||
});
|
||||
|
||||
// Test for regression caused by angular/angular#22536
|
||||
it('should parse styles correctly following the spec', () => {
|
||||
const d = getDOM().createElement('div');
|
||||
getDOM().setStyle(d, 'background-image', 'url("paper.gif")');
|
||||
expect(d.style.backgroundImage).toBe('url("paper.gif")');
|
||||
expect(d.style.getPropertyValue('background-image')).toBe('url("paper.gif")');
|
||||
expect(getDOM().getStyle(d, 'background-image')).toBe('url("paper.gif")');
|
||||
});
|
||||
|
||||
it('should parse camel-case styles correctly', () => {
|
||||
const d = getDOM().createElement('div');
|
||||
getDOM().setStyle(d, 'marginRight', '10px');
|
||||
|
@ -135,17 +135,51 @@ export class DominoAdapter extends BrowserDomAdapter {
|
||||
return href;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_readStyleAttribute(element: any): {[name: string]: string} {
|
||||
const styleMap: {[name: string]: string} = {};
|
||||
const styleAttribute = element.getAttribute('style');
|
||||
if (styleAttribute) {
|
||||
const styleList = styleAttribute.split(/;+/g);
|
||||
for (let i = 0; i < styleList.length; i++) {
|
||||
const style = styleList[i].trim();
|
||||
if (style.length > 0) {
|
||||
const colonIndex = style.indexOf(':');
|
||||
if (colonIndex === -1) {
|
||||
throw new Error(`Invalid CSS style: ${style}`);
|
||||
}
|
||||
const name = style.substr(0, colonIndex).trim();
|
||||
styleMap[name] = style.substr(colonIndex + 1).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return styleMap;
|
||||
}
|
||||
/** @internal */
|
||||
_writeStyleAttribute(element: any, styleMap: {[name: string]: string}) {
|
||||
let styleAttrValue = '';
|
||||
for (const key in styleMap) {
|
||||
const newValue = styleMap[key];
|
||||
if (newValue) {
|
||||
styleAttrValue += key + ':' + styleMap[key] + ';';
|
||||
}
|
||||
}
|
||||
element.setAttribute('style', styleAttrValue);
|
||||
}
|
||||
setStyle(element: any, styleName: string, styleValue?: string|null) {
|
||||
styleName = styleName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
element.style[styleName] = styleValue;
|
||||
const styleMap = this._readStyleAttribute(element);
|
||||
styleMap[styleName] = styleValue || '';
|
||||
this._writeStyleAttribute(element, styleMap);
|
||||
}
|
||||
removeStyle(element: any, styleName: string) {
|
||||
// IE requires '' instead of null
|
||||
// see https://github.com/angular/angular/issues/7916
|
||||
element.style[styleName] = '';
|
||||
this.setStyle(element, styleName, '');
|
||||
}
|
||||
getStyle(element: any, styleName: string): string {
|
||||
return element.style[styleName] || element.style.getPropertyValue(styleName);
|
||||
const styleMap = this._readStyleAttribute(element);
|
||||
return styleMap[styleName] || '';
|
||||
}
|
||||
hasStyle(element: any, styleName: string, styleValue?: string): boolean {
|
||||
const value = this.getStyle(element, styleName);
|
||||
|
@ -280,7 +280,8 @@ export class DefaultUrlSerializer implements UrlSerializer {
|
||||
serialize(tree: UrlTree): string {
|
||||
const segment = `/${serializeSegment(tree.root, true)}`;
|
||||
const query = serializeQueryParams(tree.queryParams);
|
||||
const fragment = typeof tree.fragment === `string` ? `#${encodeUriQuery(tree.fragment !)}` : '';
|
||||
const fragment =
|
||||
typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment !)}` : '';
|
||||
|
||||
return `${segment}${query}${fragment}`;
|
||||
}
|
||||
@ -329,13 +330,7 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
||||
* Encodes a URI string with the default encoding. This function will only ever be called from
|
||||
* `encodeUriQuery` or `encodeUriSegment` as it's the base set of encodings to be used. We need
|
||||
* a custom encoding because encodeURIComponent is too aggressive and encodes stuff that doesn't
|
||||
* have to be encoded per http://tools.ietf.org/html/rfc3986:
|
||||
* query = *( pchar / "/" / "?" )
|
||||
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
* pct-encoded = "%" HEXDIG HEXDIG
|
||||
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||||
* / "*" / "+" / "," / ";" / "="
|
||||
* have to be encoded per https://url.spec.whatwg.org.
|
||||
*/
|
||||
function encodeUriString(s: string): string {
|
||||
return encodeURIComponent(s)
|
||||
@ -346,8 +341,8 @@ function encodeUriString(s: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be used to encode both keys and values in a query string key/value or the
|
||||
* URL fragment. In the following URL, you need to call encodeUriQuery on "k", "v" and "f":
|
||||
* This function should be used to encode both keys and values in a query string key/value. In
|
||||
* the following URL, you need to call encodeUriQuery on "k" and "v":
|
||||
*
|
||||
* http://www.site.org/html;mk=mv?k=v#f
|
||||
*/
|
||||
@ -355,6 +350,16 @@ export function encodeUriQuery(s: string): string {
|
||||
return encodeUriString(s).replace(/%3B/gi, ';');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be used to encode a URL fragment. In the following URL, you need to call
|
||||
* encodeUriFragment on "f":
|
||||
*
|
||||
* http://www.site.org/html;mk=mv?k=v#f
|
||||
*/
|
||||
export function encodeUriFragment(s: string): string {
|
||||
return encodeURI(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be run on any URI segment as well as the key and value in a key/value
|
||||
* pair for matrix params. In the following URL, you need to call encodeUriSegment on "html",
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {PRIMARY_OUTLET} from '../src/shared';
|
||||
import {DefaultUrlSerializer, UrlSegmentGroup, encodeUriQuery, encodeUriSegment, serializePath} from '../src/url_tree';
|
||||
import {DefaultUrlSerializer, UrlSegmentGroup, encodeUriFragment, encodeUriQuery, encodeUriSegment, serializePath} from '../src/url_tree';
|
||||
|
||||
describe('url serializer', () => {
|
||||
const url = new DefaultUrlSerializer();
|
||||
@ -254,11 +254,11 @@ describe('url serializer', () => {
|
||||
});
|
||||
|
||||
it('should encode/decode fragment', () => {
|
||||
const u = `/one#${encodeUriQuery('one two=three four')}`;
|
||||
const u = `/one#${encodeUriFragment('one two=three four')}`;
|
||||
const tree = url.parse(u);
|
||||
|
||||
expect(tree.fragment).toEqual('one two=three four');
|
||||
expect(url.serialize(tree)).toEqual('/one#one%20two%3Dthree%20four');
|
||||
expect(url.serialize(tree)).toEqual('/one#one%20two=three%20four');
|
||||
});
|
||||
});
|
||||
|
||||
@ -311,7 +311,7 @@ describe('url serializer', () => {
|
||||
// From http://www.ietf.org/rfc/rfc3986.txt
|
||||
const unreserved = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~`;
|
||||
|
||||
it('should encode a minimal set of special characters in queryParams and fragment', () => {
|
||||
it('should encode a minimal set of special characters in queryParams', () => {
|
||||
const notEncoded = unreserved + `:@!$'*,();`;
|
||||
const encode = ` +%&=#[]/?`;
|
||||
const encoded = `%20%2B%25%26%3D%23%5B%5D%2F%3F`;
|
||||
@ -324,9 +324,9 @@ describe('url serializer', () => {
|
||||
});
|
||||
|
||||
it('should encode a minimal set of special characters in fragment', () => {
|
||||
const notEncoded = unreserved + `:@!$'*,();`;
|
||||
const encode = ` +%&=#[]/?`;
|
||||
const encoded = `%20%2B%25%26%3D%23%5B%5D%2F%3F`;
|
||||
const notEncoded = unreserved + `:@!$'*,();+&=#/?`;
|
||||
const encode = ' %<>`"[]';
|
||||
const encoded = `%20%25%3C%3E%60%22%5B%5D`;
|
||||
|
||||
const parsed = url.parse('/foo');
|
||||
|
||||
|
Reference in New Issue
Block a user