Compare commits

..

10 Commits
5.2.8 ... 5.2.9

Author SHA1 Message Date
5298b2bda3 docs: add changelog for 5.2.9 2018-03-14 14:59:27 -07:00
25ae886cad release: cut the 5.2.9 release 2018-03-14 14:56:14 -07:00
fc6dfc2e08 fix(platform-server): add styles to elements correctly (#22527)
* Partially reverts #22263 due to lack of total spec compliance
  on the server
* Maintains the camel-case styles fix

PR Close #22527
2018-03-14 14:12:32 -07:00
c0670ef52d docs(aio): add ng-japan 2018 to events (#22750)
ng-japan 2018 will be held at June 16 in Tokyo, Japan! 

https://ngjapan.org/en.html
PR Close #22750
2018-03-14 10:59:57 -07:00
fe96cafd03 fix(aio): constrain error logging to improve reporting (#22713)
The `Logger.error()` method now only accepts a single `Error` parameter
and passes this through to the error handler.
This allows the error handler to serialize the error more accurately.

The various places that use `Logger.error()` have been updated.

See #21943#issuecomment-370230047

PR Close #22713
2018-03-14 10:52:12 -07:00
ad674dad37 docs: testing - highlight dispatchEvent (#22726)
PR Close #22726
2018-03-14 10:21:42 -07:00
86517f2ad5 fix(router): correct over-encoding of URL fragment (#22687)
Relates to: #10280 #22337

PR Close #22687
2018-03-11 22:15:02 -07:00
6d9a4f8aea docs: refactor revert() and call to lifecylce hook, edit doc to changes (#22094)
PR Close #22094
2018-03-08 10:58:43 -08:00
a1efc27ff2 ci: double our cores on CircleCI (#22641)
This should cut our build time in ~half, assuming it's widely parallel.
See
https://circleci.com/docs/2.0/configuration-reference/#resource_class

Also enable bazel repository caching, and store the external
repositories in the CircleCI cache for later builds.

PR Close #22641
2018-03-07 21:00:04 -08:00
311232004c docs: add HeroService to code tabs and fix headers (#22373)
PR Close #22373
2018-03-07 18:20:54 -08:00
37 changed files with 645 additions and 568 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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> &nbsp;
<button type="reset" (click)="revert()"
<button type="button" (click)="revert()"
[disabled]="heroForm.pristine" class="btn btn-danger">Revert</button>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ When youre 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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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