diff --git a/nativescript-angular/animation-driver.ts b/nativescript-angular/animation-driver.ts deleted file mode 100644 index 3ad2be803..000000000 --- a/nativescript-angular/animation-driver.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AnimationPlayer } from "@angular/core"; -import { AnimationStyles, AnimationKeyframe } from "./private_import_core"; -import { NativeScriptAnimationPlayer } from "./animation-player"; -import { View } from "tns-core-modules/ui/core/view"; - -export abstract class AnimationDriver { - abstract animate( - element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], - 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, - _startingStyles: AnimationStyles, - keyframes: AnimationKeyframe[], - duration: number, - delay: number, - easing: string - ): AnimationPlayer { - return new NativeScriptAnimationPlayer(element, keyframes, duration, delay, easing); - } -} diff --git a/nativescript-angular/animation-player.ts b/nativescript-angular/animation-player.ts deleted file mode 100644 index c03805c7e..000000000 --- a/nativescript-angular/animation-player.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { AnimationPlayer } from "@angular/core"; -import { AnimationKeyframe } from "./private_import_core"; -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; - - private _startSubscriptions: Function[] = []; - private _doneSubscriptions: Function[] = []; - private _finished = false; - private _started = false; - private animation: KeyframeAnimation; - private target: View; - - constructor( - element: Node, - keyframes: AnimationKeyframe[], - duration: number, - delay: number, - easing: string - ) { - - this.parentPlayer = null; - - if (duration === 0) { - duration = 0.01; - } - - if (!(element instanceof View)) { - throw new Error("NativeScript: Can animate only Views!"); - } - - this.target = element; - - let keyframeAnimationInfo = new KeyframeAnimationInfo(); - keyframeAnimationInfo.duration = duration; - keyframeAnimationInfo.delay = delay; - keyframeAnimationInfo.iterations = 1; - keyframeAnimationInfo.curve = easing ? - NativeScriptAnimationPlayer.animationTimingFunctionConverter(easing) : - AnimationCurve.ease; - keyframeAnimationInfo.keyframes = new Array(); - keyframeAnimationInfo.isForwards = true; - - for (let keyframe of keyframes) { - let keyframeInfo = {}; - keyframeInfo.duration = keyframe.offset; - keyframeInfo.declarations = new Array(); - for (let style of keyframe.styles.styles) { - for (let substyle in style) { - let value = style[substyle]; - - let property = CssAnimationProperty._getByCssName(substyle); - if (property) { - if (typeof value === "string" && property._valueConverter) { - value = property._valueConverter(value); - } - keyframeInfo.declarations.push({ property: property.name, value: value }); - } else if (typeof value === "string" && substyle === "transform") { - NativeScriptAnimationPlayer.parseTransform(value, keyframeInfo); - } - } - } - keyframeAnimationInfo.keyframes.push(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); } - - 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; - } - - static 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( - NativeScriptAnimationPlayer.bezieArgumentConverter(bezierArr[0]), - NativeScriptAnimationPlayer.bezieArgumentConverter(bezierArr[1]), - NativeScriptAnimationPlayer.bezieArgumentConverter(bezierArr[2]), - NativeScriptAnimationPlayer.bezieArgumentConverter(bezierArr[3])); - } else { - throw new Error("Invalid value for animation: " + value); - } - } - } - - static bezieArgumentConverter(value): number { - let result = parseFloat(value); - result = Math.max(0.0, result); - result = Math.min(1.0, result); - return result; - } - - static 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; - } - } - - static parseTransform(value: string, animationInfo: KeyframeInfo) { - let newTransform = NativeScriptAnimationPlayer.transformConverter(value); - let values = undefined; - for (let transform in newTransform) { - switch (transform) { - case "scaleX": - animationInfo.declarations.push({ - property: "scale", - value: { x: parseFloat(newTransform[transform]), y: 1 } - }); - break; - case "scaleY": - animationInfo.declarations.push({ - property: "scale", - value: { x: 1, y: parseFloat(newTransform[transform]) } - }); - break; - case "scale": - case "scale3d": - values = newTransform[transform].split(","); - if (values.length === 2 || values.length === 3) { - animationInfo.declarations.push({ - property: "scale", - value: { x: parseFloat(values[0]), y: parseFloat(values[1]) } - }); - } - break; - case "translateX": - animationInfo.declarations.push({ - property: "translate", - value: { x: parseFloat(newTransform[transform]), y: 0 } - }); - break; - case "translateY": - animationInfo.declarations.push({ - property: "translate", - value: { x: 0, y: parseFloat(newTransform[transform]) } - }); - break; - case "translate": - case "translate3d": - values = newTransform[transform].split(","); - if (values.length === 2 || values.length === 3) { - animationInfo.declarations.push({ - 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); - } - animationInfo.declarations.push({ property: "rotate", value: val }); - break; - case "none": - animationInfo.declarations.push({ property: "scale", value: { x: 1, y: 1 } }); - animationInfo.declarations.push({ property: "translate", value: { x: 0, y: 0 } }); - animationInfo.declarations.push({ property: "rotate", value: 0 }); - break; - default: - throw new Error("Unsupported transform: " + transform); - } - } - } -} diff --git a/nativescript-angular/animations.ts b/nativescript-angular/animations.ts new file mode 100644 index 000000000..30b32f3d6 --- /dev/null +++ b/nativescript-angular/animations.ts @@ -0,0 +1,52 @@ +import { NgModule, Injectable, NgZone, Provider, RendererFactory2 } from "@angular/core"; + +import { + AnimationDriver, + ɵAnimationEngine as AnimationEngine, + ɵAnimationStyleNormalizer as AnimationStyleNormalizer, + ɵ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"; +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..d1dd2b032 --- /dev/null +++ b/nativescript-angular/animations/animation-driver.ts @@ -0,0 +1,32 @@ +import { AnimationPlayer } from "@angular/animations"; +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; +} + +export class NativeScriptAnimationDriver implements AnimationDriver { + computeStyle(element: NgView, prop: string): string { + return element.style[`css-${prop}`]; + } + + animate( + element: NgView, + keyframes: Keyframe[], + 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..0dcbd51ff --- /dev/null +++ b/nativescript-angular/animations/animation-engine.ts @@ -0,0 +1,144 @@ +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 new file mode 100644 index 000000000..fcc60d8f1 --- /dev/null +++ b/nativescript-angular/animations/animation-player.ts @@ -0,0 +1,108 @@ +import { AnimationPlayer } from "@angular/animations"; +import { + KeyframeAnimation, + KeyframeAnimationInfo, +} from "tns-core-modules/ui/animation/keyframe-animation"; + +import { NgView } from "../element-registry"; +import { Keyframe, getAnimationCurve, parseAnimationKeyframe } from "./utils"; + +export class NativeScriptAnimationPlayer implements AnimationPlayer { + public parentPlayer: AnimationPlayer = null; + + private _startSubscriptions: Function[] = []; + private _doneSubscriptions: Function[] = []; + private _finished = false; + private _started = false; + private animation: KeyframeAnimation; + + constructor( + private target: NgView, + keyframes: Keyframe[], + duration: number, + delay: number, + easing: string + ) { + this.initKeyframeAnimation(keyframes, duration, delay, easing); + } + + 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); } + + play(): void { + if (!this.animation) { + return; + } + + if (!this._started) { + this._started = true; + this._startSubscriptions.forEach(fn => fn()); + this._startSubscriptions = []; + } + + 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; + } + + 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); + + this.animation = KeyframeAnimation.keyframeAnimationFromInfo(info); + } + + private onFinish() { + if (!this._finished) { + this._finished = true; + this._started = false; + this._doneSubscriptions.forEach(fn => fn()); + this._doneSubscriptions = []; + } + } +} diff --git a/nativescript-angular/animations/dom-utils.ts b/nativescript-angular/animations/dom-utils.ts 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 new file mode 100644 index 000000000..05833e3a3 --- /dev/null +++ b/nativescript-angular/animations/utils.ts @@ -0,0 +1,139 @@ +import { + 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; +} + +interface Transformation { + property: string; + value: number | { x: number, y: number }; +} + +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) { + 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); + } +} + +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; +} + +function stringToBezieCoords(value: string): number { + let result = parseFloat(value); + if (result < 0) { + return 0; + } else if (result > 1) { + return 1; + } + + return result; +} + +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()); +} + +function parseStyle(text: string): Transformation[] { + return text.split(TRANSFORM_SPLITTER).map(stringToTransformation).filter(t => !!t); +} + +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/nativescript-angular/common/detached-loader.ts b/nativescript-angular/common/detached-loader.ts index f5af4ff49..d4ae6a73f 100644 --- a/nativescript-angular/common/detached-loader.ts +++ b/nativescript-angular/common/detached-loader.ts @@ -36,7 +36,6 @@ export class DetachedLoader { // tslint:disable-line:component-class-suffix // We are inside a promise here so no need for setTimeout - CD should trigger // after the promise. log("DetachedLoader.loadInLocation component loaded -> markForCheck"); - this.changeDetector.markForCheck(); return Promise.resolve(componentRef); } diff --git a/nativescript-angular/directives/action-bar.ts b/nativescript-angular/directives/action-bar.ts index 3337d22f2..b659310eb 100644 --- a/nativescript-angular/directives/action-bar.ts +++ b/nativescript-angular/directives/action-bar.ts @@ -3,7 +3,7 @@ import { ActionItem, ActionBar, NavigationButton } from "tns-core-modules/ui/act import { isBlank } from "../lang-facade"; import { Page } from "tns-core-modules/ui/page"; import { View } from "tns-core-modules/ui/core/view"; -import { registerElement, ViewClassMeta, NgView, TEMPLATE } from "../element-registry"; +import { registerElement, ViewClassMeta, NgView } from "../element-registry"; let actionBarMeta: ViewClassMeta = { skipAddToDom: true, @@ -17,8 +17,6 @@ let actionBarMeta: ViewClassMeta = { } else if (child instanceof ActionItem) { bar.actionItems.addItem(childView); childView.parent = bar; - } else if (child.nodeName === TEMPLATE) { - child.templateParent = parent; } else if (child.nodeName !== "#text" && child instanceof View) { bar.titleView = childView; } @@ -34,8 +32,7 @@ let actionBarMeta: ViewClassMeta = { } else if (child instanceof ActionItem) { bar.actionItems.removeItem(childView); childView.parent = null; - } else if (child.nodeName !== TEMPLATE && child instanceof View && - bar.titleView && bar.titleView === childView) { + } else if (child instanceof View && bar.titleView && bar.titleView === childView) { bar.titleView = null; } }, diff --git a/nativescript-angular/directives/list-view-comp.ts b/nativescript-angular/directives/list-view-comp.ts index e3642c000..a865c0072 100644 --- a/nativescript-angular/directives/list-view-comp.ts +++ b/nativescript-angular/directives/list-view-comp.ts @@ -62,7 +62,7 @@ export class ListViewComponent implements DoCheck, OnDestroy, AfterContentInit { private listView: ListView; private _items: any; - private _differ: IterableDiffer; + private _differ: IterableDiffer; private _templateMap: Map; @ViewChild("loader", { read: ViewContainerRef }) loader: ViewContainerRef; diff --git a/nativescript-angular/dom-adapter.ts b/nativescript-angular/dom-adapter.ts index 1427c8518..119e41d91 100644 --- a/nativescript-angular/dom-adapter.ts +++ b/nativescript-angular/dom-adapter.ts @@ -1,17 +1,17 @@ /* tslint:disable */ import { Type } from "@angular/core"; -import { DomAdapter } from "./private_import_platform-browser"; +import { ɵDomAdapter } from "@angular/platform-browser"; import { rendererLog } from "./trace"; import { print } from "./lang-facade"; -export class NativeScriptDomAdapter implements DomAdapter { +export class NativeScriptDomAdapter implements ɵDomAdapter { static makeCurrent() { // Don't register when bundling (likely AoT setup). if (!global.TNS_WEBPACK) { try { - const privateAPI = global.require("@angular/platform-browser").__platform_browser_private__; - const setRootDomAdapter = privateAPI.setRootDomAdapter; + const privateAPI = global.require("@angular/platform-browser"); + const setRootDomAdapter = privateAPI.ɵsetRootDomAdapter; rendererLog("Setting root DOM adapter..."); setRootDomAdapter(new NativeScriptDomAdapter()); @@ -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!") } @@ -145,7 +146,7 @@ export class NativeScriptDomAdapter implements DomAdapter { defaultDoc(): HTMLDocument { throw new Error("Not implemented!") } getBoundingClientRect(_el: any /** TODO #9100 */): any /** TODO #9100 */ { throw new Error("Not implemented!") } getTitle(): string { throw new Error("Not implemented!") } - setTitle(_newTitle: string): any /** TODO #9100 */ { throw new Error("Not implemented!") } + setTitle(_doc: Document, _newTitle: string): any /** TODO #9100 */ { throw new Error("Not implemented!") } elementMatches(_n: any /** TODO #9100 */, _selector: string): boolean { throw new Error("Not implemented!") } isTemplateElement(_el: any): boolean { throw new Error("Not implemented!") } isTextNode(_node: any /** TODO #9100 */): boolean { throw new Error("Not implemented!") } @@ -161,7 +162,7 @@ export class NativeScriptDomAdapter implements DomAdapter { /** TODO #9100 */ { throw new Error("Not implemented!") } supportsDOMEvents(): boolean { throw new Error("Not implemented!") } supportsNativeShadowDOM(): boolean { throw new Error("Not implemented!") } - getGlobalEventTarget(_target: string): any { throw new Error("Not implemented!") } + getGlobalEventTarget(_doc: Document, _target: string): any { throw new Error("Not implemented!") } getHistory(): History { throw new Error("Not implemented!") } getLocation(): Location { throw new Error("Not implemented!") } getBaseHref(): string { throw new Error("Not implemented!") } diff --git a/nativescript-angular/element-registry.ts b/nativescript-angular/element-registry.ts index 8686a4bea..534377b0c 100644 --- a/nativescript-angular/element-registry.ts +++ b/nativescript-angular/element-registry.ts @@ -2,16 +2,14 @@ import { View } from "tns-core-modules/ui/core/view"; export type ViewResolver = () => ViewClass; export type NgView = View & ViewExtensions; -export const TEMPLATE = "template"; - export interface ViewClassMeta { skipAddToDom?: boolean; insertChild?: (parent: NgView, child: NgView, atIndex: number) => void; removeChild?: (parent: NgView, child: NgView) => void; - isTemplateAnchor?: boolean; } export interface ViewExtensions { + nodeType: number; nodeName: string; templateParent: NgView; ngCssClasses: Map; @@ -71,11 +69,6 @@ export function isKnownView(elementName: string): boolean { elementMap.has(elementName.toLowerCase()); } -// Empty view used for template anchors -export class TemplateView extends View { -} -registerElement(TEMPLATE, () => TemplateView, { isTemplateAnchor: true }); - // Register default NativeScript components // Note: ActionBar related components are registerd together with action-bar directives. registerElement("AbsoluteLayout", () => require("tns-core-modules/ui/layouts/absolute-layout").AbsoluteLayout); @@ -117,3 +110,10 @@ registerElement("Span", () => require("tns-core-modules/text/span").Span); registerElement("DetachedContainer", () => require("tns-core-modules/ui/proxy-view-container").ProxyViewContainer, { skipAddToDom: true }); + +registerElement("DetachedText", () => require("ui/placeholder").Placeholder, + { skipAddToDom: true }); + +registerElement("Comment", () => require("ui/placeholder").Placeholder, + { skipAddToDom: false }); + diff --git a/nativescript-angular/http/ns-http.ts b/nativescript-angular/http/ns-http.ts index 70c9d3de4..6b6fff33e 100644 --- a/nativescript-angular/http/ns-http.ts +++ b/nativescript-angular/http/ns-http.ts @@ -2,6 +2,7 @@ import { Injectable } from "@angular/core"; import { Http, ConnectionBackend, + RequestOptions, RequestOptionsArgs, ResponseOptions, ResponseType, @@ -19,7 +20,7 @@ export class NSXSRFStrategy { @Injectable() export class NSHttp extends Http { - constructor(backend: ConnectionBackend, defaultOptions: any, private nsFileSystem: NSFileSystem) { + constructor(backend: ConnectionBackend, defaultOptions: RequestOptions, private nsFileSystem: NSFileSystem) { super(backend, defaultOptions); } diff --git a/nativescript-angular/index.ts b/nativescript-angular/index.ts index f8c21ffb6..e1309f312 100644 --- a/nativescript-angular/index.ts +++ b/nativescript-angular/index.ts @@ -14,17 +14,14 @@ export * from "./file-system/ns-file-system"; export * from "./modal-dialog"; export * from "./renderer"; export * from "./view-util"; -export * from "./animation-driver"; export * from "./resource-loader"; export { ViewResolver, - TEMPLATE, ViewClass, ViewClassMeta, registerElement, getViewClass, getViewMeta, isKnownView, - TemplateView } from "./element-registry"; export * from "./value-accessors/base-value-accessor"; diff --git a/nativescript-angular/nativescript.module.ts b/nativescript-angular/nativescript.module.ts index 6c84cafaa..dd84f6366 100644 --- a/nativescript-angular/nativescript.module.ts +++ b/nativescript-angular/nativescript.module.ts @@ -1,4 +1,6 @@ -import "globals"; +import "tns-core-modules/globals"; +// Require application early to work around a circular import +import "tns-core-modules/application"; import "./zone-js/dist/zone-nativescript"; import "reflect-metadata"; @@ -6,14 +8,13 @@ import "./polyfills/array"; import "./polyfills/console"; import { CommonModule } from "@angular/common"; -import { NativeScriptRootRenderer, NativeScriptRenderer } from "./renderer"; +import { NativeScriptRendererFactory } from "./renderer"; import { DetachedLoader } from "./common/detached-loader"; import { ModalDialogHost, ModalDialogService } from "./directives/dialogs"; import { ApplicationModule, ErrorHandler, - Renderer, - RootRenderer, + RendererFactory2, NgModule, NO_ERRORS_SCHEMA, } from "@angular/core"; import { @@ -38,11 +39,8 @@ export function errorHandlerFactory() { defaultFrameProvider, defaultPageProvider, defaultDeviceProvider, - - NativeScriptRootRenderer, - { provide: RootRenderer, useClass: NativeScriptRootRenderer }, - NativeScriptRenderer, - { provide: Renderer, useClass: NativeScriptRenderer }, + NativeScriptRendererFactory, + { provide: RendererFactory2, useClass: NativeScriptRendererFactory }, ModalDialogService ], entryComponents: [ diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json index 37aee05c0..e9d84752b 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -16,35 +16,37 @@ "scripts": { "tslint": "tslint --project tsconfig.json --config tslint.json", "postinstall": "node postinstall.js", - "ngc": "ngc -p tsconfig.json", "tsc": "tsc -p tsconfig.json", + "ngc": "ngc -p tsconfig.json", "prepublish": "npm run tsc && npm run ngc" }, "bin": { "update-app-ng-deps": "./bin/update-app-ng-deps" }, "dependencies": { - "@angular/common": "~2.4.3", - "@angular/compiler": "~2.4.3", - "@angular/core": "~2.4.3", - "@angular/forms": "~2.4.3", - "@angular/http": "~2.4.3", - "@angular/platform-browser": "~2.4.3", - "@angular/router": "~3.4.3", "nativescript-intl": "~0.0.8", + "@angular/core": "~4.0.0", + "@angular/common": "~4.0.0", + "@angular/compiler": "~4.0.0", + "@angular/http": "~4.0.0", + "@angular/platform-browser": "~4.0.0", + "@angular/platform-browser-dynamic": "~4.0.0", + "@angular/forms": "~4.0.0", + "@angular/router": "~4.0.0", + "rxjs": "^5.0.1", + "reflect-metadata": "~0.1.8", "punycode": "1.3.2", "querystring": "0.2.0", - "reflect-metadata": "~0.1.8", - "rxjs": "~5.0.1", "url": "0.10.3" }, "devDependencies": { - "@angular/compiler-cli": "~2.4.3", - "codelyzer": "~2.0.0", + "@angular/animations": "~4.0.0", + "@angular/compiler-cli": "~4.0.0", + "codelyzer": "~3.0.0-beta.4", "tns-core-modules": "internal-preview", - "tslint": "~4.4.0", + "tslint": "~4.5.0", "typescript": "~2.2.1", - "zone.js": "^0.7.2" + "zone.js": "^0.8.2" }, "nativescript": {} } diff --git a/nativescript-angular/platform-common.ts b/nativescript-angular/platform-common.ts index 4cb686175..a8381f5c5 100644 --- a/nativescript-angular/platform-common.ts +++ b/nativescript-angular/platform-common.ts @@ -1,5 +1,7 @@ // Initial imports and polyfills -import "globals"; +import "tns-core-modules/globals"; +// Require application early to work around a circular import +import "tns-core-modules/application"; import "./zone-js/dist/zone-nativescript"; import "reflect-metadata"; import "./polyfills/array"; diff --git a/nativescript-angular/platform.ts b/nativescript-angular/platform.ts index 42cfd3984..1c418e794 100644 --- a/nativescript-angular/platform.ts +++ b/nativescript-angular/platform.ts @@ -16,15 +16,19 @@ import { import { COMPILER_OPTIONS, PlatformRef, - OpaqueToken, - createPlatformFactory + InjectionToken, + ViewEncapsulation, + createPlatformFactory, + MissingTranslationStrategy } from "@angular/core"; -// Work around a TS bug requiring an import of OpaqueToken without using it +// Work around a TS bug requiring an imports of +// InjectionToken, ViewEncapsulation and MissingTranslationStrategy +// without using them if ((global).___TS_UNUSED) { - (() => { - return OpaqueToken; - })(); + (() => InjectionToken)(); + (() => ViewEncapsulation)(); + (() => MissingTranslationStrategy)(); } // Register DOM adapter, if possible. Dynamic platform only! diff --git a/nativescript-angular/private_import_core.ts b/nativescript-angular/private_import_core.ts deleted file mode 100644 index 58e80a668..000000000 --- a/nativescript-angular/private_import_core.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @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 - */ - -import { __core_private__ as r } from "@angular/core"; - -export type RenderDebugInfo = typeof r._RenderDebugInfo; -export let RenderDebugInfo: typeof r.RenderDebugInfo = r.RenderDebugInfo; - -export let ReflectionCapabilities: typeof r.ReflectionCapabilities = r.ReflectionCapabilities; - -export type DebugDomRootRenderer = typeof r._DebugDomRootRenderer; -export let DebugDomRootRenderer: typeof r.DebugDomRootRenderer = r.DebugDomRootRenderer; -export let reflector: typeof r.reflector = r.reflector; - -export type NoOpAnimationPlayer = typeof r._NoOpAnimationPlayer; -export let NoOpAnimationPlayer: typeof r.NoOpAnimationPlayer = r.NoOpAnimationPlayer; -export type AnimationPlayer = typeof r._AnimationPlayer; -export let AnimationPlayer: typeof r.AnimationPlayer = r.AnimationPlayer; -export type AnimationSequencePlayer = typeof r._AnimationSequencePlayer; -export let AnimationSequencePlayer: typeof r.AnimationSequencePlayer = r.AnimationSequencePlayer; -export type AnimationGroupPlayer = typeof r._AnimationGroupPlayer; -export let AnimationGroupPlayer: typeof r.AnimationGroupPlayer = r.AnimationGroupPlayer; -export type AnimationKeyframe = typeof r._AnimationKeyframe; -export let AnimationKeyframe: typeof r.AnimationKeyframe = r.AnimationKeyframe; -export type AnimationStyles = typeof r._AnimationStyles; -export let AnimationStyles: typeof r.AnimationStyles = r.AnimationStyles; -export let prepareFinalAnimationStyles: typeof r.prepareFinalAnimationStyles = - r.prepareFinalAnimationStyles; -export let balanceAnimationKeyframes: typeof r.balanceAnimationKeyframes = - r.balanceAnimationKeyframes; -export let clearStyles: typeof r.clearStyles = r.clearStyles; -export let collectAndResolveStyles: typeof r.collectAndResolveStyles = r.collectAndResolveStyles; diff --git a/nativescript-angular/private_import_platform-browser.ts b/nativescript-angular/private_import_platform-browser.ts deleted file mode 100644 index a9ab67f25..000000000 --- a/nativescript-angular/private_import_platform-browser.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @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 - */ - -import { __platform_browser_private__ as _ } from "@angular/platform-browser"; - -export type DomAdapter = typeof _._DomAdapter; -export let DomAdapter: typeof _.DomAdapter = _.DomAdapter; -export let setRootDomAdapter: typeof _.setRootDomAdapter = _.setRootDomAdapter; -export let getDOM: typeof _.getDOM = _.getDOM; diff --git a/nativescript-angular/renderer.ts b/nativescript-angular/renderer.ts index eef9fb3d5..9773b0bf9 100644 --- a/nativescript-angular/renderer.ts +++ b/nativescript-angular/renderer.ts @@ -1,183 +1,187 @@ import { Inject, Injectable, Optional, NgZone, - Renderer, RootRenderer, RenderComponentType, AnimationPlayer + Renderer2, RendererFactory2, RendererType2, + RendererStyleFlags2, ViewEncapsulation, } from "@angular/core"; -import { AnimationStyles, AnimationKeyframe } from "./private_import_core"; -import { APP_ROOT_VIEW, DEVICE } from "./platform-providers"; -import { isBlank } from "./lang-facade"; + +import { Device } from "tns-core-modules/platform"; import { View } from "tns-core-modules/ui/core/view"; import { addCss } from "tns-core-modules/application"; import { topmost } from "tns-core-modules/ui/frame"; -import { Page } from "tns-core-modules/ui/page"; + +import { APP_ROOT_VIEW, DEVICE, getRootPage } from "./platform-providers"; +import { isBlank } from "./lang-facade"; import { ViewUtil } from "./view-util"; import { NgView } from "./element-registry"; import { rendererLog as traceLog } from "./trace"; -import { escapeRegexSymbols } from "tns-core-modules/utils/utils"; -import { Device } from "tns-core-modules/platform"; -import { getRootPage } from "./platform-providers"; - -import { NativeScriptAnimationDriver } from "./animation-driver"; -// CONTENT_ATTR not exported from dom_renderer - we need it for styles application. +// CONTENT_ATTR not exported from NativeScript_renderer - we need it for styles application. +const COMPONENT_REGEX = /%COMP%/g; export const COMPONENT_VARIABLE = "%COMP%"; +export const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; export const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`; - +const ATTR_SANITIZER = /-/g; @Injectable() -export class NativeScriptRootRenderer implements RootRenderer { - private _viewUtil: ViewUtil; - private _animationDriver: NativeScriptAnimationDriver; - - protected get animationDriver(): NativeScriptAnimationDriver { - if (!this._animationDriver) { - this._animationDriver = new NativeScriptAnimationDriver(); - } - return this._animationDriver; - } +export class NativeScriptRendererFactory implements RendererFactory2 { + private componentRenderers = new Map(); + private viewUtil: ViewUtil; + private defaultRenderer: NativeScriptRenderer; + private rootNgView: NgView; constructor( - @Optional() @Inject(APP_ROOT_VIEW) private _rootView: View, + @Optional() @Inject(APP_ROOT_VIEW) rootView: View, @Inject(DEVICE) device: Device, - private _zone: NgZone + private zone: NgZone ) { - this._viewUtil = new ViewUtil(device); + this.viewUtil = new ViewUtil(device); + this.setRootNgView(rootView); + this.defaultRenderer = new NativeScriptRenderer(this.rootNgView, zone, this.viewUtil); } - private _registeredComponents = new Map(); - - public get rootView(): View { - if (!this._rootView) { - this._rootView = getRootPage() || topmost().currentPage; + private setRootNgView(rootView: any) { + if (!rootView) { + rootView = getRootPage() || topmost().currentPage; } - return this._rootView; + rootView.nodeName = "NONE"; + this.rootNgView = rootView; } - public get page(): Page { - return this.rootView.page; - } + createRenderer(element: any, type: RendererType2): NativeScriptRenderer { + if (!element || !type) { + return this.defaultRenderer; + } - public get viewUtil(): ViewUtil { - return this._viewUtil; - } + let renderer: NativeScriptRenderer = this.componentRenderers.get(type.id); + if (!isBlank(renderer)) { + return renderer; + } - renderComponent(componentProto: RenderComponentType): Renderer { - let renderer = this._registeredComponents.get(componentProto.id); - if (isBlank(renderer)) { - renderer = new NativeScriptRenderer(this, componentProto, - this.animationDriver, this._zone); - this._registeredComponents.set(componentProto.id, renderer); + if (type.encapsulation === ViewEncapsulation.Emulated) { + renderer = new EmulatedRenderer(type, this.rootNgView, this.zone, this.viewUtil); + (renderer).applyToHost(element); + } else { + renderer = this.defaultRenderer; } + + this.componentRenderers.set(type.id, renderer); return renderer; } } -@Injectable() -export class NativeScriptRenderer extends Renderer { - private componentProtoId: string; - private hasComponentStyles: boolean; +export class NativeScriptRenderer extends Renderer2 { + data: { [key: string]: any } = Object.create(null); - private get viewUtil(): ViewUtil { - return this.rootRenderer.viewUtil; + constructor( + private rootView: NgView, + private zone: NgZone, + private viewUtil: ViewUtil + ) { + super(); + traceLog("NativeScriptRenderer created"); } - constructor( - private rootRenderer: NativeScriptRootRenderer, - private componentProto: RenderComponentType, - private animationDriver: NativeScriptAnimationDriver, - private zone: NgZone) { + appendChild(parent: any, newChild: NgView): void { + traceLog(`NativeScriptRenderer.appendChild child: ${newChild} parent: ${parent}`); - super(); - let stylesLength = this.componentProto.styles.length; - this.componentProtoId = this.componentProto.id; - for (let i = 0; i < stylesLength; i++) { - this.hasComponentStyles = true; - let cssString = this.componentProto.styles[i] + ""; - const realCSS = this.replaceNgAttribute(cssString, this.componentProtoId); - addCss(realCSS); + if (parent) { + this.viewUtil.insertChild(parent, newChild); } - traceLog("NativeScriptRenderer created"); } - private attrReplacer = new RegExp(escapeRegexSymbols(CONTENT_ATTR), "g"); - private attrSanitizer = /-/g; + insertBefore(parent: NgView, newChild: NgView, refChildIndex: number): void { + traceLog(`NativeScriptRenderer.insertBefore child: ${newChild} parent: ${parent}`); - private replaceNgAttribute(input: string, componentId: string): string { - return input.replace(this.attrReplacer, - "_ng_content_" + componentId.replace(this.attrSanitizer, "_")); + if (parent) { + this.viewUtil.insertChild(parent, newChild, refChildIndex); + } } - renderComponent(componentProto: RenderComponentType): Renderer { - return this.rootRenderer.renderComponent(componentProto); + removeChild(parent: any, oldChild: NgView): void { + traceLog(`NativeScriptRenderer.removeChild child: ${oldChild} parent: ${parent}`); + + if (parent) { + this.viewUtil.removeChild(parent, oldChild); + } } selectRootElement(selector: string): NgView { traceLog("selectRootElement: " + selector); - const rootView = this.rootRenderer.rootView; - rootView.nodeName = "ROOT"; - return rootView; + return this.rootView; + } + + parentNode(node: NgView): any { + return node.parent; + } + + nextSibling(node: NgView): number { + traceLog(`NativeScriptRenderer.nextSibling ${node}`); + return this.viewUtil.nextSiblingIndex(node); + } + + createComment(_value: any) { + traceLog(`NativeScriptRenderer.createComment ${_value}`); + return this.viewUtil.createComment(); + } + + createElement(name: any, _namespace: string): NgView { + traceLog(`NativeScriptRenderer.createElement: ${name}`); + return this.viewUtil.createView(name); + } + + createText(_value: string) { + traceLog(`NativeScriptRenderer.createText ${_value}`); + return this.viewUtil.createText(); } createViewRoot(hostElement: NgView): NgView { - traceLog("CREATE VIEW ROOT: " + hostElement.nodeName); + traceLog(`NativeScriptRenderer.createViewRoot ${hostElement.nodeName}`); return hostElement; } projectNodes(parentElement: NgView, nodes: NgView[]): void { traceLog("NativeScriptRenderer.projectNodes"); - nodes.forEach((node) => { - this.viewUtil.insertChild(parentElement, node); - }); + nodes.forEach((node) => this.viewUtil.insertChild(parentElement, node)); } - attachViewAfter(anchorNode: NgView, viewRootNodes: NgView[]) { - traceLog("NativeScriptRenderer.attachViewAfter: " + anchorNode.nodeName + " " + anchorNode); - const parent = (anchorNode.parent || anchorNode.templateParent); - const insertPosition = this.viewUtil.getChildIndex(parent, anchorNode); - - viewRootNodes.forEach((node, index) => { - const childIndex = insertPosition + index + 1; - this.viewUtil.insertChild(parent, node, childIndex); - }); + destroy() { + traceLog("NativeScriptRenderer.destroy"); + // Seems to be called on component dispose only (router outlet) + // TODO: handle this when we resolve routing and navigation. } - detachView(viewRootNodes: NgView[]) { - traceLog("NativeScriptRenderer.detachView"); - for (let i = 0; i < viewRootNodes.length; i++) { - let node = viewRootNodes[i]; - this.viewUtil.removeChild(node.parent, node); - } + setAttribute(view: NgView, name: string, value: string, namespace?: string) { + traceLog(`NativeScriptRenderer.setAttribute ${view} : ${name} = ${value}, namespace: ${namespace}`); + return this.viewUtil.setProperty(view, name, value, namespace); } - public destroyView(_hostElement: NgView, _viewAllNodes: NgView[]) { - traceLog("NativeScriptRenderer.destroyView"); - // Seems to be called on component dispose only (router outlet) - // TODO: handle this when we resolve routing and navigation. + removeAttribute(_el: NgView, _name: string): void { + traceLog(`NativeScriptRenderer.removeAttribute ${_el}: ${_name}`); } - setElementProperty(renderElement: NgView, propertyName: string, propertyValue: any) { - traceLog("NativeScriptRenderer.setElementProperty " + renderElement + ": " + - propertyName + " = " + propertyValue); - this.viewUtil.setProperty(renderElement, propertyName, propertyValue); + setProperty(view: any, name: string, value: any) { + traceLog(`NativeScriptRenderer.setProperty ${view} : ${name} = ${value}`); + return this.viewUtil.setProperty(view, name, value); } - setElementAttribute(renderElement: NgView, attributeName: string, attributeValue: string) { - traceLog("NativeScriptRenderer.setElementAttribute " + renderElement + ": " + - attributeName + " = " + attributeValue); - return this.setElementProperty(renderElement, attributeName, attributeValue); + addClass(view: NgView, name: string): void { + traceLog(`NativeScriptRenderer.addClass ${name}`); + this.viewUtil.addClass(view, name); } - setElementClass(renderElement: NgView, className: string, isAdd: boolean): void { - traceLog("NativeScriptRenderer.setElementClass " + className + " - " + isAdd); + removeClass(view: NgView, name: string): void { + traceLog(`NativeScriptRenderer.removeClass ${name}`); + this.viewUtil.removeClass(view, name); + } - if (isAdd) { - this.viewUtil.addClass(renderElement, className); - } else { - this.viewUtil.removeClass(renderElement, className); - } + setStyle(view: NgView, styleName: string, value: any, _flags?: RendererStyleFlags2): void { + traceLog(`NativeScriptRenderer.setStyle: ${styleName} = ${value}`); + this.viewUtil.setStyle(view, styleName, value); } - setElementStyle(renderElement: NgView, styleName: string, styleValue: string): void { - this.viewUtil.setStyleProperty(renderElement, styleName, styleValue); + removeStyle(view: NgView, styleName: string, _flags?: RendererStyleFlags2): void { + traceLog("NativeScriptRenderer.removeStyle: ${styleName}"); + this.viewUtil.removeStyle(view, styleName); } // Used only in debug mode to serialize property changes to comment nodes, @@ -195,34 +199,12 @@ export class NativeScriptRenderer extends Renderer { traceLog("NativeScriptRenderer.invokeElementMethod " + methodName + " " + args); } - setText(_renderNode: any, _text: string) { - traceLog("NativeScriptRenderer.setText"); - } - - public createTemplateAnchor(parentElement: NgView): NgView { - traceLog("NativeScriptRenderer.createTemplateAnchor"); - return this.viewUtil.createTemplateAnchor(parentElement); - } - - public createElement(parentElement: NgView, name: string): NgView { - traceLog("NativeScriptRenderer.createElement: " + name + " parent: " + - parentElement + ", " + (parentElement ? parentElement.nodeName : "null")); - return this.viewUtil.createView(name, parentElement, (view) => { - // Set an attribute to the view to scope component-specific css. - // The property name is pre-generated by Angular. - if (this.hasComponentStyles) { - const cssAttribute = this.replaceNgAttribute(CONTENT_ATTR, this.componentProtoId); - view[cssAttribute] = true; - } - }); - } - - public createText(_parentElement: NgView, _value: string): NgView { - traceLog("NativeScriptRenderer.createText"); - return this.viewUtil.createText(); + setValue(_renderNode: any, _value: string) { + traceLog("NativeScriptRenderer.setValue"); } - public listen(renderElement: NgView, eventName: string, callback: Function): Function { + listen(renderElement: any, eventName: string, callback: (event: any) => boolean): + () => void { traceLog("NativeScriptRenderer.listen: " + eventName); // Explicitly wrap in zone let zonedCallback = (...args) => { @@ -238,21 +220,52 @@ export class NativeScriptRenderer extends Renderer { } return () => renderElement.off(eventName, zonedCallback); } +} + +class EmulatedRenderer extends NativeScriptRenderer { + private contentAttr: string; + private hostAttr: string; + + constructor( + component: RendererType2, + rootView: NgView, + zone: NgZone, + viewUtil: ViewUtil, + ) { + super(rootView, zone, viewUtil); + + const componentId = component.id.replace(ATTR_SANITIZER, "_"); + this.contentAttr = replaceNgAttribute(CONTENT_ATTR, componentId); + this.hostAttr = replaceNgAttribute(HOST_ATTR, componentId); + this.addStyles(component.styles, componentId); + } - public listenGlobal(_target: string, _eventName: string, _callback: Function): Function { - throw new Error("NativeScriptRenderer.listenGlobal() - Not implemented."); + applyToHost(view: NgView) { + super.setAttribute(view, this.hostAttr, ""); } - public animate( - element: any, - startingStyles: AnimationStyles, - keyframes: AnimationKeyframe[], - duration: number, - delay: number, - easing: string - ): AnimationPlayer { - let player = this.animationDriver.animate( - element, startingStyles, keyframes, duration, delay, easing); - return player; + appendChild(parent: any, newChild: NgView): void { + super.appendChild(parent, newChild); } + + createElement(parent: any, name: string): NgView { + const view = super.createElement(parent, name); + + // Set an attribute to the view to scope component-specific css. + // The property name is pre-generated by Angular. + super.setAttribute(view, this.contentAttr, ""); + + return view; + } + + private addStyles(styles: (string | any[])[], componentId: string) { + styles.map(s => s.toString()) + .map(s => replaceNgAttribute(s, componentId)) + .forEach(addCss); + } + +} + +function replaceNgAttribute(input: string, componentId: string): string { + return input.replace(COMPONENT_REGEX, componentId); } diff --git a/nativescript-angular/router/ns-router-link.ts b/nativescript-angular/router/ns-router-link.ts index 1cb1d9b95..e73c53b16 100644 --- a/nativescript-angular/router/ns-router-link.ts +++ b/nativescript-angular/router/ns-router-link.ts @@ -34,85 +34,89 @@ import { isString } from "tns-core-modules/utils/types"; */ @Directive({ selector: "[nsRouterLink]" }) export class NSRouterLink implements OnChanges { // tslint:disable-line:directive-class-suffix - private commands: any[] = []; - @Input() target: string; - @Input() queryParams: { [k: string]: any }; - @Input() fragment: string; + private commands: any[] = []; + @Input() target: string; + @Input() queryParams: { [k: string]: any }; + @Input() fragment: string; - @Input() clearHistory: boolean; - @Input() pageTransition: boolean | string | NavigationTransition = true; + @Input() clearHistory: boolean; + @Input() pageTransition: boolean | string | NavigationTransition = true; - urlTree: UrlTree; + urlTree: UrlTree; - private usePageRoute: boolean; + private usePageRoute: boolean; - private get currentRoute(): ActivatedRoute { - return this.usePageRoute ? this.pageRoute.activatedRoute.getValue() : this.route; - } - - constructor( - private router: Router, - private navigator: RouterExtensions, - private route: ActivatedRoute, - @Optional() private pageRoute: PageRoute) { - - this.usePageRoute = (this.pageRoute && this.route === this.pageRoute.activatedRoute.getValue()); - } - - @Input("nsRouterLink") - set params(data: any[] | string) { - if (Array.isArray(data)) { - this.commands = data; - } else { - this.commands = [data]; + private get currentRoute(): ActivatedRoute { + return this.usePageRoute ? this.pageRoute.activatedRoute.getValue() : this.route; } - } - - - @HostListener("tap") - onTap() { - routerLog("nsRouterLink.tapped: " + this.commands + " usePageRoute: " + - this.usePageRoute + " clearHistory: " + this.clearHistory + " transition: " + - JSON.stringify(this.pageTransition)); - - const transition = this.getTransition(); - - let extras: NavigationExtras & NavigationOptions = { - relativeTo: this.currentRoute, - queryParams: this.queryParams, - fragment: this.fragment, - clearHistory: this.clearHistory, - animated: transition.animated, - transition: transition.transition - }; - - this.navigator.navigate(this.commands, extras); - } - - private getTransition(): { animated: boolean, transition?: NavigationTransition } { - if (typeof this.pageTransition === "boolean") { - return { animated: this.pageTransition }; - } else if (isString(this.pageTransition)) { - if (this.pageTransition === "none" || this.pageTransition === "false") { - return { animated: false }; - } else { - return { animated: true, transition: { name: this.pageTransition } }; - } - } else { - return { - animated: true, - transition: this.pageTransition - }; - } - } - - ngOnChanges(_: {}): any { - this.updateUrlTree(); - } - private updateUrlTree(): void { - this.urlTree = this.router.createUrlTree( - this.commands, - { relativeTo: this.currentRoute, queryParams: this.queryParams, fragment: this.fragment }); - } + constructor( + private router: Router, + private navigator: RouterExtensions, + private route: ActivatedRoute, + @Optional() private pageRoute: PageRoute) { + + this.usePageRoute = (this.pageRoute && this.route === this.pageRoute.activatedRoute.getValue()); + } + + @Input("nsRouterLink") + set params(data: any[] | string) { + if (Array.isArray(data)) { + this.commands = data; + } else { + this.commands = [data]; + } + } + + + @HostListener("tap") + onTap() { + routerLog("nsRouterLink.tapped: " + this.commands + " usePageRoute: " + + this.usePageRoute + " clearHistory: " + this.clearHistory + " transition: " + + JSON.stringify(this.pageTransition)); + + const extras = this.getExtras(); + this.navigator.navigate(this.commands, extras); + } + + private getExtras() { + const transition = this.getTransition(); + const extras: NavigationExtras & NavigationOptions = { + queryParams: this.queryParams, + fragment: this.fragment, + clearHistory: this.clearHistory, + animated: transition.animated, + transition: transition.transition, + }; + + return (Object).assign(extras, + this.currentRoute.toString() !== "Route(url:'', path:'')" && this.currentRoute); + } + + private getTransition(): { animated: boolean, transition?: NavigationTransition } { + if (typeof this.pageTransition === "boolean") { + return { animated: this.pageTransition }; + } else if (isString(this.pageTransition)) { + if (this.pageTransition === "none" || this.pageTransition === "false") { + return { animated: false }; + } else { + return { animated: true, transition: { name: this.pageTransition } }; + } + } else { + return { + animated: true, + transition: this.pageTransition + }; + } + } + + ngOnChanges(_: {}): any { + this.updateUrlTree(); + } + + private updateUrlTree(): void { + this.urlTree = this.router.createUrlTree( + this.commands, + { relativeTo: this.currentRoute, queryParams: this.queryParams, fragment: this.fragment }); + } } diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts index c764f69c4..690643325 100644 --- a/nativescript-angular/router/page-router-outlet.ts +++ b/nativescript-angular/router/page-router-outlet.ts @@ -1,20 +1,21 @@ import { Attribute, ComponentFactory, ComponentRef, Directive, - ReflectiveInjector, ResolvedReflectiveProvider, ViewContainerRef, + ViewContainerRef, Inject, ComponentFactoryResolver, Injector } from "@angular/core"; -import { isPresent } from "../lang-facade"; import { RouterOutletMap, ActivatedRoute, PRIMARY_OUTLET } from "@angular/router"; -import { NSLocationStrategy } from "./ns-location-strategy"; -import { DEVICE, PAGE_FACTORY, PageFactory } from "../platform-providers"; import { Device } from "tns-core-modules/platform"; -import { routerLog } from "../trace"; -import { DetachedLoader } from "../common/detached-loader"; -import { ViewUtil } from "../view-util"; import { Frame } from "tns-core-modules/ui/frame"; import { Page, NavigatedData } from "tns-core-modules/ui/page"; import { BehaviorSubject } from "rxjs/BehaviorSubject"; +import { isPresent } from "../lang-facade"; +import { DEVICE, PAGE_FACTORY, PageFactory } from "../platform-providers"; +import { routerLog } from "../trace"; +import { DetachedLoader } from "../common/detached-loader"; +import { ViewUtil } from "../view-util"; +import { NSLocationStrategy } from "./ns-location-strategy"; + interface CacheItem { componentRef: ComponentRef; reusedRoute: PageRoute; @@ -68,7 +69,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 +95,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 +148,40 @@ 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 }])]; 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, []); + + const injector = new OutletInjector(activatedRoute, outletMap, this.location.injector); + this.currentActivatedComp = this.location.createComponent( + factory, this.location.length, injector, []); + + this.currentActivatedComp.changeDetectorRef.detectChanges(); + this.refCache.push(this.currentActivatedComp, pageRoute, outletMap, null); } else { @@ -186,16 +192,17 @@ 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 childInjector = new ChildInjector(activatedRoute, outletMap, page, this.location.injector); + + const loaderRef = this.location.createComponent( + this.detachedLoaderFactory, this.location.length, childInjector, []); + loaderRef.changeDetectorRef.detectChanges(); this.currentActivatedComp = loaderRef.instance.loadWithFactory(factory); this.loadComponentInPage(page, this.currentActivatedComp); + + this.currentActivatedComp.changeDetectorRef.detectChanges(); this.refCache.push(this.currentActivatedComp, pageRoute, outletMap, loaderRef); } } @@ -269,6 +276,38 @@ 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); + } +} + +class ChildInjector extends OutletInjector { + constructor( + route: ActivatedRoute, map: RouterOutletMap, private page: Page, parent: Injector) { + super(route, map, parent); + } + + get(token: any, notFoundValue?: any): any { + if (token === Page) { + return this.page; + } + + return super.get(token, notFoundValue); + } +} + function log(msg: string) { routerLog(msg); } diff --git a/nativescript-angular/tsconfig.json b/nativescript-angular/tsconfig.json index fc85cc25c..17fd0ba01 100644 --- a/nativescript-angular/tsconfig.json +++ b/nativescript-angular/tsconfig.json @@ -16,8 +16,16 @@ "noImplicitAny": false, "lib": [ "dom", - "es6" - ] + "es6", + "es2015.iterable" + ], + "baseUrl": ".", + "paths": { + "*": [ + "./node_modules/tns-core-modules/*", + "./node_modules/*" + ] + } }, "angularCompilerOptions": { "genDir": ".", diff --git a/nativescript-angular/view-util.ts b/nativescript-angular/view-util.ts index e9804df05..a395e961f 100644 --- a/nativescript-angular/view-util.ts +++ b/nativescript-angular/view-util.ts @@ -4,25 +4,24 @@ import { Placeholder } from "tns-core-modules/ui/placeholder"; import { ContentView } from "tns-core-modules/ui/content-view"; import { LayoutBase } from "tns-core-modules/ui/layouts/layout-base"; import { - ViewClass, getViewClass, getViewMeta, isKnownView, ViewExtensions, NgView, - TEMPLATE } from "./element-registry"; import { platformNames, Device } from "tns-core-modules/platform"; import { rendererLog as traceLog } from "./trace"; -const IOS_PREFX: string = ":ios:"; -const ANDROID_PREFX: string = ":android:"; +const XML_ATTRIBUTES = Object.freeze(["style", "rows", "columns", "fontAttributes"]); +const ELEMENT_NODE_TYPE = 1; const whiteSpaceSplitter = /\s+/; export type ViewExtensions = ViewExtensions; export type NgView = NgView; export type NgLayoutBase = LayoutBase & ViewExtensions; export type NgContentView = ContentView & ViewExtensions; +export type NgPlaceholder = Placeholder & ViewExtensions; export type BeforeAttachAction = (view: View) => void; export function isView(view: any): view is NgView { @@ -48,7 +47,7 @@ export class ViewUtil { this.isAndroid = device.os === platformNames.android; } - public insertChild(parent: any, child: NgView, atIndex = -1) { + public insertChild(parent: any, child: NgView, atIndex: number = -1) { if (!parent || child.meta.skipAddToDom) { return; } @@ -69,7 +68,7 @@ export class ViewUtil { } } else if (isContentView(parent)) { // Explicit handling of template anchors inside ContentView - if (child.meta.isTemplateAnchor) { + if (child.nodeName === "#comment") { parent._addView(child, atIndex); } else { parent.content = child; @@ -96,7 +95,7 @@ export class ViewUtil { } // Explicit handling of template anchors inside ContentView - if (child.meta.isTemplateAnchor) { + if (child.nodeName === "#comment") { parent._removeView(child); } } else if (isView(parent)) { @@ -116,92 +115,45 @@ export class ViewUtil { } } - private createAndAttach( - name: string, - viewClass: ViewClass, - parent: NgView, - beforeAttach?: BeforeAttachAction - ): NgView { - const view = new viewClass(); - view.nodeName = name; - view.meta = getViewMeta(name); - if (beforeAttach) { - beforeAttach(view); - } - if (parent) { - this.insertChild(parent, view); - } - return view; - } + public createComment(): NgView { + const commentView = this.createView("Comment"); + commentView.nodeName = "#comment"; + commentView.visibility = "collapse"; - public createView(name: string, parent: NgView, beforeAttach?: BeforeAttachAction): NgView { - if (isKnownView(name)) { - const viewClass = getViewClass(name); - return this.createAndAttach(name, viewClass, parent, beforeAttach); - } else { - return this.createViewContainer(parent, beforeAttach); - } + return commentView; } public createText(): NgView { - const text = new Placeholder(); - text.nodeName = "#text"; - text.visibility = "collapse"; - text.meta = getViewMeta("Placeholder"); - return text; - } - - public createViewContainer(parentElement: NgView, beforeAttach: BeforeAttachAction) { - traceLog("Creating view container in:" + parentElement); + const detachedText = this.createView("DetachedText"); + detachedText.nodeName = "#text"; + detachedText.visibility = "collapse"; - const layout = this.createView("ProxyViewContainer", parentElement, beforeAttach); - layout.nodeName = "ProxyViewContainer"; - return layout; + return detachedText; } - public createTemplateAnchor(parentElement: NgView) { - const viewClass = getViewClass(TEMPLATE); - const anchor = this.createAndAttach(TEMPLATE, viewClass, parentElement); - anchor.templateParent = parentElement; - anchor.visibility = "collapse"; - traceLog("Created templateAnchor: " + anchor); - return anchor; - } + public createView(name: string): NgView { + traceLog(`Creating view: ${name}`); - private isXMLAttribute(name: string): boolean { - switch (name) { - case "style": return true; - case "rows": return true; - case "columns": return true; - case "fontAttributes": return true; - default: return false; + if (!isKnownView(name)) { + name = "ProxyViewContainer"; } - } + const viewClass = getViewClass(name); + let view = new viewClass(); + view.nodeName = name; + view.meta = getViewMeta(name); - private platformFilter(attribute: string): string { - let lowered = attribute.toLowerCase(); - if (lowered.indexOf(IOS_PREFX) === 0) { - if (this.isIos) { - return attribute.substr(IOS_PREFX.length); - } else { - return null; - } - } + // we're setting the node type of the view + // to 'element' because of checks done in the + // dom animation engine: + // tslint:disable-next-line:max-line-length + // https://github.com/angular/angular/blob/master/packages/animations/browser/src/render/dom_animation_engine.ts#L70-L81 + view.nodeType = ELEMENT_NODE_TYPE; - if (lowered.indexOf(ANDROID_PREFX) === 0) { - if (this.isAndroid) { - return attribute.substr(ANDROID_PREFX.length); - } else { - return null; - } - } - - return attribute; + return view; } - public setProperty(view: NgView, attributeName: string, value: any): void { - attributeName = this.platformFilter(attributeName); - if (!attributeName) { + public setProperty(view: NgView, attributeName: string, value: any, namespace?: string): void { + if (namespace && !this.runsIn(namespace)) { return; } @@ -229,6 +181,34 @@ export class ViewUtil { } } + // finds the node in the parent's views and returns the next index + // returns -1 if the node has no parent or next sibling + public nextSiblingIndex(node: NgView): number { + const parent = node.parent; + if (!parent) { + return -1; + } + + let index = 0; + let found = false; + parent.eachChild(child => { + if (child === node) { + found = true; + } + + index += 1; + return !found; + }); + + return found ? index : -1; + } + + private runsIn(platform: string): boolean { + return (platform === "ios" && this.isIos) || + (platform === "android" && this.isAndroid); + } + + private setPropertyInternal(view: NgView, attributeName: string, value: any): void { traceLog("Setting attribute: " + attributeName); @@ -236,7 +216,7 @@ export class ViewUtil { if (attributeName === "class") { this.setClasses(view, value); - } else if (this.isXMLAttribute(attributeName)) { + } else if (XML_ATTRIBUTES.indexOf(attributeName) !== -1) { view._applyXmlAttribute(attributeName, value); } else if (propMap.has(attributeName)) { // We have a lower-upper case mapped property. @@ -308,16 +288,12 @@ export class ViewUtil { view.className = classValue; } - public setStyleProperty(view: NgView, styleName: string, styleValue: string): void { - traceLog("setStyleProperty: " + styleName + " = " + styleValue); - - let name = styleName; - let resolvedValue = styleValue; // this.resolveCssValue(styleValue); + public setStyle(view: NgView, styleName: string, value: any) { + view.style[styleName] = value; + } - if (resolvedValue !== null) { - view.style[name] = resolvedValue; - } else { - view.style[name] = unsetValue; - } + public removeStyle(view: NgView, styleName: string) { + view.style[styleName] = unsetValue; } } + diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 0772c7ced..3f803858a 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -1,13 +1,6 @@ -//import "globals"; -// import "./modules"; -//global.registerModule("./main-page", function () { return require("./main-page"); }); - -//import * as profiling from "./profiling"; -//profiling.start("application-start"); - -// "nativescript-angular/application" import should be first in order to load some required settings (like globals and reflect-metadata) import { NativeScriptModule } from "nativescript-angular/nativescript.module"; import { 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"; @@ -70,9 +63,12 @@ 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)) + imports.push(NativeScriptRouterModule.forRoot(componentType.routes)); } let exports: any[] = []; if (componentType.exports) { @@ -83,20 +79,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 { } @@ -112,7 +110,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)); @@ -136,10 +134,10 @@ platformNativeScriptDynamic().bootstrapModule(makeExampleModule(RendererTest)); // 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; +// Livesync test +let cachedUrl: string; onBeforeLivesync.subscribe((moduleRef) => { console.log("------- onBeforeLivesync"); if (moduleRef) { @@ -158,5 +156,5 @@ onAfterLivesync.subscribe((moduleRef) => { } }); -//platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LivesyncApp)); +// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LivesyncApp)); console.log("APP RESTART"); diff --git a/ng-sample/app/examples/action-bar/action-bar-test.ts b/ng-sample/app/examples/action-bar/action-bar-test.ts index a4e23ed4e..2186e2427 100644 --- a/ng-sample/app/examples/action-bar/action-bar-test.ts +++ b/ng-sample/app/examples/action-bar/action-bar-test.ts @@ -1,5 +1,5 @@ -import { Component } from '@angular/core'; -import { Page} from "ui/page"; +import { Component } from "@angular/core"; +import { Page } from "ui/page"; @Component({ selector: "first", @@ -29,7 +29,6 @@ class FirstComponent { @Component({ selector: "nested-component", template: ` - @@ -70,7 +69,7 @@ class SecondComponent { } @Component({ - selector: 'action-bar-test', + selector: "action-bar-test", template: ` @@ -79,14 +78,12 @@ class SecondComponent { }) export class ActionBarTest { static routes = [ - { path: '', component: FirstComponent }, - { path: 'second', component: SecondComponent }, - ] + { path: "", component: FirstComponent }, + { path: "second", component: SecondComponent }, + ]; static entries = [ FirstComponent, SecondComponent, - ] + ]; } - - 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 ce527cac9..307534574 100644 --- a/ng-sample/app/examples/animation/animation-enter-leave-test.ts +++ b/ng-sample/app/examples/animation/animation-enter-leave-test.ts @@ -1,39 +1,39 @@ -import {Component, trigger, style, animate, state, transition } from '@angular/core'; +import {Component, trigger, style, animate, state, transition } from "@angular/core"; @Component({ - selector: 'animation-enter-leave', - templateUrl: './examples/animation/animation-enter-leave-test.html', - styleUrls: [ './examples/animation/animation-enter-leave-test.css' ], + selector: "animation-enter-leave", + templateUrl: "./examples/animation/animation-enter-leave-test.html", + 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 })), - transition('void => *', [ animate('600ms ease-out') ]), - transition('* => void', [ animate('600ms ease-out')]) + trigger("state", [ + 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")]) ]) ] }) export class AnimationEnterLeaveTest { - + public items: Array; constructor() { this.items = []; - for (var i = 0; i < 3; i++) { + for (let i = 0; i < 3; i++) { this.items.push("Item " + i); } } - + onAddTap() { this.items.push("Item " + (this.items.length + 1)); } - + onRemoveAllTap() { this.items = []; } - + onItemTap(event) { - var index = this.items.indexOf(event.object.text); + let index = this.items.indexOf(event.object.text); this.items.splice(index, 1); } } diff --git a/ng-sample/app/examples/animation/animation-keyframes-test.ts b/ng-sample/app/examples/animation/animation-keyframes-test.ts index f4c5a48ca..dd5d6ace7 100644 --- a/ng-sample/app/examples/animation/animation-keyframes-test.ts +++ b/ng-sample/app/examples/animation/animation-keyframes-test.ts @@ -1,34 +1,34 @@ -import {Component, trigger, style, animate, state, transition, keyframes } from '@angular/core'; +import {Component, trigger, style, animate, state, transition, keyframes } from "@angular/core"; @Component({ - selector: 'animation-states', + selector: "animation-states", template: ` `, animations: [ - trigger('state', [ - state('active', style({ transform: 'translateX(0)', opacity: 1 })), - state('inactive', style({ transform: 'translateX(0)', opacity: 0.2 })), - transition('inactive => active', [ + trigger("state", [ + state("active", style({ transform: "translateX(0)", opacity: 1 })), + 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: 1, transform: 'translateX(15)', offset: 0.3}), - style({opacity: 1, transform: 'translateX(0)', offset: 1.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}) ])) ]), - transition('active => inactive', [ + transition("active => inactive", [ animate(300, keyframes([ - style({opacity: 1, transform: 'translateX(0)', offset: 0}), - style({opacity: 1, transform: 'translateX(-15)', offset: 0.7}), - style({opacity: 0.2, transform: 'translateX(100)', offset: 1.0}) + style({opacity: 1, transform: "translateX(0)", offset: 0}), + style({opacity: 1, transform: "translateX(-15)", offset: 0.7}), + style({opacity: 0.2, transform: "translateX(100)", offset: 1.0}) ])) ]) ]) ] }) export class AnimationKeyframesTest { - + isOn = false; onTap() { diff --git a/ng-sample/app/examples/animation/animation-ngclass-test.ts b/ng-sample/app/examples/animation/animation-ngclass-test.ts index 516dc34f0..c0d7b0ba7 100644 --- a/ng-sample/app/examples/animation/animation-ngclass-test.ts +++ b/ng-sample/app/examples/animation/animation-ngclass-test.ts @@ -13,11 +13,6 @@ export class AnimationNgClassTest { onTap() { this.isOn = !this.isOn; - if (this.isOn) { - this.text = "Toggled"; - } - else { - this.text = "Normal"; - } + this.text = this.isOn ? "Toggled" : "Normal"; } } diff --git a/ng-sample/app/examples/animation/animation-states-test.ts b/ng-sample/app/examples/animation/animation-states-test.ts index b72ae4df9..ba6288ea5 100644 --- a/ng-sample/app/examples/animation/animation-states-test.ts +++ b/ng-sample/app/examples/animation/animation-states-test.ts @@ -1,22 +1,22 @@ -import {Component, trigger, style, animate, state, transition } from '@angular/core'; +import {Component, trigger, style, animate, state, transition } from "@angular/core"; @Component({ - selector: 'animation-states', + 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') ]), + 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") ]), ]) ] }) export class AnimationStatesTest { - + isOn = false; onTap() { diff --git a/ng-sample/app/examples/http/http-test.ts b/ng-sample/app/examples/http/http-test.ts index c661a749b..ae2c4b8ca 100644 --- a/ng-sample/app/examples/http/http-test.ts +++ b/ng-sample/app/examples/http/http-test.ts @@ -1,15 +1,17 @@ -import {Component} from '@angular/core'; -import {Http} from '@angular/http'; -import 'rxjs/add/operator/map'; +import {Component} from "@angular/core"; +import {Http} from "@angular/http"; +import "rxjs/add/operator/map"; /* IMPORTANT -In order to test out the full image example, to fix the App Transport Security error in iOS 9, you will need to follow this after adding the iOS platform: +In order to test out the full image example, +to fix the App Transport Security error in iOS 9, +you will need to follow this after adding the iOS platform: https://blog.nraboy.com/2015/12/fix-ios-9-app-transport-security-issues-in-nativescript/ */ @Component({ - selector: 'http-test', + selector: "http-test", template: ` @@ -28,12 +30,12 @@ export class HttpTest { public title: string; public description: string; - constructor(private http: Http) { - + constructor(private http: Http) { + } - + public loadLocal() { - this.http.get('~/examples/http/data.json').map(res => res.json()).subscribe((response: any) => { + this.http.get("~/examples/http/data.json").map(res => res.json()).subscribe((response: any) => { let user = response.results[0]; this.title = user.title; this.description = user.description; diff --git a/ng-sample/app/examples/image/image-test.ts b/ng-sample/app/examples/image/image-test.ts index 6d21c9840..8df56a6e6 100644 --- a/ng-sample/app/examples/image/image-test.ts +++ b/ng-sample/app/examples/image/image-test.ts @@ -1,13 +1,15 @@ -import {Component} from '@angular/core'; +import { Component } from "@angular/core"; /* IMPORTANT -In order to test out the full image example, to fix the App Transport Security error in iOS 9, you will need to follow this after adding the iOS platform: +In order to test out the full image example, +to fix the App Transport Security error in iOS 9, +you will need to follow this after adding the iOS platform: https://blog.nraboy.com/2015/12/fix-ios-9-app-transport-security-issues-in-nativescript/ */ @Component({ - selector: 'image-test', + selector: "image-test", template: ` @@ -22,19 +24,19 @@ https://blog.nraboy.com/2015/12/fix-ios-9-app-transport-security-issues-in-nativ ` }) export class ImageTest { - + public currentImage: string; private images: Array = [ - 'res://300x300.jpg', - '~/examples/image/img/Default.png', - 'http://www.codeproject.com/KB/mobile/883465/NativeScript.png' + "res://300x300.jpg", + "~/examples/image/img/Default.png", + "http://www.codeproject.com/KB/mobile/883465/NativeScript.png" ]; private cnt: number = 0; - constructor() { + constructor() { this.currentImage = this.images[this.cnt]; } - + changeImage(direction: number) { if (direction > 0) { this.cnt++; @@ -50,12 +52,12 @@ export class ImageTest { } this.currentImage = this.images[this.cnt]; } - + addImage(e: any, name: string): void { - if (name.indexOf('http') === -1) { + if (name.indexOf("http") === -1) { alert(`Must be a valid url to an image starting with 'http'!`); } else { - this.images.push(name); + this.images.push(name); this.currentImage = this.images[this.images.length - 1]; } } diff --git a/ng-sample/app/examples/list/data.service.ts b/ng-sample/app/examples/list/data.service.ts index e7139138b..2aef70c26 100644 --- a/ng-sample/app/examples/list/data.service.ts +++ b/ng-sample/app/examples/list/data.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable } from "@angular/core"; import { BehaviorSubject } from "rxjs/BehaviorSubject"; export class DataItem { @@ -10,13 +10,13 @@ export class DataService { private _intervalId; private _counter = 0; private _currentItems: Array; - + public items$: BehaviorSubject>; constructor() { this._currentItems = []; - for (var i = 0; i < 3; i++) { - this.appendItem() + for (let i = 0; i < 3; i++) { + this.appendItem(); } this.items$ = new BehaviorSubject(this._currentItems); @@ -51,4 +51,5 @@ export class DataService { this._currentItems.push(new DataItem(this._counter, "data item " + this._counter)); this._counter++; } -} \ No newline at end of file +} + diff --git a/ng-sample/app/examples/list/list-test-async.ts b/ng-sample/app/examples/list/list-test-async.ts index 5b0b76bcf..146ee9cba 100644 --- a/ng-sample/app/examples/list/list-test-async.ts +++ b/ng-sample/app/examples/list/list-test-async.ts @@ -1,12 +1,12 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from "@angular/core"; import { DataItem, DataService } from "./data.service"; import { Observable } from "rxjs/Observable"; import { BehaviorSubject } from "rxjs/BehaviorSubject"; import "rxjs/add/operator/combineLatest"; @Component({ - selector: 'list-test-async', - styleUrls: ['examples/list/styles.css'], + selector: "list-test-async", + styleUrls: ["examples/list/styles.css"], providers: [DataService], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @@ -29,7 +29,11 @@ import "rxjs/add/operator/combineLatest"; - + ` }) @@ -54,8 +58,8 @@ export class ListTestAsync { } @Component({ - selector: 'list-test-async-filter', - styleUrls: ['examples/list/styles.css'], + selector: "list-test-async-filter", + styleUrls: ["examples/list/styles.css"], providers: [DataService], changeDetection: ChangeDetectionStrategy.OnPush, template: ` diff --git a/ng-sample/app/examples/list/list-test.ts b/ng-sample/app/examples/list/list-test.ts index 1a08a1075..ccec8a964 100644 --- a/ng-sample/app/examples/list/list-test.ts +++ b/ng-sample/app/examples/list/list-test.ts @@ -1,14 +1,14 @@ -import {Component, Input, WrappedValue, ChangeDetectionStrategy, AfterViewChecked, DoCheck} from '@angular/core'; -import {Label} from 'ui/label'; -import {ObservableArray} from 'data/observable-array'; +import {Component, Input, WrappedValue, ChangeDetectionStrategy, AfterViewChecked, DoCheck} from "@angular/core"; +import {Label} from "ui/label"; +import {ObservableArray} from "data/observable-array"; class DataItem { constructor(public id: number, public name: string) { } } @Component({ - selector: 'item-component', - styleUrls: ['examples/list/styles.css'], + selector: "item-component", + styleUrls: ["examples/list/styles.css"], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @@ -34,8 +34,8 @@ export class ItemComponent implements AfterViewChecked, DoCheck { } @Component({ - selector: 'list-test', - styleUrls: ['examples/list/styles.css'], + selector: "list-test", + styleUrls: ["examples/list/styles.css"], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @@ -58,7 +58,7 @@ export class ItemComponent implements AfterViewChecked, DoCheck { // IN-PLACE TEMPLATE // @@ -70,7 +70,7 @@ export class ListTest { constructor() { this.myItems = []; this.counter = 0; - for (var i = 0; i < 100; i++) { + for (let i = 0; i < 100; i++) { this.myItems.push(new DataItem(i, "data item " + i)); this.counter = i; } @@ -87,5 +87,5 @@ export class ListTest { public static entries = [ ItemComponent - ] + ]; } diff --git a/ng-sample/app/examples/list/template-selector.ts b/ng-sample/app/examples/list/template-selector.ts index 1fce9e816..4557b7e9c 100644 --- a/ng-sample/app/examples/list/template-selector.ts +++ b/ng-sample/app/examples/list/template-selector.ts @@ -1,4 +1,4 @@ -import { Component, Input, ChangeDetectionStrategy, DoCheck } from '@angular/core'; +import { Component, Input, ChangeDetectionStrategy, DoCheck } from "@angular/core"; class DataItem { private static count = 0; @@ -9,8 +9,8 @@ class DataItem { } @Component({ - selector: 'item-component', - styleUrls: ['examples/list/styles.css'], + selector: "item-component", + styleUrls: ["examples/list/styles.css"], changeDetection: ChangeDetectionStrategy.OnPush, template: `` }) @@ -20,8 +20,8 @@ export class ItemComponent implements DoCheck { } @Component({ - selector: 'header-component', - styleUrls: ['examples/list/styles.css'], + selector: "header-component", + styleUrls: ["examples/list/styles.css"], changeDetection: ChangeDetectionStrategy.OnPush, template: `` }) @@ -31,14 +31,17 @@ export class HeaderComponent implements DoCheck { } @Component({ - selector: 'list-test', - styleUrls: ['examples/list/styles.css'], + selector: "list-test", + styleUrls: ["examples/list/styles.css"], changeDetection: ChangeDetectionStrategy.OnPush, template: ` - +