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/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..236012db7 100644 --- a/nativescript-angular/animations/animation-driver.ts +++ b/nativescript-angular/animations/animation-driver.ts @@ -1,22 +1,117 @@ import { AnimationPlayer } from "@angular/animations"; +import { AnimationDriver } from "@angular/animations/browser"; +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 +121,70 @@ 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) { + return true; + } + + if (selector.match(element)) { + result.matches.push(element); + return multi; + } + + return true; } 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..0f84b8345 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -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",