From c74785440afc75c5c29ed378a537bdb9967b05e2 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Tue, 13 Jun 2017 16:48:56 +0300 Subject: [PATCH 01/28] fix(page-router-outlet): upgrade to be compliant with ng 4.2 - don't run change detection every time an outlet is activated - drop `routerOutletMap`s caused by https://github.com/angular/angular/pull/16510 --- nativescript-angular/animations.ts | 13 +- .../animations/animation-engine.ts | 145 --------- .../animations/animation-player.ts | 6 +- nativescript-angular/package.json | 32 +- .../router/page-router-outlet.ts | 285 ++++++++++++------ ng-sample/package.json | 20 +- tests/package.json | 18 +- 7 files changed, 234 insertions(+), 285 deletions(-) delete mode 100644 nativescript-angular/animations/animation-engine.ts diff --git a/nativescript-angular/animations.ts b/nativescript-angular/animations.ts index 30b32f3d6..267eb22a0 100644 --- a/nativescript-angular/animations.ts +++ b/nativescript-angular/animations.ts @@ -1,21 +1,25 @@ 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"; import { NativeScriptModule } from "./nativescript.module"; import { NativeScriptRendererFactory } from "./renderer"; @Injectable() -export class InjectableAnimationEngine extends NativeScriptAnimationEngine { +export class InjectableAnimationEngine extends AnimationEngine { constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) { super(driver, normalizer); } @@ -35,6 +39,7 @@ export function instanciateDefaultStyleNormalizer() { } export const NATIVESCRIPT_ANIMATIONS_PROVIDERS: Provider[] = [ + {provide: AnimationBuilder, useClass: BrowserAnimationBuilder}, {provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver}, {provide: AnimationStyleNormalizer, useFactory: instanciateDefaultStyleNormalizer}, {provide: AnimationEngine, useClass: InjectableAnimationEngine}, { diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts deleted file mode 100644 index b5e0911dc..000000000 --- a/nativescript-angular/animations/animation-engine.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* 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; - } - - // 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); - } - - return true; - }); - - for (let i = 0; i < elms.length; i++) { - const elm = elms[i]; - const activePlayers = this._getElementAnimation(elm); - if (activePlayers) { - activePlayers.forEach(player => player.destroy()); - } - - 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..5066008c4 100644 --- a/nativescript-angular/animations/animation-player.ts +++ b/nativescript-angular/animations/animation-player.ts @@ -17,13 +17,15 @@ 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 { } diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json index 05ad285e2..17b8dfd16 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -43,27 +43,27 @@ "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.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/router": "~4.2.4", "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.4", + "@angular/common": "~4.2.4", + "@angular/compiler": "~4.2.4", + "@angular/compiler-cli": "~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/router": "~4.2.4", "codelyzer": "^3.1.2", "rxjs": "^5.4.2", "tns-core-modules": "next", diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts index 5c019a1e0..cda2ca491 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,14 +66,38 @@ 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; @@ -69,7 +106,8 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix 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 +115,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 +216,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 +372,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 +384,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/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", From 415c938464a5f274a28245f6c38fea23688a9cc5 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Tue, 27 Jun 2017 13:47:07 +0300 Subject: [PATCH 02/28] chore: exclude hooks from source control --- .gitignore | 4 +++- ng-sample/hooks/after-prepare/nativescript-dev-webpack.js | 1 - ng-sample/hooks/before-livesync/nativescript-angular-sync.js | 1 - ng-sample/hooks/before-prepare/nativescript-dev-typescript.js | 1 - ng-sample/hooks/before-watch/nativescript-dev-typescript.js | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 ng-sample/hooks/after-prepare/nativescript-dev-webpack.js delete mode 100644 ng-sample/hooks/before-livesync/nativescript-angular-sync.js delete mode 100644 ng-sample/hooks/before-prepare/nativescript-dev-typescript.js delete mode 100644 ng-sample/hooks/before-watch/nativescript-dev-typescript.js 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/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"); From c64a80be9cd5e0f77620aed5a12c8a7bd9b7e9b7 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 6 Jul 2017 11:50:26 +0300 Subject: [PATCH 03/28] style: typo in variable name --- nativescript-angular/animations.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nativescript-angular/animations.ts b/nativescript-angular/animations.ts index 267eb22a0..0e5d9111e 100644 --- a/nativescript-angular/animations.ts +++ b/nativescript-angular/animations.ts @@ -34,15 +34,16 @@ export function instantiateRendererFactory( 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: AnimationEngine, useClass: InjectableAnimationEngine}, + { provide: RendererFactory2, useFactory: instantiateRendererFactory, deps: [NativeScriptRendererFactory, AnimationEngine, NgZone] From 6d76d6403a830a05fbad84ffa5c3129ffe31e208 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 6 Jul 2017 11:51:05 +0300 Subject: [PATCH 04/28] style: code formatting --- .../animations/animation-player.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/nativescript-angular/animations/animation-player.ts b/nativescript-angular/animations/animation-player.ts index 5066008c4..c8f69bf00 100644 --- a/nativescript-angular/animations/animation-player.ts +++ b/nativescript-angular/animations/animation-player.ts @@ -2,8 +2,8 @@ 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"; export class NativeScriptAnimationPlayer implements AnimationPlayer { public parentPlayer: AnimationPlayer = null; @@ -24,7 +24,9 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { this.initKeyframeAnimation(keyframes, duration, delay, easing); } - get totalTime(): number { return this.delay + this.duration; } + get totalTime(): number { + return this.delay + this.duration; + } init(): void { } @@ -50,7 +52,7 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { this.animation.play(this.target) .then(() => this.onFinish()) - .catch((_e) => { }); + .catch((_e) => {}); } pause(): void { @@ -90,11 +92,13 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { } private onFinish() { - if (!this._finished) { - this._finished = true; - this._started = false; - this._doneSubscriptions.forEach(fn => fn()); - this._doneSubscriptions = []; + if (this._finished) { + return; } + + this._finished = true; + this._started = false; + this._doneSubscriptions.forEach(fn => fn()); + this._doneSubscriptions = []; } } From 6bde2e4f299cfe6a6ddddc5d13a219b04965b89d Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 6 Jul 2017 11:52:00 +0300 Subject: [PATCH 05/28] refactor: add trace for animations --- nativescript-angular/renderer.ts | 8 ++++++-- nativescript-angular/trace.ts | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) 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/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); } From 3072da5961f142f1c1775e720376e1aedcc5c28a Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 6 Jul 2017 11:52:22 +0300 Subject: [PATCH 06/28] feat: implement simple animation driver --- .../animations/animation-driver.ts | 76 ++++++++++++++++--- .../animations/animation-player.ts | 13 ++++ 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts index eeb385e42..f878ede05 100644 --- a/nativescript-angular/animations/animation-driver.ts +++ b/nativescript-angular/animations/animation-driver.ts @@ -1,21 +1,68 @@ import { AnimationPlayer } from "@angular/animations"; +import { AnimationDriver } from "@angular/animations/browser"; -import { NgView } from "../element-registry"; import { NativeScriptAnimationPlayer } from "./animation-player"; import { Keyframe } from "./utils"; - -export abstract class AnimationDriver { - abstract animate( - element: any, - keyframes: Keyframe[], - duration: number, - delay: number, - easing: string - ): AnimationPlayer; -} +import { NgView } from "../element-registry"; +import { animationsLog as traceLog } from "../trace"; export class NativeScriptAnimationDriver implements AnimationDriver { + matchesElement(_element: any, _selector: string): boolean { + traceLog( + `NativeScriptAnimationDriver.matchesElement ` + + `element: ${_element}, selector: ${_selector}` + ); + + // this method is never called since ng 4.2.5 + throw new Error("Method not implemented."); + } + + containsElement(elm1: NgView, elm2: NgView): boolean { + traceLog( + `NativeScriptAnimationDriver.containsElement ` + + `element1: ${elm1}, element2: ${elm2}` + ); + + let found = false; + elm1.eachChild(child => { + if (child === elm2) { + found = true; + } + + return !found; + }); + + return found; + } + + // traverse children and check if they have the provided class + query(element: any, selector: string, multi: boolean): any[] { + traceLog( + `NativeScriptAnimationDriver.query` + + `element: ${element}, selector: ${selector} ` + + `multi: ${multi}` + ); + + let results = []; + element.eachChild(child => { + if (child[selector]) { + results.push(child); + + return !multi; + } + + return false; + }); + + return results; + } + computeStyle(element: NgView, prop: string): string { + traceLog( + `NativeScriptAnimationDriver.computeStyle ` + + `element: ${element}, prop: ${prop}` + ); + return element.style[`css-${prop}`]; } @@ -26,6 +73,13 @@ 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); } diff --git a/nativescript-angular/animations/animation-player.ts b/nativescript-angular/animations/animation-player.ts index c8f69bf00..935e78264 100644 --- a/nativescript-angular/animations/animation-player.ts +++ b/nativescript-angular/animations/animation-player.ts @@ -4,6 +4,7 @@ import { KeyframeAnimation } 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; @@ -40,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; } @@ -64,17 +67,23 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { } 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 { + traceLog(`NativeScriptAnimationPlayer.destroy`); + this.reset(); this.onFinish(); } @@ -88,10 +97,14 @@ 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() { + traceLog(`NativeScriptAnimationPlayer.onFinish`); + if (this._finished) { return; } From 6a2c160397ce06a7b9704575ce5c7974985a5383 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 6 Jul 2017 15:10:00 +0300 Subject: [PATCH 07/28] refactor: use eachDescendant instead of eachChild for containsElement --- nativescript-angular/animations/animation-driver.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts index f878ede05..617562e21 100644 --- a/nativescript-angular/animations/animation-driver.ts +++ b/nativescript-angular/animations/animation-driver.ts @@ -1,5 +1,6 @@ import { AnimationPlayer } from "@angular/animations"; import { AnimationDriver } from "@angular/animations/browser"; +import { eachDescendant } from "tns-core-modules/ui/core/view"; import { NativeScriptAnimationPlayer } from "./animation-player"; import { Keyframe } from "./utils"; @@ -8,12 +9,7 @@ import { animationsLog as traceLog } from "../trace"; export class NativeScriptAnimationDriver implements AnimationDriver { matchesElement(_element: any, _selector: string): boolean { - traceLog( - `NativeScriptAnimationDriver.matchesElement ` + - `element: ${_element}, selector: ${_selector}` - ); - - // this method is never called since ng 4.2.5 + // this method is never called since NG 4.2.5 throw new Error("Method not implemented."); } @@ -24,7 +20,7 @@ export class NativeScriptAnimationDriver implements AnimationDriver { ); let found = false; - elm1.eachChild(child => { + eachDescendant(elm1, child => { if (child === elm2) { found = true; } @@ -38,7 +34,7 @@ export class NativeScriptAnimationDriver implements AnimationDriver { // traverse children and check if they have the provided class query(element: any, selector: string, multi: boolean): any[] { traceLog( - `NativeScriptAnimationDriver.query` + + `NativeScriptAnimationDriver.query ` + `element: ${element}, selector: ${selector} ` + `multi: ${multi}` ); From 8765313b77c879eb6f34ea0545cc1fd21d2e681e Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 6 Jul 2017 17:54:30 +0300 Subject: [PATCH 08/28] refactor: implement NSTransitionEngine and NativeScriptAnimationEngine --- .../animations/animation-engine.ts | 16 + nativescript-angular/animations/dom-utils.ts | 77 --- .../animations/transition-animation-engine.ts | 496 ++++++++++++++++++ 3 files changed, 512 insertions(+), 77 deletions(-) create mode 100644 nativescript-angular/animations/animation-engine.ts delete mode 100644 nativescript-angular/animations/dom-utils.ts create mode 100644 nativescript-angular/animations/transition-animation-engine.ts diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts new file mode 100644 index 000000000..5d69ba55d --- /dev/null +++ b/nativescript-angular/animations/animation-engine.ts @@ -0,0 +1,16 @@ +import { + AnimationDriver, + ɵAnimationEngine as AnimationEngine, +} from "@angular/animations/browser"; +import { + AnimationStyleNormalizer +} from "@angular/animations/browser/src/dsl/style_normalization/animation_style_normalizer"; + +import { NSTransitionAnimationEngine } from "./transition-animation-engine"; + +export class NativeScriptAnimationEngine extends AnimationEngine { + constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) { + super(driver, normalizer); + (this)._transitionEngine = new NSTransitionAnimationEngine(driver, normalizer); + } +} 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/transition-animation-engine.ts b/nativescript-angular/animations/transition-animation-engine.ts new file mode 100644 index 000000000..1a629264d --- /dev/null +++ b/nativescript-angular/animations/transition-animation-engine.ts @@ -0,0 +1,496 @@ +import { + TransitionAnimationEngine, + TransitionAnimationPlayer, + QueuedTransition, + ElementAnimationState, + REMOVAL_FLAG, +} from "@angular/animations/browser/src/render/transition_animation_engine"; +import { AUTO_STYLE, ɵPRE_STYLE as PRE_STYLE, AnimationPlayer, ɵStyleData } from "@angular/animations"; +import { AnimationDriver } from "@angular/animations/browser"; + +import { ElementInstructionMap } from "@angular/animations/browser/src/dsl/element_instruction_map"; +import { AnimationTransitionInstruction } from "@angular/animations/browser/src/dsl/animation_transition_instruction"; +import { + ENTER_CLASSNAME, + LEAVE_CLASSNAME, + NG_ANIMATING_SELECTOR, + setStyles, +} from "@angular/animations/browser/src/util"; +import { getOrSetAsInMap, optimizeGroupPlayer } from "@angular/animations/browser/src/render/shared"; +import { unsetValue } from "tns-core-modules/ui/core/view"; + +import { NgView } from "../element-registry"; + +const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = { + namespaceId: "", + setForRemoval: null, + hasAnimation: false, + removedBeforeQueried: true +}; + +function eraseStylesOverride(element: NgView, styles: ɵStyleData) { + if (element["style"]) { + Object.keys(styles).forEach(prop => { + element.style[prop] = unsetValue; + }); + } +} + +// 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(() => 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( + // 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(() => 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; + + // 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 => { + 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 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)); +} From b8034d8c75e2e00400467e4f0e3d8ddb2f0e4cf9 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 6 Jul 2017 17:55:49 +0300 Subject: [PATCH 09/28] refactor: use NativeScriptAnimationEngine in Animations module --- nativescript-angular/animations.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nativescript-angular/animations.ts b/nativescript-angular/animations.ts index 0e5d9111e..7b5f2d2f3 100644 --- a/nativescript-angular/animations.ts +++ b/nativescript-angular/animations.ts @@ -4,7 +4,6 @@ import { AnimationBuilder } from "@angular/animations"; import { AnimationDriver, - ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, } from "@angular/animations/browser"; @@ -14,12 +13,13 @@ import { ɵBrowserAnimationBuilder as BrowserAnimationBuilder, } from "@angular/platform-browser/animations"; +import { NativeScriptAnimationEngine } from "./animations/animation-engine"; import { NativeScriptAnimationDriver } from "./animations/animation-driver"; import { NativeScriptModule } from "./nativescript.module"; import { NativeScriptRendererFactory } from "./renderer"; @Injectable() -export class InjectableAnimationEngine extends AnimationEngine { +export class InjectableAnimationEngine extends NativeScriptAnimationEngine { constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) { super(driver, normalizer); } @@ -30,7 +30,7 @@ export function instantiateSupportedAnimationDriver() { } export function instantiateRendererFactory( - renderer: NativeScriptRendererFactory, engine: AnimationEngine, zone: NgZone) { + renderer: NativeScriptRendererFactory, engine: NativeScriptAnimationEngine, zone: NgZone) { return new AnimationRendererFactory(renderer, engine, zone); } @@ -42,11 +42,11 @@ export const NATIVESCRIPT_ANIMATIONS_PROVIDERS: Provider[] = [ {provide: AnimationBuilder, useClass: BrowserAnimationBuilder}, {provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver}, {provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer}, - {provide: AnimationEngine, useClass: InjectableAnimationEngine}, + {provide: NativeScriptAnimationEngine, useClass: InjectableAnimationEngine}, { provide: RendererFactory2, useFactory: instantiateRendererFactory, - deps: [NativeScriptRendererFactory, AnimationEngine, NgZone] + deps: [NativeScriptRendererFactory, NativeScriptAnimationEngine, NgZone] } ]; From 4548af3abfdf22ec3575b5cae0e7ec3a3094683a Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 7 Jul 2017 14:31:55 +0300 Subject: [PATCH 10/28] refactor: embed non-exported code from angular --- .../dsl/element_instruction_map.ts | 36 + .../private-imports/render/shared.ts | 164 ++ .../render/transition_animation_engine.ts | 1420 +++++++++++++++++ .../animations/private-imports/util.ts | 221 +++ .../animations/transition-animation-engine.ts | 10 +- 5 files changed, 1846 insertions(+), 5 deletions(-) create mode 100644 nativescript-angular/animations/private-imports/dsl/element_instruction_map.ts create mode 100644 nativescript-angular/animations/private-imports/render/shared.ts create mode 100644 nativescript-angular/animations/private-imports/render/transition_animation_engine.ts create mode 100644 nativescript-angular/animations/private-imports/util.ts 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..e251c68e5 --- /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(); } +} 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..ba7c86bad --- /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; 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..f4d32070f --- /dev/null +++ b/nativescript-angular/animations/private-imports/render/transition_animation_engine.ts @@ -0,0 +1,1420 @@ +/** + * @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 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 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, NG_ANIMATING_CLASSNAME); + } + + player.onDone(() => { + removeClass(element, NG_ANIMATING_CLASSNAME); + + 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 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); } + + 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); + } + } + + 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 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)) { + 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..dd87a558f --- /dev/null +++ b/nativescript-angular/animations/private-imports/util.ts @@ -0,0 +1,221 @@ +/** + * @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; +} diff --git a/nativescript-angular/animations/transition-animation-engine.ts b/nativescript-angular/animations/transition-animation-engine.ts index 1a629264d..91cb0a8de 100644 --- a/nativescript-angular/animations/transition-animation-engine.ts +++ b/nativescript-angular/animations/transition-animation-engine.ts @@ -4,19 +4,19 @@ import { QueuedTransition, ElementAnimationState, REMOVAL_FLAG, -} from "@angular/animations/browser/src/render/transition_animation_engine"; +} from "./private-imports/render/transition_animation_engine"; import { AUTO_STYLE, ɵPRE_STYLE as PRE_STYLE, AnimationPlayer, ɵStyleData } from "@angular/animations"; import { AnimationDriver } from "@angular/animations/browser"; -import { ElementInstructionMap } from "@angular/animations/browser/src/dsl/element_instruction_map"; +import { ElementInstructionMap } from "./private-imports/dsl/element_instruction_map"; import { AnimationTransitionInstruction } from "@angular/animations/browser/src/dsl/animation_transition_instruction"; import { ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_SELECTOR, setStyles, -} from "@angular/animations/browser/src/util"; -import { getOrSetAsInMap, optimizeGroupPlayer } from "@angular/animations/browser/src/render/shared"; +} from "./private-imports/util"; +import { getOrSetAsInMap, optimizeGroupPlayer } from "./private-imports/render/shared"; import { unsetValue } from "tns-core-modules/ui/core/view"; import { NgView } from "../element-registry"; @@ -121,7 +121,7 @@ export class NSTransitionAnimationEngine extends TransitionAnimationEngine { const element = entry.element; - // the below check is skipped, because it"s + // the below check is skipped, because it's // irrelevant in the NativeScript context // if (!bodyNode || !this.driver.containsElement(bodyNode, element)) { // player.destroy(); From 71040315769b090a03cd53eb6bf8e9b1e6157ada Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 7 Jul 2017 14:45:24 +0300 Subject: [PATCH 11/28] refactor(ng-sample): animation examples style --- ng-sample/app/app.ts | 15 ++++-- .../animation/animation-enter-leave-test.ts | 20 +++++-- .../animation/animation-keyframes-test.ts | 9 +++- .../animation/animation-ngclass-test.ts | 3 +- .../animation/animation-states-multi-test.ts | 54 +++++++++++++++++++ .../animation/animation-states-test.ts | 18 +++++-- 6 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 ng-sample/app/examples/animation/animation-states-multi-test.ts diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 27679de61..13a4dd786 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(); @@ -128,10 +135,10 @@ 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(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..2633cd66a 100644 --- a/ng-sample/app/examples/animation/animation-keyframes-test.ts +++ b/ng-sample/app/examples/animation/animation-keyframes-test.ts @@ -1,4 +1,11 @@ -import {Component, trigger, style, animate, state, transition, keyframes } from "@angular/core"; +import { Component } from '@angular/core'; +import { + trigger, + state, + style, + animate, + transition +} 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..a8d36709f 100644 --- a/ng-sample/app/examples/animation/animation-states-test.ts +++ b/ng-sample/app/examples/animation/animation-states-test.ts @@ -1,22 +1,30 @@ -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") ]), + + transition("* => active", [ animate("600ms ease-out") ]), + transition("* => inactive", [ animate("600ms ease-out") ]), ]) ] }) export class AnimationStatesTest { - isOn = false; onTap() { From 6cb2bac08dd448eb31988d6a485a356a107066b1 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 7 Jul 2017 14:50:07 +0300 Subject: [PATCH 12/28] chore: update deps --- nativescript-angular/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json index 17b8dfd16..0be3f48e5 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -67,8 +67,8 @@ "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" } } From bf370afc5329deec0dd88a02a6fa3dd6288b9fe6 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 7 Jul 2017 14:51:36 +0300 Subject: [PATCH 13/28] style: remove no-shadowed-variable rule --- nativescript-angular/tslint.json | 2 +- ng-sample/app/examples/animation/animation-keyframes-test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nativescript-angular/tslint.json b/nativescript-angular/tslint.json index b9630d813..0a64add5d 100644 --- a/nativescript-angular/tslint.json +++ b/nativescript-angular/tslint.json @@ -57,7 +57,7 @@ "no-empty": false, "no-eval": true, "no-null-keyword": false, - "no-shadowed-variable": true, + "no-shadowed-variable": false, "no-string-literal": false, "no-switch-case-fall-through": true, "no-unused-expression": true, diff --git a/ng-sample/app/examples/animation/animation-keyframes-test.ts b/ng-sample/app/examples/animation/animation-keyframes-test.ts index 2633cd66a..005542546 100644 --- a/ng-sample/app/examples/animation/animation-keyframes-test.ts +++ b/ng-sample/app/examples/animation/animation-keyframes-test.ts @@ -4,7 +4,8 @@ import { state, style, animate, - transition + transition, + keyframes, } from '@angular/animations'; @Component({ From 2f61dacb8fa76ff187c71ba5561f763e4b337eac Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Sat, 8 Jul 2017 10:49:19 +0300 Subject: [PATCH 14/28] refactor: always set camelCase props for animations --- .../animations/animation-driver.ts | 8 +++- .../animations/transition-animation-engine.ts | 40 +++++++++++++------ nativescript-angular/animations/utils.ts | 5 +++ 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts index 617562e21..90bd7d399 100644 --- a/nativescript-angular/animations/animation-driver.ts +++ b/nativescript-angular/animations/animation-driver.ts @@ -3,7 +3,10 @@ import { AnimationDriver } from "@angular/animations/browser"; import { eachDescendant } from "tns-core-modules/ui/core/view"; import { NativeScriptAnimationPlayer } from "./animation-player"; -import { Keyframe } from "./utils"; +import { + Keyframe, + dashCaseToCamelCase, +} from "./utils"; import { NgView } from "../element-registry"; import { animationsLog as traceLog } from "../trace"; @@ -59,7 +62,8 @@ export class NativeScriptAnimationDriver implements AnimationDriver { `element: ${element}, prop: ${prop}` ); - return element.style[`css-${prop}`]; + const camelCaseProp = dashCaseToCamelCase(prop); + return element.style[camelCaseProp]; } animate( diff --git a/nativescript-angular/animations/transition-animation-engine.ts b/nativescript-angular/animations/transition-animation-engine.ts index 91cb0a8de..26ff339d3 100644 --- a/nativescript-angular/animations/transition-animation-engine.ts +++ b/nativescript-angular/animations/transition-animation-engine.ts @@ -1,3 +1,8 @@ +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, @@ -5,20 +10,15 @@ import { ElementAnimationState, REMOVAL_FLAG, } from "./private-imports/render/transition_animation_engine"; -import { AUTO_STYLE, ɵPRE_STYLE as PRE_STYLE, AnimationPlayer, ɵStyleData } from "@angular/animations"; -import { AnimationDriver } from "@angular/animations/browser"; - import { ElementInstructionMap } from "./private-imports/dsl/element_instruction_map"; -import { AnimationTransitionInstruction } from "@angular/animations/browser/src/dsl/animation_transition_instruction"; +import { getOrSetAsInMap, optimizeGroupPlayer } from "./private-imports/render/shared"; import { ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_SELECTOR, - setStyles, } from "./private-imports/util"; -import { getOrSetAsInMap, optimizeGroupPlayer } from "./private-imports/render/shared"; -import { unsetValue } from "tns-core-modules/ui/core/view"; +import { dashCaseToCamelCase } from "./utils"; import { NgView } from "../element-registry"; const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = { @@ -29,11 +29,25 @@ const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = { }; function eraseStylesOverride(element: NgView, styles: ɵStyleData) { - if (element["style"]) { - Object.keys(styles).forEach(prop => { - element.style[prop] = unsetValue; - }); + 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 => { + const camelCaseProp = dashCaseToCamelCase(prop); + element.style[camelCaseProp] = styles[prop]; + }) } // extending Angular's TransitionAnimationEngine @@ -137,7 +151,7 @@ export class NSTransitionAnimationEngine extends TransitionAnimationEngine { // an animation and cancel the previously running animations. if (entry.isFallbackTransition) { player.onStart(() => eraseStylesOverride(element, instruction.fromStyles)); - player.onDestroy(() => setStyles(element, instruction.toStyles)); + player.onDestroy(() => setStylesOverride(element, instruction.toStyles)); skippedPlayers.push(player); return; } @@ -261,7 +275,7 @@ export class NSTransitionAnimationEngine extends TransitionAnimationEngine { } } else { eraseStylesOverride(element, instruction.fromStyles); - player.onDestroy(() => setStyles(element, instruction.toStyles)); + player.onDestroy(() => setStylesOverride(element, instruction.toStyles)); subPlayers.push(player); } }); 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, From ceeb86920729f4889652392505c7f2f92dd20cc0 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Sat, 8 Jul 2017 10:50:51 +0300 Subject: [PATCH 15/28] refactor: add multi-states animation example --- nativescript-angular/animations/transition-animation-engine.ts | 2 +- ng-sample/app/app.ts | 2 ++ ng-sample/app/examples/animation/animation-states-test.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nativescript-angular/animations/transition-animation-engine.ts b/nativescript-angular/animations/transition-animation-engine.ts index 26ff339d3..a3f71f03b 100644 --- a/nativescript-angular/animations/transition-animation-engine.ts +++ b/nativescript-angular/animations/transition-animation-engine.ts @@ -47,7 +47,7 @@ function setStylesOverride(element: NgView, styles: ɵStyleData) { Object.keys(styles).forEach(prop => { const camelCaseProp = dashCaseToCamelCase(prop); element.style[camelCaseProp] = styles[prop]; - }) + }); } // extending Angular's TransitionAnimationEngine diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 13a4dd786..2b0c85ed1 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -49,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: [ @@ -139,6 +140,7 @@ const customPageFactoryProvider = { // animations 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-states-test.ts b/ng-sample/app/examples/animation/animation-states-test.ts index a8d36709f..effe5e3d1 100644 --- a/ng-sample/app/examples/animation/animation-states-test.ts +++ b/ng-sample/app/examples/animation/animation-states-test.ts @@ -17,7 +17,7 @@ import { trigger("state", [ state("inactive", style({ "background-color": "red" })), - state("active", style({ "background-color": "green" })), + state("active", style({ "backgroundColor": "green" })), transition("* => active", [ animate("600ms ease-out") ]), transition("* => inactive", [ animate("600ms ease-out") ]), From b54a8a086cb2595cea75faace938300b16962bc0 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Sat, 8 Jul 2017 12:43:58 +0300 Subject: [PATCH 16/28] refactor: enable * states --- .../animations/transition-animation-engine.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/nativescript-angular/animations/transition-animation-engine.ts b/nativescript-angular/animations/transition-animation-engine.ts index a3f71f03b..bb411d5b9 100644 --- a/nativescript-angular/animations/transition-animation-engine.ts +++ b/nativescript-angular/animations/transition-animation-engine.ts @@ -21,13 +21,6 @@ import { import { dashCaseToCamelCase } from "./utils"; import { NgView } from "../element-registry"; -const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = { - namespaceId: "", - setForRemoval: null, - hasAnimation: false, - removedBeforeQueried: true -}; - function eraseStylesOverride(element: NgView, styles: ɵStyleData) { if (!element.style) { return; @@ -46,7 +39,7 @@ function setStylesOverride(element: NgView, styles: ɵStyleData) { Object.keys(styles).forEach(prop => { const camelCaseProp = dashCaseToCamelCase(prop); - element.style[camelCaseProp] = styles[prop]; + element.style[camelCaseProp] = styles[camelCaseProp]; }); } @@ -428,13 +421,7 @@ function cloakAndComputeStyles( 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; - } + styles[prop] = driver.computeStyle(element, prop, defaultStyle); }); valuesMap.set(element, styles); }); From 40f5e15797de4cfc078d28eec5964b44d7740a4a Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Sat, 8 Jul 2017 12:57:16 +0300 Subject: [PATCH 17/28] refactor: update private imports from ng animations --- .../dsl/element_instruction_map.ts | 2 +- .../private-imports/render/shared.ts | 12 ++-- .../render/transition_animation_engine.ts | 66 ++++++++++++++++--- .../animations/private-imports/util.ts | 3 +- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/nativescript-angular/animations/private-imports/dsl/element_instruction_map.ts b/nativescript-angular/animations/private-imports/dsl/element_instruction_map.ts index e251c68e5..407db074f 100644 --- a/nativescript-angular/animations/private-imports/dsl/element_instruction_map.ts +++ b/nativescript-angular/animations/private-imports/dsl/element_instruction_map.ts @@ -33,4 +33,4 @@ export class ElementInstructionMap { 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 index ba7c86bad..efbdc0742 100644 --- a/nativescript-angular/animations/private-imports/render/shared.ts +++ b/nativescript-angular/animations/private-imports/render/shared.ts @@ -5,8 +5,8 @@ * 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"; @@ -24,7 +24,7 @@ export function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer } export function normalizeKeyframes( - _driver: AnimationDriver, normalizer: AnimationStyleNormalizer, _element: any, + driver: AnimationDriver, normalizer: AnimationStyleNormalizer, element: any, keyframes: ɵStyleData[], preStyles: ɵStyleData = {}, postStyles: ɵStyleData = {}): ɵStyleData[] { const errors: string[] = []; @@ -122,11 +122,11 @@ export function parseTimelineCommand(command: string): [string, string] { 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) => +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) => { + (element: any, selector: string, multi: boolean) => { return []; }; @@ -161,4 +161,4 @@ if (typeof Element != 'undefined') { export const matchesElement = _matches; export const containsElement = _contains; -export const invokeQuery = _query; +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 index f4d32070f..bea738936 100644 --- a/nativescript-angular/animations/private-imports/render/transition_animation_engine.ts +++ b/nativescript-angular/animations/private-imports/render/transition_animation_engine.ts @@ -5,7 +5,6 @@ * 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'; @@ -23,6 +22,11 @@ import {AnimationDriver} from '@angular/animations/browser/src/render/animation_ 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: '', @@ -205,6 +209,16 @@ export class AnimationTransitionNamespace { 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 => { @@ -230,12 +244,11 @@ export class AnimationTransitionNamespace { {element, triggerName, transition, fromState, toState, player, isFallbackTransition}); if (!isFallbackTransition) { - addClass(element, NG_ANIMATING_CLASSNAME); + addClass(element, QUEUED_CLASSNAME); + player.onStart(() => { removeClass(element, QUEUED_CLASSNAME); }); } player.onDone(() => { - removeClass(element, NG_ANIMATING_CLASSNAME); - let index = this.players.indexOf(player); if (index >= 0) { this.players.splice(index, 1); @@ -259,7 +272,7 @@ export class AnimationTransitionNamespace { deregister(name: string) { delete this._triggers[name]; - this._engine.statesByElement.forEach((stateMap, _element) => { delete stateMap[name]; }); + this._engine.statesByElement.forEach((stateMap, element) => { delete stateMap[name]; }); this._elementListeners.forEach((listeners, element) => { this._elementListeners.set( @@ -392,7 +405,7 @@ export class AnimationTransitionNamespace { } } - insertNode(element: any, _parent: any): void { addClass(element, this._hostClassName); } + insertNode(element: any, parent: any): void { addClass(element, this._hostClassName); } drainQueuedTransitions(microtaskId: number): QueueInstruction[] { const instructions: QueueInstruction[] = []; @@ -464,6 +477,8 @@ export class TransitionAnimationEngine { public playersByElement = new Map(); public playersByQueriedElement = new Map(); public statesByElement = new Map(); + public disabledNodes = new Set(); + public totalAnimations = 0; public totalQueuedPlayers = 0; @@ -477,7 +492,7 @@ export class TransitionAnimationEngine { public collectedLeaveElements: any[] = []; // this method is designed to be overridden by the code that uses this engine - public onRemovalComplete = (_element: any, _context: any) => {}; + public onRemovalComplete = (element: any, context: any) => {}; _onRemovalComplete(element: any, context: any) { this.onRemovalComplete(element, context); } @@ -605,6 +620,18 @@ export class TransitionAnimationEngine { 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); @@ -702,6 +729,14 @@ export class TransitionAnimationEngine { } 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) { @@ -759,6 +794,14 @@ export class TransitionAnimationEngine { 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)) : @@ -808,7 +851,7 @@ export class TransitionAnimationEngine { } const instruction = this._buildInstruction(entry, subTimelines) !; - if ((instruction).errors && (instruction).errors.length) { + if (instruction.errors && instruction.errors.length) { erroneousTransitions.push(instruction); return; } @@ -864,7 +907,7 @@ export class TransitionAnimationEngine { 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`; }); + instruction.errors !.forEach(error => { msg += `- ${error}\n`; }); }); allPlayers.forEach(player => player.destroy()); @@ -919,6 +962,11 @@ export class TransitionAnimationEngine { // 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); diff --git a/nativescript-angular/animations/private-imports/util.ts b/nativescript-angular/animations/private-imports/util.ts index dd87a558f..f7f7c69b8 100644 --- a/nativescript-angular/animations/private-imports/util.ts +++ b/nativescript-angular/animations/private-imports/util.ts @@ -5,7 +5,6 @@ * 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'; @@ -218,4 +217,4 @@ export function dashCaseToCamelCase(input: string): string { export function allowPreviousPlayerStylesMerge(duration: number, delay: number) { return duration === 0 || delay === 0; -} +} \ No newline at end of file From 68f68946cc16b21d703bd546b5e973955891a012 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Sat, 8 Jul 2017 12:57:33 +0300 Subject: [PATCH 18/28] refactor: move no-unused-variable from tsconfig to tslint --- nativescript-angular/package.json | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json index 0be3f48e5..0f84b8345 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -43,27 +43,27 @@ "reflect-metadata": "^0.1.8" }, "peerDependencies": { - "@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/router": "~4.2.4", + "@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.2.4", - "@angular/common": "~4.2.4", - "@angular/compiler": "~4.2.4", - "@angular/compiler-cli": "~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/router": "~4.2.4", + "@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", From cfc1371d2bf1b8bdeaf72c456a0d22d4ef798ee1 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Sat, 8 Jul 2017 12:57:59 +0300 Subject: [PATCH 19/28] refactor: update states animations example --- .../app/examples/animation/animation-states-test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ng-sample/app/examples/animation/animation-states-test.ts b/ng-sample/app/examples/animation/animation-states-test.ts index effe5e3d1..15cd88f20 100644 --- a/ng-sample/app/examples/animation/animation-states-test.ts +++ b/ng-sample/app/examples/animation/animation-states-test.ts @@ -16,11 +16,18 @@ import { animations: [ trigger("state", [ - state("inactive", style({ "background-color": "red" })), - state("active", style({ "backgroundColor": "green" })), + 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") ]), + ]) ] }) From 4dc654447664c9512fa6df7e8b2084ecbb1447a4 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 14 Jul 2017 14:35:21 +0300 Subject: [PATCH 20/28] fix(animations): override transitionEngine element removal --- nativescript-angular/animations/animation-engine.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts index 5d69ba55d..c181d1561 100644 --- a/nativescript-angular/animations/animation-engine.ts +++ b/nativescript-angular/animations/animation-engine.ts @@ -12,5 +12,13 @@ export class NativeScriptAnimationEngine extends AnimationEngine { constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) { super(driver, normalizer); (this)._transitionEngine = new NSTransitionAnimationEngine(driver, normalizer); + + (this)._transitionEngine.onRemovalComplete = (element, delegate) => { + const parent = delegate && delegate.parentNode(element); + if (parent) { + delegate.removeChild(parent, element); + } + }; + } } From 80712572fbaae6967156d30a64100a8fe0322351 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 14 Jul 2017 14:55:54 +0300 Subject: [PATCH 21/28] lint: remove unused variables --- nativescript-angular/router/page-router-outlet.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts index cda2ca491..9f7541ec9 100644 --- a/nativescript-angular/router/page-router-outlet.ts +++ b/nativescript-angular/router/page-router-outlet.ts @@ -103,9 +103,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l private detachedLoaderFactory: ComponentFactory; private itemsToDestroy: CacheItem[] = []; - private currentActivatedComp: ComponentRef; - private currentActivatedRoute: ActivatedRoute; - private name: string; private viewUtil: ViewUtil; From d89f8d818e2cc82cc2d316e87cc3e154577bd782 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 14 Jul 2017 16:23:33 +0300 Subject: [PATCH 22/28] fix(animations): ignore setting '*' property for styles --- nativescript-angular/animations/animation-engine.ts | 1 - .../animations/transition-animation-engine.ts | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts index c181d1561..7c8f874fc 100644 --- a/nativescript-angular/animations/animation-engine.ts +++ b/nativescript-angular/animations/animation-engine.ts @@ -19,6 +19,5 @@ export class NativeScriptAnimationEngine extends AnimationEngine { delegate.removeChild(parent, element); } }; - } } diff --git a/nativescript-angular/animations/transition-animation-engine.ts b/nativescript-angular/animations/transition-animation-engine.ts index bb411d5b9..e31bbea9d 100644 --- a/nativescript-angular/animations/transition-animation-engine.ts +++ b/nativescript-angular/animations/transition-animation-engine.ts @@ -38,6 +38,10 @@ function setStylesOverride(element: NgView, styles: ɵStyleData) { } Object.keys(styles).forEach(prop => { + if (styles[prop] === "*") { + return; + } + const camelCaseProp = dashCaseToCamelCase(prop); element.style[camelCaseProp] = styles[camelCaseProp]; }); From 6b6208bd0a7c764c35b94b62fa093c9137316ad9 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 20 Jul 2017 18:22:50 +0300 Subject: [PATCH 23/28] feat(animations): implement query selector for ani driver - using createSelector from core modules - custom matching for dynamically added classes - requires next version of tns-core-modules --- .../animations/animation-driver.ts | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts index 90bd7d399..8baf030e0 100644 --- a/nativescript-angular/animations/animation-driver.ts +++ b/nativescript-angular/animations/animation-driver.ts @@ -10,6 +10,8 @@ import { import { NgView } from "../element-registry"; import { animationsLog as traceLog } from "../trace"; +import { createSelector, SelectorCore } from "tns-core-modules/ui/styling/css-selector"; + export class NativeScriptAnimationDriver implements AnimationDriver { matchesElement(_element: any, _selector: string): boolean { // this method is never called since NG 4.2.5 @@ -34,28 +36,50 @@ export class NativeScriptAnimationDriver implements AnimationDriver { return found; } - // traverse children and check if they have the provided class - query(element: any, selector: string, multi: boolean): any[] { + query(element: NgView, selector: string, multi: boolean): NgView[] { traceLog( `NativeScriptAnimationDriver.query ` + `element: ${element}, selector: ${selector} ` + `multi: ${multi}` ); + const selectors = selector.split(",").map(s => s.trim()); + + const nsSelectors: SelectorCore[] = selectors.map(createSelector); + const classSelectors = selectors + .filter(s => s.startsWith(".")) + .map(s => s.substring(1)); + + return this.visitDescendants(element, nsSelectors, classSelectors, multi); + } + + private visitDescendants( + element: NgView, + nsSelectors: SelectorCore[], + classSelectors: string[], + multi: boolean): NgView[] { + let results = []; - element.eachChild(child => { - if (child[selector]) { + eachDescendant(element, child => { + if (nsSelectors.some(s => s.match(child)) || + classSelectors.some(s => this.hasClass(child, s))) { results.push(child); - - return !multi; + return multi; } - return false; + return true; }); return results; } + // 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: any, cls: string) { + return element["$$classes"][cls]; + } + computeStyle(element: NgView, prop: string): string { traceLog( `NativeScriptAnimationDriver.computeStyle ` + From f646bd4762b02dfc711a132370dfefc903b03c86 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 20 Jul 2017 18:27:14 +0300 Subject: [PATCH 24/28] fix(animations): skip `InvisibleNode`s when querying for elements --- nativescript-angular/animations/animation-driver.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts index 8baf030e0..62240a3a3 100644 --- a/nativescript-angular/animations/animation-driver.ts +++ b/nativescript-angular/animations/animation-driver.ts @@ -7,7 +7,7 @@ import { Keyframe, dashCaseToCamelCase, } from "./utils"; -import { NgView } from "../element-registry"; +import { NgView, InvisibleNode } from "../element-registry"; import { animationsLog as traceLog } from "../trace"; import { createSelector, SelectorCore } from "tns-core-modules/ui/styling/css-selector"; @@ -61,8 +61,13 @@ export class NativeScriptAnimationDriver implements AnimationDriver { let results = []; eachDescendant(element, child => { + if (child instanceof InvisibleNode) { + return true; + } + if (nsSelectors.some(s => s.match(child)) || classSelectors.some(s => this.hasClass(child, s))) { + results.push(child); return multi; } From 9bebc2a0f4f60eecf8e62b477fa185542195145c Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 21 Jul 2017 17:36:20 +0300 Subject: [PATCH 25/28] fix(animations): implement containsElement method for driver --- .../animations/animation-driver.ts | 177 +++++++++++++----- 1 file changed, 126 insertions(+), 51 deletions(-) diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts index 62240a3a3..236012db7 100644 --- a/nativescript-angular/animations/animation-driver.ts +++ b/nativescript-angular/animations/animation-driver.ts @@ -12,77 +12,96 @@ import { animationsLog as traceLog } from "../trace"; import { createSelector, SelectorCore } from "tns-core-modules/ui/styling/css-selector"; -export class NativeScriptAnimationDriver implements AnimationDriver { - matchesElement(_element: any, _selector: string): boolean { - // this method is never called since NG 4.2.5 - throw new Error("Method not implemented."); - } +interface ViewMatchResult { + found: boolean; +} - containsElement(elm1: NgView, elm2: NgView): boolean { - traceLog( - `NativeScriptAnimationDriver.containsElement ` + - `element1: ${elm1}, element2: ${elm2}` - ); +interface ViewMatchParams { + originalView: NgView; +} - let found = false; - eachDescendant(elm1, child => { - if (child === elm2) { - found = true; - } +interface QueryParams { + selector: Selector; + multi: boolean; +} + +interface QueryResult { + matches: NgView[]; +} - return !found; - }); +class Selector { + private nsSelectors: SelectorCore[]; + private classSelectors: string[]; - return found; + constructor(rawSelector: string) { + this.parse(rawSelector); } - query(element: NgView, selector: string, multi: boolean): NgView[] { - traceLog( - `NativeScriptAnimationDriver.query ` + - `element: ${element}, selector: ${selector} ` + - `multi: ${multi}` - ); + match(element: NgView): boolean { + return this.nsSelectorMatch(element) || this.classSelectorsMatch(element); + } - const selectors = selector.split(",").map(s => s.trim()); + private parse(rawSelector: string) { + const selectors = rawSelector.split(",").map(s => s.trim()); - const nsSelectors: SelectorCore[] = selectors.map(createSelector); - const classSelectors = selectors + this.nsSelectors = selectors.map(createSelector); + this.classSelectors = selectors .filter(s => s.startsWith(".")) .map(s => s.substring(1)); + } - return this.visitDescendants(element, nsSelectors, classSelectors, multi); + private nsSelectorMatch(element: NgView) { + return this.nsSelectors.some(s => s.match(element)); } - private visitDescendants( - element: NgView, - nsSelectors: SelectorCore[], - classSelectors: string[], - multi: boolean): NgView[] { + private classSelectorsMatch(element: NgView) { + return this.classSelectors.some(s => this.hasClass(element, s)); + } - let results = []; - eachDescendant(element, child => { - if (child instanceof InvisibleNode) { - return true; - } + // 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]; + } +} - if (nsSelectors.some(s => s.match(child)) || - classSelectors.some(s => this.hasClass(child, s))) { +export class NativeScriptAnimationDriver implements AnimationDriver { + matchesElement(element: NgView, rawSelector: string): boolean { + traceLog( + `NativeScriptAnimationDriver.matchesElement ` + + `element: ${element}, selector: ${rawSelector}` + ); - results.push(child); - return multi; - } + const selector = this.makeSelector(rawSelector); + return selector.match(element); + } - return true; - }); - return results; + 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; } - // 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: any, cls: string) { - return element["$$classes"][cls]; + 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 { @@ -112,4 +131,60 @@ export class NativeScriptAnimationDriver implements AnimationDriver { 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; } From b3cd8876f13642f90c484ff564531f3b3fca5a9b Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Sat, 22 Jul 2017 12:53:57 +0300 Subject: [PATCH 26/28] fix(animations): implement finish method for player --- nativescript-angular/animations/animation-player.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nativescript-angular/animations/animation-player.ts b/nativescript-angular/animations/animation-player.ts index 935e78264..3e0472cdd 100644 --- a/nativescript-angular/animations/animation-player.ts +++ b/nativescript-angular/animations/animation-player.ts @@ -59,11 +59,10 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { } 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 { @@ -83,8 +82,6 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { destroy(): void { traceLog(`NativeScriptAnimationPlayer.destroy`); - - this.reset(); this.onFinish(); } From e3086455e1ed04cee907491b19af3b8f07b479f7 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Sat, 22 Jul 2017 16:44:30 +0300 Subject: [PATCH 27/28] refactor: separate NativeScriptCommonModule from NativeScriptModule All common providers, such as CommonModule (from @angular/common), Frame, Page, Device and Modals are moved from NativeScriptModule to NativeScriptCommonModule. It can be required from "nativescript-angular/common". The NativeScriptRouterModule also imports the common module now. Importing NativeScriptModule more than once causes reinstantiating of the NativeScriptRenderer, which breaks animations. BREAKING CHANGES: NativeScriptModule should be imported only in the root application module (usually named AppModule). It provides essential the Renderer, ModuleLoader and exports the ApplicationModule. All other NgModules in the app (both feature and lazy-loaded ones) should import the NativeScriptCommonModule instead. The behaviour is alligned with BrowserModule and CommonModule in web Angular apps. https://angular.io/guide/ngmodule-faq#q-browser-vs-common-module --- nativescript-angular/common.ts | 40 +++++++++++++++++++++ nativescript-angular/nativescript.module.ts | 26 +++----------- nativescript-angular/router.ts | 4 +-- 3 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 nativescript-angular/common.ts 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/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, From 848887410f591f6e1b2223d2c70c65b28a48a973 Mon Sep 17 00:00:00 2001 From: vakrilov Date: Wed, 26 Jul 2017 16:24:32 +0300 Subject: [PATCH 28/28] style(tslint): no-shadowed-variable enabled --- nativescript-angular/tslint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nativescript-angular/tslint.json b/nativescript-angular/tslint.json index 0a64add5d..b9630d813 100644 --- a/nativescript-angular/tslint.json +++ b/nativescript-angular/tslint.json @@ -57,7 +57,7 @@ "no-empty": false, "no-eval": true, "no-null-keyword": false, - "no-shadowed-variable": false, + "no-shadowed-variable": true, "no-string-literal": false, "no-switch-case-fall-through": true, "no-unused-expression": true,