Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
28a92b2bcd | |||
48be539824 | |||
d788c679b6 | |||
a38f14b39c | |||
6a5e46cedd | |||
6316e5df71 | |||
90fca7c879 | |||
d871ae2dc6 | |||
44e84d87f9 | |||
b9e979e0a5 | |||
cb2aa41782 | |||
189a7e3750 |
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,3 +1,16 @@
|
||||
<a name="2.4.1"></a>
|
||||
## [2.4.1](https://github.com/angular/angular/compare/2.4.0...2.4.1) (2016-12-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** always quote string map key values in AOT code ([#13602](https://github.com/angular/angular/issues/13602)) ([6a5e46c](https://github.com/angular/angular/commit/6a5e46c))
|
||||
* **animations:** always recover from a failed animation step ([#13604](https://github.com/angular/angular/issues/13604)) ([d788c67](https://github.com/angular/angular/commit/d788c67))
|
||||
* **compiler:** ignore `@import` in comments ([#13368](https://github.com/angular/angular/issues/13368)) ([6316e5d](https://github.com/angular/angular/commit/6316e5d)), closes [#12196](https://github.com/angular/angular/issues/12196)
|
||||
* **core:** improve error message when component factory cannot be found ([#13541](https://github.com/angular/angular/issues/13541)) ([b9e979e](https://github.com/angular/angular/commit/b9e979e)), closes [#12678](https://github.com/angular/angular/issues/12678)
|
||||
* **router:** should reset location if a navigation by location is successful ([#13545](https://github.com/angular/angular/issues/13545)) ([a38f14b](https://github.com/angular/angular/commit/a38f14b)), closes [#13491](https://github.com/angular/angular/issues/13491)
|
||||
|
||||
|
||||
|
||||
<a name="2.4.0"></a>
|
||||
# [2.4.0 stability-interjection](https://github.com/angular/angular/compare/2.3.1...2.4.0) (2016-12-20)
|
||||
|
||||
|
12
DEVELOPER.md
12
DEVELOPER.md
@ -74,6 +74,15 @@ use in these instructions.
|
||||
*Option 2*: defining a bash alias like `alias nbin='PATH=$(npm bin):$PATH'` as detailed in this
|
||||
[Stackoverflow answer](http://stackoverflow.com/questions/9679932/how-to-use-package-installed-locally-in-node-modules/15157360#15157360) and used like this: e.g., `nbin gulp build`.
|
||||
|
||||
## Installing Bower Modules
|
||||
|
||||
Now run `bower` to install additional dependencies:
|
||||
|
||||
```shell
|
||||
# Install other Angular project dependencies (bower.json)
|
||||
bower install
|
||||
```
|
||||
|
||||
## Windows only
|
||||
|
||||
In order to create the right symlinks, run **as administrator**:
|
||||
@ -155,3 +164,6 @@ For subsequent snapshots, just run
|
||||
``` shell
|
||||
$ ./scripts/publish/publish-build-artifacts.sh [github username]
|
||||
```
|
||||
|
||||
The script will publish the build snapshot to a branch with the same name as your current branch,
|
||||
and create it if it doesn't exist.
|
||||
|
@ -89,9 +89,18 @@ export class NgForRow {
|
||||
@Directive({selector: '[ngFor][ngForOf]'})
|
||||
export class NgFor implements DoCheck, OnChanges {
|
||||
@Input() ngForOf: any;
|
||||
@Input() ngForTrackBy: TrackByFn;
|
||||
@Input()
|
||||
set ngForTrackBy(fn: TrackByFn) {
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`trackBy must be a function, but received ${JSON.stringify(fn)}`);
|
||||
}
|
||||
this._trackByFn = fn;
|
||||
}
|
||||
|
||||
get ngForTrackBy(): TrackByFn { return this._trackByFn; }
|
||||
|
||||
private _differ: IterableDiffer = null;
|
||||
private _trackByFn: TrackByFn;
|
||||
|
||||
constructor(
|
||||
private _viewContainer: ViewContainerRef, private _template: TemplateRef<NgForRow>,
|
||||
@ -119,7 +128,7 @@ export class NgFor implements DoCheck, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
ngDoCheck(): void {
|
||||
if (this._differ) {
|
||||
const changes = this._differ.diff(this.ngForOf);
|
||||
if (changes) this._applyChanges(changes);
|
||||
|
@ -294,6 +294,16 @@ export function main() {
|
||||
}));
|
||||
|
||||
describe('track by', () => {
|
||||
it('should throw if trackBy is not a function', async(() => {
|
||||
const template =
|
||||
`<template ngFor let-item [ngForOf]="items" [ngForTrackBy]="item?.id"></template>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [{id: 1}, {id: 2}];
|
||||
expect(() => fixture.detectChanges())
|
||||
.toThrowError(/trackBy must be a function, but received null/);
|
||||
}));
|
||||
|
||||
it('should set the context to the component instance', async(() => {
|
||||
const template =
|
||||
`<template ngFor let-item [ngForOf]="items" [ngForTrackBy]="trackByContext.bind(this)"></template>`;
|
||||
|
@ -66,7 +66,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
|
||||
ast.styles.forEach(entry => {
|
||||
const entries =
|
||||
Object.keys(entry).map((key): [string, o.Expression] => [key, o.literal(entry[key])]);
|
||||
stylesArr.push(o.literalMap(entries));
|
||||
stylesArr.push(o.literalMap(entries, null, true));
|
||||
});
|
||||
|
||||
return o.importExpr(createIdentifier(Identifiers.AnimationStyles)).instantiate([
|
||||
@ -322,12 +322,13 @@ class _AnimationBuilder implements AnimationAstVisitor {
|
||||
if (isPresent(value)) {
|
||||
const styleMap: any[] = [];
|
||||
Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); });
|
||||
variableValue = o.literalMap(styleMap);
|
||||
variableValue = o.literalMap(styleMap, null, true);
|
||||
}
|
||||
lookupMap.push([stateName, variableValue]);
|
||||
});
|
||||
|
||||
const compiledStatesMapStmt = this._statesMapVar.set(o.literalMap(lookupMap)).toDeclStmt();
|
||||
const compiledStatesMapStmt =
|
||||
this._statesMapVar.set(o.literalMap(lookupMap, null, true)).toDeclStmt();
|
||||
const statements: o.Statement[] = [compiledStatesMapStmt, fnStatement];
|
||||
|
||||
return new AnimationEntryCompileResult(this.animationName, statements, fnVariable);
|
||||
|
@ -894,8 +894,10 @@ export function literalArr(values: Expression[], type: Type = null): LiteralArra
|
||||
return new LiteralArrayExpr(values, type);
|
||||
}
|
||||
|
||||
export function literalMap(values: [string, Expression][], type: MapType = null): LiteralMapExpr {
|
||||
return new LiteralMapExpr(values.map(entry => new LiteralMapEntry(entry[0], entry[1])), type);
|
||||
export function literalMap(
|
||||
values: [string, Expression][], type: MapType = null, quoted: boolean = false): LiteralMapExpr {
|
||||
return new LiteralMapExpr(
|
||||
values.map(entry => new LiteralMapEntry(entry[0], entry[1], quoted)), type);
|
||||
}
|
||||
|
||||
export function not(expr: Expression): NotExpr {
|
||||
|
@ -9,8 +9,6 @@
|
||||
// Some of the code comes from WebComponents.JS
|
||||
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
|
||||
|
||||
import {isBlank, isPresent} from './facade/lang';
|
||||
|
||||
import {UrlResolver} from './url_resolver';
|
||||
|
||||
export class StyleWithImports {
|
||||
@ -18,8 +16,8 @@ export class StyleWithImports {
|
||||
}
|
||||
|
||||
export function isStyleUrlResolvable(url: string): boolean {
|
||||
if (isBlank(url) || url.length === 0 || url[0] == '/') return false;
|
||||
const schemeMatch = url.match(_urlWithSchemaRe);
|
||||
if (url == null || url.length === 0 || url[0] == '/') return false;
|
||||
const schemeMatch = url.match(URL_WITH_SCHEMA_REGEXP);
|
||||
return schemeMatch === null || schemeMatch[1] == 'package' || schemeMatch[1] == 'asset';
|
||||
}
|
||||
|
||||
@ -30,17 +28,20 @@ export function isStyleUrlResolvable(url: string): boolean {
|
||||
export function extractStyleUrls(
|
||||
resolver: UrlResolver, baseUrl: string, cssText: string): StyleWithImports {
|
||||
const foundUrls: string[] = [];
|
||||
const modifiedCssText = cssText.replace(_cssImportRe, function(...m: string[]) {
|
||||
const url = m[1] || m[2];
|
||||
if (!isStyleUrlResolvable(url)) {
|
||||
// Do not attempt to resolve non-package absolute URLs with URI scheme
|
||||
return m[0];
|
||||
}
|
||||
foundUrls.push(resolver.resolve(baseUrl, url));
|
||||
return '';
|
||||
});
|
||||
|
||||
const modifiedCssText =
|
||||
cssText.replace(CSS_COMMENT_REGEXP, '').replace(CSS_IMPORT_REGEXP, (...m: string[]) => {
|
||||
const url = m[1] || m[2];
|
||||
if (!isStyleUrlResolvable(url)) {
|
||||
// Do not attempt to resolve non-package absolute URLs with URI scheme
|
||||
return m[0];
|
||||
}
|
||||
foundUrls.push(resolver.resolve(baseUrl, url));
|
||||
return '';
|
||||
});
|
||||
return new StyleWithImports(modifiedCssText, foundUrls);
|
||||
}
|
||||
|
||||
const _cssImportRe = /@import\s+(?:url\()?\s*(?:(?:['"]([^'"]*))|([^;\)\s]*))[^;]*;?/g;
|
||||
const _urlWithSchemaRe = /^([^:/?#]+):/;
|
||||
const CSS_IMPORT_REGEXP = /@import\s+(?:url\()?\s*(?:(?:['"]([^'"]*))|([^;\)\s]*))[^;]*;?/g;
|
||||
const CSS_COMMENT_REGEXP = /\/\*.+?\*\//g;
|
||||
const URL_WITH_SCHEMA_REGEXP = /^([^:/?#]+):/;
|
||||
|
@ -113,6 +113,14 @@ export function main() {
|
||||
expect(emitStmt(o.literalMap([['someKey', o.literal(1)]]).toStmt())).toEqual(`{someKey: 1};`);
|
||||
});
|
||||
|
||||
it('should apply quotes to each entry within a map produced with literalMap when true', () => {
|
||||
expect(
|
||||
emitStmt(
|
||||
o.literalMap([['a', o.literal('a')], ['*', o.literal('star')]], null, true).toStmt())
|
||||
.replace(/\s+/gm, ''))
|
||||
.toEqual(`{'a':'a','*':'star'};`);
|
||||
});
|
||||
|
||||
it('should support blank literals', () => {
|
||||
expect(emitStmt(o.literal(null).toStmt())).toEqual('(null as any);');
|
||||
expect(emitStmt(o.literal(undefined).toStmt())).toEqual('(undefined as any);');
|
||||
|
@ -36,6 +36,17 @@ export function main() {
|
||||
expect(styleWithImports.styleUrls).toEqual(['http://ng.io/1.css', 'http://ng.io/2.css']);
|
||||
});
|
||||
|
||||
it('should ignore "@import" in comments', () => {
|
||||
const css = `
|
||||
@import '1.css';
|
||||
/*@import '2.css';*/
|
||||
`;
|
||||
const styleWithImports = extractStyleUrls(urlResolver, 'http://ng.io', css);
|
||||
expect(styleWithImports.style.trim()).toEqual('');
|
||||
expect(styleWithImports.styleUrls).toContain('http://ng.io/1.css');
|
||||
expect(styleWithImports.styleUrls).not.toContain('http://ng.io/2.css');
|
||||
});
|
||||
|
||||
it('should extract "@import url()" urls', () => {
|
||||
const css = `
|
||||
@import url('3.css');
|
||||
|
@ -19,7 +19,8 @@ import {ComponentFactory} from './component_factory';
|
||||
*/
|
||||
export class NoComponentFactoryError extends BaseError {
|
||||
constructor(public component: Function) {
|
||||
super(`No component factory found for ${stringify(component)}`);
|
||||
super(
|
||||
`No component factory found for ${stringify(component)}. Did you add it to @NgModule.entryComponents?`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,13 +17,11 @@ import {WebAnimationsDriver} from '@angular/platform-browser/src/dom/web_animati
|
||||
import {WebAnimationsPlayer} from '@angular/platform-browser/src/dom/web_animations_player';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver';
|
||||
|
||||
import {DomAnimatePlayer} from '../../../platform-browser/src/dom/dom_animate_player';
|
||||
import {ApplicationRef, Component, HostBinding, HostListener, NgModule, NgZone, destroyPlatform} from '../../index';
|
||||
import {DEFAULT_STATE} from '../../src/animation/animation_constants';
|
||||
import {AnimationGroupPlayer} from '../../src/animation/animation_group_player';
|
||||
import {AnimationKeyframe} from '../../src/animation/animation_keyframe';
|
||||
import {AnimationPlayer} from '../../src/animation/animation_player';
|
||||
import {AnimationPlayer, NoOpAnimationPlayer} from '../../src/animation/animation_player';
|
||||
import {AnimationStyles} from '../../src/animation/animation_styles';
|
||||
import {AnimationTransitionEvent} from '../../src/animation/animation_transition_event';
|
||||
import {AUTO_STYLE, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata';
|
||||
@ -2244,6 +2242,42 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
}));
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should recover if an animation driver or player throws an error during an animation',
|
||||
fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DummyIfCmp],
|
||||
providers: [{provide: AnimationDriver, useClass: ErroneousAnimationDriver}],
|
||||
imports: [CommonModule]
|
||||
});
|
||||
TestBed.overrideComponent(DummyIfCmp, {
|
||||
set: {
|
||||
template: `
|
||||
<div [@myAnimation]="exp" (@myAnimation.start)="callback1($event)" (@myAnimation.done)="callback2($event)"></div>
|
||||
`,
|
||||
animations: [trigger('myAnimation', [transition(
|
||||
'* => *',
|
||||
[
|
||||
animate(1000, style({transform: 'noooooo'})),
|
||||
])])]
|
||||
}
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(DummyIfCmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
let started = false;
|
||||
let done = false;
|
||||
cmp.callback1 = (event: AnimationTransitionEvent) => started = true;
|
||||
cmp.callback2 = (event: AnimationTransitionEvent) => done = true;
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(started).toBe(true);
|
||||
expect(done).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('full animation integration tests', () => {
|
||||
if (!getDOM().supportsWebAnimation()) return;
|
||||
|
||||
@ -2537,3 +2571,11 @@ class ExtendedWebAnimationsDriver extends WebAnimationsDriver {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
class ErroneousAnimationDriver extends MockAnimationDriver {
|
||||
animate(
|
||||
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
|
||||
duration: number, delay: number, easing: string): WebAnimationsPlayer {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ const _chromeNumKeyPadMap = {
|
||||
* @security Tread carefully! Interacting with the DOM directly is dangerous and
|
||||
* can introduce XSS risks.
|
||||
*/
|
||||
/* tslint:disable:requireParameterType */
|
||||
/* tslint:disable:requireParameterType no-console */
|
||||
export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
parse(templateHtml: string) { throw new Error('parse not implemented'); }
|
||||
static makeCurrent() { setRootDomAdapter(new BrowserDomAdapter()); }
|
||||
@ -82,7 +82,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
if (console.error) {
|
||||
console.error(error);
|
||||
} else {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
@ -90,7 +89,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
|
||||
log(error: string): void {
|
||||
if (window.console) {
|
||||
// tslint:disable-next-line:no-console
|
||||
window.console.log && window.console.log(error);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {APP_ID, Inject, Injectable, RenderComponentType, Renderer, RootRenderer, ViewEncapsulation} from '@angular/core';
|
||||
|
||||
import {isBlank, isPresent, stringify} from '../facade/lang';
|
||||
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, DirectRenderer, RenderDebugInfo} from '../private_import_core';
|
||||
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, DirectRenderer, NoOpAnimationPlayer, RenderDebugInfo} from '../private_import_core';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {DOCUMENT} from './dom_tokens';
|
||||
@ -262,8 +262,12 @@ export class DomRenderer implements Renderer {
|
||||
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
|
||||
duration: number, delay: number, easing: string,
|
||||
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
|
||||
return this._animationDriver.animate(
|
||||
element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
|
||||
try {
|
||||
return this._animationDriver.animate(
|
||||
element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
|
||||
} catch (e) {
|
||||
return new NoOpAnimationPlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,3 +21,5 @@ export type RenderDebugInfo = typeof r._RenderDebugInfo;
|
||||
export var RenderDebugInfo: typeof r.RenderDebugInfo = r.RenderDebugInfo;
|
||||
export type DebugDomRootRenderer = typeof r._DebugDomRootRenderer;
|
||||
export var DebugDomRootRenderer: typeof r.DebugDomRootRenderer = r.DebugDomRootRenderer;
|
||||
export type NoOpAnimationPlayer = typeof r._NoOpAnimationPlayer;
|
||||
export var NoOpAnimationPlayer: typeof r.NoOpAnimationPlayer = r.NoOpAnimationPlayer;
|
||||
|
@ -10,7 +10,7 @@ import {APP_ID, Inject, Injectable, NgZone, RenderComponentType, Renderer, RootR
|
||||
import {AnimationDriver, DOCUMENT} from '@angular/platform-browser';
|
||||
|
||||
import {isBlank, isPresent, stringify} from './facade/lang';
|
||||
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, RenderDebugInfo} from './private_import_core';
|
||||
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, NoOpAnimationPlayer, RenderDebugInfo} from './private_import_core';
|
||||
import {NAMESPACE_URIS, SharedStylesHost, flattenStyles, getDOM, isNamespaced, shimContentAttribute, shimHostAttribute, splitNamespace} from './private_import_platform-browser';
|
||||
|
||||
const TEMPLATE_COMMENT_TEXT = 'template bindings={}';
|
||||
@ -208,8 +208,12 @@ export class ServerRenderer implements Renderer {
|
||||
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
|
||||
duration: number, delay: number, easing: string,
|
||||
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
|
||||
return this._animationDriver.animate(
|
||||
element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
|
||||
try {
|
||||
return this._animationDriver.animate(
|
||||
element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
|
||||
} catch (e) {
|
||||
return new NoOpAnimationPlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,6 +266,8 @@ function defaultErrorHandler(error: any): any {
|
||||
throw error;
|
||||
}
|
||||
|
||||
type NavigationSource = 'imperative' | 'popstate' | 'hashchange';
|
||||
|
||||
type NavigationParams = {
|
||||
id: number,
|
||||
rawUrl: UrlTree,
|
||||
@ -273,7 +275,7 @@ type NavigationParams = {
|
||||
resolve: any,
|
||||
reject: any,
|
||||
promise: Promise<boolean>,
|
||||
imperative: boolean,
|
||||
source: NavigationSource,
|
||||
};
|
||||
|
||||
|
||||
@ -374,20 +376,8 @@ export class Router {
|
||||
if (!this.locationSubscription) {
|
||||
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
|
||||
const rawUrlTree = this.urlSerializer.parse(change['url']);
|
||||
const lastNavigation = this.navigations.value;
|
||||
|
||||
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
|
||||
// and that navigation results in 'replaceState' that leads to the same URL,
|
||||
// we should skip those.
|
||||
if (lastNavigation && lastNavigation.imperative &&
|
||||
lastNavigation.rawUrl.toString() === rawUrlTree.toString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.scheduleNavigation(
|
||||
rawUrlTree, false, {skipLocationChange: change['pop'], replaceUrl: true});
|
||||
}, 0);
|
||||
const source: NavigationSource = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
|
||||
setTimeout(() => { this.scheduleNavigation(rawUrlTree, source, {replaceUrl: true}); }, 0);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -505,12 +495,12 @@ export class Router {
|
||||
Promise<boolean> {
|
||||
if (url instanceof UrlTree) {
|
||||
return this.scheduleNavigation(
|
||||
this.urlHandlingStrategy.merge(url, this.rawUrlTree), true, extras);
|
||||
this.urlHandlingStrategy.merge(url, this.rawUrlTree), 'imperative', extras);
|
||||
}
|
||||
|
||||
const urlTree = this.urlSerializer.parse(url);
|
||||
return this.scheduleNavigation(
|
||||
this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree), true, extras);
|
||||
this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree), 'imperative', extras);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -585,8 +575,26 @@ export class Router {
|
||||
.subscribe(() => {});
|
||||
}
|
||||
|
||||
private scheduleNavigation(rawUrl: UrlTree, imperative: boolean, extras: NavigationExtras):
|
||||
private scheduleNavigation(rawUrl: UrlTree, source: NavigationSource, extras: NavigationExtras):
|
||||
Promise<boolean> {
|
||||
const lastNavigation = this.navigations.value;
|
||||
|
||||
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
|
||||
// and that navigation results in 'replaceState' that leads to the same URL,
|
||||
// we should skip those.
|
||||
if (lastNavigation && source !== 'imperative' && lastNavigation.source === 'imperative' &&
|
||||
lastNavigation.rawUrl.toString() === rawUrl.toString()) {
|
||||
return null; // return value is not used
|
||||
}
|
||||
|
||||
// Because of a bug in IE and Edge, the location class fires two events (popstate and
|
||||
// hashchange)
|
||||
// every single time. The second one should be ignored. Otherwise, the URL will flicker.
|
||||
if (lastNavigation && source == 'hashchange' && lastNavigation.source === 'popstate' &&
|
||||
lastNavigation.rawUrl.toString() === rawUrl.toString()) {
|
||||
return null; // return value is not used
|
||||
}
|
||||
|
||||
let resolve: any = null;
|
||||
let reject: any = null;
|
||||
|
||||
@ -596,7 +604,7 @@ export class Router {
|
||||
});
|
||||
|
||||
const id = ++this.navigationId;
|
||||
this.navigations.next({id, imperative, rawUrl, extras, resolve, reject, promise});
|
||||
this.navigations.next({id, source, rawUrl, extras, resolve, reject, promise});
|
||||
|
||||
// Make sure that the error is propagated even though `processNavigations` catch
|
||||
// handler does not rethrow
|
||||
|
@ -389,6 +389,43 @@ describe('Integration', () => {
|
||||
expect(cmp.recordedUrls()).toEqual(['one/two', 'three/four']);
|
||||
})));
|
||||
|
||||
describe('should reset location if a navigation by location is successful', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{
|
||||
provide: 'in1Second',
|
||||
useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
||||
let res: any = null;
|
||||
const p = new Promise(_ => res = _);
|
||||
setTimeout(() => res(true), 1000);
|
||||
return p;
|
||||
}
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('work', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([{path: 'simple', component: SimpleCmp, canActivate: ['in1Second']}]);
|
||||
|
||||
// Trigger two location changes to the same URL.
|
||||
// Because of the guard the order will look as follows:
|
||||
// - location change 'simple'
|
||||
// - start processing the change, start a guard
|
||||
// - location change 'simple'
|
||||
// - the first location change gets canceled, the URL gets reset to '/'
|
||||
// - the second location change gets finished, the URL should be reset to '/simple'
|
||||
(<any>location).simulateUrlPop('/simple');
|
||||
(<any>location).simulateUrlPop('/simple');
|
||||
|
||||
tick(2000);
|
||||
advance(fixture);
|
||||
|
||||
expect(location.path()).toEqual('/simple');
|
||||
})));
|
||||
});
|
||||
|
||||
it('should support secondary routes', fakeAsync(inject([Router], (router: Router) => {
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
@ -1415,7 +1452,7 @@ describe('Integration', () => {
|
||||
log.push('called');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"private": true,
|
||||
"branchPattern": "2.0.*",
|
||||
"description": "Angular 2 - a web framework for modern web apps",
|
||||
|
@ -23,17 +23,20 @@ function publishRepo {
|
||||
cd $REPO_DIR && \
|
||||
git init && \
|
||||
git remote add origin $REPO_URL && \
|
||||
git fetch origin master --depth=1 && \
|
||||
git checkout origin/master && \
|
||||
git checkout -b master
|
||||
# use the remote branch if it exists
|
||||
if git ls-remote --exit-code origin ${BRANCH}; then
|
||||
git fetch origin ${BRANCH} --depth=1 && \
|
||||
git checkout origin/${BRANCH}
|
||||
fi
|
||||
git checkout -b "${BRANCH}"
|
||||
)
|
||||
|
||||
# copy over build artifacts into the repo directory
|
||||
rm -rf $REPO_DIR/*
|
||||
cp -R $ARTIFACTS_DIR/* $REPO_DIR/
|
||||
|
||||
# Replace $$ANGULAR_VESION$$ with the build version.
|
||||
BUILD_VER="2.0.0-${SHORT_SHA}"
|
||||
# Replace $$ANGULAR_VERSION$$ with the build version.
|
||||
BUILD_VER="${LATEST_TAG}+${SHORT_SHA}"
|
||||
if [[ ${TRAVIS} ]]; then
|
||||
find $REPO_DIR/ -type f -name package.json -print0 | xargs -0 sed -i "s/\\\$\\\$ANGULAR_VERSION\\\$\\\$/${BUILD_VER}/g"
|
||||
|
||||
@ -59,7 +62,7 @@ function publishRepo {
|
||||
git add --all && \
|
||||
git commit -m "${COMMIT_MSG}" && \
|
||||
git tag "${BUILD_VER}" && \
|
||||
git push origin master --tags --force
|
||||
git push origin "${BRANCH}" --tags --force
|
||||
)
|
||||
}
|
||||
|
||||
@ -85,6 +88,7 @@ function publishPackages {
|
||||
COMMIT_MSG=`git log --oneline | head -n1`
|
||||
COMMITTER_USER_NAME=`git --no-pager show -s --format='%cN' HEAD`
|
||||
COMMITTER_USER_EMAIL=`git --no-pager show -s --format='%cE' HEAD`
|
||||
LATEST_TAG=`git describe --tags --abbrev=0`
|
||||
|
||||
publishRepo "${COMPONENT}" "${JS_BUILD_ARTIFACTS_DIR}"
|
||||
done
|
||||
@ -93,6 +97,7 @@ function publishPackages {
|
||||
}
|
||||
|
||||
# See DEVELOPER.md for help
|
||||
BRANCH=${TRAVIS_BRANCH:-$(git symbolic-ref --short HEAD)}
|
||||
if [ $# -gt 0 ]; then
|
||||
ORG=$1
|
||||
publishPackages "ssh"
|
||||
@ -103,5 +108,5 @@ elif [[ \
|
||||
ORG="angular"
|
||||
publishPackages "http"
|
||||
else
|
||||
echo "Not building the upstream/master branch, build artifacts won't be published."
|
||||
echo "Not building the upstream/${BRANCH} branch, build artifacts won't be published."
|
||||
fi
|
||||
|
Reference in New Issue
Block a user