diff --git a/.gitignore b/.gitignore
index ca784df6e..9995f912d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,12 +27,14 @@ tests/test-output.txt
tests/platforms
tests/lib
tests/node_modules
+tests/hooks
ng-sample/app/**/*.js
ng-sample/app/global.d.ts
ng-sample/platforms
ng-sample/lib
ng-sample/node_modules
+ng-sample/hooks
ng-sample/app/nativescript-angular
startup-test/platforms
@@ -52,4 +54,4 @@ startup-test/node_modules
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
-!.vscode/extensions.json
\ No newline at end of file
+!.vscode/extensions.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d8b3c3bc..5bddf5fb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,57 @@
+
+# [4.2.0](https://github.com/NativeScript/nativescript-angular/compare/3.1.3...4.2.0) (2017-08-09)
+
+### Features
+
+* Angular 4.2 support ([#842](https://github.com/NativeScript/nativescript-angular/issues/842)) ([eb3fd81](https://github.com/NativeScript/nativescript-angular/commit/eb3fd81))
+
+
+### BREAKING CHANGES
+
+* `NativeScriptModule` should be imported only in the root application
+module (usually named `AppModule`).
+All other NgModules in the app (both feature and lazy-loaded ones)
+should import the `NativeScriptCommonModule` instead.
+The behaviour is alligned with `BrowserModule` and `CommonModule` in web
+Angular apps described in [this](https://angular.io/guide/ngmodule-faq#q-browser-vs-common-module) guide.
+
+Migration steps:
+In all NgModules, except the root one (`AppModule`), replace:
+```
+import { NativeScriptModule } from "nativescript-angular/nativescript.module";
+…
+@NgModule({
+ imports: [
+ NativeScriptModule,
+ ]
+…
+})
+```
+with:
+```
+import { NativeScriptCommonModule } from "nativescript-angular/common";
+…
+@NgModule({
+ imports: [
+ NativeScriptCommonModule,
+ ]
+…
+})
+```
+
+
+
+
+## [3.1.3](https://github.com/NativeScript/nativescript-angular/compare/3.1.2...3.1.3) (2017-07-19)
+
+
+### Bug Fixes
+
+* **action-bar:** ignore InvisibleNodes when adding title ([#903](https://github.com/NativeScript/nativescript-angular/issues/903)) ([8308e45](https://github.com/NativeScript/nativescript-angular/commit/8308e45)), closes [#897](https://github.com/NativeScript/nativescript-angular/issues/897)
+* asynchronously destroy items evicted on clearHistory navigation ([#847](https://github.com/NativeScript/nativescript-angular/issues/847)) ([448412a](https://github.com/NativeScript/nativescript-angular/commit/448412a)), closes [#829](https://github.com/NativeScript/nativescript-angular/issues/829)
+
+
+
## [3.1.2](https://github.com/NativeScript/nativescript-angular/compare/3.1.1...3.1.2) (2017-07-12)
diff --git a/nativescript-angular/animations.ts b/nativescript-angular/animations.ts
index 30b32f3d6..7b5f2d2f3 100644
--- a/nativescript-angular/animations.ts
+++ b/nativescript-angular/animations.ts
@@ -1,13 +1,17 @@
import { NgModule, Injectable, NgZone, Provider, RendererFactory2 } from "@angular/core";
+import { AnimationBuilder } from "@angular/animations";
+
import {
AnimationDriver,
- ɵAnimationEngine as AnimationEngine,
ɵAnimationStyleNormalizer as AnimationStyleNormalizer,
- ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer
+ ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer,
} from "@angular/animations/browser";
-import { ɵAnimationRendererFactory as AnimationRendererFactory } from "@angular/platform-browser/animations";
+import {
+ ɵAnimationRendererFactory as AnimationRendererFactory,
+ ɵBrowserAnimationBuilder as BrowserAnimationBuilder,
+} from "@angular/platform-browser/animations";
import { NativeScriptAnimationEngine } from "./animations/animation-engine";
import { NativeScriptAnimationDriver } from "./animations/animation-driver";
@@ -26,21 +30,23 @@ export function instantiateSupportedAnimationDriver() {
}
export function instantiateRendererFactory(
- renderer: NativeScriptRendererFactory, engine: AnimationEngine, zone: NgZone) {
+ renderer: NativeScriptRendererFactory, engine: NativeScriptAnimationEngine, zone: NgZone) {
return new AnimationRendererFactory(renderer, engine, zone);
}
-export function instanciateDefaultStyleNormalizer() {
+export function instantiateDefaultStyleNormalizer() {
return new WebAnimationsStyleNormalizer();
}
export const NATIVESCRIPT_ANIMATIONS_PROVIDERS: Provider[] = [
+ {provide: AnimationBuilder, useClass: BrowserAnimationBuilder},
{provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver},
- {provide: AnimationStyleNormalizer, useFactory: instanciateDefaultStyleNormalizer},
- {provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
+ {provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer},
+ {provide: NativeScriptAnimationEngine, useClass: InjectableAnimationEngine},
+ {
provide: RendererFactory2,
useFactory: instantiateRendererFactory,
- deps: [NativeScriptRendererFactory, AnimationEngine, NgZone]
+ deps: [NativeScriptRendererFactory, NativeScriptAnimationEngine, NgZone]
}
];
diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts
index eeb385e42..369450208 100644
--- a/nativescript-angular/animations/animation-driver.ts
+++ b/nativescript-angular/animations/animation-driver.ts
@@ -1,22 +1,118 @@
import { AnimationPlayer } from "@angular/animations";
+import { AnimationDriver } from "@angular/animations/browser";
+import { ProxyViewContainer } from "tns-core-modules/ui/proxy-view-container";
+import { eachDescendant } from "tns-core-modules/ui/core/view";
-import { NgView } from "../element-registry";
import { NativeScriptAnimationPlayer } from "./animation-player";
-import { Keyframe } from "./utils";
+import {
+ Keyframe,
+ dashCaseToCamelCase,
+} from "./utils";
+import { NgView, InvisibleNode } from "../element-registry";
+import { animationsLog as traceLog } from "../trace";
-export abstract class AnimationDriver {
- abstract animate(
- element: any,
- keyframes: Keyframe[],
- duration: number,
- delay: number,
- easing: string
- ): AnimationPlayer;
+import { createSelector, SelectorCore } from "tns-core-modules/ui/styling/css-selector";
+
+interface ViewMatchResult {
+ found: boolean;
+}
+
+interface ViewMatchParams {
+ originalView: NgView;
+}
+
+interface QueryParams {
+ selector: Selector;
+ multi: boolean;
+}
+
+interface QueryResult {
+ matches: NgView[];
+}
+
+class Selector {
+ private nsSelectors: SelectorCore[];
+ private classSelectors: string[];
+
+ constructor(rawSelector: string) {
+ this.parse(rawSelector);
+ }
+
+ match(element: NgView): boolean {
+ return this.nsSelectorMatch(element) || this.classSelectorsMatch(element);
+ }
+
+ private parse(rawSelector: string) {
+ const selectors = rawSelector.split(",").map(s => s.trim());
+
+ this.nsSelectors = selectors.map(createSelector);
+ this.classSelectors = selectors
+ .filter(s => s.startsWith("."))
+ .map(s => s.substring(1));
+ }
+
+ private nsSelectorMatch(element: NgView) {
+ return this.nsSelectors.some(s => s.match(element));
+ }
+
+ private classSelectorsMatch(element: NgView) {
+ return this.classSelectors.some(s => this.hasClass(element, s));
+ }
+
+ // we're using that instead of match for classes
+ // that are dynamically added by the animation engine
+ // such as .ng-trigger, that's added for every :enter view
+ private hasClass(element: NgView, cls: string) {
+ return element && element["$$classes"] && element["$$classes"][cls];
+ }
}
export class NativeScriptAnimationDriver implements AnimationDriver {
+ matchesElement(element: NgView, rawSelector: string): boolean {
+ traceLog(
+ `NativeScriptAnimationDriver.matchesElement ` +
+ `element: ${element}, selector: ${rawSelector}`
+ );
+
+ const selector = this.makeSelector(rawSelector);
+ return selector.match(element);
+ }
+
+
+ containsElement(elm1: NgView, elm2: NgView): boolean {
+ traceLog(
+ `NativeScriptAnimationDriver.containsElement ` +
+ `element1: ${elm1}, element2: ${elm2}`
+ );
+
+ const params: ViewMatchParams = { originalView: elm2 };
+ const result: ViewMatchResult = this.visitDescendants(elm1, viewMatches, params);
+
+ return result.found;
+ }
+
+ query(element: NgView, rawSelector: string, multi: boolean): NgView[] {
+ traceLog(
+ `NativeScriptAnimationDriver.query ` +
+ `element: ${element}, selector: ${rawSelector} ` +
+ `multi: ${multi}`
+ );
+
+ const selector = this.makeSelector(rawSelector);
+ const params: QueryParams = { selector, multi };
+ const result: QueryResult = this.visitDescendants(element, queryDescendants, params);
+
+ return result.matches || [];
+ }
+
computeStyle(element: NgView, prop: string): string {
- return element.style[`css-${prop}`];
+ traceLog(
+ `NativeScriptAnimationDriver.computeStyle ` +
+ `element: ${element}, prop: ${prop}`
+ );
+
+ const camelCaseProp = dashCaseToCamelCase(prop);
+ return element.style[camelCaseProp];
}
animate(
@@ -26,7 +122,74 @@ export class NativeScriptAnimationDriver implements AnimationDriver {
delay: number,
easing: string
): AnimationPlayer {
+ traceLog(
+ `NativeScriptAnimationDriver.animate ` +
+ `element: ${element}, keyframes: ${keyframes} ` +
+ `duration: ${duration}, delay: ${delay} ` +
+ `easing: ${easing}`
+ );
+
return new NativeScriptAnimationPlayer(
element, keyframes, duration, delay, easing);
}
+
+ private makeSelector(rawSelector: string): Selector {
+ return new Selector(rawSelector);
+ }
+
+ private visitDescendants(
+ element: NgView,
+ cb: (child: NgView, result: any, params: any) => boolean,
+ cbParams: any): any {
+
+ const result = {};
+ // fill the result obj with the result from the callback function
+ eachDescendant(element, (child: NgView) => cb(child, result, cbParams));
+
+ return result;
+ }
+}
+
+function viewMatches(
+ element: NgView,
+ result: ViewMatchResult,
+ params: ViewMatchParams
+): boolean {
+
+ if (element === params.originalView) {
+ result.found = true;
+ }
+
+ return !result.found;
+}
+
+function queryDescendants(
+ element: NgView,
+ result: QueryResult,
+ params: QueryParams
+): boolean {
+
+ if (!result.matches) {
+ result.matches = [];
+ }
+
+ const { selector, multi } = params;
+
+ // skip comment and text nodes
+ // because they are not actual Views
+ // and cannot be animated
+ if (element instanceof InvisibleNode || !selector.match(element)) {
+ return true;
+ }
+
+ if (element instanceof ProxyViewContainer) {
+ element.eachChild((child: NgView) => {
+ result.matches.push(child);
+ return true;
+ });
+ } else {
+ result.matches.push(element);
+ }
+
+ return multi;
}
diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts
index b5e0911dc..7c8f874fc 100644
--- a/nativescript-angular/animations/animation-engine.ts
+++ b/nativescript-angular/animations/animation-engine.ts
@@ -1,145 +1,23 @@
-/* tslint:disable */
-import { ɵDomAnimationEngine as DomAnimationEngine } from "@angular/animations/browser";
-import { AnimationEvent, AnimationPlayer } from "@angular/animations";
-
-import { NgView } from "../element-registry";
import {
- copyArray,
- cssClasses,
- deleteFromArrayMap,
- eraseStylesOverride,
- getOrSetAsInMap,
- makeAnimationEvent,
- optimizeGroupPlayer,
- setStyles,
-} from "./dom-utils";
-
-const MARKED_FOR_ANIMATION_CLASSNAME = "ng-animating";
-const MARKED_FOR_ANIMATION_SELECTOR = ".ng-animating";
-
-interface QueuedAnimationTransitionTuple {
- element: NgView;
- player: AnimationPlayer;
- triggerName: string;
- event: AnimationEvent;
-}
-
-// we are extending Angular's animation engine and
-// overriding a few methods that work on the DOM
-export class NativeScriptAnimationEngine extends DomAnimationEngine {
- // this method is almost completely copied from
- // the original animation engine, just replaced
- // a few method invocations with overriden ones
- animateTransition(element: NgView, instruction: any): AnimationPlayer {
- const triggerName = instruction.triggerName;
-
- let previousPlayers: AnimationPlayer[];
- if (instruction.isRemovalTransition) {
- previousPlayers = this._onRemovalTransitionOverride(element);
- } else {
- previousPlayers = [];
- const existingTransitions = this._getTransitionAnimation(element);
- const existingPlayer = existingTransitions ? existingTransitions[triggerName] : null;
- if (existingPlayer) {
- previousPlayers.push(existingPlayer);
- }
- }
-
- // it's important to do this step before destroying the players
- // so that the onDone callback below won"t fire before this
- eraseStylesOverride(element, instruction.fromStyles);
-
- // we first run this so that the previous animation player
- // data can be passed into the successive animation players
- let totalTime = 0;
- const players = instruction.timelines.map((timelineInstruction, i) => {
- totalTime = Math.max(totalTime, timelineInstruction.totalTime);
- return (this)._buildPlayer(element, timelineInstruction, previousPlayers, i);
- });
-
- previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
- const player = optimizeGroupPlayer(players);
- player.onDone(() => {
- player.destroy();
- const elmTransitionMap = this._getTransitionAnimation(element);
- if (elmTransitionMap) {
- delete elmTransitionMap[triggerName];
- if (Object.keys(elmTransitionMap).length === 0) {
- (this)._activeTransitionAnimations.delete(element);
- }
- }
- deleteFromArrayMap((this)._activeElementAnimations, element, player);
- setStyles(element, instruction.toStyles);
- });
-
- const elmTransitionMap = getOrSetAsInMap((this)._activeTransitionAnimations, element, {});
- elmTransitionMap[triggerName] = player;
-
- this._queuePlayerOverride(
- element, triggerName, player,
- makeAnimationEvent(
- element, triggerName, instruction.fromState, instruction.toState,
- null, // this will be filled in during event creation
- totalTime));
-
- return player;
- }
+ AnimationDriver,
+ ɵAnimationEngine as AnimationEngine,
+} from "@angular/animations/browser";
+import {
+ AnimationStyleNormalizer
+} from "@angular/animations/browser/src/dsl/style_normalization/animation_style_normalizer";
- // overriden to use eachChild method of View
- // instead of DOM querySelectorAll
- private _onRemovalTransitionOverride(element: NgView): AnimationPlayer[] {
- // when a parent animation is set to trigger a removal we want to
- // find all of the children that are currently animating and clear
- // them out by destroying each of them.
- let elms = [];
- element.eachChild(child => {
- if (cssClasses(child).get(MARKED_FOR_ANIMATION_SELECTOR)) {
- elms.push(child);
- }
+import { NSTransitionAnimationEngine } from "./transition-animation-engine";
- return true;
- });
+export class NativeScriptAnimationEngine extends AnimationEngine {
+ constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
+ super(driver, normalizer);
+ (this)._transitionEngine = new NSTransitionAnimationEngine(driver, normalizer);
- for (let i = 0; i < elms.length; i++) {
- const elm = elms[i];
- const activePlayers = this._getElementAnimation(elm);
- if (activePlayers) {
- activePlayers.forEach(player => player.destroy());
+ (this)._transitionEngine.onRemovalComplete = (element, delegate) => {
+ const parent = delegate && delegate.parentNode(element);
+ if (parent) {
+ delegate.removeChild(parent, element);
}
-
- const activeTransitions = this._getTransitionAnimation(elm);
- if (activeTransitions) {
- Object.keys(activeTransitions).forEach(triggerName => {
- const player = activeTransitions[triggerName];
- if (player) {
- player.destroy();
- }
- });
- }
- }
-
- // we make a copy of the array because the actual source array is modified
- // each time a player is finished/destroyed (the forEach loop would fail otherwise)
- return copyArray(this._getElementAnimation(element));
- }
-
- // overriden to use cssClasses method to access native element's styles
- // instead of DOM element's classList
- private _queuePlayerOverride(
- element: NgView, triggerName: string, player: AnimationPlayer, event: AnimationEvent) {
- const tuple = { element, player, triggerName, event };
- (this)._queuedTransitionAnimations.push(tuple);
- player.init();
-
- cssClasses(element).set(MARKED_FOR_ANIMATION_CLASSNAME, true);
- player.onDone(() => cssClasses(element).set(MARKED_FOR_ANIMATION_CLASSNAME, false));
- }
-
- private _getElementAnimation(element: NgView) {
- return (this)._activeElementAnimations.get(element);
- }
-
- private _getTransitionAnimation(element: NgView) {
- return (this)._activeTransitionAnimations.get(element);
+ };
}
}
diff --git a/nativescript-angular/animations/animation-player.ts b/nativescript-angular/animations/animation-player.ts
index fdaeb4503..3e0472cdd 100644
--- a/nativescript-angular/animations/animation-player.ts
+++ b/nativescript-angular/animations/animation-player.ts
@@ -2,8 +2,9 @@ import { AnimationPlayer } from "@angular/animations";
import { KeyframeAnimation }
from "tns-core-modules/ui/animation/keyframe-animation";
-import { NgView } from "../element-registry";
import { Keyframe, createKeyframeAnimation } from "./utils";
+import { NgView } from "../element-registry";
+import { animationsLog as traceLog } from "../trace";
export class NativeScriptAnimationPlayer implements AnimationPlayer {
public parentPlayer: AnimationPlayer = null;
@@ -17,13 +18,17 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer {
constructor(
private target: NgView,
keyframes: Keyframe[],
- duration: number,
- delay: number,
+ private duration: number,
+ private delay: number,
easing: string
) {
this.initKeyframeAnimation(keyframes, duration, delay, easing);
}
+ get totalTime(): number {
+ return this.delay + this.duration;
+ }
+
init(): void {
}
@@ -36,6 +41,8 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer {
onDestroy(fn: Function): void { this._doneSubscriptions.push(fn); }
play(): void {
+ traceLog(`NativeScriptAnimationPlayer.play`);
+
if (!this.animation) {
return;
}
@@ -48,30 +55,33 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer {
this.animation.play(this.target)
.then(() => this.onFinish())
- .catch((_e) => { });
+ .catch((_e) => {});
}
pause(): void {
- throw new Error("AnimationPlayer.pause method is not supported!");
}
finish(): void {
- throw new Error("AnimationPlayer.finish method is not supported!");
+ this.onFinish();
}
reset(): void {
+ traceLog(`NativeScriptAnimationPlayer.reset`);
+
if (this.animation && this.animation.isPlaying) {
this.animation.cancel();
}
}
restart(): void {
+ traceLog(`NativeScriptAnimationPlayer.restart`);
+
this.reset();
this.play();
}
destroy(): void {
- this.reset();
+ traceLog(`NativeScriptAnimationPlayer.destroy`);
this.onFinish();
}
@@ -84,15 +94,21 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer {
}
private initKeyframeAnimation(keyframes: Keyframe[], duration: number, delay: number, easing: string) {
+ traceLog(`NativeScriptAnimationPlayer.initKeyframeAnimation`);
+
this.animation = createKeyframeAnimation(keyframes, duration, delay, easing);
}
private onFinish() {
- if (!this._finished) {
- this._finished = true;
- this._started = false;
- this._doneSubscriptions.forEach(fn => fn());
- this._doneSubscriptions = [];
+ traceLog(`NativeScriptAnimationPlayer.onFinish`);
+
+ if (this._finished) {
+ return;
}
+
+ this._finished = true;
+ this._started = false;
+ this._doneSubscriptions.forEach(fn => fn());
+ this._doneSubscriptions = [];
}
}
diff --git a/nativescript-angular/animations/dom-utils.ts b/nativescript-angular/animations/dom-utils.ts
deleted file mode 100644
index 00584f70e..000000000
--- a/nativescript-angular/animations/dom-utils.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import {
- AnimationEvent,
- AnimationPlayer,
- NoopAnimationPlayer,
- ɵAnimationGroupPlayer,
- ɵStyleData,
-} from "@angular/animations";
-import { unsetValue } from "tns-core-modules/ui/core/view";
-
-import { NgView } from "../element-registry";
-
-// overriden to use the default 'unsetValue'
-// instead of empty string ''
-export function eraseStylesOverride(element: NgView, styles: ɵStyleData) {
- if (element["style"]) {
- Object.keys(styles).forEach(prop => {
- element.style[prop] = unsetValue;
- });
- }
-}
-
-export function cssClasses(element: NgView) {
- if (!element.ngCssClasses) {
- element.ngCssClasses = new Map();
- }
- return element.ngCssClasses;
-}
-
-// The following functions are from
-// the original DomAnimationEngine
-export function getOrSetAsInMap(map: Map, key: any, defaultValue: any) {
- let value = map.get(key);
- if (!value) {
- map.set(key, value = defaultValue);
- }
- return value;
-}
-
-export function deleteFromArrayMap(map: Map, key: any, value: any) {
- let arr = map.get(key);
- if (arr) {
- const index = arr.indexOf(value);
- if (index >= 0) {
- arr.splice(index, 1);
- if (arr.length === 0) {
- map.delete(key);
- }
- }
- }
-}
-
-export function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
- switch (players.length) {
- case 0:
- return new NoopAnimationPlayer();
- case 1:
- return players[0];
- default:
- return new ɵAnimationGroupPlayer(players);
- }
-}
-
-export function copyArray(source: any[]): any[] {
- return source ? source.splice(0) : [];
-}
-
-export function makeAnimationEvent(
- element: NgView, triggerName: string, fromState: string, toState: string, phaseName: string,
- totalTime: number): AnimationEvent {
- return {element, triggerName, fromState, toState, phaseName, totalTime};
-}
-
-export function setStyles(element: NgView, styles: ɵStyleData) {
- if (element["style"]) {
- Object.keys(styles).forEach(prop => element.style[prop] = styles[prop]);
- }
-}
diff --git a/nativescript-angular/animations/private-imports/dsl/element_instruction_map.ts b/nativescript-angular/animations/private-imports/dsl/element_instruction_map.ts
new file mode 100644
index 000000000..407db074f
--- /dev/null
+++ b/nativescript-angular/animations/private-imports/dsl/element_instruction_map.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/* tslint:disable */
+import {AnimationTimelineInstruction} from '@angular/animations/browser/src/dsl/animation_timeline_instruction';
+
+export class ElementInstructionMap {
+ private _map = new Map();
+
+ consume(element: any): AnimationTimelineInstruction[] {
+ let instructions = this._map.get(element);
+ if (instructions) {
+ this._map.delete(element);
+ } else {
+ instructions = [];
+ }
+ return instructions;
+ }
+
+ append(element: any, instructions: AnimationTimelineInstruction[]) {
+ let existingInstructions = this._map.get(element);
+ if (!existingInstructions) {
+ this._map.set(element, existingInstructions = []);
+ }
+ existingInstructions.push(...instructions);
+ }
+
+ has(element: any): boolean { return this._map.has(element); }
+
+ clear() { this._map.clear(); }
+}
\ No newline at end of file
diff --git a/nativescript-angular/animations/private-imports/render/shared.ts b/nativescript-angular/animations/private-imports/render/shared.ts
new file mode 100644
index 000000000..efbdc0742
--- /dev/null
+++ b/nativescript-angular/animations/private-imports/render/shared.ts
@@ -0,0 +1,164 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+/* tslint:disable */
+
+import {AUTO_STYLE, AnimationEvent, AnimationPlayer, NoopAnimationPlayer, ɵAnimationGroupPlayer, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
+
+import {AnimationStyleNormalizer} from "@angular/animations/browser/src/dsl/style_normalization/animation_style_normalizer";
+import {AnimationDriver} from "@angular/animations/browser";
+
+export function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
+ switch (players.length) {
+ case 0:
+ return new NoopAnimationPlayer();
+ case 1:
+ return players[0];
+ default:
+ return new ɵAnimationGroupPlayer(players);
+ }
+}
+
+export function normalizeKeyframes(
+ driver: AnimationDriver, normalizer: AnimationStyleNormalizer, element: any,
+ keyframes: ɵStyleData[], preStyles: ɵStyleData = {},
+ postStyles: ɵStyleData = {}): ɵStyleData[] {
+ const errors: string[] = [];
+ const normalizedKeyframes: ɵStyleData[] = [];
+ let previousOffset = -1;
+ let previousKeyframe: ɵStyleData|null = null;
+ keyframes.forEach(kf => {
+ const offset = kf['offset'] as number;
+ const isSameOffset = offset == previousOffset;
+ const normalizedKeyframe: ɵStyleData = (isSameOffset && previousKeyframe) || {};
+ Object.keys(kf).forEach(prop => {
+ let normalizedProp = prop;
+ let normalizedValue = kf[prop];
+ if (normalizedValue == PRE_STYLE) {
+ normalizedValue = preStyles[prop];
+ } else if (normalizedValue == AUTO_STYLE) {
+ normalizedValue = postStyles[prop];
+ } else if (prop != 'offset') {
+ normalizedProp = normalizer.normalizePropertyName(prop, errors);
+ normalizedValue = normalizer.normalizeStyleValue(prop, normalizedProp, kf[prop], errors);
+ }
+ normalizedKeyframe[normalizedProp] = normalizedValue;
+ });
+ if (!isSameOffset) {
+ normalizedKeyframes.push(normalizedKeyframe);
+ }
+ previousKeyframe = normalizedKeyframe;
+ previousOffset = offset;
+ });
+ if (errors.length) {
+ const LINE_START = '\n - ';
+ throw new Error(
+ `Unable to animate due to the following errors:${LINE_START}${errors.join(LINE_START)}`);
+ }
+
+ return normalizedKeyframes;
+}
+
+export function listenOnPlayer(
+ player: AnimationPlayer, eventName: string, event: AnimationEvent | undefined,
+ callback: (event: any) => any) {
+ switch (eventName) {
+ case 'start':
+ player.onStart(() => callback(event && copyAnimationEvent(event, 'start', player.totalTime)));
+ break;
+ case 'done':
+ player.onDone(() => callback(event && copyAnimationEvent(event, 'done', player.totalTime)));
+ break;
+ case 'destroy':
+ player.onDestroy(
+ () => callback(event && copyAnimationEvent(event, 'destroy', player.totalTime)));
+ break;
+ }
+}
+
+export function copyAnimationEvent(
+ e: AnimationEvent, phaseName?: string, totalTime?: number): AnimationEvent {
+ const event = makeAnimationEvent(
+ e.element, e.triggerName, e.fromState, e.toState, phaseName || e.phaseName,
+ totalTime == undefined ? e.totalTime : totalTime);
+ const data = (e as any)['_data'];
+ if (data != null) {
+ (event as any)['_data'] = data;
+ }
+ return event;
+}
+
+export function makeAnimationEvent(
+ element: any, triggerName: string, fromState: string, toState: string, phaseName: string = '',
+ totalTime: number = 0): AnimationEvent {
+ return {element, triggerName, fromState, toState, phaseName, totalTime};
+}
+
+export function getOrSetAsInMap(
+ map: Map| {[key: string]: any}, key: any, defaultValue: any) {
+ let value: any;
+ if (map instanceof Map) {
+ value = map.get(key);
+ if (!value) {
+ map.set(key, value = defaultValue);
+ }
+ } else {
+ value = map[key];
+ if (!value) {
+ value = map[key] = defaultValue;
+ }
+ }
+ return value;
+}
+
+export function parseTimelineCommand(command: string): [string, string] {
+ const separatorPos = command.indexOf(':');
+ const id = command.substring(1, separatorPos);
+ const action = command.substr(separatorPos + 1);
+ return [id, action];
+}
+
+let _contains: (elm1: any, elm2: any) => boolean = (elm1: any, elm2: any) => false;
+let _matches: (element: any, selector: string) => boolean = (element: any, selector: string) =>
+ false;
+let _query: (element: any, selector: string, multi: boolean) => any[] =
+ (element: any, selector: string, multi: boolean) => {
+ return [];
+ };
+
+if (typeof Element != 'undefined') {
+ // this is well supported in all browsers
+ _contains = (elm1: any, elm2: any) => { return elm1.contains(elm2) as boolean; };
+
+ if (Element.prototype.matches) {
+ _matches = (element: any, selector: string) => element.matches(selector);
+ } else {
+ const proto = Element.prototype as any;
+ const fn = proto.matchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector ||
+ proto.oMatchesSelector || proto.webkitMatchesSelector;
+ if (fn) {
+ _matches = (element: any, selector: string) => fn.apply(element, [selector]);
+ }
+ }
+
+ _query = (element: any, selector: string, multi: boolean): any[] => {
+ let results: any[] = [];
+ if (multi) {
+ results.push(...element.querySelectorAll(selector));
+ } else {
+ const elm = element.querySelector(selector);
+ if (elm) {
+ results.push(elm);
+ }
+ }
+ return results;
+ };
+}
+
+export const matchesElement = _matches;
+export const containsElement = _contains;
+export const invokeQuery = _query;
\ No newline at end of file
diff --git a/nativescript-angular/animations/private-imports/render/transition_animation_engine.ts b/nativescript-angular/animations/private-imports/render/transition_animation_engine.ts
new file mode 100644
index 000000000..bea738936
--- /dev/null
+++ b/nativescript-angular/animations/private-imports/render/transition_animation_engine.ts
@@ -0,0 +1,1468 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+/* tslint:disable */
+import {AUTO_STYLE, AnimationOptions, AnimationPlayer, NoopAnimationPlayer, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
+
+import {AnimationTimelineInstruction} from '@angular/animations/browser/src/dsl/animation_timeline_instruction';
+import {AnimationTransitionFactory} from '@angular/animations/browser/src/dsl/animation_transition_factory';
+import {AnimationTransitionInstruction} from '@angular/animations/browser/src/dsl/animation_transition_instruction';
+import {AnimationTrigger} from '@angular/animations/browser/src/dsl/animation_trigger';
+
+import {ElementInstructionMap} from '../dsl/element_instruction_map';
+
+import {AnimationStyleNormalizer} from '@angular/animations/browser/src/dsl/style_normalization/animation_style_normalizer';
+import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_ANIMATING_SELECTOR, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util';
+
+import {AnimationDriver} from '@angular/animations/browser/src/render/animation_driver';
+
+import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from "../render/shared";
+
+const QUEUED_CLASSNAME = 'ng-animate-queued';
+const QUEUED_SELECTOR = '.ng-animate-queued';
+const DISABLED_CLASSNAME = 'ng-animate-disabled';
+const DISABLED_SELECTOR = '.ng-animate-disabled';
+
+const EMPTY_PLAYER_ARRAY: TransitionAnimationPlayer[] = [];
+const NULL_REMOVAL_STATE: ElementAnimationState = {
+ namespaceId: '',
+ setForRemoval: null,
+ hasAnimation: false,
+ removedBeforeQueried: false
+};
+const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = {
+ namespaceId: '',
+ setForRemoval: null,
+ hasAnimation: false,
+ removedBeforeQueried: true
+};
+
+interface TriggerListener {
+ name: string;
+ phase: string;
+ callback: (event: any) => any;
+}
+
+export interface QueueInstruction {
+ element: any;
+ triggerName: string;
+ fromState: StateValue;
+ toState: StateValue;
+ transition: AnimationTransitionFactory;
+ player: TransitionAnimationPlayer;
+ isFallbackTransition: boolean;
+}
+
+export const REMOVAL_FLAG = '__ng_removed';
+
+export interface ElementAnimationState {
+ setForRemoval: any;
+ hasAnimation: boolean;
+ namespaceId: string;
+ removedBeforeQueried: boolean;
+}
+
+export class StateValue {
+ public value: string;
+ public options: AnimationOptions;
+
+ constructor(input: any) {
+ const isObj = input && input.hasOwnProperty('value');
+ const value = isObj ? input['value'] : input;
+ this.value = normalizeTriggerValue(value);
+ if (isObj) {
+ const options = copyObj(input as any);
+ delete options['value'];
+ this.options = options as AnimationOptions;
+ } else {
+ this.options = {};
+ }
+ if (!this.options.params) {
+ this.options.params = {};
+ }
+ }
+
+ absorbOptions(options: AnimationOptions) {
+ const newParams = options.params;
+ if (newParams) {
+ const oldParams = this.options.params !;
+ Object.keys(newParams).forEach(prop => {
+ if (oldParams[prop] == null) {
+ oldParams[prop] = newParams[prop];
+ }
+ });
+ }
+ }
+}
+
+export const VOID_VALUE = 'void';
+export const DEFAULT_STATE_VALUE = new StateValue(VOID_VALUE);
+export const DELETED_STATE_VALUE = new StateValue('DELETED');
+
+export class AnimationTransitionNamespace {
+ public players: TransitionAnimationPlayer[] = [];
+
+ private _triggers: {[triggerName: string]: AnimationTrigger} = {};
+ private _queue: QueueInstruction[] = [];
+
+ private _elementListeners = new Map();
+
+ private _hostClassName: string;
+
+ constructor(
+ public id: string, public hostElement: any, private _engine: TransitionAnimationEngine) {
+ this._hostClassName = 'ng-tns-' + id;
+ addClass(hostElement, this._hostClassName);
+ }
+
+ listen(element: any, name: string, phase: string, callback: (event: any) => boolean): () => any {
+ if (!this._triggers.hasOwnProperty(name)) {
+ throw new Error(
+ `Unable to listen on the animation trigger event "${phase}" because the animation trigger "${name}" doesn\'t exist!`);
+ }
+
+ if (phase == null || phase.length == 0) {
+ throw new Error(
+ `Unable to listen on the animation trigger "${name}" because the provided event is undefined!`);
+ }
+
+ if (!isTriggerEventValid(phase)) {
+ throw new Error(
+ `The provided animation trigger event "${phase}" for the animation trigger "${name}" is not supported!`);
+ }
+
+ const listeners = getOrSetAsInMap(this._elementListeners, element, []);
+ const data = {name, phase, callback};
+ listeners.push(data);
+
+ const triggersWithStates = getOrSetAsInMap(this._engine.statesByElement, element, {});
+ if (!triggersWithStates.hasOwnProperty(name)) {
+ addClass(element, NG_TRIGGER_CLASSNAME);
+ addClass(element, NG_TRIGGER_CLASSNAME + '-' + name);
+ triggersWithStates[name] = null;
+ }
+
+ return () => {
+ // the event listener is removed AFTER the flush has occurred such
+ // that leave animations callbacks can fire (otherwise if the node
+ // is removed in between then the listeners would be deregistered)
+ this._engine.afterFlush(() => {
+ const index = listeners.indexOf(data);
+ if (index >= 0) {
+ listeners.splice(index, 1);
+ }
+
+ if (!this._triggers[name]) {
+ delete triggersWithStates[name];
+ }
+ });
+ };
+ }
+
+ register(name: string, ast: AnimationTrigger): boolean {
+ if (this._triggers[name]) {
+ // throw
+ return false;
+ } else {
+ this._triggers[name] = ast;
+ return true;
+ }
+ }
+
+ private _getTrigger(name: string) {
+ const trigger = this._triggers[name];
+ if (!trigger) {
+ throw new Error(`The provided animation trigger "${name}" has not been registered!`);
+ }
+ return trigger;
+ }
+
+ trigger(element: any, triggerName: string, value: any, defaultToFallback: boolean = true):
+ TransitionAnimationPlayer|undefined {
+ const trigger = this._getTrigger(triggerName);
+ const player = new TransitionAnimationPlayer(this.id, triggerName, element);
+
+ let triggersWithStates = this._engine.statesByElement.get(element);
+ if (!triggersWithStates) {
+ addClass(element, NG_TRIGGER_CLASSNAME);
+ addClass(element, NG_TRIGGER_CLASSNAME + '-' + triggerName);
+ this._engine.statesByElement.set(element, triggersWithStates = {});
+ }
+
+ let fromState = triggersWithStates[triggerName];
+ const toState = new StateValue(value);
+
+ const isObj = value && value.hasOwnProperty('value');
+ if (!isObj && fromState) {
+ toState.absorbOptions(fromState.options);
+ }
+
+ triggersWithStates[triggerName] = toState;
+
+ if (!fromState) {
+ fromState = DEFAULT_STATE_VALUE;
+ } else if (fromState === DELETED_STATE_VALUE) {
+ return player;
+ }
+
+ const isRemoval = toState.value === VOID_VALUE;
+
+ // normally this isn't reached by here, however, if an object expression
+ // is passed in then it may be a new object each time. Comparing the value
+ // is important since that will stay the same despite there being a new object.
+ // The removal arc here is special cased because the same element is triggered
+ // twice in the event that it contains animations on the outer/inner portions
+ // of the host container
+ if (!isRemoval && fromState.value === toState.value) return;
+
+ const playersOnElement: TransitionAnimationPlayer[] =
+ getOrSetAsInMap(this._engine.playersByElement, element, []);
+ playersOnElement.forEach(player => {
+ // only remove the player if it is queued on the EXACT same trigger/namespace
+ // we only also deal with queued players here because if the animation has
+ // started then we want to keep the player alive until the flush happens
+ // (which is where the previousPlayers are passed into the new palyer)
+ if (player.namespaceId == this.id && player.triggerName == triggerName && player.queued) {
+ player.destroy();
+ }
+ });
+
+ let transition = trigger.matchTransition(fromState.value, toState.value);
+ let isFallbackTransition = false;
+ if (!transition) {
+ if (!defaultToFallback) return;
+ transition = trigger.fallbackTransition;
+ isFallbackTransition = true;
+ }
+
+ this._engine.totalQueuedPlayers++;
+ this._queue.push(
+ {element, triggerName, transition, fromState, toState, player, isFallbackTransition});
+
+ if (!isFallbackTransition) {
+ addClass(element, QUEUED_CLASSNAME);
+ player.onStart(() => { removeClass(element, QUEUED_CLASSNAME); });
+ }
+
+ player.onDone(() => {
+ let index = this.players.indexOf(player);
+ if (index >= 0) {
+ this.players.splice(index, 1);
+ }
+
+ const players = this._engine.playersByElement.get(element);
+ if (players) {
+ let index = players.indexOf(player);
+ if (index >= 0) {
+ players.splice(index, 1);
+ }
+ }
+ });
+
+ this.players.push(player);
+ playersOnElement.push(player);
+
+ return player;
+ }
+
+ deregister(name: string) {
+ delete this._triggers[name];
+
+ this._engine.statesByElement.forEach((stateMap, element) => { delete stateMap[name]; });
+
+ this._elementListeners.forEach((listeners, element) => {
+ this._elementListeners.set(
+ element, listeners.filter(entry => { return entry.name != name; }));
+ });
+ }
+
+ clearElementCache(element: any) {
+ this._engine.statesByElement.delete(element);
+ this._elementListeners.delete(element);
+ const elementPlayers = this._engine.playersByElement.get(element);
+ if (elementPlayers) {
+ elementPlayers.forEach(player => player.destroy());
+ this._engine.playersByElement.delete(element);
+ }
+ }
+
+ private _destroyInnerNodes(rootElement: any, context: any, animate: boolean = false) {
+ this._engine.driver.query(rootElement, NG_TRIGGER_SELECTOR, true).forEach(elm => {
+ if (animate && containsClass(elm, this._hostClassName)) {
+ const innerNs = this._engine.namespacesByHostElement.get(elm);
+
+ // special case for a host element with animations on the same element
+ if (innerNs) {
+ innerNs.removeNode(elm, context, true);
+ }
+
+ this.removeNode(elm, context, true);
+ } else {
+ this.clearElementCache(elm);
+ }
+ });
+ }
+
+ removeNode(element: any, context: any, doNotRecurse?: boolean): void {
+ const engine = this._engine;
+
+ if (!doNotRecurse && element.childElementCount) {
+ this._destroyInnerNodes(element, context, true);
+ }
+
+ const triggerStates = engine.statesByElement.get(element);
+ if (triggerStates) {
+ const players: TransitionAnimationPlayer[] = [];
+ Object.keys(triggerStates).forEach(triggerName => {
+ // this check is here in the event that an element is removed
+ // twice (both on the host level and the component level)
+ if (this._triggers[triggerName]) {
+ const player = this.trigger(element, triggerName, VOID_VALUE, false);
+ if (player) {
+ players.push(player);
+ }
+ }
+ });
+
+ if (players.length) {
+ engine.markElementAsRemoved(this.id, element, true, context);
+ optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
+ return;
+ }
+ }
+
+ // find the player that is animating and make sure that the
+ // removal is delayed until that player has completed
+ let containsPotentialParentTransition = false;
+ if (engine.totalAnimations) {
+ const currentPlayers =
+ engine.players.length ? engine.playersByQueriedElement.get(element) : [];
+
+ // when this `if statement` does not continue forward it means that
+ // a previous animation query has selected the current element and
+ // is animating it. In this situation want to continue fowards and
+ // allow the element to be queued up for animation later.
+ if (currentPlayers && currentPlayers.length) {
+ containsPotentialParentTransition = true;
+ } else {
+ let parent = element;
+ while (parent = parent.parentNode) {
+ const triggers = engine.statesByElement.get(parent);
+ if (triggers) {
+ containsPotentialParentTransition = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // at this stage we know that the element will either get removed
+ // during flush or will be picked up by a parent query. Either way
+ // we need to fire the listeners for this element when it DOES get
+ // removed (once the query parent animation is done or after flush)
+ const listeners = this._elementListeners.get(element);
+ if (listeners) {
+ const visitedTriggers = new Set();
+ listeners.forEach(listener => {
+ const triggerName = listener.name;
+ if (visitedTriggers.has(triggerName)) return;
+ visitedTriggers.add(triggerName);
+
+ const trigger = this._triggers[triggerName];
+ const transition = trigger.fallbackTransition;
+ const elementStates = engine.statesByElement.get(element) !;
+ const fromState = elementStates[triggerName] || DEFAULT_STATE_VALUE;
+ const toState = new StateValue(VOID_VALUE);
+ const player = new TransitionAnimationPlayer(this.id, triggerName, element);
+
+ this._engine.totalQueuedPlayers++;
+ this._queue.push({
+ element,
+ triggerName,
+ transition,
+ fromState,
+ toState,
+ player,
+ isFallbackTransition: true
+ });
+ });
+ }
+
+ // whether or not a parent has an animation we need to delay the deferral of the leave
+ // operation until we have more information (which we do after flush() has been called)
+ if (containsPotentialParentTransition) {
+ engine.markElementAsRemoved(this.id, element, false, context);
+ } else {
+ // we do this after the flush has occurred such
+ // that the callbacks can be fired
+ engine.afterFlush(() => this.clearElementCache(element));
+ engine.destroyInnerAnimations(element);
+ engine._onRemovalComplete(element, context);
+ }
+ }
+
+ insertNode(element: any, parent: any): void { addClass(element, this._hostClassName); }
+
+ drainQueuedTransitions(microtaskId: number): QueueInstruction[] {
+ const instructions: QueueInstruction[] = [];
+ this._queue.forEach(entry => {
+ const player = entry.player;
+ if (player.destroyed) return;
+
+ const element = entry.element;
+ const listeners = this._elementListeners.get(element);
+ if (listeners) {
+ listeners.forEach((listener: TriggerListener) => {
+ if (listener.name == entry.triggerName) {
+ const baseEvent = makeAnimationEvent(
+ element, entry.triggerName, entry.fromState.value, entry.toState.value);
+ (baseEvent as any)['_data'] = microtaskId;
+ listenOnPlayer(entry.player, listener.phase, baseEvent, listener.callback);
+ }
+ });
+ }
+
+ if (player.markedForDestroy) {
+ this._engine.afterFlush(() => {
+ // now we can destroy the element properly since the event listeners have
+ // been bound to the player
+ player.destroy();
+ });
+ } else {
+ instructions.push(entry);
+ }
+ });
+
+ this._queue = [];
+
+ return instructions.sort((a, b) => {
+ // if depCount == 0 them move to front
+ // otherwise if a contains b then move back
+ const d0 = a.transition.ast.depCount;
+ const d1 = b.transition.ast.depCount;
+ if (d0 == 0 || d1 == 0) {
+ return d0 - d1;
+ }
+ return this._engine.driver.containsElement(a.element, b.element) ? 1 : -1;
+ });
+ }
+
+ destroy(context: any) {
+ this.players.forEach(p => p.destroy());
+ this._destroyInnerNodes(this.hostElement, context);
+ }
+
+ elementContainsData(element: any): boolean {
+ let containsData = false;
+ if (this._elementListeners.has(element)) containsData = true;
+ containsData =
+ (this._queue.find(entry => entry.element === element) ? true : false) || containsData;
+ return containsData;
+ }
+}
+
+export interface QueuedTransition {
+ element: any;
+ instruction: AnimationTransitionInstruction;
+ player: TransitionAnimationPlayer;
+}
+
+export class TransitionAnimationEngine {
+ public players: TransitionAnimationPlayer[] = [];
+ public newHostElements = new Map();
+ public playersByElement = new Map();
+ public playersByQueriedElement = new Map();
+ public statesByElement = new Map();
+ public disabledNodes = new Set();
+
+ public totalAnimations = 0;
+ public totalQueuedPlayers = 0;
+
+ private _namespaceLookup: {[id: string]: AnimationTransitionNamespace} = {};
+ private _namespaceList: AnimationTransitionNamespace[] = [];
+ private _flushFns: (() => any)[] = [];
+ private _whenQuietFns: (() => any)[] = [];
+
+ public namespacesByHostElement = new Map();
+ public collectedEnterElements: any[] = [];
+ public collectedLeaveElements: any[] = [];
+
+ // this method is designed to be overridden by the code that uses this engine
+ public onRemovalComplete = (element: any, context: any) => {};
+
+ _onRemovalComplete(element: any, context: any) { this.onRemovalComplete(element, context); }
+
+ constructor(public driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
+
+ get queuedPlayers(): TransitionAnimationPlayer[] {
+ const players: TransitionAnimationPlayer[] = [];
+ this._namespaceList.forEach(ns => {
+ ns.players.forEach(player => {
+ if (player.queued) {
+ players.push(player);
+ }
+ });
+ });
+ return players;
+ }
+
+ createNamespace(namespaceId: string, hostElement: any) {
+ const ns = new AnimationTransitionNamespace(namespaceId, hostElement, this);
+ if (hostElement.parentNode) {
+ this._balanceNamespaceList(ns, hostElement);
+ } else {
+ // defer this later until flush during when the host element has
+ // been inserted so that we know exactly where to place it in
+ // the namespace list
+ this.newHostElements.set(hostElement, ns);
+
+ // given that this host element is apart of the animation code, it
+ // may or may not be inserted by a parent node that is an of an
+ // animation renderer type. If this happens then we can still have
+ // access to this item when we query for :enter nodes. If the parent
+ // is a renderer then the set data-structure will normalize the entry
+ this.collectEnterElement(hostElement);
+ }
+ return this._namespaceLookup[namespaceId] = ns;
+ }
+
+ private _balanceNamespaceList(ns: AnimationTransitionNamespace, hostElement: any) {
+ const limit = this._namespaceList.length - 1;
+ if (limit >= 0) {
+ let found = false;
+ for (let i = limit; i >= 0; i--) {
+ const nextNamespace = this._namespaceList[i];
+ if (this.driver.containsElement(nextNamespace.hostElement, hostElement)) {
+ this._namespaceList.splice(i + 1, 0, ns);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ this._namespaceList.splice(0, 0, ns);
+ }
+ } else {
+ this._namespaceList.push(ns);
+ }
+
+ this.namespacesByHostElement.set(hostElement, ns);
+ return ns;
+ }
+
+ register(namespaceId: string, hostElement: any) {
+ let ns = this._namespaceLookup[namespaceId];
+ if (!ns) {
+ ns = this.createNamespace(namespaceId, hostElement);
+ }
+ return ns;
+ }
+
+ registerTrigger(namespaceId: string, name: string, trigger: AnimationTrigger) {
+ let ns = this._namespaceLookup[namespaceId];
+ if (ns && ns.register(name, trigger)) {
+ this.totalAnimations++;
+ }
+ }
+
+ destroy(namespaceId: string, context: any) {
+ if (!namespaceId) return;
+
+ const ns = this._fetchNamespace(namespaceId);
+
+ this.afterFlush(() => {
+ this.namespacesByHostElement.delete(ns.hostElement);
+ delete this._namespaceLookup[namespaceId];
+ const index = this._namespaceList.indexOf(ns);
+ if (index >= 0) {
+ this._namespaceList.splice(index, 1);
+ }
+ });
+
+ this.afterFlushAnimationsDone(() => ns.destroy(context));
+ }
+
+ private _fetchNamespace(id: string) { return this._namespaceLookup[id]; }
+
+ trigger(namespaceId: string, element: any, name: string, value: any): boolean {
+ if (isElementNode(element)) {
+ this._fetchNamespace(namespaceId).trigger(element, name, value);
+ return true;
+ }
+ return false;
+ }
+
+ insertNode(namespaceId: string, element: any, parent: any, insertBefore: boolean): void {
+ if (!isElementNode(element)) return;
+
+ // special case for when an element is removed and reinserted (move operation)
+ // when this occurs we do not want to use the element for deletion later
+ const details = element[REMOVAL_FLAG] as ElementAnimationState;
+ if (details && details.setForRemoval) {
+ details.setForRemoval = false;
+ }
+
+ // in the event that the namespaceId is blank then the caller
+ // code does not contain any animation code in it, but it is
+ // just being called so that the node is marked as being inserted
+ if (namespaceId) {
+ this._fetchNamespace(namespaceId).insertNode(element, parent);
+ }
+
+ // only *directives and host elements are inserted before
+ if (insertBefore) {
+ this.collectEnterElement(element);
+ }
+ }
+
+ collectEnterElement(element: any) { this.collectedEnterElements.push(element); }
+
+ markElementAsDisabled(element: any, value: boolean) {
+ if (value) {
+ if (!this.disabledNodes.has(element)) {
+ this.disabledNodes.add(element);
+ addClass(element, DISABLED_CLASSNAME);
+ }
+ } else if (this.disabledNodes.has(element)) {
+ this.disabledNodes.delete(element);
+ removeClass(element, DISABLED_CLASSNAME);
+ }
+ }
+
+ removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void {
+ if (!isElementNode(element)) {
+ this._onRemovalComplete(element, context);
+ return;
+ }
+
+ const ns = namespaceId ? this._fetchNamespace(namespaceId) : null;
+ if (ns) {
+ ns.removeNode(element, context, doNotRecurse);
+ } else {
+ this.markElementAsRemoved(namespaceId, element, false, context);
+ }
+ }
+
+ markElementAsRemoved(namespaceId: string, element: any, hasAnimation?: boolean, context?: any) {
+ this.collectedLeaveElements.push(element);
+ element[REMOVAL_FLAG] = {
+ namespaceId,
+ setForRemoval: context, hasAnimation,
+ removedBeforeQueried: false
+ };
+ }
+
+ listen(
+ namespaceId: string, element: any, name: string, phase: string,
+ callback: (event: any) => boolean): () => any {
+ if (isElementNode(element)) {
+ return this._fetchNamespace(namespaceId).listen(element, name, phase, callback);
+ }
+ return () => {};
+ }
+
+ private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
+ return (entry).transition.build(
+ this.driver, entry.element, entry.fromState.value, entry.toState.value,
+ entry.toState.options, subTimelines);
+ }
+
+ destroyInnerAnimations(containerElement: any) {
+ let elements = this.driver.query(containerElement, NG_TRIGGER_SELECTOR, true);
+ elements.forEach(element => {
+ const players = this.playersByElement.get(element);
+ if (players) {
+ players.forEach(player => {
+ // special case for when an element is set for destruction, but hasn't started.
+ // in this situation we want to delay the destruction until the flush occurs
+ // so that any event listeners attached to the player are triggered.
+ if (player.queued) {
+ player.markedForDestroy = true;
+ } else {
+ player.destroy();
+ }
+ });
+ }
+ const stateMap = this.statesByElement.get(element);
+ if (stateMap) {
+ Object.keys(stateMap).forEach(triggerName => stateMap[triggerName] = DELETED_STATE_VALUE);
+ }
+ });
+
+ if (this.playersByQueriedElement.size == 0) return;
+
+ elements = this.driver.query(containerElement, NG_ANIMATING_SELECTOR, true);
+ if (elements.length) {
+ elements.forEach(element => {
+ const players = this.playersByQueriedElement.get(element);
+ if (players) {
+ players.forEach(player => player.finish());
+ }
+ });
+ }
+ }
+
+ whenRenderingDone(): Promise {
+ return new Promise(resolve => {
+ if (this.players.length) {
+ return optimizeGroupPlayer(this.players).onDone(() => resolve());
+ } else {
+ resolve();
+ }
+ });
+ }
+
+ processLeaveNode(element: any) {
+ const details = element[REMOVAL_FLAG] as ElementAnimationState;
+ if (details && details.setForRemoval) {
+ // this will prevent it from removing it twice
+ element[REMOVAL_FLAG] = NULL_REMOVAL_STATE;
+ if (details.namespaceId) {
+ this.destroyInnerAnimations(element);
+ const ns = this._fetchNamespace(details.namespaceId);
+ if (ns) {
+ ns.clearElementCache(element);
+ }
+ }
+ this._onRemovalComplete(element, details.setForRemoval);
+ }
+
+ if (this.driver.matchesElement(element, DISABLED_SELECTOR)) {
+ this.markElementAsDisabled(element, false);
+ }
+
+ this.driver.query(element, DISABLED_SELECTOR, true).forEach(node => {
+ this.markElementAsDisabled(element, false);
+ });
+ }
+
+ flush(microtaskId: number = -1) {
+ let players: AnimationPlayer[] = [];
+ if (this.newHostElements.size) {
+ this.newHostElements.forEach((ns, element) => this._balanceNamespaceList(ns, element));
+ this.newHostElements.clear();
+ }
+
+ if (this._namespaceList.length &&
+ (this.totalQueuedPlayers || this.collectedLeaveElements.length)) {
+ const cleanupFns: Function[] = [];
+ try {
+ players = this._flushAnimations(cleanupFns, microtaskId);
+ } finally {
+ for (let i = 0; i < cleanupFns.length; i++) {
+ cleanupFns[i]();
+ }
+ }
+ } else {
+ for (let i = 0; i < this.collectedLeaveElements.length; i++) {
+ const element = this.collectedLeaveElements[i];
+ this.processLeaveNode(element);
+ }
+ }
+
+ this.totalQueuedPlayers = 0;
+ this.collectedEnterElements.length = 0;
+ this.collectedLeaveElements.length = 0;
+ this._flushFns.forEach(fn => fn());
+ this._flushFns = [];
+
+ if (this._whenQuietFns.length) {
+ // we move these over to a variable so that
+ // if any new callbacks are registered in another
+ // flush they do not populate the existing set
+ const quietFns = this._whenQuietFns;
+ this._whenQuietFns = [];
+
+ if (players.length) {
+ optimizeGroupPlayer(players).onDone(() => { quietFns.forEach(fn => fn()); });
+ } else {
+ quietFns.forEach(fn => fn());
+ }
+ }
+ }
+
+ private _flushAnimations(cleanupFns: Function[], microtaskId: number):
+ TransitionAnimationPlayer[] {
+ const subTimelines = new ElementInstructionMap();
+ const skippedPlayers: TransitionAnimationPlayer[] = [];
+ const skippedPlayersMap = new Map();
+ const queuedInstructions: QueuedTransition[] = [];
+ const queriedElements = new Map();
+ const allPreStyleElements = new Map>();
+ const allPostStyleElements = new Map>();
+
+ const disabledElementsSet = new Set();
+ this.disabledNodes.forEach(node => {
+ const nodesThatAreDisabled = this.driver.query(node, QUEUED_SELECTOR, true);
+ for (let i = 0; i < nodesThatAreDisabled.length; i++) {
+ disabledElementsSet.add(nodesThatAreDisabled[i]);
+ }
+ });
+
+ const bodyNode = getBodyNode();
+ const allEnterNodes: any[] = this.collectedEnterElements.length ?
+ this.collectedEnterElements.filter(createIsRootFilterFn(this.collectedEnterElements)) :
+ [];
+
+ // this must occur before the instructions are built below such that
+ // the :enter queries match the elements (since the timeline queries
+ // are fired during instruction building).
+ for (let i = 0; i < allEnterNodes.length; i++) {
+ addClass(allEnterNodes[i], ENTER_CLASSNAME);
+ }
+
+ const allLeaveNodes: any[] = [];
+ const leaveNodesWithoutAnimations: any[] = [];
+ for (let i = 0; i < this.collectedLeaveElements.length; i++) {
+ const element = this.collectedLeaveElements[i];
+ const details = element[REMOVAL_FLAG] as ElementAnimationState;
+ if (details && details.setForRemoval) {
+ addClass(element, LEAVE_CLASSNAME);
+ allLeaveNodes.push(element);
+ if (!details.hasAnimation) {
+ leaveNodesWithoutAnimations.push(element);
+ }
+ }
+ }
+
+ cleanupFns.push(() => {
+ allEnterNodes.forEach(element => removeClass(element, ENTER_CLASSNAME));
+ allLeaveNodes.forEach(element => {
+ removeClass(element, LEAVE_CLASSNAME);
+ this.processLeaveNode(element);
+ });
+ });
+
+ const allPlayers: TransitionAnimationPlayer[] = [];
+ const erroneousTransitions: AnimationTransitionInstruction[] = [];
+ for (let i = this._namespaceList.length - 1; i >= 0; i--) {
+ const ns = this._namespaceList[i];
+ ns.drainQueuedTransitions(microtaskId).forEach(entry => {
+ const player = entry.player;
+ allPlayers.push(player);
+
+ const element = entry.element;
+ if (!bodyNode || !this.driver.containsElement(bodyNode, element)) {
+ player.destroy();
+ return;
+ }
+
+ const instruction = this._buildInstruction(entry, subTimelines) !;
+ if (instruction.errors && instruction.errors.length) {
+ erroneousTransitions.push(instruction);
+ return;
+ }
+
+ // if a unmatched transition is queued to go then it SHOULD NOT render
+ // an animation and cancel the previously running animations.
+ if (entry.isFallbackTransition) {
+ player.onStart(() => eraseStyles(element, instruction.fromStyles));
+ player.onDestroy(() => setStyles(element, instruction.toStyles));
+ skippedPlayers.push(player);
+ return;
+ }
+
+ // this means that if a parent animation uses this animation as a sub trigger
+ // then it will instruct the timeline builder to not add a player delay, but
+ // instead stretch the first keyframe gap up until the animation starts. The
+ // reason this is important is to prevent extra initialization styles from being
+ // required by the user in the animation.
+ instruction.timelines.forEach(tl => tl.stretchStartingKeyframe = true);
+
+ subTimelines.append(element, instruction.timelines);
+
+ const tuple = {instruction, player, element};
+
+ queuedInstructions.push(tuple);
+
+ instruction.queriedElements.forEach(
+ element => getOrSetAsInMap(queriedElements, element, []).push(player));
+
+ instruction.preStyleProps.forEach((stringMap, element) => {
+ const props = Object.keys(stringMap);
+ if (props.length) {
+ let setVal: Set = allPreStyleElements.get(element) !;
+ if (!setVal) {
+ allPreStyleElements.set(element, setVal = new Set());
+ }
+ props.forEach(prop => setVal.add(prop));
+ }
+ });
+
+ instruction.postStyleProps.forEach((stringMap, element) => {
+ const props = Object.keys(stringMap);
+ let setVal: Set = allPostStyleElements.get(element) !;
+ if (!setVal) {
+ allPostStyleElements.set(element, setVal = new Set());
+ }
+ props.forEach(prop => setVal.add(prop));
+ });
+ });
+ }
+
+ if (erroneousTransitions.length) {
+ let msg = `Unable to process animations due to the following failed trigger transitions\n`;
+ erroneousTransitions.forEach(instruction => {
+ msg += `@${instruction.triggerName} has failed due to:\n`;
+ instruction.errors !.forEach(error => { msg += `- ${error}\n`; });
+ });
+
+ allPlayers.forEach(player => player.destroy());
+ throw new Error(msg);
+ }
+
+ // these can only be detected here since we have a map of all the elements
+ // that have animations attached to them...
+ const enterNodesWithoutAnimations: any[] = [];
+ for (let i = 0; i < allEnterNodes.length; i++) {
+ const element = allEnterNodes[i];
+ if (!subTimelines.has(element)) {
+ enterNodesWithoutAnimations.push(element);
+ }
+ }
+
+ const allPreviousPlayersMap = new Map();
+ let sortedParentElements: any[] = [];
+ queuedInstructions.forEach(entry => {
+ const element = entry.element;
+ if (subTimelines.has(element)) {
+ sortedParentElements.unshift(element);
+ this._beforeAnimationBuild(
+ entry.player.namespaceId, entry.instruction, allPreviousPlayersMap);
+ }
+ });
+
+ skippedPlayers.forEach(player => {
+ const element = player.element;
+ const previousPlayers =
+ this._getPreviousPlayers(element, false, player.namespaceId, player.triggerName, null);
+ previousPlayers.forEach(
+ prevPlayer => { getOrSetAsInMap(allPreviousPlayersMap, element, []).push(prevPlayer); });
+ });
+
+ allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy()));
+
+ // PRE STAGE: fill the ! styles
+ const preStylesMap = allPreStyleElements.size ?
+ cloakAndComputeStyles(
+ this.driver, enterNodesWithoutAnimations, allPreStyleElements, PRE_STYLE) :
+ new Map();
+
+ // POST STAGE: fill the * styles
+ const postStylesMap = cloakAndComputeStyles(
+ this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
+
+ const rootPlayers: TransitionAnimationPlayer[] = [];
+ const subPlayers: TransitionAnimationPlayer[] = [];
+ queuedInstructions.forEach(entry => {
+ const {element, player, instruction} = entry;
+ // this means that it was never consumed by a parent animation which
+ // means that it is independent and therefore should be set for animation
+ if (subTimelines.has(element)) {
+ if (disabledElementsSet.has(element)) {
+ skippedPlayers.push(player);
+ return;
+ }
+
+ const innerPlayer = this._buildAnimation(
+ player.namespaceId, instruction, allPreviousPlayersMap, skippedPlayersMap, preStylesMap,
+ postStylesMap);
+ player.setRealPlayer(innerPlayer);
+
+ let parentHasPriority: any = null;
+ for (let i = 0; i < sortedParentElements.length; i++) {
+ const parent = sortedParentElements[i];
+ if (parent === element) break;
+ if (this.driver.containsElement(parent, element)) {
+ parentHasPriority = parent;
+ break;
+ }
+ }
+
+ if (parentHasPriority) {
+ const parentPlayers = this.playersByElement.get(parentHasPriority);
+ if (parentPlayers && parentPlayers.length) {
+ player.parentPlayer = optimizeGroupPlayer(parentPlayers);
+ }
+ skippedPlayers.push(player);
+ } else {
+ rootPlayers.push(player);
+ }
+ } else {
+ eraseStyles(element, instruction.fromStyles);
+ player.onDestroy(() => setStyles(element, instruction.toStyles));
+ subPlayers.push(player);
+ }
+ });
+
+ subPlayers.forEach(player => {
+ const playersForElement = skippedPlayersMap.get(player.element);
+ if (playersForElement && playersForElement.length) {
+ const innerPlayer = optimizeGroupPlayer(playersForElement);
+ player.setRealPlayer(innerPlayer);
+ }
+ });
+
+ // the reason why we don't actually play the animation is
+ // because all that a skipped player is designed to do is to
+ // fire the start/done transition callback events
+ skippedPlayers.forEach(player => {
+ if (player.parentPlayer) {
+ player.parentPlayer.onDestroy(() => player.destroy());
+ } else {
+ player.destroy();
+ }
+ });
+
+ // run through all of the queued removals and see if they
+ // were picked up by a query. If not then perform the removal
+ // operation right away unless a parent animation is ongoing.
+ for (let i = 0; i < allLeaveNodes.length; i++) {
+ const element = allLeaveNodes[i];
+ const details = element[REMOVAL_FLAG] as ElementAnimationState;
+ removeClass(element, LEAVE_CLASSNAME);
+
+ // this means the element has a removal animation that is being
+ // taken care of and therefore the inner elements will hang around
+ // until that animation is over (or the parent queried animation)
+ if (details && details.hasAnimation) continue;
+
+ let players: AnimationPlayer[] = [];
+
+ // if this element is queried or if it contains queried children
+ // then we want for the element not to be removed from the page
+ // until the queried animations have finished
+ if (queriedElements.size) {
+ let queriedPlayerResults = queriedElements.get(element);
+ if (queriedPlayerResults && queriedPlayerResults.length) {
+ players.push(...queriedPlayerResults);
+ }
+
+ let queriedInnerElements = this.driver.query(element, NG_ANIMATING_SELECTOR, true);
+ for (let j = 0; j < queriedInnerElements.length; j++) {
+ let queriedPlayers = queriedElements.get(queriedInnerElements[j]);
+ if (queriedPlayers && queriedPlayers.length) {
+ players.push(...queriedPlayers);
+ }
+ }
+ }
+ if (players.length) {
+ removeNodesAfterAnimationDone(this, element, players);
+ } else {
+ this.processLeaveNode(element);
+ }
+ }
+
+ // this is required so the cleanup method doesn't remove them
+ allLeaveNodes.length = 0;
+
+ rootPlayers.forEach(player => {
+ this.players.push(player);
+ player.onDone(() => {
+ player.destroy();
+
+ const index = this.players.indexOf(player);
+ this.players.splice(index, 1);
+ });
+ player.play();
+ });
+
+ return rootPlayers;
+ }
+
+ elementContainsData(namespaceId: string, element: any) {
+ let containsData = false;
+ const details = element[REMOVAL_FLAG] as ElementAnimationState;
+ if (details && details.setForRemoval) containsData = true;
+ if (this.playersByElement.has(element)) containsData = true;
+ if (this.playersByQueriedElement.has(element)) containsData = true;
+ if (this.statesByElement.has(element)) containsData = true;
+ return this._fetchNamespace(namespaceId).elementContainsData(element) || containsData;
+ }
+
+ afterFlush(callback: () => any) { this._flushFns.push(callback); }
+
+ afterFlushAnimationsDone(callback: () => any) { this._whenQuietFns.push(callback); }
+
+ private _getPreviousPlayers(
+ element: string, isQueriedElement: boolean, namespaceId?: string, triggerName?: string,
+ toStateValue?: any): TransitionAnimationPlayer[] {
+ let players: TransitionAnimationPlayer[] = [];
+ if (isQueriedElement) {
+ const queriedElementPlayers = this.playersByQueriedElement.get(element);
+ if (queriedElementPlayers) {
+ players = queriedElementPlayers;
+ }
+ } else {
+ const elementPlayers = this.playersByElement.get(element);
+ if (elementPlayers) {
+ const isRemovalAnimation = !toStateValue || toStateValue == VOID_VALUE;
+ elementPlayers.forEach(player => {
+ if (player.queued) return;
+ if (!isRemovalAnimation && player.triggerName != triggerName) return;
+ players.push(player);
+ });
+ }
+ }
+ if (namespaceId || triggerName) {
+ players = players.filter(player => {
+ if (namespaceId && namespaceId != player.namespaceId) return false;
+ if (triggerName && triggerName != player.triggerName) return false;
+ return true;
+ });
+ }
+ return players;
+ }
+
+ private _beforeAnimationBuild(
+ namespaceId: string, instruction: AnimationTransitionInstruction,
+ allPreviousPlayersMap: Map) {
+ // it's important to do this step before destroying the players
+ // so that the onDone callback below won't fire before this
+ eraseStyles(instruction.element, instruction.fromStyles);
+
+ const triggerName = instruction.triggerName;
+ const rootElement = instruction.element;
+
+ // when a removal animation occurs, ALL previous players are collected
+ // and destroyed (even if they are outside of the current namespace)
+ const targetNameSpaceId: string|undefined =
+ instruction.isRemovalTransition ? undefined : namespaceId;
+ const targetTriggerName: string|undefined =
+ instruction.isRemovalTransition ? undefined : triggerName;
+
+ instruction.timelines.map(timelineInstruction => {
+ const element = timelineInstruction.element;
+ const isQueriedElement = element !== rootElement;
+ const players = getOrSetAsInMap(allPreviousPlayersMap, element, []);
+ const previousPlayers = this._getPreviousPlayers(
+ element, isQueriedElement, targetNameSpaceId, targetTriggerName, instruction.toState);
+ previousPlayers.forEach(player => {
+ const realPlayer = player.getRealPlayer() as any;
+ if (realPlayer.beforeDestroy) {
+ realPlayer.beforeDestroy();
+ }
+ players.push(player);
+ });
+ });
+ }
+
+ private _buildAnimation(
+ namespaceId: string, instruction: AnimationTransitionInstruction,
+ allPreviousPlayersMap: Map,
+ skippedPlayersMap: Map, preStylesMap: Map,
+ postStylesMap: Map): AnimationPlayer {
+ const triggerName = instruction.triggerName;
+ const rootElement = instruction.element;
+
+ // we first run this so that the previous animation player
+ // data can be passed into the successive animation players
+ const allQueriedPlayers: TransitionAnimationPlayer[] = [];
+ const allConsumedElements = new Set();
+ const allSubElements = new Set();
+ const allNewPlayers = instruction.timelines.map(timelineInstruction => {
+ const element = timelineInstruction.element;
+ allConsumedElements.add(element);
+
+ // FIXME (matsko): make sure to-be-removed animations are removed properly
+ const details = element[REMOVAL_FLAG];
+ if (details && details.removedBeforeQueried) return new NoopAnimationPlayer();
+
+ const isQueriedElement = element !== rootElement;
+ const previousPlayers =
+ (allPreviousPlayersMap.get(element) || EMPTY_PLAYER_ARRAY).map(p => p.getRealPlayer());
+
+ const preStyles = preStylesMap.get(element);
+ const postStyles = postStylesMap.get(element);
+ const keyframes = normalizeKeyframes(
+ this.driver, this._normalizer, element, timelineInstruction.keyframes, preStyles,
+ postStyles);
+ const player = this._buildPlayer(timelineInstruction, keyframes, previousPlayers);
+
+ // this means that this particular player belongs to a sub trigger. It is
+ // important that we match this player up with the corresponding (@trigger.listener)
+ if (timelineInstruction.subTimeline && skippedPlayersMap) {
+ allSubElements.add(element);
+ }
+
+ if (isQueriedElement) {
+ const wrappedPlayer = new TransitionAnimationPlayer(namespaceId, triggerName, element);
+ wrappedPlayer.setRealPlayer(player);
+ allQueriedPlayers.push(wrappedPlayer);
+ }
+
+ return player;
+ });
+
+ allQueriedPlayers.forEach(player => {
+ getOrSetAsInMap(this.playersByQueriedElement, player.element, []).push(player);
+ player.onDone(() => deleteOrUnsetInMap(this.playersByQueriedElement, player.element, player));
+ });
+
+ allConsumedElements.forEach(element => addClass(element, NG_ANIMATING_CLASSNAME));
+ const player = optimizeGroupPlayer(allNewPlayers);
+ player.onDestroy(() => {
+ allConsumedElements.forEach(element => removeClass(element, NG_ANIMATING_CLASSNAME));
+ setStyles(rootElement, instruction.toStyles);
+ });
+
+ // this basically makes all of the callbacks for sub element animations
+ // be dependent on the upper players for when they finish
+ allSubElements.forEach(
+ element => { getOrSetAsInMap(skippedPlayersMap, element, []).push(player); });
+
+ return player;
+ }
+
+ private _buildPlayer(
+ instruction: AnimationTimelineInstruction, keyframes: ɵStyleData[],
+ previousPlayers: AnimationPlayer[]): AnimationPlayer {
+ if (keyframes.length > 0) {
+ return this.driver.animate(
+ instruction.element, keyframes, instruction.duration, instruction.delay,
+ instruction.easing, previousPlayers);
+ }
+
+ // special case for when an empty transition|definition is provided
+ // ... there is no point in rendering an empty animation
+ return new NoopAnimationPlayer();
+ }
+}
+
+export class TransitionAnimationPlayer implements AnimationPlayer {
+ private _player: AnimationPlayer = new NoopAnimationPlayer();
+ private _containsRealPlayer = false;
+
+ private _queuedCallbacks: {[name: string]: (() => any)[]} = {};
+ private _destroyed = false;
+ public parentPlayer: AnimationPlayer;
+
+ public markedForDestroy: boolean = false;
+
+ constructor(public namespaceId: string, public triggerName: string, public element: any) {}
+
+ get queued() { return this._containsRealPlayer == false; }
+
+ get destroyed() { return this._destroyed; }
+
+ setRealPlayer(player: AnimationPlayer) {
+ if (this._containsRealPlayer) return;
+
+ this._player = player;
+ Object.keys(this._queuedCallbacks).forEach(phase => {
+ this._queuedCallbacks[phase].forEach(
+ callback => listenOnPlayer(player, phase, undefined, callback));
+ });
+ this._queuedCallbacks = {};
+ this._containsRealPlayer = true;
+ }
+
+ getRealPlayer() { return this._player; }
+
+ private _queueEvent(name: string, callback: (event: any) => any): void {
+ getOrSetAsInMap(this._queuedCallbacks, name, []).push(callback);
+ }
+
+ onDone(fn: () => void): void {
+ if (this.queued) {
+ this._queueEvent('done', fn);
+ }
+ this._player.onDone(fn);
+ }
+
+ onStart(fn: () => void): void {
+ if (this.queued) {
+ this._queueEvent('start', fn);
+ }
+ this._player.onStart(fn);
+ }
+
+ onDestroy(fn: () => void): void {
+ if (this.queued) {
+ this._queueEvent('destroy', fn);
+ }
+ this._player.onDestroy(fn);
+ }
+
+ init(): void { this._player.init(); }
+
+ hasStarted(): boolean { return this.queued ? false : this._player.hasStarted(); }
+
+ play(): void { !this.queued && this._player.play(); }
+
+ pause(): void { !this.queued && this._player.pause(); }
+
+ restart(): void { !this.queued && this._player.restart(); }
+
+ finish(): void { this._player.finish(); }
+
+ destroy(): void {
+ this._destroyed = true;
+ this._player.destroy();
+ }
+
+ reset(): void { !this.queued && this._player.reset(); }
+
+ setPosition(p: any): void {
+ if (!this.queued) {
+ this._player.setPosition(p);
+ }
+ }
+
+ getPosition(): number { return this.queued ? 0 : this._player.getPosition(); }
+
+ get totalTime(): number { return this._player.totalTime; }
+}
+
+function deleteOrUnsetInMap(map: Map| {[key: string]: any}, key: any, value: any) {
+ let currentValues: any[]|null|undefined;
+ if (map instanceof Map) {
+ currentValues = map.get(key);
+ if (currentValues) {
+ if (currentValues.length) {
+ const index = currentValues.indexOf(value);
+ currentValues.splice(index, 1);
+ }
+ if (currentValues.length == 0) {
+ map.delete(key);
+ }
+ }
+ } else {
+ currentValues = map[key];
+ if (currentValues) {
+ if (currentValues.length) {
+ const index = currentValues.indexOf(value);
+ currentValues.splice(index, 1);
+ }
+ if (currentValues.length == 0) {
+ delete map[key];
+ }
+ }
+ }
+ return currentValues;
+}
+
+function normalizeTriggerValue(value: any): string {
+ switch (typeof value) {
+ case 'boolean':
+ return value ? '1' : '0';
+ default:
+ return value != null ? value.toString() : null;
+ }
+}
+
+function isElementNode(node: any) {
+ return node && node['nodeType'] === 1;
+}
+
+function isTriggerEventValid(eventName: string): boolean {
+ return eventName == 'start' || eventName == 'done';
+}
+
+function cloakElement(element: any, value?: string) {
+ const oldValue = element.style.display;
+ element.style.display = value != null ? value : 'none';
+ return oldValue;
+}
+
+function cloakAndComputeStyles(
+ driver: AnimationDriver, elements: any[], elementPropsMap: Map>,
+ defaultStyle: string): Map {
+ const cloakVals = elements.map(element => cloakElement(element));
+ const valuesMap = new Map();
+
+ elementPropsMap.forEach((props: Set, element: any) => {
+ const styles: ɵStyleData = {};
+ props.forEach(prop => {
+ const value = styles[prop] = driver.computeStyle(element, prop, defaultStyle);
+
+ // there is no easy way to detect this because a sub element could be removed
+ // by a parent animation element being detached.
+ if (!value || value.length == 0) {
+ element[REMOVAL_FLAG] = NULL_REMOVED_QUERIED_STATE;
+ }
+ });
+ valuesMap.set(element, styles);
+ });
+
+ elements.forEach((element, i) => cloakElement(element, cloakVals[i]));
+ return valuesMap;
+}
+
+/*
+Since the Angular renderer code will return a collection of inserted
+nodes in all areas of a DOM tree, it's up to this algorithm to figure
+out which nodes are roots.
+By placing all nodes into a set and traversing upwards to the edge,
+the recursive code can figure out if a clean path from the DOM node
+to the edge container is clear. If no other node is detected in the
+set then it is a root element.
+This algorithm also keeps track of all nodes along the path so that
+if other sibling nodes are also tracked then the lookup process can
+skip a lot of steps in between and avoid traversing the entire tree
+multiple times to the edge.
+ */
+function createIsRootFilterFn(nodes: any): (node: any) => boolean {
+ const nodeSet = new Set(nodes);
+ const knownRootContainer = new Set();
+ let isRoot: (node: any) => boolean;
+ isRoot = node => {
+ if (!node) return true;
+ if (nodeSet.has(node.parentNode)) return false;
+ if (knownRootContainer.has(node.parentNode)) return true;
+ if (isRoot(node.parentNode)) {
+ knownRootContainer.add(node);
+ return true;
+ }
+ return false;
+ };
+ return isRoot;
+}
+
+const CLASSES_CACHE_KEY = '$$classes';
+function containsClass(element: any, className: string): boolean {
+ if (element.classList) {
+ return element.classList.contains(className);
+ } else {
+ const classes = element[CLASSES_CACHE_KEY];
+ return classes && classes[className];
+ }
+}
+
+function addClass(element: any, className: string) {
+ if (element.classList) {
+ element.classList.add(className);
+ } else {
+ let classes: {[className: string]: boolean} = element[CLASSES_CACHE_KEY];
+ if (!classes) {
+ classes = element[CLASSES_CACHE_KEY] = {};
+ }
+ classes[className] = true;
+ }
+}
+
+function removeClass(element: any, className: string) {
+ if (element.classList) {
+ element.classList.remove(className);
+ } else {
+ let classes: {[className: string]: boolean} = element[CLASSES_CACHE_KEY];
+ if (classes) {
+ delete classes[className];
+ }
+ }
+}
+
+function getBodyNode(): any|null {
+ if (typeof document != 'undefined') {
+ return document.body;
+ }
+ return null;
+}
+
+function removeNodesAfterAnimationDone(
+ engine: TransitionAnimationEngine, element: any, players: AnimationPlayer[]) {
+ optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
+}
\ No newline at end of file
diff --git a/nativescript-angular/animations/private-imports/util.ts b/nativescript-angular/animations/private-imports/util.ts
new file mode 100644
index 000000000..f7f7c69b8
--- /dev/null
+++ b/nativescript-angular/animations/private-imports/util.ts
@@ -0,0 +1,220 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+/* tslint:disable */
+import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleData} from '@angular/animations';
+
+export const ONE_SECOND = 1000;
+
+export const ENTER_CLASSNAME = 'ng-enter';
+export const LEAVE_CLASSNAME = 'ng-leave';
+export const ENTER_SELECTOR = '.ng-enter';
+export const LEAVE_SELECTOR = '.ng-leave';
+export const NG_TRIGGER_CLASSNAME = 'ng-trigger';
+export const NG_TRIGGER_SELECTOR = '.ng-trigger';
+export const NG_ANIMATING_CLASSNAME = 'ng-animating';
+export const NG_ANIMATING_SELECTOR = '.ng-animating';
+
+export function resolveTimingValue(value: string | number) {
+ if (typeof value == 'number') return value;
+
+ const matches = (value as string).match(/^(-?[\.\d]+)(m?s)/);
+ if (!matches || matches.length < 2) return 0;
+
+ return _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
+}
+
+function _convertTimeValueToMS(value: number, unit: string): number {
+ switch (unit) {
+ case 's':
+ return value * ONE_SECOND;
+ default: // ms or something else
+ return value;
+ }
+}
+
+export function resolveTiming(
+ timings: string | number | AnimateTimings, errors: any[], allowNegativeValues?: boolean) {
+ return timings.hasOwnProperty('duration') ?
+ timings :
+ parseTimeExpression(timings, errors, allowNegativeValues);
+}
+
+function parseTimeExpression(
+ exp: string | number, errors: string[], allowNegativeValues?: boolean): AnimateTimings {
+ const regex = /^(-?[\.\d]+)(m?s)(?:\s+(-?[\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;
+ let duration: number;
+ let delay: number = 0;
+ let easing: string = '';
+ if (typeof exp === 'string') {
+ const matches = exp.match(regex);
+ if (matches === null) {
+ errors.push(`The provided timing value "${exp}" is invalid.`);
+ return {duration: 0, delay: 0, easing: ''};
+ }
+
+ duration = _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
+
+ const delayMatch = matches[3];
+ if (delayMatch != null) {
+ delay = _convertTimeValueToMS(Math.floor(parseFloat(delayMatch)), matches[4]);
+ }
+
+ const easingVal = matches[5];
+ if (easingVal) {
+ easing = easingVal;
+ }
+ } else {
+ duration = exp;
+ }
+
+ if (!allowNegativeValues) {
+ let containsErrors = false;
+ let startIndex = errors.length;
+ if (duration < 0) {
+ errors.push(`Duration values below 0 are not allowed for this animation step.`);
+ containsErrors = true;
+ }
+ if (delay < 0) {
+ errors.push(`Delay values below 0 are not allowed for this animation step.`);
+ containsErrors = true;
+ }
+ if (containsErrors) {
+ errors.splice(startIndex, 0, `The provided timing value "${exp}" is invalid.`);
+ }
+ }
+
+ return {duration, delay, easing};
+}
+
+export function copyObj(
+ obj: {[key: string]: any}, destination: {[key: string]: any} = {}): {[key: string]: any} {
+ Object.keys(obj).forEach(prop => { destination[prop] = obj[prop]; });
+ return destination;
+}
+
+export function normalizeStyles(styles: ɵStyleData | ɵStyleData[]): ɵStyleData {
+ const normalizedStyles: ɵStyleData = {};
+ if (Array.isArray(styles)) {
+ styles.forEach(data => copyStyles(data, false, normalizedStyles));
+ } else {
+ copyStyles(styles, false, normalizedStyles);
+ }
+ return normalizedStyles;
+}
+
+export function copyStyles(
+ styles: ɵStyleData, readPrototype: boolean, destination: ɵStyleData = {}): ɵStyleData {
+ if (readPrototype) {
+ // we make use of a for-in loop so that the
+ // prototypically inherited properties are
+ // revealed from the backFill map
+ for (let prop in styles) {
+ destination[prop] = styles[prop];
+ }
+ } else {
+ copyObj(styles, destination);
+ }
+ return destination;
+}
+
+export function setStyles(element: any, styles: ɵStyleData) {
+ if (element['style']) {
+ Object.keys(styles).forEach(prop => {
+ const camelProp = dashCaseToCamelCase(prop);
+ element.style[camelProp] = styles[prop];
+ });
+ }
+}
+
+export function eraseStyles(element: any, styles: ɵStyleData) {
+ if (element['style']) {
+ Object.keys(styles).forEach(prop => {
+ const camelProp = dashCaseToCamelCase(prop);
+ element.style[camelProp] = '';
+ });
+ }
+}
+
+export function normalizeAnimationEntry(steps: AnimationMetadata | AnimationMetadata[]):
+ AnimationMetadata {
+ if (Array.isArray(steps)) {
+ if (steps.length == 1) return steps[0];
+ return sequence(steps);
+ }
+ return steps as AnimationMetadata;
+}
+
+export function validateStyleParams(
+ value: string | number, options: AnimationOptions, errors: any[]) {
+ const params = options.params || {};
+ if (typeof value !== 'string') return;
+
+ const matches = value.toString().match(PARAM_REGEX);
+ if (matches) {
+ matches.forEach(varName => {
+ if (!params.hasOwnProperty(varName)) {
+ errors.push(
+ `Unable to resolve the local animation param ${varName} in the given list of values`);
+ }
+ });
+ }
+}
+
+const PARAM_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
+export function interpolateParams(
+ value: string | number, params: {[name: string]: any}, errors: any[]): string|number {
+ const original = value.toString();
+ const str = original.replace(PARAM_REGEX, (_, varName) => {
+ let localVal = params[varName];
+ // this means that the value was never overidden by the data passed in by the user
+ if (!params.hasOwnProperty(varName)) {
+ errors.push(`Please provide a value for the animation param ${varName}`);
+ localVal = '';
+ }
+ return localVal.toString();
+ });
+
+ // we do this to assert that numeric values stay as they are
+ return str == original ? value : str;
+}
+
+export function iteratorToArray(iterator: any): any[] {
+ const arr: any[] = [];
+ let item = iterator.next();
+ while (!item.done) {
+ arr.push(item.value);
+ item = iterator.next();
+ }
+ return arr;
+}
+
+export function mergeAnimationOptions(
+ source: AnimationOptions, destination: AnimationOptions): AnimationOptions {
+ if (source.params) {
+ const p0 = source.params;
+ if (!destination.params) {
+ destination.params = {};
+ }
+ const p1 = destination.params;
+ Object.keys(p0).forEach(param => {
+ if (!p1.hasOwnProperty(param)) {
+ p1[param] = p0[param];
+ }
+ });
+ }
+ return destination;
+}
+
+const DASH_CASE_REGEXP = /-+([a-z0-9])/g;
+export function dashCaseToCamelCase(input: string): string {
+ return input.replace(DASH_CASE_REGEXP, (...m: any[]) => m[1].toUpperCase());
+}
+
+export function allowPreviousPlayerStylesMerge(duration: number, delay: number) {
+ return duration === 0 || delay === 0;
+}
\ No newline at end of file
diff --git a/nativescript-angular/animations/transition-animation-engine.ts b/nativescript-angular/animations/transition-animation-engine.ts
new file mode 100644
index 000000000..e31bbea9d
--- /dev/null
+++ b/nativescript-angular/animations/transition-animation-engine.ts
@@ -0,0 +1,501 @@
+import { AUTO_STYLE, ɵPRE_STYLE as PRE_STYLE, AnimationPlayer, ɵStyleData } from "@angular/animations";
+import { AnimationDriver } from "@angular/animations/browser";
+import { AnimationTransitionInstruction } from "@angular/animations/browser/src/dsl/animation_transition_instruction";
+import { unsetValue } from "tns-core-modules/ui/core/view";
+
+import {
+ TransitionAnimationEngine,
+ TransitionAnimationPlayer,
+ QueuedTransition,
+ ElementAnimationState,
+ REMOVAL_FLAG,
+} from "./private-imports/render/transition_animation_engine";
+import { ElementInstructionMap } from "./private-imports/dsl/element_instruction_map";
+import { getOrSetAsInMap, optimizeGroupPlayer } from "./private-imports/render/shared";
+import {
+ ENTER_CLASSNAME,
+ LEAVE_CLASSNAME,
+ NG_ANIMATING_SELECTOR,
+} from "./private-imports/util";
+
+import { dashCaseToCamelCase } from "./utils";
+import { NgView } from "../element-registry";
+
+function eraseStylesOverride(element: NgView, styles: ɵStyleData) {
+ if (!element.style) {
+ return;
+ }
+
+ Object.keys(styles).forEach(prop => {
+ const camelCaseProp = dashCaseToCamelCase(prop);
+ element.style[camelCaseProp] = unsetValue;
+ });
+}
+
+function setStylesOverride(element: NgView, styles: ɵStyleData) {
+ if (!element.style) {
+ return;
+ }
+
+ Object.keys(styles).forEach(prop => {
+ if (styles[prop] === "*") {
+ return;
+ }
+
+ const camelCaseProp = dashCaseToCamelCase(prop);
+ element.style[camelCaseProp] = styles[camelCaseProp];
+ });
+}
+
+// extending Angular's TransitionAnimationEngine
+// and overriding a few methods that work on the DOM
+export class NSTransitionAnimationEngine extends TransitionAnimationEngine {
+ flush(microtaskId: number = -1) {
+ let players: AnimationPlayer[] = [];
+ if (this.newHostElements.size) {
+ this.newHostElements.forEach((ns, element) => (this)._balanceNamespaceList(ns, element));
+ this.newHostElements.clear();
+ }
+
+ if ((this)._namespaceList.length &&
+ (this.totalQueuedPlayers || this.collectedLeaveElements.length)) {
+ players = this._flushAnimationsOverride(microtaskId);
+ } else {
+ for (let i = 0; i < this.collectedLeaveElements.length; i++) {
+ const element = this.collectedLeaveElements[i];
+ this.processLeaveNode(element);
+ }
+ }
+
+ this.totalQueuedPlayers = 0;
+ this.collectedEnterElements.length = 0;
+ this.collectedLeaveElements.length = 0;
+ (this)._flushFns.forEach(fn => fn());
+ (this)._flushFns = [];
+
+ if ((this)._whenQuietFns.length) {
+ // we move these over to a variable so that
+ // if any new callbacks are registered in another
+ // flush they do not populate the existing set
+ const quietFns = (this)._whenQuietFns;
+ (this)._whenQuietFns = [];
+
+ if (players.length) {
+ optimizeGroupPlayer(players).onDone(() => { quietFns.forEach(fn => fn()); });
+ } else {
+ quietFns.forEach(fn => fn());
+ }
+ }
+ }
+
+ // _flushAnimationsOverride is almost the same as
+ // _flushAnimations from Angular"s TransitionAnimationEngine.
+ // A few dom-specific method invocations are replaced
+ private _flushAnimationsOverride(microtaskId: number): TransitionAnimationPlayer[] {
+ const subTimelines = new ElementInstructionMap();
+ const skippedPlayers: TransitionAnimationPlayer[] = [];
+ const skippedPlayersMap = new Map();
+ const queuedInstructions: QueuedTransition[] = [];
+ const queriedElements = new Map();
+ const allPreStyleElements = new Map>();
+ const allPostStyleElements = new Map>();
+
+ const allEnterNodes: any[] = this.collectedEnterElements.length ?
+ this.collectedEnterElements.filter(createIsRootFilterFn(this.collectedEnterElements)) :
+ [];
+
+ // this must occur before the instructions are built below such that
+ // the :enter queries match the elements (since the timeline queries
+ // are fired during instruction building).
+ for (let i = 0; i < allEnterNodes.length; i++) {
+ addClass(allEnterNodes[i], ENTER_CLASSNAME);
+ }
+
+ const allLeaveNodes: any[] = [];
+ const leaveNodesWithoutAnimations: any[] = [];
+ for (let i = 0; i < this.collectedLeaveElements.length; i++) {
+ const element = this.collectedLeaveElements[i];
+ const details = element[REMOVAL_FLAG] as ElementAnimationState;
+ if (details && details.setForRemoval) {
+ addClass(element, LEAVE_CLASSNAME);
+ allLeaveNodes.push(element);
+ if (!details.hasAnimation) {
+ leaveNodesWithoutAnimations.push(element);
+ }
+ }
+ }
+
+ for (let i = (this)._namespaceList.length - 1; i >= 0; i--) {
+ const ns = (this)._namespaceList[i];
+ ns.drainQueuedTransitions(microtaskId).forEach(entry => {
+ const player = entry.player;
+
+ const element = entry.element;
+
+ // the below check is skipped, because it's
+ // irrelevant in the NativeScript context
+ // if (!bodyNode || !this.driver.containsElement(bodyNode, element)) {
+ // player.destroy();
+ // return;
+ // }
+
+ const instruction = (this)._buildInstruction(entry, subTimelines);
+ if (!instruction) {
+ return;
+ }
+
+ // if a unmatched transition is queued to go then it SHOULD NOT render
+ // an animation and cancel the previously running animations.
+ if (entry.isFallbackTransition) {
+ player.onStart(() => eraseStylesOverride(element, instruction.fromStyles));
+ player.onDestroy(() => setStylesOverride(element, instruction.toStyles));
+ skippedPlayers.push(player);
+ return;
+ }
+
+ // this means that if a parent animation uses this animation as a sub trigger
+ // then it will instruct the timeline builder to not add a player delay, but
+ // instead stretch the first keyframe gap up until the animation starts. The
+ // reason this is important is to prevent extra initialization styles from being
+ // required by the user in the animation.
+ instruction.timelines.forEach(tl => tl.stretchStartingKeyframe = true);
+
+ subTimelines.append(element, instruction.timelines);
+
+ const tuple = { instruction, player, element };
+
+ queuedInstructions.push(tuple);
+
+ instruction.queriedElements.forEach(
+ // tslint:disable-next-line:no-shadowed-variable
+ element => getOrSetAsInMap(queriedElements, element, []).push(player));
+
+ // tslint:disable-next-line:no-shadowed-variable
+ instruction.preStyleProps.forEach((stringMap, element) => {
+ const props = Object.keys(stringMap);
+ if (props.length) {
+ let setVal: Set = allPreStyleElements.get(element)!;
+ if (!setVal) {
+ allPreStyleElements.set(element, setVal = new Set());
+ }
+ props.forEach(prop => setVal.add(prop));
+ }
+ });
+
+ // tslint:disable-next-line:no-shadowed-variable
+ instruction.postStyleProps.forEach((stringMap, element) => {
+ const props = Object.keys(stringMap);
+ let setVal: Set = allPostStyleElements.get(element)!;
+ if (!setVal) {
+ allPostStyleElements.set(element, setVal = new Set());
+ }
+ props.forEach(prop => setVal.add(prop));
+ });
+ });
+ }
+
+ // these can only be detected here since we have a map of all the elements
+ // that have animations attached to them...
+ const enterNodesWithoutAnimations: any[] = [];
+ for (let i = 0; i < allEnterNodes.length; i++) {
+ const element = allEnterNodes[i];
+ if (!subTimelines.has(element)) {
+ enterNodesWithoutAnimations.push(element);
+ }
+ }
+
+ const allPreviousPlayersMap = new Map();
+ let sortedParentElements: any[] = [];
+ queuedInstructions.forEach(entry => {
+ const element = entry.element;
+ if (subTimelines.has(element)) {
+ sortedParentElements.unshift(element);
+ this._beforeAnimationBuildOverride(
+ entry.player.namespaceId, entry.instruction, allPreviousPlayersMap);
+ }
+ });
+
+ skippedPlayers.forEach(player => {
+ const element = player.element;
+ const previousPlayers =
+ (this)._getPreviousPlayers(element, false, player.namespaceId, player.triggerName, null);
+ previousPlayers.forEach(
+ prevPlayer => { getOrSetAsInMap(allPreviousPlayersMap, element, []).push(prevPlayer); });
+ });
+
+ allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy()));
+
+ // PRE STAGE: fill the ! styles
+ const preStylesMap = allPreStyleElements.size ?
+ cloakAndComputeStyles(
+ this.driver, enterNodesWithoutAnimations, allPreStyleElements, PRE_STYLE) :
+ new Map();
+
+ // POST STAGE: fill the * styles
+ const postStylesMap = cloakAndComputeStyles(
+ this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
+
+ const rootPlayers: TransitionAnimationPlayer[] = [];
+ const subPlayers: TransitionAnimationPlayer[] = [];
+ queuedInstructions.forEach(entry => {
+ const { element, player, instruction } = entry;
+ // this means that it was never consumed by a parent animation which
+ // means that it is independent and therefore should be set for animation
+ if (subTimelines.has(element)) {
+ const innerPlayer = (this)._buildAnimation(
+ player.namespaceId, instruction, allPreviousPlayersMap, skippedPlayersMap, preStylesMap,
+ postStylesMap);
+ player.setRealPlayer(innerPlayer);
+
+ let parentHasPriority: any = null;
+ for (let i = 0; i < sortedParentElements.length; i++) {
+ const parent = sortedParentElements[i];
+
+ if (parent === element) {
+ break;
+ }
+
+ if (this.driver.containsElement(parent, element)) {
+ parentHasPriority = parent;
+ break;
+ }
+ }
+
+ if (parentHasPriority) {
+ const parentPlayers = this.playersByElement.get(parentHasPriority);
+ if (parentPlayers && parentPlayers.length) {
+ player.parentPlayer = optimizeGroupPlayer(parentPlayers);
+ }
+ skippedPlayers.push(player);
+ } else {
+ rootPlayers.push(player);
+ }
+ } else {
+ eraseStylesOverride(element, instruction.fromStyles);
+ player.onDestroy(() => setStylesOverride(element, instruction.toStyles));
+ subPlayers.push(player);
+ }
+ });
+
+ subPlayers.forEach(player => {
+ const playersForElement = skippedPlayersMap.get(player.element);
+ if (playersForElement && playersForElement.length) {
+ const innerPlayer = optimizeGroupPlayer(playersForElement);
+ player.setRealPlayer(innerPlayer);
+ }
+ });
+
+ // the reason why we don"t actually play the animation is
+ // because all that a skipped player is designed to do is to
+ // fire the start/done transition callback events
+ skippedPlayers.forEach(player => {
+ if (player.parentPlayer) {
+ player.parentPlayer.onDestroy(() => player.destroy());
+ } else {
+ player.destroy();
+ }
+ });
+
+ // run through all of the queued removals and see if they
+ // were picked up by a query. If not then perform the removal
+ // operation right away unless a parent animation is ongoing.
+ for (let i = 0; i < allLeaveNodes.length; i++) {
+ const element = allLeaveNodes[i];
+ const details = element[REMOVAL_FLAG] as ElementAnimationState;
+
+ // this means the element has a removal animation that is being
+ // taken care of and therefore the inner elements will hang around
+ // until that animation is over (or the parent queried animation)
+ if (details && details.hasAnimation) {
+ continue;
+ }
+
+ let players: AnimationPlayer[] = [];
+
+ // if this element is queried or if it contains queried children
+ // then we want for the element not to be removed from the page
+ // until the queried animations have finished
+ if (queriedElements.size) {
+ let queriedPlayerResults = queriedElements.get(element);
+ if (queriedPlayerResults && queriedPlayerResults.length) {
+ players.push(...queriedPlayerResults);
+ }
+
+ let queriedInnerElements = this.driver.query(element, NG_ANIMATING_SELECTOR, true);
+ for (let j = 0; j < queriedInnerElements.length; j++) {
+ let queriedPlayers = queriedElements.get(queriedInnerElements[j]);
+ if (queriedPlayers && queriedPlayers.length) {
+ players.push(...queriedPlayers);
+ }
+ }
+ }
+ if (players.length) {
+ removeNodesAfterAnimationDone(this, element, players);
+ } else {
+ this.processLeaveNode(element);
+ }
+ }
+
+ rootPlayers.forEach(player => {
+ this.players.push(player);
+ player.onDone(() => {
+ player.destroy();
+
+ const index = this.players.indexOf(player);
+ this.players.splice(index, 1);
+ });
+ player.play();
+ });
+
+ allEnterNodes.forEach(element => removeClass(element, ENTER_CLASSNAME));
+
+ return rootPlayers;
+ }
+
+ elementContainsData(namespaceId: string, element: any) {
+ let containsData = false;
+ const details = element[REMOVAL_FLAG] as ElementAnimationState;
+
+ if (details && details.setForRemoval) {
+ containsData = true;
+ }
+
+ if (this.playersByElement.has(element)) {
+ containsData = true;
+ }
+
+ if (this.playersByQueriedElement.has(element)) {
+ containsData = true;
+ }
+
+ if (this.statesByElement.has(element)) {
+ containsData = true;
+ }
+
+ return (this)._fetchNamespace(namespaceId).elementContainsData(element) || containsData;
+ }
+
+ private _beforeAnimationBuildOverride(
+ namespaceId: string, instruction: AnimationTransitionInstruction,
+ allPreviousPlayersMap: Map) {
+ // it"s important to do this step before destroying the players
+ // so that the onDone callback below won"t fire before this
+ eraseStylesOverride(instruction.element, instruction.fromStyles);
+
+ const triggerName = instruction.triggerName;
+ const rootElement = instruction.element;
+
+ // when a removal animation occurs, ALL previous players are collected
+ // and destroyed (even if they are outside of the current namespace)
+ const targetNameSpaceId: string | undefined =
+ instruction.isRemovalTransition ? undefined : namespaceId;
+ const targetTriggerName: string | undefined =
+ instruction.isRemovalTransition ? undefined : triggerName;
+
+ instruction.timelines.map(timelineInstruction => {
+ const element = timelineInstruction.element;
+ const isQueriedElement = element !== rootElement;
+ const players = getOrSetAsInMap(allPreviousPlayersMap, element, []);
+ const previousPlayers = (this)._getPreviousPlayers(
+ element, isQueriedElement, targetNameSpaceId, targetTriggerName, instruction.toState);
+ previousPlayers.forEach(player => {
+ const realPlayer = player.getRealPlayer() as any;
+ if (realPlayer.beforeDestroy) {
+ realPlayer.beforeDestroy();
+ }
+ players.push(player);
+ });
+ });
+ }
+}
+
+
+function cloakElement(element: any, value?: string) {
+ const oldValue = element.style.display;
+ element.style.display = value != null ? value : "none";
+ return oldValue;
+}
+
+function cloakAndComputeStyles(
+ driver: AnimationDriver, elements: any[], elementPropsMap: Map>,
+ defaultStyle: string): Map {
+ const cloakVals = elements.map(element => cloakElement(element));
+ const valuesMap = new Map();
+
+ elementPropsMap.forEach((props: Set, element: any) => {
+ const styles: ɵStyleData = {};
+ props.forEach(prop => {
+ styles[prop] = driver.computeStyle(element, prop, defaultStyle);
+ });
+ valuesMap.set(element, styles);
+ });
+
+ elements.forEach((element, i) => cloakElement(element, cloakVals[i]));
+ return valuesMap;
+}
+
+/*
+Since the Angular renderer code will return a collection of inserted
+nodes in all areas of a DOM tree, it"s up to this algorithm to figure
+out which nodes are roots.
+By placing all nodes into a set and traversing upwards to the edge,
+the recursive code can figure out if a clean path from the DOM node
+to the edge container is clear. If no other node is detected in the
+set then it is a root element.
+This algorithm also keeps track of all nodes along the path so that
+if other sibling nodes are also tracked then the lookup process can
+skip a lot of steps in between and avoid traversing the entire tree
+multiple times to the edge.
+ */
+function createIsRootFilterFn(nodes: any): (node: any) => boolean {
+ const nodeSet = new Set(nodes);
+ const knownRootContainer = new Set();
+ let isRoot: (node: any) => boolean;
+ isRoot = node => {
+ if (!node) {
+ return true;
+ }
+ if (nodeSet.has(node.parentNode)) {
+ return false;
+ }
+ if (knownRootContainer.has(node.parentNode)) {
+ return true;
+ }
+ if (isRoot(node.parentNode)) {
+ knownRootContainer.add(node);
+ return true;
+ }
+ return false;
+ };
+ return isRoot;
+}
+
+const CLASSES_CACHE_KEY = "$$classes";
+
+function addClass(element: any, className: string) {
+ if (element.classList) {
+ element.classList.add(className);
+ } else {
+ let classes: { [className: string]: boolean } = element[CLASSES_CACHE_KEY];
+ if (!classes) {
+ classes = element[CLASSES_CACHE_KEY] = {};
+ }
+ classes[className] = true;
+ }
+}
+
+function removeClass(element: any, className: string) {
+ if (element.classList) {
+ element.classList.remove(className);
+ } else {
+ let classes: { [className: string]: boolean } = element[CLASSES_CACHE_KEY];
+ if (classes) {
+ delete classes[className];
+ }
+ }
+}
+
+function removeNodesAfterAnimationDone(
+ engine: TransitionAnimationEngine, element: any, players: AnimationPlayer[]) {
+ optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
+}
diff --git a/nativescript-angular/animations/utils.ts b/nativescript-angular/animations/utils.ts
index d8dc38f18..60b0a1045 100644
--- a/nativescript-angular/animations/utils.ts
+++ b/nativescript-angular/animations/utils.ts
@@ -12,6 +12,11 @@ export interface Keyframe {
offset: number;
}
+const DASH_CASE_REGEXP = /-+([a-z0-9])/g;
+export function dashCaseToCamelCase(input: string): string {
+ return input.replace(DASH_CASE_REGEXP, (...m: any[]) => m[1].toUpperCase());
+}
+
export function createKeyframeAnimation(
styles: Keyframe[],
duration: number,
diff --git a/nativescript-angular/common.ts b/nativescript-angular/common.ts
new file mode 100644
index 000000000..c6b119eef
--- /dev/null
+++ b/nativescript-angular/common.ts
@@ -0,0 +1,40 @@
+import { CommonModule } from "@angular/common";
+import {
+ NO_ERRORS_SCHEMA,
+ NgModule,
+} from "@angular/core";
+
+import {
+ ModalDialogHost,
+ ModalDialogService,
+} from "./directives/dialogs";
+import {
+ defaultDeviceProvider,
+ defaultFrameProvider,
+ defaultPageProvider,
+} from "./platform-providers";
+import { NS_DIRECTIVES } from "./directives";
+
+@NgModule({
+ declarations: [
+ ModalDialogHost,
+ ...NS_DIRECTIVES,
+ ],
+ providers: [
+ ModalDialogService,
+ defaultDeviceProvider,
+ defaultFrameProvider,
+ defaultPageProvider,
+ ],
+ imports: [
+ CommonModule,
+ ],
+ exports: [
+ CommonModule,
+ ModalDialogHost,
+ ...NS_DIRECTIVES,
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+})
+export class NativeScriptCommonModule {
+}
diff --git a/nativescript-angular/nativescript.module.ts b/nativescript-angular/nativescript.module.ts
index 2405ef605..e5b34007e 100644
--- a/nativescript-angular/nativescript.module.ts
+++ b/nativescript-angular/nativescript.module.ts
@@ -7,7 +7,6 @@ import "reflect-metadata";
import "./polyfills/array";
import "./polyfills/console";
-import { CommonModule } from "@angular/common";
import {
ApplicationModule,
ErrorHandler,
@@ -17,18 +16,9 @@ import {
SystemJsNgModuleLoader,
} from "@angular/core";
+import { NativeScriptCommonModule } from "./common";
import { NativeScriptRendererFactory } from "./renderer";
import { DetachedLoader } from "./common/detached-loader";
-import {
- ModalDialogHost,
- ModalDialogService,
-} from "./directives/dialogs";
-import {
- defaultDeviceProvider,
- defaultFrameProvider,
- defaultPageProvider,
-} from "./platform-providers";
-import { NS_DIRECTIVES } from "./directives";
export function errorHandlerFactory() {
return new ErrorHandler(true);
@@ -37,32 +27,24 @@ export function errorHandlerFactory() {
@NgModule({
declarations: [
DetachedLoader,
- ModalDialogHost,
- ...NS_DIRECTIVES,
],
providers: [
- ModalDialogService,
NativeScriptRendererFactory,
SystemJsNgModuleLoader,
- defaultDeviceProvider,
- defaultFrameProvider,
- defaultPageProvider,
{ provide: ErrorHandler, useFactory: errorHandlerFactory },
- { provide: RendererFactory2, useClass: NativeScriptRendererFactory },
+ { provide: RendererFactory2, useExisting: NativeScriptRendererFactory },
],
entryComponents: [
DetachedLoader,
],
imports: [
- CommonModule,
ApplicationModule,
+ NativeScriptCommonModule,
],
exports: [
- CommonModule,
ApplicationModule,
+ NativeScriptCommonModule,
DetachedLoader,
- ModalDialogHost,
- ...NS_DIRECTIVES,
],
schemas: [NO_ERRORS_SCHEMA]
})
diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json
index 05ad285e2..2685c0fb6 100644
--- a/nativescript-angular/package.json
+++ b/nativescript-angular/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "nativescript-angular",
- "version": "3.1.3",
+ "version": "4.2.0",
"description": "An Angular renderer that lets you build mobile apps with NativeScript.",
"homepage": "https://www.nativescript.org/",
"bugs": "https://github.com/NativeScript/nativescript-angular/issues",
@@ -43,32 +43,32 @@
"reflect-metadata": "^0.1.8"
},
"peerDependencies": {
- "@angular/common": "~4.0.0 || ~4.1.0",
- "@angular/compiler": "~4.0.0 || ~4.1.0",
- "@angular/core": "~4.0.0 || ~4.1.0",
- "@angular/forms": "~4.0.0 || ~4.1.0",
- "@angular/http": "~4.0.0 || ~4.1.0",
- "@angular/platform-browser": "~4.0.0 || ~4.1.0",
- "@angular/router": "~4.0.0 || ~4.1.0",
+ "@angular/common": "~4.2.5",
+ "@angular/compiler": "~4.2.5",
+ "@angular/core": "~4.2.5",
+ "@angular/forms": "~4.2.5",
+ "@angular/http": "~4.2.5",
+ "@angular/platform-browser": "~4.2.5",
+ "@angular/router": "~4.2.5",
"rxjs": "^5.0.1",
"tns-core-modules": "^3.1.0 || >3.2.0-",
"zone.js": "^0.8.4"
},
"devDependencies": {
- "@angular/animations": "~4.0.0 || ~4.1.0",
- "@angular/common": "~4.0.0 || ~4.1.0",
- "@angular/compiler": "~4.0.0 || ~4.1.0",
- "@angular/compiler-cli": "~4.0.0 || ~4.1.0",
- "@angular/core": "~4.0.0 || ~4.1.0",
- "@angular/forms": "~4.0.0 || ~4.1.0",
- "@angular/http": "~4.0.0 || ~4.1.0",
- "@angular/platform-browser": "~4.0.0 || ~4.1.0",
- "@angular/router": "~4.0.0 || ~4.1.0",
+ "@angular/animations": "~4.2.5",
+ "@angular/common": "~4.2.5",
+ "@angular/compiler": "~4.2.5",
+ "@angular/compiler-cli": "~4.2.5",
+ "@angular/core": "~4.2.5",
+ "@angular/forms": "~4.2.5",
+ "@angular/http": "~4.2.5",
+ "@angular/platform-browser": "~4.2.5",
+ "@angular/router": "~4.2.5",
"codelyzer": "^3.1.2",
"rxjs": "^5.4.2",
"tns-core-modules": "next",
- "tslint": "^5.1.0",
- "typescript": "^2.4.0",
- "zone.js": "^0.8.4"
+ "tslint": "^5.5.0",
+ "typescript": "^2.4.1",
+ "zone.js": "^0.8.12"
}
}
diff --git a/nativescript-angular/renderer.ts b/nativescript-angular/renderer.ts
index 8f3826e2d..ddf247c0b 100644
--- a/nativescript-angular/renderer.ts
+++ b/nativescript-angular/renderer.ts
@@ -104,12 +104,13 @@ export class NativeScriptRenderer extends Renderer2 {
@profile
selectRootElement(selector: string): NgView {
- traceLog("selectRootElement: " + selector);
+ traceLog("NativeScriptRenderer.selectRootElement: " + selector);
return this.rootView;
}
@profile
parentNode(node: NgView): any {
+ traceLog("NativeScriptRenderer.parentNode for node: " + node);
return node.parent || node.templateParent;
}
@@ -217,7 +218,10 @@ export class NativeScriptRenderer extends Renderer2 {
@profile
setValue(_renderNode: any, _value: string) {
- traceLog("NativeScriptRenderer.setValue");
+ traceLog(
+ `NativeScriptRenderer.setValue ` +
+ `renderNode: ${_renderNode}, value: ${_value}`
+ );
}
@profile
diff --git a/nativescript-angular/router.ts b/nativescript-angular/router.ts
index c61228382..d5494bcfb 100644
--- a/nativescript-angular/router.ts
+++ b/nativescript-angular/router.ts
@@ -14,7 +14,7 @@ import { PageRouterOutlet } from "./router/page-router-outlet";
import { NSLocationStrategy, LocationState } from "./router/ns-location-strategy";
import { NativescriptPlatformLocation } from "./router/ns-platform-location";
import { RouterExtensions } from "./router/router-extensions";
-import { NativeScriptModule } from "./nativescript.module";
+import { NativeScriptCommonModule } from "./common";
export { PageRoute } from "./router/page-router-outlet";
export { RouterExtensions } from "./router/router-extensions";
@@ -40,7 +40,7 @@ export type LocationState = LocationState;
],
imports: [
RouterModule,
- NativeScriptModule
+ NativeScriptCommonModule,
],
exports: [
RouterModule,
diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts
index 5c019a1e0..9f7541ec9 100644
--- a/nativescript-angular/router/page-router-outlet.ts
+++ b/nativescript-angular/router/page-router-outlet.ts
@@ -1,13 +1,21 @@
import {
- Attribute, ComponentFactory, ComponentRef, Directive,
- ViewContainerRef, Type, InjectionToken,
- Inject, ComponentFactoryResolver, Injector
+ Attribute, ChangeDetectorRef,
+ ComponentFactory, ComponentFactoryResolver, ComponentRef,
+ Directive, Inject, InjectionToken, Injector,
+ OnDestroy, OnInit,
+ Type, ViewContainerRef,
} from "@angular/core";
-import { profile } from "tns-core-modules/profiling";
-import { RouterOutletMap, ActivatedRoute, PRIMARY_OUTLET } from "@angular/router";
+import {
+ ActivatedRoute,
+ ChildrenOutletContexts,
+ PRIMARY_OUTLET,
+} from "@angular/router";
+
import { Device } from "tns-core-modules/platform";
import { Frame } from "tns-core-modules/ui/frame";
import { Page, NavigatedData } from "tns-core-modules/ui/page";
+import { profile } from "tns-core-modules/profiling";
+
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { isPresent } from "../lang-facade";
@@ -17,32 +25,37 @@ import { DetachedLoader } from "../common/detached-loader";
import { ViewUtil } from "../view-util";
import { NSLocationStrategy } from "./ns-location-strategy";
-interface CacheItem {
- componentRef: ComponentRef;
- reusedRoute: PageRoute;
- outletMap: RouterOutletMap;
- loaderRef?: ComponentRef;
-}
-
export class PageRoute {
activatedRoute: BehaviorSubject;
+
constructor(startRoute: ActivatedRoute) {
this.activatedRoute = new BehaviorSubject(startRoute);
}
}
+class ChildInjector implements Injector {
+ constructor(
+ private providers: ProviderMap,
+ private parent: Injector
+ ) {}
+
+ get(token: Type|InjectionToken, notFoundValue?: T): T {
+ return this.providers.get(token) || this.parent.get(token, notFoundValue);
+ }
+}
+
/**
* Reference Cache
*/
class RefCache {
- private cache: Array = new Array();
-
- public push(
- componentRef: ComponentRef,
- reusedRoute: PageRoute,
- outletMap: RouterOutletMap,
- loaderRef: ComponentRef) {
- this.cache.push({ componentRef, reusedRoute, outletMap, loaderRef });
+ private cache = new Array();
+
+ public get length(): number {
+ return this.cache.length;
+ }
+
+ public push(cacheItem: CacheItem) {
+ this.cache.push(cacheItem);
}
public pop(): CacheItem {
@@ -53,23 +66,45 @@ class RefCache {
return this.cache[this.cache.length - 1];
}
- public get length(): number {
- return this.cache.length;
+ public clear(): void {
+ while (this.length) {
+ RefCache.destroyItem(this.pop());
+ }
+ }
+
+ public static destroyItem(item: CacheItem) {
+ if (isPresent(item.componentRef)) {
+ item.componentRef.destroy();
+ }
+
+ if (isPresent(item.loaderRef)) {
+ item.loaderRef.destroy();
+ }
}
}
+interface CacheItem {
+ componentRef: ComponentRef;
+ reusedRoute: PageRoute;
+ loaderRef?: ComponentRef;
+}
+
+
+type ProviderMap = Map|InjectionToken, any>;
+
+const log = (msg: string) => routerLog(msg);
+
@Directive({ selector: "page-router-outlet" }) // tslint:disable-line:directive-selector
-export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix
- private viewUtil: ViewUtil;
+export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-line:directive-class-suffix
+ private activated: ComponentRef|null = null;
+ private _activatedRoute: ActivatedRoute|null = null;
private refCache: RefCache = new RefCache();
private isInitialPage: boolean = true;
private detachedLoaderFactory: ComponentFactory;
private itemsToDestroy: CacheItem[] = [];
- private currentActivatedComp: ComponentRef;
- private currentActivatedRoute: ActivatedRoute;
-
- public outletMap: RouterOutletMap;
+ private name: string;
+ private viewUtil: ViewUtil;
/** @deprecated from Angular since v4 */
get locationInjector(): Injector { return this.location.injector; }
@@ -77,70 +112,97 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix
get locationFactoryResolver(): ComponentFactoryResolver { return this.resolver; }
get isActivated(): boolean {
- return !!this.currentActivatedComp;
+ return !!this.activated;
}
get component(): Object {
- if (!this.currentActivatedComp) {
+ if (!this.activated) {
throw new Error("Outlet is not activated");
}
- return this.currentActivatedComp.instance;
+ return this.activated.instance;
}
get activatedRoute(): ActivatedRoute {
- if (!this.currentActivatedComp) {
+ if (!this.activated) {
throw new Error("Outlet is not activated");
}
- return this.currentActivatedRoute;
+ return this._activatedRoute;
}
constructor(
- parentOutletMap: RouterOutletMap,
+ private parentContexts: ChildrenOutletContexts,
private location: ViewContainerRef,
@Attribute("name") name: string,
private locationStrategy: NSLocationStrategy,
private componentFactoryResolver: ComponentFactoryResolver,
private resolver: ComponentFactoryResolver,
private frame: Frame,
+ private changeDetector: ChangeDetectorRef,
@Inject(DEVICE) device: Device,
- @Inject(PAGE_FACTORY) private pageFactory: PageFactory) {
+ @Inject(PAGE_FACTORY) private pageFactory: PageFactory
+ ) {
- parentOutletMap.registerOutlet(name ? name : PRIMARY_OUTLET, this);
+ this.name = name || PRIMARY_OUTLET;
+ parentContexts.onChildOutletCreated(this.name, this);
this.viewUtil = new ViewUtil(device);
this.detachedLoaderFactory = resolver.resolveComponentFactory(DetachedLoader);
log("DetachedLoaderFactory loaded");
}
+ ngOnDestroy(): void {
+ this.parentContexts.onChildOutletDestroyed(this.name);
+ }
+
+ ngOnInit(): void {
+ if (this.isActivated) {
+ return;
+ }
+
+ // If the outlet was not instantiated at the time the route got activated we need to populate
+ // the outlet when it is initialized (ie inside a NgIf)
+ const context = this.parentContexts.getContext(this.name);
+ if (!context || !context.route) {
+ return;
+ }
+
+ if (context.attachRef) {
+ // `attachRef` is populated when there is an existing component to mount
+ this.attach(context.attachRef, context.route);
+ } else {
+ // otherwise the component defined in the configuration is created
+ this.activateWith(context.route, context.resolver || null);
+ }
+ }
+
deactivate(): void {
if (this.locationStrategy._isPageNavigatingBack()) {
log("PageRouterOutlet.deactivate() while going back - should destroy");
+ if (!this.isActivated) {
+ return;
+ }
+
const poppedItem = this.refCache.pop();
const poppedRef = poppedItem.componentRef;
- if (this.currentActivatedComp !== poppedRef) {
+ if (this.activated !== poppedRef) {
throw new Error("Current componentRef is different for cached componentRef");
}
- this.destroyCacheItem(poppedItem);
- this.currentActivatedComp = null;
-
+ RefCache.destroyItem(poppedItem);
+ this.activated = null;
} else {
log("PageRouterOutlet.deactivate() while going forward - do nothing");
}
}
- private clearRefCache() {
- while (this.refCache.length > 0) {
- this.itemsToDestroy.push(this.refCache.pop());
- }
- }
private destroyQueuedCacheItems() {
while (this.itemsToDestroy.length > 0) {
this.destroyCacheItem(this.itemsToDestroy.pop());
}
}
+
private destroyCacheItem(poppedItem: CacheItem) {
if (isPresent(poppedItem.componentRef)) {
poppedItem.componentRef.destroy();
@@ -151,92 +213,131 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix
}
}
+ /**
+ * Called when the `RouteReuseStrategy` instructs to detach the subtree
+ */
+ detach(): ComponentRef {
+ if (!this.isActivated) {
+ throw new Error("Outlet is not activated");
+ }
+
+ this.location.detach();
+ const cmp = this.activated;
+ this.activated = null;
+ this._activatedRoute = null;
+ return cmp;
+ }
+
+ /**
+ * Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
+ */
+ attach(ref: ComponentRef, activatedRoute: ActivatedRoute) {
+ log("PageRouterOutlet.attach()" +
+ "when RouteReuseStrategy instructs to re-attach " +
+ "previously detached subtree");
+
+ this.activated = ref;
+ this._activatedRoute = activatedRoute;
+ this.location.insert(ref.hostView);
+ }
+
/**
* Called by the Router to instantiate a new component during the commit phase of a navigation.
* This method in turn is responsible for calling the `routerOnActivate` hook of its child.
*/
@profile
activateWith(
- activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null,
- outletMap: RouterOutletMap): void {
- this.outletMap = outletMap;
- this.currentActivatedRoute = activatedRoute;
+ activatedRoute: ActivatedRoute,
+ resolver: ComponentFactoryResolver|null
+ ): void {
+
+ log("PageRouterOutlet.activateWith() - " +
+ "instanciating new component during commit phase of a navigation");
+ this._activatedRoute = activatedRoute;
resolver = resolver || this.resolver;
if (this.locationStrategy._isPageNavigatingBack()) {
- this.activateOnGoBack(activatedRoute, outletMap);
+ this.activateOnGoBack(activatedRoute);
} else {
- this.activateOnGoForward(activatedRoute, outletMap, resolver);
+ this.activateOnGoForward(activatedRoute, resolver);
}
}
private activateOnGoForward(
activatedRoute: ActivatedRoute,
- outletMap: RouterOutletMap,
- loadedResolver: ComponentFactoryResolver): void {
- const pageRoute = new PageRoute(activatedRoute);
+ loadedResolver: ComponentFactoryResolver
+ ): void {
- let providers = new Map();
- providers.set(PageRoute, pageRoute);
- providers.set(ActivatedRoute, activatedRoute);
- providers.set(RouterOutletMap, outletMap);
+ const pageRoute = new PageRoute(activatedRoute);
+ const providers = this.initProvidersMap(activatedRoute, pageRoute);
const childInjector = new ChildInjector(providers, this.location.injector);
-
const factory = this.getComponentFactory(activatedRoute, loadedResolver);
+
if (this.isInitialPage) {
log("PageRouterOutlet.activate() initial page - just load component");
this.isInitialPage = false;
- this.currentActivatedComp = this.location.createComponent(
+ this.activated = this.location.createComponent(
factory, this.location.length, childInjector, []);
- this.currentActivatedComp.changeDetectorRef.detectChanges();
-
- this.refCache.push(this.currentActivatedComp, pageRoute, outletMap, null);
+ this.changeDetector.markForCheck();
+ this.refCache.push({
+ componentRef: this.activated,
+ reusedRoute: pageRoute,
+ loaderRef: null,
+ });
} else {
log("PageRouterOutlet.activate() forward navigation - " +
"create detached loader in the loader container");
const page = this.pageFactory({
isNavigation: true,
- componentType: factory.componentType
+ componentType: factory.componentType,
});
providers.set(Page, page);
const loaderRef = this.location.createComponent(
this.detachedLoaderFactory, this.location.length, childInjector, []);
- loaderRef.changeDetectorRef.detectChanges();
+ this.changeDetector.markForCheck();
- this.currentActivatedComp = loaderRef.instance.loadWithFactory(factory);
- this.loadComponentInPage(page, this.currentActivatedComp);
+ this.activated = loaderRef.instance.loadWithFactory(factory);
+ this.loadComponentInPage(page, this.activated);
- this.currentActivatedComp.changeDetectorRef.detectChanges();
- this.refCache.push(this.currentActivatedComp, pageRoute, outletMap, loaderRef);
+ this.refCache.push({
+ componentRef: this.activated,
+ reusedRoute: pageRoute,
+ loaderRef,
+ });
}
}
- private activateOnGoBack(
+ private initProvidersMap(
activatedRoute: ActivatedRoute,
- outletMap: RouterOutletMap): void {
+ pageRoute: PageRoute
+ ): ProviderMap {
+
+ const providers = new Map();
+ providers.set(PageRoute, pageRoute);
+ providers.set(ActivatedRoute, activatedRoute);
+
+ const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
+ providers.set(ChildrenOutletContexts, childContexts);
+
+ return providers;
+ }
+
+ private activateOnGoBack(activatedRoute: ActivatedRoute): void {
log("PageRouterOutlet.activate() - Back navigation, so load from cache");
this.locationStrategy._finishBackPageNavigation();
- let cacheItem = this.refCache.peek();
+ const cacheItem = this.refCache.peek();
cacheItem.reusedRoute.activatedRoute.next(activatedRoute);
- this.outletMap = cacheItem.outletMap;
-
- // HACK: Fill the outlet map provided by the router, with the outlets that we have
- // cached. This is needed because the component is taken from the cache and not
- // created - so it will not register its child router-outlets to the newly created
- // outlet map.
- (Object).assign(outletMap, cacheItem.outletMap);
-
- this.currentActivatedComp = cacheItem.componentRef;
+ this.activated = cacheItem.componentRef;
}
@profile
@@ -268,7 +369,7 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix
// Clear refCache if navigation with clearHistory
if (navOptions.clearHistory) {
- this.clearRefCache();
+ this.refCache.clear();
}
}
@@ -280,25 +381,8 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix
const snapshot = activatedRoute._futureSnapshot;
const component = snapshot._routeConfig.component;
- if (loadedResolver) {
- return loadedResolver.resolveComponentFactory(component);
- } else {
- return this.componentFactoryResolver.resolveComponentFactory(component);
- }
+ return loadedResolver ?
+ loadedResolver.resolveComponentFactory(component) :
+ this.componentFactoryResolver.resolveComponentFactory(component);
}
}
-
-class ChildInjector implements Injector {
- constructor(
- private providers: Map|InjectionToken, any>,
- private parent: Injector
- ) {}
-
- get(token: Type|InjectionToken, notFoundValue?: T): T {
- return this.providers.get(token) || this.parent.get(token, notFoundValue);
- }
-}
-
-function log(msg: string) {
- routerLog(msg);
-}
diff --git a/nativescript-angular/trace.ts b/nativescript-angular/trace.ts
index 71324d6d5..800b96b22 100644
--- a/nativescript-angular/trace.ts
+++ b/nativescript-angular/trace.ts
@@ -1,9 +1,14 @@
import { write, categories, messageType } from "tns-core-modules/trace";
+export const animationsTraceCategory = "ns-animations";
export const rendererTraceCategory = "ns-renderer";
export const routerTraceCategory = "ns-router";
export const listViewTraceCategory = "ns-list-view";
+export function animationsLog(message: string): void {
+ write(message, animationsTraceCategory);
+}
+
export function rendererLog(msg): void {
write(msg, rendererTraceCategory);
}
diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts
index 27679de61..2b0c85ed1 100644
--- a/ng-sample/app/app.ts
+++ b/ng-sample/app/app.ts
@@ -7,12 +7,19 @@ import { Router } from "@angular/router";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { NativeScriptFormsModule } from "nativescript-angular/forms";
import { NativeScriptHttpModule } from "nativescript-angular/http";
-import { rendererTraceCategory, routerTraceCategory, listViewTraceCategory } from "nativescript-angular/trace";
+import {
+ rendererTraceCategory,
+ routerTraceCategory,
+ listViewTraceCategory,
+ animationsTraceCategory,
+} from "nativescript-angular/trace";
import { PAGE_FACTORY, PageFactory, PageFactoryOptions } from "nativescript-angular/platform-providers";
import { Page } from "ui/page";
import { Color } from "color";
import { setCategories, enable } from "trace";
-// setCategories(rendererTraceCategory);
+setCategories(
+ `${animationsTraceCategory},${rendererTraceCategory}`
+);
// setCategories(routerTraceCategory);
// setCategories(listViewTraceCategory);
enable();
@@ -42,6 +49,7 @@ import { AnimationEnterLeaveTest } from "./examples/animation/animation-enter-le
import { AnimationKeyframesTest } from "./examples/animation/animation-keyframes-test";
import { AnimationNgClassTest } from "./examples/animation/animation-ngclass-test";
import { AnimationStatesTest } from "./examples/animation/animation-states-test";
+import { AnimationStatesMultiTest } from "./examples/animation/animation-states-multi-test";
@NgModule({
declarations: [
@@ -128,10 +136,11 @@ const customPageFactoryProvider = {
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(PageRouterOutletAppComponent));
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(PageRouterOutletNestedAppComponent));
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(ClearHistoryAppComponent));
-//platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LoginAppComponent));
+// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LoginAppComponent));
// animations
-// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest));
+platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest));
+// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesMultiTest));
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationNgClassTest));
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationKeyframesTest));
// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationEnterLeaveTest));
diff --git a/ng-sample/app/examples/animation/animation-enter-leave-test.ts b/ng-sample/app/examples/animation/animation-enter-leave-test.ts
index 307534574..3f4511ddc 100644
--- a/ng-sample/app/examples/animation/animation-enter-leave-test.ts
+++ b/ng-sample/app/examples/animation/animation-enter-leave-test.ts
@@ -1,4 +1,11 @@
-import {Component, trigger, style, animate, state, transition } from "@angular/core";
+import { Component } from '@angular/core';
+import {
+ trigger,
+ state,
+ style,
+ animate,
+ transition
+} from '@angular/animations';
@Component({
selector: "animation-enter-leave",
@@ -6,8 +13,15 @@ import {Component, trigger, style, animate, state, transition } from "@angular/c
styleUrls: [ "./examples/animation/animation-enter-leave-test.css" ],
animations: [
trigger("state", [
- state("in", style({ "background-color": "red", "opacity": 1 })),
- state("void", style({ "background-color": "white", "opacity": 0 })),
+ state("in", style({
+ "background-color": "red",
+ "opacity": 1,
+ })),
+
+ state("void", style({
+ "background-color": "white",
+ "opacity": 0,
+ })),
transition("void => *", [ animate("600ms ease-out") ]),
transition("* => void", [ animate("600ms ease-out")])
])
diff --git a/ng-sample/app/examples/animation/animation-keyframes-test.ts b/ng-sample/app/examples/animation/animation-keyframes-test.ts
index dd5d6ace7..005542546 100644
--- a/ng-sample/app/examples/animation/animation-keyframes-test.ts
+++ b/ng-sample/app/examples/animation/animation-keyframes-test.ts
@@ -1,4 +1,12 @@
-import {Component, trigger, style, animate, state, transition, keyframes } from "@angular/core";
+import { Component } from '@angular/core';
+import {
+ trigger,
+ state,
+ style,
+ animate,
+ transition,
+ keyframes,
+} from '@angular/animations';
@Component({
selector: "animation-states",
diff --git a/ng-sample/app/examples/animation/animation-ngclass-test.ts b/ng-sample/app/examples/animation/animation-ngclass-test.ts
index c0d7b0ba7..9c9ccf6ed 100644
--- a/ng-sample/app/examples/animation/animation-ngclass-test.ts
+++ b/ng-sample/app/examples/animation/animation-ngclass-test.ts
@@ -1,5 +1,4 @@
-import {Component} from "@angular/core";
-import {Observable} from "data/observable";
+import { Component } from "@angular/core";
@Component({
selector: "main-component",
diff --git a/ng-sample/app/examples/animation/animation-states-multi-test.ts b/ng-sample/app/examples/animation/animation-states-multi-test.ts
new file mode 100644
index 000000000..7b96906e4
--- /dev/null
+++ b/ng-sample/app/examples/animation/animation-states-multi-test.ts
@@ -0,0 +1,54 @@
+import { Component } from '@angular/core';
+import {
+ trigger,
+ state,
+ style,
+ animate,
+ transition
+} from '@angular/animations';
+
+@Component({
+ selector: "animation-states-multi",
+ template: `
+
+
+
+ `,
+ animations: [
+ trigger('heroState', [
+ state('inactive', style({
+ backgroundColor: 'green',
+ transform: 'scale(1)'
+ })),
+ state('active', style({
+ backgroundColor: 'red',
+ transform: 'scale(2)'
+ })),
+
+ transition('inactive => active', animate('100ms ease-in')),
+ transition('active => inactive', animate('100ms ease-out')),
+ ])
+ ]
+})
+export class AnimationStatesMultiTest {
+ heroes = [
+ {
+ name: "Windstorm",
+ state: "inactive"
+ },
+ {
+ name: "Batman",
+ state: "active"
+ },
+ ];
+
+ toggleState(hero) {
+ hero.state = hero.state === "active" ? "inactive" : "active";
+ }
+}
+
+
diff --git a/ng-sample/app/examples/animation/animation-states-test.ts b/ng-sample/app/examples/animation/animation-states-test.ts
index ba6288ea5..15cd88f20 100644
--- a/ng-sample/app/examples/animation/animation-states-test.ts
+++ b/ng-sample/app/examples/animation/animation-states-test.ts
@@ -1,22 +1,37 @@
-import {Component, trigger, style, animate, state, transition } from "@angular/core";
+import { Component } from "@angular/core";
+import {
+ trigger,
+ style,
+ animate,
+ state,
+ transition,
+} from "@angular/animations";
@Component({
selector: "animation-states",
template: `
-
+
`,
animations: [
trigger("state", [
- state("inactive", style({ "background-color": "red" })),
- state("active", style({ "background-color": "green" })),
- transition("inactive => active", [ animate("600ms ease-out") ]),
- transition("active => inactive", [ animate("600ms ease-out") ]),
+
+ state("inactive", style({
+ backgroundColor: "red",
+ transform: "scale(0.7)",
+ })),
+ state("active", style({
+ backgroundColor: "green",
+ transform: "scale(2)",
+ })),
+
+ transition("* => active", [ animate("600ms ease-out") ]),
+ transition("* => inactive", [ animate("600ms ease-out") ]),
+
])
]
})
export class AnimationStatesTest {
-
isOn = false;
onTap() {
diff --git a/ng-sample/hooks/after-prepare/nativescript-dev-webpack.js b/ng-sample/hooks/after-prepare/nativescript-dev-webpack.js
deleted file mode 100644
index 6e2939411..000000000
--- a/ng-sample/hooks/after-prepare/nativescript-dev-webpack.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require("nativescript-dev-webpack/after-prepare-hook.js");
diff --git a/ng-sample/hooks/before-livesync/nativescript-angular-sync.js b/ng-sample/hooks/before-livesync/nativescript-angular-sync.js
deleted file mode 100644
index 5a8510cb2..000000000
--- a/ng-sample/hooks/before-livesync/nativescript-angular-sync.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require("nativescript-angular/hooks/before-livesync");
diff --git a/ng-sample/hooks/before-prepare/nativescript-dev-typescript.js b/ng-sample/hooks/before-prepare/nativescript-dev-typescript.js
deleted file mode 100644
index 7116ebdb4..000000000
--- a/ng-sample/hooks/before-prepare/nativescript-dev-typescript.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require("nativescript-dev-typescript/lib/before-prepare.js");
diff --git a/ng-sample/hooks/before-watch/nativescript-dev-typescript.js b/ng-sample/hooks/before-watch/nativescript-dev-typescript.js
deleted file mode 100644
index 9a6ef6600..000000000
--- a/ng-sample/hooks/before-watch/nativescript-dev-typescript.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require("nativescript-dev-typescript/lib/watch.js");
diff --git a/ng-sample/package.json b/ng-sample/package.json
index c1f91caf3..1d07d71b5 100644
--- a/ng-sample/package.json
+++ b/ng-sample/package.json
@@ -32,15 +32,15 @@
},
"homepage": "https://github.com/NativeScript/template-hello-world",
"dependencies": {
- "@angular/animations": "~4.1.0",
- "@angular/common": "~4.1.0",
- "@angular/compiler": "~4.1.0",
- "@angular/core": "~4.1.0",
- "@angular/forms": "~4.1.0",
- "@angular/http": "~4.1.0",
- "@angular/platform-browser": "~4.1.0",
- "@angular/platform-browser-dynamic": "~4.1.0",
- "@angular/router": "~4.1.0",
+ "@angular/animations": "~4.2.4",
+ "@angular/common": "~4.2.4",
+ "@angular/compiler": "~4.2.4",
+ "@angular/core": "~4.2.4",
+ "@angular/forms": "~4.2.4",
+ "@angular/http": "~4.2.4",
+ "@angular/platform-browser": "~4.2.4",
+ "@angular/platform-browser-dynamic": "~4.2.4",
+ "@angular/router": "~4.2.4",
"nativescript-angular": "file:../nativescript-angular",
"rxjs": "^5.3.0",
"tns-core-modules": "next",
@@ -60,4 +60,4 @@
"scripts": {
"tslint": "tslint --project tsconfig.json --config tslint.json"
}
-}
\ No newline at end of file
+}
diff --git a/tests/package.json b/tests/package.json
index 8e664679e..d23bf511b 100644
--- a/tests/package.json
+++ b/tests/package.json
@@ -26,15 +26,15 @@
],
"homepage": "http://nativescript.org",
"dependencies": {
- "@angular/animations": "~4.1.0",
- "@angular/common": "~4.1.0",
- "@angular/compiler": "~4.1.0",
- "@angular/core": "~4.1.0",
- "@angular/forms": "~4.1.0",
- "@angular/http": "~4.1.0",
- "@angular/platform-browser": "~4.1.0",
- "@angular/platform-browser-dynamic": "~4.1.0",
- "@angular/router": "~4.1.0",
+ "@angular/animations": "~4.2.4",
+ "@angular/common": "~4.2.4",
+ "@angular/compiler": "~4.2.4",
+ "@angular/core": "~4.2.4",
+ "@angular/forms": "~4.2.4",
+ "@angular/http": "~4.2.4",
+ "@angular/platform-browser": "~4.2.4",
+ "@angular/platform-browser-dynamic": "~4.2.4",
+ "@angular/router": "~4.2.4",
"nativescript-angular": "../nativescript-angular",
"nativescript-unit-test-runner": "^0.3.4",
"rxjs": "^5.2.0",