From da02769d58aa2e46ce732a66e1afa073b9508a04 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 16 Mar 2017 15:42:56 +0200 Subject: [PATCH 1/9] feat(animations): introduce NativeScriptAnimationsModule Also introduce new NativeScriptAnimationsEngine that extends DomAnimationsEngine and overrides methods that work with DOM tree. BREAKING CHANGE: To use animations, you need to import the NativeScriptAnimationsModule from "nativescript-angular/animations" in your root NgModule. --- nativescript-angular/animations.ts | 51 +++ .../animations/animation-driver.ts | 31 ++ .../animations/animation-engine.ts | 206 ++++++++++++ .../animations/animation-player.ts | 306 ++++++++++++++++++ nativescript-angular/index.ts | 1 + ng-sample/app/app.ts | 22 +- 6 files changed, 608 insertions(+), 9 deletions(-) create mode 100644 nativescript-angular/animations.ts create mode 100644 nativescript-angular/animations/animation-driver.ts create mode 100644 nativescript-angular/animations/animation-engine.ts create mode 100644 nativescript-angular/animations/animation-player.ts diff --git a/nativescript-angular/animations.ts b/nativescript-angular/animations.ts new file mode 100644 index 000000000..2896645be --- /dev/null +++ b/nativescript-angular/animations.ts @@ -0,0 +1,51 @@ +import { NgModule, Injectable, NgZone, Provider, RendererFactory2 } from "@angular/core"; + +import { + AnimationDriver, + ɵAnimationEngine as AnimationEngine, + ɵAnimationStyleNormalizer as AnimationStyleNormalizer, + ɵAnimationRendererFactory as AnimationRendererFactory, + ɵg as WebAnimationsStyleNormalizer +} 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 { + constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) { + super(driver, normalizer); + } +} + +export function instantiateSupportedAnimationDriver() { + return new NativeScriptAnimationDriver(); +} + +export function instantiateRendererFactory( + renderer: NativeScriptRendererFactory, engine: AnimationEngine, zone: NgZone) { + return new AnimationRendererFactory(renderer, engine, zone); +} + +export function instanciateDefaultStyleNormalizer() { + return new WebAnimationsStyleNormalizer(); +} + +export const NATIVESCRIPT_ANIMATIONS_PROVIDERS: Provider[] = [ + {provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver}, + {provide: AnimationStyleNormalizer, useFactory: instanciateDefaultStyleNormalizer}, + {provide: AnimationEngine, useClass: InjectableAnimationEngine}, { + provide: RendererFactory2, + useFactory: instantiateRendererFactory, + deps: [NativeScriptRendererFactory, AnimationEngine, NgZone] + } +]; + +@NgModule({ + imports: [NativeScriptModule], + providers: NATIVESCRIPT_ANIMATIONS_PROVIDERS, +}) +export class NativeScriptAnimationsModule { +} diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts new file mode 100644 index 000000000..3ab6cc10f --- /dev/null +++ b/nativescript-angular/animations/animation-driver.ts @@ -0,0 +1,31 @@ +import { AnimationPlayer } from "@angular/animations"; +import { View } from "tns-core-modules/ui/core/view"; + +import { NativeScriptAnimationPlayer } from "./animation-player"; + +export abstract class AnimationDriver { + abstract animate( + element: any, + keyframes: {[key: string]: string | number}[], + duration: number, + delay: number, + easing: string + ): AnimationPlayer; +} + +export class NativeScriptAnimationDriver implements AnimationDriver { + computeStyle(element: any, prop: string): string { + const view = element; + return view.style[`css-${prop}`]; + } + + animate( + element: any, + keyframes: {[key: string]: string | number}[], + duration: number, + delay: number, + easing: string + ): AnimationPlayer { + return new NativeScriptAnimationPlayer(element, keyframes, duration, delay, easing); + } +} diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts new file mode 100644 index 000000000..6999c1a8c --- /dev/null +++ b/nativescript-angular/animations/animation-engine.ts @@ -0,0 +1,206 @@ +import { ɵDomAnimationEngine as DomAnimationEngine } from "@angular/platform-browser/animations"; +import { + AnimationEvent, + AnimationPlayer, + NoopAnimationPlayer, + ɵAnimationGroupPlayer, + ɵStyleData, +} from "@angular/animations"; + +import { unsetValue } from "tns-core-modules/ui/core/view"; + +import { NgView } from "../element-registry"; + +const MARKED_FOR_ANIMATION = "ng-animate"; + +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 => { + totalTime = Math.max(totalTime, timelineInstruction.totalTime); + return (this)._buildPlayer(element, timelineInstruction, previousPlayers); + }); + + 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)) { + 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, true); + player.onDone(() => cssClasses(element).set(MARKED_FOR_ANIMATION, false)); + } + + private _getElementAnimation(element: NgView) { + return (this)._activeElementAnimations.get(element); + } + + private _getTransitionAnimation(element: NgView) { + return (this)._activeTransitionAnimations.get(element); + } +} + +// overriden to use the default 'unsetValue' +// instead of empty string '' +function eraseStylesOverride(element: NgView, styles: ɵStyleData) { + if (element["style"]) { + Object.keys(styles).forEach(prop => { + element.style[prop] = unsetValue; + }); + } +} + +function cssClasses(element: NgView) { + if (!element.ngCssClasses) { + element.ngCssClasses = new Map(); + } + return element.ngCssClasses; +} + +function getOrSetAsInMap(map: Map, key: any, defaultValue: any) { + let value = map.get(key); + if (!value) { + map.set(key, value = defaultValue); + } + return value; +} + +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); + } + } + } +} + +function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer { + switch (players.length) { + case 0: + return new NoopAnimationPlayer(); + case 1: + return players[0]; + default: + return new ɵAnimationGroupPlayer(players); + } +} + +function copyArray(source: any[]): any[] { + return source ? source.splice(0) : []; +} + +function makeAnimationEvent( + element: NgView, triggerName: string, fromState: string, toState: string, phaseName: string, + totalTime: number): AnimationEvent { + return {element, triggerName, fromState, toState, phaseName, totalTime}; +} + +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/animation-player.ts b/nativescript-angular/animations/animation-player.ts new file mode 100644 index 000000000..61e70b504 --- /dev/null +++ b/nativescript-angular/animations/animation-player.ts @@ -0,0 +1,306 @@ +import { AnimationPlayer } from "@angular/animations"; +import { + KeyframeAnimation, + KeyframeAnimationInfo, + KeyframeInfo, + KeyframeDeclaration +} from "tns-core-modules/ui/animation/keyframe-animation"; +import { View } from "tns-core-modules/ui/core/view"; +import { AnimationCurve } from "tns-core-modules/ui/enums"; +import { isString } from "tns-core-modules/utils/types"; +import { CssAnimationProperty } from "tns-core-modules/ui/core/properties"; + +export class NativeScriptAnimationPlayer implements AnimationPlayer { + + public parentPlayer: AnimationPlayer = null; + + private _startSubscriptions: Function[] = []; + private _doneSubscriptions: Function[] = []; + private _finished = false; + private _started = false; + private animation: KeyframeAnimation; + private target: View; + + constructor( + element: Node, + keyframes: {[key: string]: string | number}[], + duration: number, + delay: number, + easing: string + ) { + if (!(element instanceof View)) { + throw new Error("NativeScript: Can animate only Views!"); + } + + if (duration === 0) { + duration = 0.01; + } + + this.target = element; + + let keyframeAnimationInfo = new KeyframeAnimationInfo(); + keyframeAnimationInfo.duration = duration; + keyframeAnimationInfo.delay = delay; + keyframeAnimationInfo.iterations = 1; + keyframeAnimationInfo.curve = easing ? + animationTimingFunctionConverter(easing) : + AnimationCurve.ease; + keyframeAnimationInfo.keyframes = new Array(); + keyframeAnimationInfo.isForwards = true; + + keyframeAnimationInfo.keyframes = keyframes.map(styles => { + let keyframeInfo = {}; + keyframeInfo.duration = styles.offset; + keyframeInfo.declarations = Object.keys(styles).reduce((declarations, prop) => { + let value = styles[prop]; + + const property = CssAnimationProperty._getByCssName(prop); + if (property) { + if (typeof value === "string" && property._valueConverter) { + value = property._valueConverter(value); + } + declarations.push({ property: property.name, value }); + } else if (typeof value === "string" && prop === "transform") { + declarations.push(parseTransform(value)); + } + + + return declarations; + }, new Array()); + + return keyframeInfo; + }); + + keyframeAnimationInfo.keyframes = keyframes.map(styles => { + let keyframeInfo = {}; + keyframeInfo.duration = styles.offset; + keyframeInfo.declarations = Object.keys(styles).map((prop, _index) => { + let value = styles[prop]; + + const property = CssAnimationProperty._getByCssName(prop); + if (property) { + if (typeof value === "string" && property._valueConverter) { + value = property._valueConverter(value); + } + return { property: property.name, value: value }; + } else if (typeof value === "string" && prop === "transform") { + return parseTransform(value); + } + }).filter(declaration => !!declaration); + + return keyframeInfo; + }); + + this.animation = KeyframeAnimation.keyframeAnimationFromInfo(keyframeAnimationInfo); + } + + init(): void { + } + + hasStarted(): boolean { + return this._started; + } + + onStart(fn: Function): void { this._startSubscriptions.push(fn); } + onDone(fn: Function): void { this._doneSubscriptions.push(fn); } + onDestroy(fn: Function): void { this._doneSubscriptions.push(fn); } + + private _onStart() { + if (!this._started) { + this._started = true; + this._startSubscriptions.forEach(fn => fn()); + this._startSubscriptions = []; + } + } + + private _onFinish() { + if (!this._finished) { + this._finished = true; + this._started = false; + this._doneSubscriptions.forEach(fn => fn()); + this._doneSubscriptions = []; + } + } + + play(): void { + if (this.animation) { + this._onStart(); + this.animation.play(this.target) + .then(() => { this._onFinish(); }) + .catch((_e) => { }); + } + } + + pause(): void { + throw new Error("AnimationPlayer.pause method is not supported!"); + } + + finish(): void { + throw new Error("AnimationPlayer.finish method is not supported!"); + } + + reset(): void { + if (this.animation && this.animation.isPlaying) { + this.animation.cancel(); + } + } + + restart(): void { + this.reset(); + this.play(); + } + + destroy(): void { + this.reset(); + this._onFinish(); + } + + setPosition(_p: any): void { + throw new Error("AnimationPlayer.setPosition method is not supported!"); + } + + getPosition(): number { + return 0; + } + +} + +function parseTransform(value: string): KeyframeDeclaration { + let newTransform = transformConverter(value); + let values = undefined; + for (let transform in newTransform) { + switch (transform) { + case "scaleX": + return { + property: "scale", + value: { x: parseFloat(newTransform[transform]), y: 1 } + }; + case "scaleY": + return { + property: "scale", + value: { x: 1, y: parseFloat(newTransform[transform]) } + }; + case "scale": + case "scale3d": + values = newTransform[transform].split(","); + if (values.length === 2 || values.length === 3) { + return { + property: "scale", + value: { x: parseFloat(values[0]), y: parseFloat(values[1]) } + }; + } + break; + case "translateX": + return { + property: "translate", + value: { x: parseFloat(newTransform[transform]), y: 0 } + }; + case "translateY": + return { + property: "translate", + value: { x: 0, y: parseFloat(newTransform[transform]) } + }; + case "translate": + case "translate3d": + values = newTransform[transform].split(","); + if (values.length === 2 || values.length === 3) { + return { + property: "translate", + value: { + x: parseFloat(values[0]), + y: parseFloat(values[1]) + } + }; + } + break; + case "rotate": + let text = newTransform[transform]; + let val = parseFloat(text); + if (text.slice(-3) === "rad") { + val = val * (180.0 / Math.PI); + } + + return { property: "rotate", value: val }; + // case "none": + // return [ + // { property: "scale", value: { x: 1, y: 1 } }, + // { property: "translate", value: { x: 0, y: 0 } }, + // { property: "rotate", value: 0 }, + // ]; + default: + throw new Error("Unsupported transform: " + transform); + } + } +} + +function transformConverter(value: any): Object { + if (value === "none") { + let operations = {}; + operations[value] = value; + return operations; + } else if (isString(value)) { + let operations = {}; + let operator = ""; + let pos = 0; + while (pos < value.length) { + if (value[pos] === " " || value[pos] === ",") { + pos++; + } else if (value[pos] === "(") { + let start = pos + 1; + while (pos < value.length && value[pos] !== ")") { + pos++; + } + let operand = value.substring(start, pos); + operations[operator] = operand.trim(); + operator = ""; + pos++; + } else { + operator += value[pos++]; + } + } + return operations; + } else { + return undefined; + } +} + + +function animationTimingFunctionConverter(value): any { + switch (value) { + case "ease": + return AnimationCurve.ease; + case "linear": + return AnimationCurve.linear; + case "ease-in": + return AnimationCurve.easeIn; + case "ease-out": + return AnimationCurve.easeOut; + case "ease-in-out": + return AnimationCurve.easeInOut; + case "spring": + return AnimationCurve.spring; + default: + if (value.indexOf("cubic-bezier(") === 0) { + let bezierArr = value.substring(13).split(/[,]+/); + if (bezierArr.length !== 4) { + throw new Error("Invalid value for animation: " + value); + } + return AnimationCurve.cubicBezier( + bezieArgumentConverter(bezierArr[0]), + bezieArgumentConverter(bezierArr[1]), + bezieArgumentConverter(bezierArr[2]), + bezieArgumentConverter(bezierArr[3])); + } else { + throw new Error("Invalid value for animation: " + value); + } + } +} + + +function bezieArgumentConverter(value): number { + let result = parseFloat(value); + result = Math.max(0.0, result); + result = Math.min(1.0, result); + return result; +} + diff --git a/nativescript-angular/index.ts b/nativescript-angular/index.ts index 74963bb2e..7470e4f8a 100644 --- a/nativescript-angular/index.ts +++ b/nativescript-angular/index.ts @@ -1,5 +1,6 @@ import "application"; +export * from "./animations"; export * from "./platform-common"; export * from "./platform"; export * from "./platform-static"; diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 2dc904021..b455f6d00 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -7,6 +7,8 @@ // "nativescript-angular/application" import should be first in order to load some required settings (like globals and reflect-metadata) import { NativeScriptModule, platformNativeScriptDynamic } from "nativescript-angular/platform"; +import { NativeScriptAnimationsModule } from "nativescript-angular/animations"; + import { onAfterLivesync, onBeforeLivesync } from "nativescript-angular/platform-common"; import { NgModule } from "@angular/core"; import { Router } from "@angular/router"; @@ -69,7 +71,7 @@ import { AnimationStatesTest } from "./examples/animation/animation-states-test" class ExampleModule { } function makeExampleModule(componentType) { - let imports: any[] = [ExampleModule]; + let imports: any[] = [NativeScriptAnimationsModule, ExampleModule]; if (componentType.routes) { imports.push(NativeScriptRouterModule.forRoot(componentType.routes)) } @@ -82,20 +84,22 @@ function makeExampleModule(componentType) { entries = componentType.entries; } entries.push(componentType); + let providers = []; if (componentType.providers) { - providers = componentType.providers; + providers = [componentType.providers]; } + @NgModule({ bootstrap: [componentType], - imports: imports, + imports, entryComponents: entries, declarations: [ ...entries, ...exports, ], - providers: providers, - exports: exports, + providers, + exports, }) class ExampleModuleForComponent { } @@ -111,7 +115,7 @@ const customPageFactoryProvider = { } }; -platformNativeScriptDynamic().bootstrapModule(makeExampleModule(RendererTest)); +// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(RendererTest)); // platformNativeScriptDynamic(undefined, [customPageFactoryProvider]).bootstrapModule(makeExampleModule(RendererTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(TabViewTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(Benchmark)); @@ -132,10 +136,10 @@ platformNativeScriptDynamic().bootstrapModule(makeExampleModule(RendererTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LoginAppComponent)); // animations -// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest)); -// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationNgClassTest)); +// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest)); +// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationNgClassTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationKeyframesTest)); -// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationEnterLeaveTest)); +platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationEnterLeaveTest)); //Livesync test var cachedUrl: string; From ef9586c8af97348cf707895d2dacfddc61b86cf9 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Thu, 16 Mar 2017 18:54:34 +0200 Subject: [PATCH 2/9] refactor(animations): separate utils functions --- .../animations/animation-engine.ts | 87 +++---------------- nativescript-angular/animations/utils.ts | 78 +++++++++++++++++ 2 files changed, 90 insertions(+), 75 deletions(-) create mode 100644 nativescript-angular/animations/utils.ts diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts index 6999c1a8c..b27d00697 100644 --- a/nativescript-angular/animations/animation-engine.ts +++ b/nativescript-angular/animations/animation-engine.ts @@ -1,15 +1,17 @@ import { ɵDomAnimationEngine as DomAnimationEngine } from "@angular/platform-browser/animations"; -import { - AnimationEvent, - AnimationPlayer, - NoopAnimationPlayer, - ɵAnimationGroupPlayer, - ɵStyleData, -} from "@angular/animations"; - -import { unsetValue } from "tns-core-modules/ui/core/view"; +import { AnimationEvent, AnimationPlayer } from "@angular/animations"; import { NgView } from "../element-registry"; +import { + eraseStylesOverride, + cssClasses, + getOrSetAsInMap, + deleteFromArrayMap, + optimizeGroupPlayer, + copyArray, + makeAnimationEvent, + setStyles +} from "./utils"; const MARKED_FOR_ANIMATION = "ng-animate"; @@ -138,69 +140,4 @@ export class NativeScriptAnimationEngine extends DomAnimationEngine { private _getTransitionAnimation(element: NgView) { return (this)._activeTransitionAnimations.get(element); } -} - -// overriden to use the default 'unsetValue' -// instead of empty string '' -function eraseStylesOverride(element: NgView, styles: ɵStyleData) { - if (element["style"]) { - Object.keys(styles).forEach(prop => { - element.style[prop] = unsetValue; - }); - } -} - -function cssClasses(element: NgView) { - if (!element.ngCssClasses) { - element.ngCssClasses = new Map(); - } - return element.ngCssClasses; -} - -function getOrSetAsInMap(map: Map, key: any, defaultValue: any) { - let value = map.get(key); - if (!value) { - map.set(key, value = defaultValue); - } - return value; -} - -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); - } - } - } -} - -function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer { - switch (players.length) { - case 0: - return new NoopAnimationPlayer(); - case 1: - return players[0]; - default: - return new ɵAnimationGroupPlayer(players); - } -} - -function copyArray(source: any[]): any[] { - return source ? source.splice(0) : []; -} - -function makeAnimationEvent( - element: NgView, triggerName: string, fromState: string, toState: string, phaseName: string, - totalTime: number): AnimationEvent { - return {element, triggerName, fromState, toState, phaseName, totalTime}; -} - -function setStyles(element: NgView, styles: ɵStyleData) { - if (element["style"]) { - Object.keys(styles).forEach(prop => element.style[prop] = styles[prop]); - } -} +} \ No newline at end of file diff --git a/nativescript-angular/animations/utils.ts b/nativescript-angular/animations/utils.ts new file mode 100644 index 000000000..db136840c --- /dev/null +++ b/nativescript-angular/animations/utils.ts @@ -0,0 +1,78 @@ +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]); + } +} From 03bf5271191b352764e63de3d34ba5ed382e635f Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Fri, 17 Mar 2017 15:58:44 +0200 Subject: [PATCH 3/9] refactor: simplify AnimationPlayer code --- .../animations/animation-driver.ts | 17 +- .../animations/animation-engine.ts | 14 +- .../animations/animation-player.ts | 258 ++---------------- nativescript-angular/animations/dom-utils.ts | 77 ++++++ nativescript-angular/animations/utils.ts | 173 ++++++++---- ng-sample/app/app.ts | 6 +- .../animation/animation-keyframes-test.ts | 2 +- 7 files changed, 244 insertions(+), 303 deletions(-) create mode 100644 nativescript-angular/animations/dom-utils.ts diff --git a/nativescript-angular/animations/animation-driver.ts b/nativescript-angular/animations/animation-driver.ts index 3ab6cc10f..d1dd2b032 100644 --- a/nativescript-angular/animations/animation-driver.ts +++ b/nativescript-angular/animations/animation-driver.ts @@ -1,12 +1,13 @@ import { AnimationPlayer } from "@angular/animations"; -import { View } from "tns-core-modules/ui/core/view"; +import { NgView } from "../element-registry"; import { NativeScriptAnimationPlayer } from "./animation-player"; +import { Keyframe } from "./utils"; export abstract class AnimationDriver { abstract animate( element: any, - keyframes: {[key: string]: string | number}[], + keyframes: Keyframe[], duration: number, delay: number, easing: string @@ -14,18 +15,18 @@ export abstract class AnimationDriver { } export class NativeScriptAnimationDriver implements AnimationDriver { - computeStyle(element: any, prop: string): string { - const view = element; - return view.style[`css-${prop}`]; + computeStyle(element: NgView, prop: string): string { + return element.style[`css-${prop}`]; } animate( - element: any, - keyframes: {[key: string]: string | number}[], + element: NgView, + keyframes: Keyframe[], duration: number, delay: number, easing: string ): AnimationPlayer { - return new NativeScriptAnimationPlayer(element, keyframes, duration, delay, easing); + return new NativeScriptAnimationPlayer( + element, keyframes, duration, delay, easing); } } diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts index b27d00697..2e8b06f75 100644 --- a/nativescript-angular/animations/animation-engine.ts +++ b/nativescript-angular/animations/animation-engine.ts @@ -3,15 +3,15 @@ import { AnimationEvent, AnimationPlayer } from "@angular/animations"; import { NgView } from "../element-registry"; import { - eraseStylesOverride, + copyArray, cssClasses, - getOrSetAsInMap, deleteFromArrayMap, - optimizeGroupPlayer, - copyArray, + eraseStylesOverride, + getOrSetAsInMap, makeAnimationEvent, - setStyles -} from "./utils"; + optimizeGroupPlayer, + setStyles, +} from "./dom-utils"; const MARKED_FOR_ANIMATION = "ng-animate"; @@ -140,4 +140,4 @@ export class NativeScriptAnimationEngine extends DomAnimationEngine { private _getTransitionAnimation(element: NgView) { return (this)._activeTransitionAnimations.get(element); } -} \ No newline at end of file +} diff --git a/nativescript-angular/animations/animation-player.ts b/nativescript-angular/animations/animation-player.ts index 61e70b504..fcc60d8f1 100644 --- a/nativescript-angular/animations/animation-player.ts +++ b/nativescript-angular/animations/animation-player.ts @@ -2,16 +2,12 @@ import { AnimationPlayer } from "@angular/animations"; import { KeyframeAnimation, KeyframeAnimationInfo, - KeyframeInfo, - KeyframeDeclaration } from "tns-core-modules/ui/animation/keyframe-animation"; -import { View } from "tns-core-modules/ui/core/view"; -import { AnimationCurve } from "tns-core-modules/ui/enums"; -import { isString } from "tns-core-modules/utils/types"; -import { CssAnimationProperty } from "tns-core-modules/ui/core/properties"; -export class NativeScriptAnimationPlayer implements AnimationPlayer { +import { NgView } from "../element-registry"; +import { Keyframe, getAnimationCurve, parseAnimationKeyframe } from "./utils"; +export class NativeScriptAnimationPlayer implements AnimationPlayer { public parentPlayer: AnimationPlayer = null; private _startSubscriptions: Function[] = []; @@ -19,79 +15,15 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { private _finished = false; private _started = false; private animation: KeyframeAnimation; - private target: View; constructor( - element: Node, - keyframes: {[key: string]: string | number}[], + private target: NgView, + keyframes: Keyframe[], duration: number, delay: number, easing: string ) { - if (!(element instanceof View)) { - throw new Error("NativeScript: Can animate only Views!"); - } - - if (duration === 0) { - duration = 0.01; - } - - this.target = element; - - let keyframeAnimationInfo = new KeyframeAnimationInfo(); - keyframeAnimationInfo.duration = duration; - keyframeAnimationInfo.delay = delay; - keyframeAnimationInfo.iterations = 1; - keyframeAnimationInfo.curve = easing ? - animationTimingFunctionConverter(easing) : - AnimationCurve.ease; - keyframeAnimationInfo.keyframes = new Array(); - keyframeAnimationInfo.isForwards = true; - - keyframeAnimationInfo.keyframes = keyframes.map(styles => { - let keyframeInfo = {}; - keyframeInfo.duration = styles.offset; - keyframeInfo.declarations = Object.keys(styles).reduce((declarations, prop) => { - let value = styles[prop]; - - const property = CssAnimationProperty._getByCssName(prop); - if (property) { - if (typeof value === "string" && property._valueConverter) { - value = property._valueConverter(value); - } - declarations.push({ property: property.name, value }); - } else if (typeof value === "string" && prop === "transform") { - declarations.push(parseTransform(value)); - } - - - return declarations; - }, new Array()); - - return keyframeInfo; - }); - - keyframeAnimationInfo.keyframes = keyframes.map(styles => { - let keyframeInfo = {}; - keyframeInfo.duration = styles.offset; - keyframeInfo.declarations = Object.keys(styles).map((prop, _index) => { - let value = styles[prop]; - - const property = CssAnimationProperty._getByCssName(prop); - if (property) { - if (typeof value === "string" && property._valueConverter) { - value = property._valueConverter(value); - } - return { property: property.name, value: value }; - } else if (typeof value === "string" && prop === "transform") { - return parseTransform(value); - } - }).filter(declaration => !!declaration); - - return keyframeInfo; - }); - - this.animation = KeyframeAnimation.keyframeAnimationFromInfo(keyframeAnimationInfo); + this.initKeyframeAnimation(keyframes, duration, delay, easing); } init(): void { @@ -105,30 +37,20 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { onDone(fn: Function): void { this._doneSubscriptions.push(fn); } onDestroy(fn: Function): void { this._doneSubscriptions.push(fn); } - private _onStart() { + play(): void { + if (!this.animation) { + return; + } + if (!this._started) { this._started = true; this._startSubscriptions.forEach(fn => fn()); this._startSubscriptions = []; } - } - private _onFinish() { - if (!this._finished) { - this._finished = true; - this._started = false; - this._doneSubscriptions.forEach(fn => fn()); - this._doneSubscriptions = []; - } - } - - play(): void { - if (this.animation) { - this._onStart(); - this.animation.play(this.target) - .then(() => { this._onFinish(); }) - .catch((_e) => { }); - } + this.animation.play(this.target) + .then(() => this.onFinish()) + .catch((_e) => { }); } pause(): void { @@ -152,7 +74,7 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { destroy(): void { this.reset(); - this._onFinish(); + this.onFinish(); } setPosition(_p: any): void { @@ -163,144 +85,24 @@ export class NativeScriptAnimationPlayer implements AnimationPlayer { return 0; } -} - -function parseTransform(value: string): KeyframeDeclaration { - let newTransform = transformConverter(value); - let values = undefined; - for (let transform in newTransform) { - switch (transform) { - case "scaleX": - return { - property: "scale", - value: { x: parseFloat(newTransform[transform]), y: 1 } - }; - case "scaleY": - return { - property: "scale", - value: { x: 1, y: parseFloat(newTransform[transform]) } - }; - case "scale": - case "scale3d": - values = newTransform[transform].split(","); - if (values.length === 2 || values.length === 3) { - return { - property: "scale", - value: { x: parseFloat(values[0]), y: parseFloat(values[1]) } - }; - } - break; - case "translateX": - return { - property: "translate", - value: { x: parseFloat(newTransform[transform]), y: 0 } - }; - case "translateY": - return { - property: "translate", - value: { x: 0, y: parseFloat(newTransform[transform]) } - }; - case "translate": - case "translate3d": - values = newTransform[transform].split(","); - if (values.length === 2 || values.length === 3) { - return { - property: "translate", - value: { - x: parseFloat(values[0]), - y: parseFloat(values[1]) - } - }; - } - break; - case "rotate": - let text = newTransform[transform]; - let val = parseFloat(text); - if (text.slice(-3) === "rad") { - val = val * (180.0 / Math.PI); - } + private initKeyframeAnimation(keyframes: Keyframe[], duration: number, delay: number, easing: string) { + let info = new KeyframeAnimationInfo(); + info.isForwards = true; + info.iterations = 1; + info.duration = duration === 0 ? 0.01 : duration; + info.delay = delay; + info.curve = getAnimationCurve(easing); + info.keyframes = keyframes.map(parseAnimationKeyframe); - return { property: "rotate", value: val }; - // case "none": - // return [ - // { property: "scale", value: { x: 1, y: 1 } }, - // { property: "translate", value: { x: 0, y: 0 } }, - // { property: "rotate", value: 0 }, - // ]; - default: - throw new Error("Unsupported transform: " + transform); - } + this.animation = KeyframeAnimation.keyframeAnimationFromInfo(info); } -} -function transformConverter(value: any): Object { - if (value === "none") { - let operations = {}; - operations[value] = value; - return operations; - } else if (isString(value)) { - let operations = {}; - let operator = ""; - let pos = 0; - while (pos < value.length) { - if (value[pos] === " " || value[pos] === ",") { - pos++; - } else if (value[pos] === "(") { - let start = pos + 1; - while (pos < value.length && value[pos] !== ")") { - pos++; - } - let operand = value.substring(start, pos); - operations[operator] = operand.trim(); - operator = ""; - pos++; - } else { - operator += value[pos++]; - } + private onFinish() { + if (!this._finished) { + this._finished = true; + this._started = false; + this._doneSubscriptions.forEach(fn => fn()); + this._doneSubscriptions = []; } - return operations; - } else { - return undefined; } } - - -function animationTimingFunctionConverter(value): any { - switch (value) { - case "ease": - return AnimationCurve.ease; - case "linear": - return AnimationCurve.linear; - case "ease-in": - return AnimationCurve.easeIn; - case "ease-out": - return AnimationCurve.easeOut; - case "ease-in-out": - return AnimationCurve.easeInOut; - case "spring": - return AnimationCurve.spring; - default: - if (value.indexOf("cubic-bezier(") === 0) { - let bezierArr = value.substring(13).split(/[,]+/); - if (bezierArr.length !== 4) { - throw new Error("Invalid value for animation: " + value); - } - return AnimationCurve.cubicBezier( - bezieArgumentConverter(bezierArr[0]), - bezieArgumentConverter(bezierArr[1]), - bezieArgumentConverter(bezierArr[2]), - bezieArgumentConverter(bezierArr[3])); - } else { - throw new Error("Invalid value for animation: " + value); - } - } -} - - -function bezieArgumentConverter(value): number { - let result = parseFloat(value); - result = Math.max(0.0, result); - result = Math.min(1.0, result); - return result; -} - diff --git a/nativescript-angular/animations/dom-utils.ts b/nativescript-angular/animations/dom-utils.ts new file mode 100644 index 000000000..00584f70e --- /dev/null +++ b/nativescript-angular/animations/dom-utils.ts @@ -0,0 +1,77 @@ +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/utils.ts b/nativescript-angular/animations/utils.ts index db136840c..05833e3a3 100644 --- a/nativescript-angular/animations/utils.ts +++ b/nativescript-angular/animations/utils.ts @@ -1,78 +1,139 @@ 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; - }); - } + KeyframeDeclaration, + KeyframeInfo, +} from "tns-core-modules/ui/animation/keyframe-animation"; +import { CssAnimationProperty } from "tns-core-modules/ui/core/properties"; +import { AnimationCurve } from "tns-core-modules/ui/enums"; + +export interface Keyframe { + [key: string]: string | number; } -export function cssClasses(element: NgView) { - if (!element.ngCssClasses) { - element.ngCssClasses = new Map(); - } - return element.ngCssClasses; +interface Transformation { + property: string; + value: number | { x: number, y: number }; } -// The following functions are from -// the original DomAnimationEngine -export function getOrSetAsInMap(map: Map, key: any, defaultValue: any) { - let value = map.get(key); +const TRANSFORM_MATCHER = new RegExp(/(.+)\((.+)\)/); +const TRANSFORM_SPLITTER = new RegExp(/[\s,]+/); + +const STYLE_TRANSFORMATION_MAP = Object.freeze({ + "scale": value => ({ property: "scale", value }), + "scale3d": value => ({ property: "scale", value }), + "scaleX": value => ({ property: "scale", value: { x: value, y: 1 } }), + "scaleY": value => ({ property: "scale", value: { x: 1, y: value } }), + + "translate": value => ({ property: "translate", value }), + "translate3d": value => ({ property: "translate", value }), + "translateX": value => ({ property: "translate", value: { x: value, y: 0 } }), + "translateY": value => ({ property: "translate", value: { x: 0, y: value } }), + + "rotate": value => ({ property: "rotate", value }), + + "none": _value => [ + { property: "scale", value: { x: 1, y: 1 } }, + { property: "translate", value: { x: 0, y: 0 } }, + { property: "rotate", value: 0 }, + ], +}); + +const STYLE_CURVE_MAP = Object.freeze({ + "ease": AnimationCurve.ease, + "linear": AnimationCurve.linear, + "ease-in": AnimationCurve.easeIn, + "ease-out": AnimationCurve.easeOut, + "ease-in-out": AnimationCurve.easeInOut, + "spring": AnimationCurve.spring, +}); + +export function getAnimationCurve(value: string): any { if (!value) { - map.set(key, value = defaultValue); + return AnimationCurve.ease; + } + + const curve = STYLE_CURVE_MAP[value]; + if (curve) { + return curve; + } + + const [, property = "", pointsString = ""] = TRANSFORM_MATCHER.exec(value) || []; + const coords = pointsString.split(TRANSFORM_SPLITTER).map(stringToBezieCoords); + + if (property !== "cubic-bezier" || coords.length !== 4) { + throw new Error(`Invalid value for animation: ${value}`); + } else { + return (AnimationCurve).cubicBezier(...coords); } - 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 parseAnimationKeyframe(styles: Keyframe) { + let keyframeInfo = {}; + keyframeInfo.duration = styles.offset; + keyframeInfo.declarations = Object.keys(styles).reduce((declarations, prop) => { + let value = styles[prop]; + + const property = CssAnimationProperty._getByCssName(prop); + if (property) { + if (typeof value === "string" && property._valueConverter) { + value = property._valueConverter(value); } + declarations.push({ property: property.name, value }); + } else if (typeof value === "string" && prop === "transform") { + declarations.push(...parseTransformation(value)); } - } + + return declarations; + }, new Array()); + + return keyframeInfo; } -export function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer { - switch (players.length) { - case 0: - return new NoopAnimationPlayer(); - case 1: - return players[0]; - default: - return new ɵAnimationGroupPlayer(players); +function stringToBezieCoords(value: string): number { + let result = parseFloat(value); + if (result < 0) { + return 0; + } else if (result > 1) { + return 1; } + + return result; } -export function copyArray(source: any[]): any[] { - return source ? source.splice(0) : []; +function parseTransformation(styleString: string): KeyframeDeclaration[] { + return parseStyle(styleString) + .reduce((transformations, style) => { + const transform = STYLE_TRANSFORMATION_MAP[style.property](style.value); + + if (Array.isArray(transform)) { + transformations.push(...transform); + } else if (typeof transform !== "undefined") { + transformations.push(transform); + } + + return transformations; + }, new Array()); } -export function makeAnimationEvent( - element: NgView, triggerName: string, fromState: string, toState: string, phaseName: string, - totalTime: number): AnimationEvent { - return {element, triggerName, fromState, toState, phaseName, totalTime}; +function parseStyle(text: string): Transformation[] { + return text.split(TRANSFORM_SPLITTER).map(stringToTransformation).filter(t => !!t); } -export function setStyles(element: NgView, styles: ɵStyleData) { - if (element["style"]) { - Object.keys(styles).forEach(prop => element.style[prop] = styles[prop]); +function stringToTransformation(text: string): Transformation { + const [, property = "", stringValue = ""] = TRANSFORM_MATCHER.exec(text) || []; + if (!property) { + return; } + + const [x, y] = stringValue.split(",").map(parseFloat); + if (x && y) { + return { property, value: {x, y} }; + } else { + let value: number = x; + + if (stringValue.slice(-3) === "rad") { + value *= 180.0 / Math.PI; + } + + return { property, value }; + } } diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index b455f6d00..f17467c59 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -136,10 +136,10 @@ const customPageFactoryProvider = { // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LoginAppComponent)); // animations -// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest)); -// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationNgClassTest)); +platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest)); +// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationNgClassTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationKeyframesTest)); -platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationEnterLeaveTest)); +// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationEnterLeaveTest)); //Livesync test var cachedUrl: string; diff --git a/ng-sample/app/examples/animation/animation-keyframes-test.ts b/ng-sample/app/examples/animation/animation-keyframes-test.ts index f4c5a48ca..b9da4a5f0 100644 --- a/ng-sample/app/examples/animation/animation-keyframes-test.ts +++ b/ng-sample/app/examples/animation/animation-keyframes-test.ts @@ -12,7 +12,7 @@ import {Component, trigger, style, animate, state, transition, keyframes } from state('inactive', style({ transform: 'translateX(0)', opacity: 0.2 })), transition('inactive => active', [ animate(300, keyframes([ - style({opacity: 0.2, transform: 'translateX(-100)', offset: 0}), + style({opacity: 0.2, transform: 'translateX(-100),translateY(100)', offset: 0}), style({opacity: 1, transform: 'translateX(15)', offset: 0.3}), style({opacity: 1, transform: 'translateX(0)', offset: 1.0}) ])) From ec05efa208591f49452f4f5bca57303f5432ee33 Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Mon, 20 Mar 2017 11:24:59 +0200 Subject: [PATCH 4/9] chore(ng): update to Angular 4.0.0-rc.5 --- nativescript-angular/package.json | 22 +++++++++++----------- ng-sample/package.json | 16 ++++++++-------- tests/package.json | 14 +++++++------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json index 5c9133f34..2d06f8ff2 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -25,15 +25,15 @@ }, "dependencies": { "nativescript-intl": "~0.0.8", - "@angular/animations": "4.0.0-rc.3", - "@angular/core": "4.0.0-rc.3", - "@angular/common": "4.0.0-rc.3", - "@angular/compiler": "4.0.0-rc.3", - "@angular/http": "4.0.0-rc.3", - "@angular/platform-browser": "4.0.0-rc.3", - "@angular/platform-browser-dynamic": "4.0.0-rc.3", - "@angular/forms": "4.0.0-rc.3", - "@angular/router": "4.0.0-rc.3", + "@angular/animations": "4.0.0-rc.5", + "@angular/core": "4.0.0-rc.5", + "@angular/common": "4.0.0-rc.5", + "@angular/compiler": "4.0.0-rc.5", + "@angular/http": "4.0.0-rc.5", + "@angular/platform-browser": "4.0.0-rc.5", + "@angular/platform-browser-dynamic": "4.0.0-rc.5", + "@angular/forms": "4.0.0-rc.5", + "@angular/router": "4.0.0-rc.5", "rxjs": "^5.0.1", "reflect-metadata": "~0.1.8", "punycode": "1.3.2", @@ -41,12 +41,12 @@ "url": "0.10.3" }, "devDependencies": { - "@angular/compiler-cli": "4.0.0-rc.3", + "@angular/compiler-cli": "4.0.0-rc.5", "codelyzer": "~2.0.0", "tns-core-modules": "internal-preview", "tslint": "~4.4.0", "typescript": "~2.2.1", - "zone.js": "^0.7.2" + "zone.js": "^0.8.2" }, "nativescript": {} } diff --git a/ng-sample/package.json b/ng-sample/package.json index ceff974d9..f6ff24b12 100644 --- a/ng-sample/package.json +++ b/ng-sample/package.json @@ -23,14 +23,14 @@ }, "homepage": "https://github.com/NativeScript/template-hello-world", "dependencies": { - "@angular/common": "4.0.0-rc.3", - "@angular/compiler": "4.0.0-rc.3", - "@angular/core": "4.0.0-rc.3", - "@angular/forms": "4.0.0-rc.3", - "@angular/http": "4.0.0-rc.3", - "@angular/platform-browser": "4.0.0-rc.3", - "@angular/platform-browser-dynamic": "4.0.0-rc.3", - "@angular/router": "4.0.0-rc.3", + "@angular/common": "4.0.0-rc.5", + "@angular/compiler": "4.0.0-rc.5", + "@angular/core": "4.0.0-rc.5", + "@angular/forms": "4.0.0-rc.5", + "@angular/http": "4.0.0-rc.5", + "@angular/platform-browser": "4.0.0-rc.5", + "@angular/platform-browser-dynamic": "4.0.0-rc.5", + "@angular/router": "4.0.0-rc.5", "nativescript-angular": "file:../nativescript-angular", "rxjs": "~5.0.1", "tns-core-modules": "internal-preview" diff --git a/tests/package.json b/tests/package.json index f65d641aa..13deea910 100644 --- a/tests/package.json +++ b/tests/package.json @@ -26,13 +26,13 @@ ], "homepage": "http://nativescript.org", "dependencies": { - "@angular/common": "4.0.0-rc.3", - "@angular/compiler": "4.0.0-rc.3", - "@angular/core": "4.0.0-rc.3", - "@angular/http": "4.0.0-rc.3", - "@angular/platform-browser": "4.0.0-rc.3", - "@angular/platform-browser-dynamic": "4.0.0-rc.3", - "@angular/router": "4.0.0-rc.3", + "@angular/common": "4.0.0-rc.5", + "@angular/compiler": "4.0.0-rc.5", + "@angular/core": "4.0.0-rc.5", + "@angular/http": "4.0.0-rc.5", + "@angular/platform-browser": "4.0.0-rc.5", + "@angular/platform-browser-dynamic": "4.0.0-rc.5", + "@angular/router": "4.0.0-rc.5", "nativescript-angular": "internal-preview", "nativescript-unit-test-runner": "^0.3.4", "rxjs": "~5.0.1", From c9f64bf9a7ff29c775dd52df60e7fc0befdbba6e Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Mon, 20 Mar 2017 11:25:38 +0200 Subject: [PATCH 5/9] fix(dom_adapter): add missing `contains` method signature Needed to properly implement browser's DomAdapter. Caused by: https://github.com/angular/angular/commit/a4076c70ccb85cdbb3b38ca33d366746120178e5 --- nativescript-angular/dom-adapter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/nativescript-angular/dom-adapter.ts b/nativescript-angular/dom-adapter.ts index e558c7832..119e41d91 100644 --- a/nativescript-angular/dom-adapter.ts +++ b/nativescript-angular/dom-adapter.ts @@ -49,6 +49,7 @@ export class NativeScriptDomAdapter implements ɵDomAdapter { getProperty(_el: Element, _name: string): any { throw new Error("Not implemented!") } invoke(_el: Element, _methodName: string, _args: any[]): any { throw new Error("Not implemented!") } + contains(_nodeA: any, _nodeB: any): any /** TODO #9100 */ { throw new Error("Not implemented!") } parse(_templateHtml: string): any /** TODO #9100 */ { throw new Error("Not implemented!") } query(_selector: string): any { throw new Error("Not implemented!") } querySelector(_el: any /** TODO #9100 */, _selector: string): HTMLElement { throw new Error("Not implemented!") } From 47ea246ea037ffd4c1a65137ee43717f899437df Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Mon, 20 Mar 2017 11:30:42 +0200 Subject: [PATCH 6/9] fix(renderer): use flags in `setStyle` and `removeStyle` instead of booleans caused by: https://github.com/angular/angular/pull/15045 --- nativescript-angular/renderer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nativescript-angular/renderer.ts b/nativescript-angular/renderer.ts index 7352f96a6..71a83cb4c 100644 --- a/nativescript-angular/renderer.ts +++ b/nativescript-angular/renderer.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, Optional, NgZone, Renderer2, RendererFactory2, RendererType2, - ViewEncapsulation, + RendererStyleFlags2, ViewEncapsulation, } from "@angular/core"; import { APP_ROOT_VIEW, DEVICE } from "./platform-providers"; import { isBlank } from "./lang-facade"; @@ -173,12 +173,12 @@ export class NativeScriptRenderer extends Renderer2 { this.viewUtil.removeClass(view, name); } - setStyle(view: NgView, styleName: string, value: any, _hasVendorPrefix?: boolean, _hasImportant?: boolean): void { + setStyle(view: NgView, styleName: string, value: any, _flags?: RendererStyleFlags2): void { traceLog(`NativeScriptRenderer.setStyle: ${styleName} = ${value}`); this.viewUtil.setStyle(view, styleName, value); } - removeStyle(view: NgView, styleName: string, _hasVendorPrefix?: boolean): void { + removeStyle(view: NgView, styleName: string, _flags?: RendererStyleFlags2): void { traceLog("NativeScriptRenderer.removeStyle: ${styleName}"); this.viewUtil.removeStyle(view, styleName); } From b5f3f4dab1091bba10a7c7488953e01a972e522f Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Mon, 20 Mar 2017 11:33:43 +0200 Subject: [PATCH 7/9] fix(page-router-outlet): activateWith instead of activate method caused by: https://github.com/angular/angular/pull/15044 --- .../router/page-router-outlet.ts | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts index b4b9a2797..1208dd214 100644 --- a/nativescript-angular/router/page-router-outlet.ts +++ b/nativescript-angular/router/page-router-outlet.ts @@ -1,6 +1,6 @@ import { Attribute, ComponentFactory, ComponentRef, Directive, - ReflectiveInjector, ResolvedReflectiveProvider, ViewContainerRef, + ViewContainerRef, Inject, ComponentFactoryResolver, Injector } from "@angular/core"; import { isPresent } from "../lang-facade"; @@ -68,7 +68,9 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix public outletMap: RouterOutletMap; - get locationInjector(): Injector { return this.containerRef.injector; } + /** @deprecated from Angular since v4 */ + get locationInjector(): Injector { return this.location.injector; } + /** @deprecated from Angular since v4 */ get locationFactoryResolver(): ComponentFactoryResolver { return this.resolver; } get isActivated(): boolean { @@ -92,7 +94,7 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix constructor( parentOutletMap: RouterOutletMap, - private containerRef: ViewContainerRef, + private location: ViewContainerRef, @Attribute("name") name: string, private locationStrategy: NSLocationStrategy, private componentFactoryResolver: ComponentFactoryResolver, @@ -145,37 +147,35 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix * 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. */ - activate( - activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, - providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void { + activateWith( + activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null, + outletMap: RouterOutletMap): void { this.outletMap = outletMap; this.currentActivatedRoute = activatedRoute; + resolver = resolver || this.resolver; + if (this.locationStrategy._isPageNavigatingBack()) { this.activateOnGoBack(activatedRoute, outletMap); } else { - this.activateOnGoForward(activatedRoute, providers, outletMap, resolver, injector); + this.activateOnGoForward(activatedRoute, outletMap, resolver); } } private activateOnGoForward( activatedRoute: ActivatedRoute, - providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap, - loadedResolver: ComponentFactoryResolver, - injector: Injector): void { + loadedResolver: ComponentFactoryResolver): void { const factory = this.getComponentFactory(activatedRoute, loadedResolver); const pageRoute = new PageRoute(activatedRoute); - providers = [...providers, ...ReflectiveInjector.resolve( - [{ provide: PageRoute, useValue: pageRoute }])]; + const inj = new OutletInjector(activatedRoute, outletMap, this.location.injector); if (this.isInitialPage) { log("PageRouterOutlet.activate() initial page - just load component"); this.isInitialPage = false; - const inj = ReflectiveInjector.fromResolvedProviders(providers, injector); - this.currentActivatedComp = this.containerRef.createComponent( - factory, this.containerRef.length, inj, []); + this.currentActivatedComp = this.location.createComponent( + factory, this.location.length, inj, []); this.currentActivatedComp.changeDetectorRef.detectChanges(); @@ -189,13 +189,8 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix isNavigation: true, componentType: factory.componentType }); - const pageResolvedProvider = ReflectiveInjector.resolve([ - { provide: Page, useValue: page } - ]); - const childInjector = ReflectiveInjector.fromResolvedProviders( - [...providers, ...pageResolvedProvider], injector); - const loaderRef = this.containerRef.createComponent( - this.detachedLoaderFactory, this.containerRef.length, childInjector, []); + const loaderRef = this.location.createComponent( + this.detachedLoaderFactory, this.location.length, inj, []); loaderRef.changeDetectorRef.detectChanges(); this.currentActivatedComp = loaderRef.instance.loadWithFactory(factory); @@ -275,6 +270,23 @@ export class PageRouterOutlet { // tslint:disable-line:directive-class-suffix } } +class OutletInjector implements Injector { + constructor( + private route: ActivatedRoute, private map: RouterOutletMap, private parent: Injector) { } + + get(token: any, notFoundValue?: any): any { + if (token === ActivatedRoute) { + return this.route; + } + + if (token === RouterOutletMap) { + return this.map; + } + + return this.parent.get(token, notFoundValue); + } +} + function log(msg: string) { routerLog(msg); } From 28c103b7a06017a5bbf92500a3d7cc2d0614672b Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Mon, 20 Mar 2017 14:17:15 +0200 Subject: [PATCH 8/9] fix(animations): use imports from @angular/animations/browser --- nativescript-angular/animations.ts | 7 ++++--- nativescript-angular/animations/animation-engine.ts | 2 +- nativescript-angular/index.ts | 1 - nativescript-angular/package.json | 2 +- ng-sample/app/app.ts | 9 ++++++--- ng-sample/package.json | 5 +++-- tests/package.json | 4 ++-- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/nativescript-angular/animations.ts b/nativescript-angular/animations.ts index 2896645be..30b32f3d6 100644 --- a/nativescript-angular/animations.ts +++ b/nativescript-angular/animations.ts @@ -4,9 +4,10 @@ import { AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, - ɵAnimationRendererFactory as AnimationRendererFactory, - ɵg as WebAnimationsStyleNormalizer -} from "@angular/platform-browser/animations"; + ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer +} from "@angular/animations/browser"; + +import { ɵAnimationRendererFactory as AnimationRendererFactory } from "@angular/platform-browser/animations"; import { NativeScriptAnimationEngine } from "./animations/animation-engine"; import { NativeScriptAnimationDriver } from "./animations/animation-driver"; diff --git a/nativescript-angular/animations/animation-engine.ts b/nativescript-angular/animations/animation-engine.ts index 2e8b06f75..f9fab7b44 100644 --- a/nativescript-angular/animations/animation-engine.ts +++ b/nativescript-angular/animations/animation-engine.ts @@ -1,4 +1,4 @@ -import { ɵDomAnimationEngine as DomAnimationEngine } from "@angular/platform-browser/animations"; +import { ɵDomAnimationEngine as DomAnimationEngine } from "@angular/animations/browser"; import { AnimationEvent, AnimationPlayer } from "@angular/animations"; import { NgView } from "../element-registry"; diff --git a/nativescript-angular/index.ts b/nativescript-angular/index.ts index 7470e4f8a..74963bb2e 100644 --- a/nativescript-angular/index.ts +++ b/nativescript-angular/index.ts @@ -1,6 +1,5 @@ import "application"; -export * from "./animations"; export * from "./platform-common"; export * from "./platform"; export * from "./platform-static"; diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json index 2d06f8ff2..60de0b199 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -25,7 +25,6 @@ }, "dependencies": { "nativescript-intl": "~0.0.8", - "@angular/animations": "4.0.0-rc.5", "@angular/core": "4.0.0-rc.5", "@angular/common": "4.0.0-rc.5", "@angular/compiler": "4.0.0-rc.5", @@ -41,6 +40,7 @@ "url": "0.10.3" }, "devDependencies": { + "@angular/animations": "4.0.0-rc.5", "@angular/compiler-cli": "4.0.0-rc.5", "codelyzer": "~2.0.0", "tns-core-modules": "internal-preview", diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index f17467c59..738ca625b 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -71,7 +71,10 @@ import { AnimationStatesTest } from "./examples/animation/animation-states-test" class ExampleModule { } function makeExampleModule(componentType) { - let imports: any[] = [NativeScriptAnimationsModule, ExampleModule]; + let imports: any[] = [ + NativeScriptAnimationsModule, + ExampleModule, + ]; if (componentType.routes) { imports.push(NativeScriptRouterModule.forRoot(componentType.routes)) } @@ -136,9 +139,9 @@ const customPageFactoryProvider = { // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LoginAppComponent)); // animations -platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest)); +// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationNgClassTest)); -// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationKeyframesTest)); +platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationKeyframesTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationEnterLeaveTest)); //Livesync test diff --git a/ng-sample/package.json b/ng-sample/package.json index f6ff24b12..0c0385fc3 100644 --- a/ng-sample/package.json +++ b/ng-sample/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://github.com/NativeScript/template-hello-world", "dependencies": { + "@angular/animations": "4.0.0-rc.5", "@angular/common": "4.0.0-rc.5", "@angular/compiler": "4.0.0-rc.5", "@angular/core": "4.0.0-rc.5", @@ -33,10 +34,10 @@ "@angular/router": "4.0.0-rc.5", "nativescript-angular": "file:../nativescript-angular", "rxjs": "~5.0.1", - "tns-core-modules": "internal-preview" + "tns-core-modules": "internal-preview", + "zone.js": "~0.8.2" }, "devDependencies": { - "zone.js": "~0.7.2", "babel-traverse": "6.9.0", "babel-types": "6.10.0", "babylon": "6.8.1", diff --git a/tests/package.json b/tests/package.json index 13deea910..8258c4c40 100644 --- a/tests/package.json +++ b/tests/package.json @@ -36,6 +36,7 @@ "nativescript-angular": "internal-preview", "nativescript-unit-test-runner": "^0.3.4", "rxjs": "~5.0.1", + "zone.js": "^0.8.2", "tns-core-modules": "internal-preview" }, "devDependencies": { @@ -58,8 +59,7 @@ "socket.io": "1.4.8", "socket.io-client": "1.4.8", "typescript": "~2.2.0", - "wd": "0.4.0", - "zone.js": "^0.7.2" + "wd": "0.4.0" }, "scripts": { "updateTests": "grunt updateTests", From 960702bdc4acc90a43693010229dc5f15051287c Mon Sep 17 00:00:00 2001 From: sis0k0 Date: Mon, 20 Mar 2017 14:36:13 +0200 Subject: [PATCH 9/9] build: fix animations bundle on postinstall revert after this fix: https://github.com/angular/angular/pull/15300 --- nativescript-angular/postinstall.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nativescript-angular/postinstall.js b/nativescript-angular/postinstall.js index c0eb367da..28b8ea45f 100644 --- a/nativescript-angular/postinstall.js +++ b/nativescript-angular/postinstall.js @@ -1,9 +1,21 @@ var fs = require("fs"); var os = require("os"); +var path = require("path"); var hookHelper = require("./hooks/hook-helper"); var projectDir = hookHelper.findProjectDir(); if (projectDir) { + var bundlePath = path.join(projectDir, "node_modules/@angular/animations/browser/package.json"); + + try { + var content = require(bundlePath); + content.main = "../bundles/animations-browser.umd.js"; + console.log(content) + fs.writeFileSync(bundlePath, JSON.stringify(content), "utf8"); + } catch(e) { + console.error(e.message); + } + var hooksDir = hookHelper.getHooksDir(), beforeLivesyncHookDir = hookHelper.getBeforeLivesyncHookDir(), content = 'module.exports = require("nativescript-angular/hooks/before-livesync");';