diff --git a/tests/app/app/main-page.css b/tests/app/app/button-page.css
similarity index 100%
rename from tests/app/app/main-page.css
rename to tests/app/app/button-page.css
diff --git a/tests/app/app/button-page.xml b/tests/app/app/button-page.xml
new file mode 100644
index 000000000..21dc437ae
--- /dev/null
+++ b/tests/app/app/button-page.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/app/livesync/livesync-button-page.ts b/tests/app/livesync/livesync-button-page.ts
new file mode 100644
index 000000000..d42484063
--- /dev/null
+++ b/tests/app/livesync/livesync-button-page.ts
@@ -0,0 +1,3 @@
+export function onLoaded() {
+ console.log("Button page loaded!");
+}
diff --git a/tests/app/livesync/livesync-button-page.xml b/tests/app/livesync/livesync-button-page.xml
new file mode 100644
index 000000000..21dc437ae
--- /dev/null
+++ b/tests/app/livesync/livesync-button-page.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/app/livesync/livesync-label-page.ts b/tests/app/livesync/livesync-label-page.ts
new file mode 100644
index 000000000..2c2aa20c2
--- /dev/null
+++ b/tests/app/livesync/livesync-label-page.ts
@@ -0,0 +1,3 @@
+export function onLoaded() {
+ console.log("Label page loaded!");
+}
diff --git a/tests/app/livesync/livesync-label-page.xml b/tests/app/livesync/livesync-label-page.xml
new file mode 100644
index 000000000..483c877cc
--- /dev/null
+++ b/tests/app/livesync/livesync-label-page.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/app/livesync/livesync-tests.ts b/tests/app/livesync/livesync-tests.ts
index da1cc9b2c..7eafac365 100644
--- a/tests/app/livesync/livesync-tests.ts
+++ b/tests/app/livesync/livesync-tests.ts
@@ -1,37 +1,29 @@
-import * as app from "tns-core-modules/application/application";
-import * as frame from "tns-core-modules/ui/frame";
import * as helper from "../ui/helper";
import * as TKUnit from "../TKUnit";
+
+import * as app from "tns-core-modules/application/application";
+import * as frame from "tns-core-modules/ui/frame";
+
import { Color } from "tns-core-modules/color";
-import { parse } from "tns-core-modules/ui/builder";
+import { isAndroid } from "tns-core-modules/platform";
+import { createViewFromEntry } from "tns-core-modules/ui/builder";
import { Page } from "tns-core-modules/ui/page";
+import { Frame } from "tns-core-modules/ui/frame";
const appCssFileName = "./app/application.css";
const appNewCssFileName = "./app/app-new.css";
const appNewScssFileName = "./app/app-new.scss";
-const appJsFileName = "./app/app.js";
-const appTsFileName = "./app/app.ts";
-const mainPageCssFileName = "./app/main-page.css";
-const mainPageHtmlFileName = "./app/main-page.html";
-const mainPageXmlFileName = "./app/main-page.xml";
+const buttonCssFileName = "./app/button-page.css";
+
+const buttonPageModuleName = "livesync/livesync-button-page";
+const buttonHtmlPageFileName = "./livesync/livesync-button-page.html";
+const buttonXmlPageFileName = "./livesync/livesync-button-page.xml";
+const buttonJsPageFileName = "./livesync/livesync-button-page.js";
+const buttonTsPageFileName = "./livesync/livesync-button-page.ts";
+const labelPageModuleName = "livesync/livesync-label-page";
-const black = new Color("black");
const green = new Color("green");
-const mainPageTemplate = `
-
-
-
-
- `;
-
-const pageTemplate = `
-
-
-
-
- `;
-
export function test_onLiveSync_ModuleContext_AppStyle_AppNewCss() {
_test_onLiveSync_ModuleContext_AppStyle(appNewCssFileName);
}
@@ -48,29 +40,29 @@ export function test_onLiveSync_ModuleContext_ModuleUndefined() {
_test_onLiveSync_ModuleContext({ type: "script", path: undefined });
}
-export function test_onLiveSync_ModuleContext_Script_AppJs() {
- _test_onLiveSync_ModuleContext({ type: "script", path: appJsFileName });
+export function test_onLiveSync_ModuleContext_Script_JsFile() {
+ _test_onLiveSync_ModuleReplace({ type: "script", path: buttonJsPageFileName });
}
-export function test_onLiveSync_ModuleContext_Script_AppTs() {
- _test_onLiveSync_ModuleContext({ type: "script", path: appTsFileName });
+export function test_onLiveSync_ModuleContext_Script_TsFile() {
+ _test_onLiveSync_ModuleReplace({ type: "script", path: buttonTsPageFileName });
}
-export function test_onLiveSync_ModuleContext_Style_MainPageCss() {
- _test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: mainPageCssFileName });
+export function test_onLiveSync_ModuleContext_Style_CssFile() {
+ _test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: buttonCssFileName });
}
-export function test_onLiveSync_ModuleContext_Markup_MainPageHtml() {
- _test_onLiveSync_ModuleContext({ type: "markup", path: mainPageHtmlFileName });
+export function test_onLiveSync_ModuleContext_Markup_HtmlFile() {
+ _test_onLiveSync_ModuleReplace({ type: "markup", path: buttonHtmlPageFileName });
}
-export function test_onLiveSync_ModuleContext_Markup_MainPageXml() {
- _test_onLiveSync_ModuleContext({ type: "markup", path: mainPageXmlFileName });
+export function test_onLiveSync_ModuleContext_Markup_XmlFile() {
+ _test_onLiveSync_ModuleReplace({ type: "markup", path: buttonXmlPageFileName });
}
-export function setUpModule() {
- const mainPage = parse(mainPageTemplate);
- helper.navigate(() => mainPage);
+export function setUp() {
+ const labelPage = createViewFromEntry(({ moduleName: labelPageModuleName }));
+ helper.navigate(() => labelPage);
}
export function tearDown() {
@@ -79,32 +71,29 @@ export function tearDown() {
function _test_onLiveSync_ModuleContext_AppStyle(styleFileName: string) {
const pageBeforeNavigation = helper.getCurrentPage();
+ const buttonPage = createViewFromEntry(({ moduleName: buttonPageModuleName }));
+ helper.navigateWithHistory(() => buttonPage);
- const page = parse(pageTemplate);
- helper.navigateWithHistory(() => page);
app.setCssFileName(styleFileName);
-
const pageBeforeLiveSync = helper.getCurrentPage();
global.__onLiveSync({ type: "style", path: styleFileName });
const pageAfterLiveSync = helper.getCurrentPage();
TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString());
-
- TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "App styles NOT applied - livesync navigation executed!");
- TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!");
- TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!");
+ TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Can NOT go back!");
+ TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different!");
+ TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
helper.goBack();
-
const pageAfterNavigationBack = helper.getCurrentPage();
TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, green, "App styles NOT applied on back navigation!");
- TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!");
+ TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different");
TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
}
function _test_onLiveSync_ModuleContext(context: { type, path }) {
- const page = parse(pageTemplate);
- helper.navigateWithHistory(() => page);
+ const buttonPage = createViewFromEntry(({ moduleName: buttonPageModuleName }));
+ helper.navigateWithHistory(() => buttonPage);
global.__onLiveSync({ type: context.type, path: context.path });
TKUnit.waitUntilReady(() => !!frame.topmost());
@@ -113,27 +102,53 @@ function _test_onLiveSync_ModuleContext(context: { type, path }) {
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded);
}
+function _test_onLiveSync_ModuleReplace(context: { type, path }) {
+ const pageBeforeNavigation = helper.getCurrentPage();
+ const buttonPage = createViewFromEntry(({ moduleName: buttonPageModuleName }));
+ helper.navigateWithHistory(() => buttonPage);
+
+ global.__onLiveSync({ type: context.type, path: context.path });
+ const topmostFrame = frame.topmost();
+ waitUntilLivesyncComplete(topmostFrame);
+ TKUnit.assertTrue(topmostFrame.currentPage.getViewById("button").isLoaded, "Button page is NOT loaded!");
+ TKUnit.assertEqual(topmostFrame.backStack.length, 1, "Backstack is clean!");
+ TKUnit.assertTrue(topmostFrame.canGoBack(), "Can NOT go back!");
+
+ helper.goBack();
+ const pageAfterBackNavigation = helper.getCurrentPage();
+ TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded, "Label page is NOT loaded!");
+ TKUnit.assertEqual(topmostFrame.backStack.length, 0, "Backstack is NOT clean!");
+ TKUnit.assertEqual(pageBeforeNavigation, pageAfterBackNavigation, "Pages are different!");
+}
+
function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) {
const pageBeforeNavigation = helper.getCurrentPage();
-
- const page = parse(pageTemplate);
- helper.navigateWithHistory(() => page);
+ const buttonPage = createViewFromEntry(({ moduleName: buttonPageModuleName }));
+ helper.navigateWithHistory(() => buttonPage);
const pageBeforeLiveSync = helper.getCurrentPage();
- pageBeforeLiveSync._moduleName = "main-page";
+ pageBeforeLiveSync._moduleName = "button-page";
+
global.__onLiveSync({ type: context.type, path: context.path });
+ const topmostFrame = frame.topmost();
+ waitUntilLivesyncComplete(topmostFrame);
const pageAfterLiveSync = helper.getCurrentPage();
TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString());
-
- TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Local styles NOT applied - livesync navigation executed!");
- TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!");
- TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!");
+ TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Can NOT go back!");
+ TKUnit.assertEqual(topmostFrame.backStack.length, 1, "Backstack is clean!");
+ TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
helper.goBack();
-
const pageAfterNavigationBack = helper.getCurrentPage();
- TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, black, "App styles applied on back navigation!");
- TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!");
+ TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different!");
TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
-}
\ No newline at end of file
+}
+
+function waitUntilLivesyncComplete(frame: Frame) {
+ if (isAndroid) {
+ TKUnit.waitUntilReady(() => frame._executingEntry === null);
+ } else {
+ TKUnit.waitUntilReady(() => frame.currentPage.isLoaded);
+ }
+}
diff --git a/tests/package.json b/tests/package.json
index b821ee2b6..4be9613ee 100644
--- a/tests/package.json
+++ b/tests/package.json
@@ -5,11 +5,11 @@
"repository": "",
"nativescript": {
"id": "org.nativescript.UnitTestApp",
- "tns-ios": {
- "version": "5.2.0"
- },
"tns-android": {
- "version": "5.2.1"
+ "version": "5.3.1"
+ },
+ "tns-ios": {
+ "version": "5.3.1"
}
},
"dependencies": {
diff --git a/tns-core-modules/application/application-common.ts b/tns-core-modules/application/application-common.ts
index b585578fa..20b39cd50 100644
--- a/tns-core-modules/application/application-common.ts
+++ b/tns-core-modules/application/application-common.ts
@@ -83,22 +83,20 @@ export function livesync(rootView: View, context?: ModuleContext) {
events.notify({ eventName: "livesync", object: app });
const liveSyncCore = global.__onLiveSyncCore;
let reapplyAppStyles = false;
- let reapplyLocalStyles = false;
+ // ModuleContext is available only for Hot Module Replacement
if (context && context.path) {
- const extensions = ["css", "scss"];
+ const styleExtensions = ["css", "scss"];
const appStylesFullFileName = getCssFileName();
const appStylesFileName = appStylesFullFileName.substring(0, appStylesFullFileName.lastIndexOf(".") + 1);
- reapplyAppStyles = extensions.some(ext => context.path === appStylesFileName.concat(ext));
- if (!reapplyAppStyles) {
- reapplyLocalStyles = extensions.some(ext => context.path.endsWith(ext));
- }
+ reapplyAppStyles = styleExtensions.some(ext => context.path === appStylesFileName.concat(ext));
}
+ // Handle application styles
if (reapplyAppStyles && rootView) {
rootView._onCssStateChange();
} else if (liveSyncCore) {
- reapplyLocalStyles ? liveSyncCore(context) : liveSyncCore();
+ liveSyncCore(context);
}
}
diff --git a/tns-core-modules/application/application.ios.ts b/tns-core-modules/application/application.ios.ts
index 0622e733f..3d38f5889 100644
--- a/tns-core-modules/application/application.ios.ts
+++ b/tns-core-modules/application/application.ios.ts
@@ -10,6 +10,7 @@ import {
notify, launchEvent, resumeEvent, suspendEvent, exitEvent, lowMemoryEvent,
orientationChangedEvent, setApplication, livesync, displayedEvent, getCssFileName
} from "./application-common";
+import { ModuleType } from "../ui/core/view/view-common";
// First reexport so that app module is initialized.
export * from "./application-common";
@@ -106,6 +107,7 @@ class IOSApplication implements IOSApplicationDefinition {
get delegate(): typeof UIApplicationDelegate {
return this._delegate;
}
+
set delegate(value: typeof UIApplicationDelegate) {
if (this._delegate !== value) {
this._delegate = value;
@@ -228,8 +230,16 @@ class IOSApplication implements IOSApplicationDefinition {
}
public _onLivesync(context?: ModuleContext): void {
- // If view can't handle livesync set window controller.
- if (this._rootView && !this._rootView._onLivesync(context)) {
+ // Handle application root module
+ const isAppRootModuleChanged = context && context.path && context.path.includes(getMainEntry().moduleName) && context.type !== ModuleType.style;
+
+ // Set window content when:
+ // + Application root module is changed
+ // + View did not handle the change
+ // Note:
+ // The case when neither app root module is changed, nor livesync is handled on View,
+ // then changes will not apply until navigate forward to the module.
+ if (isAppRootModuleChanged || (this._rootView && !this._rootView._onLivesync(context))) {
this.setWindowContent();
}
}
@@ -258,7 +268,6 @@ class IOSApplication implements IOSApplicationDefinition {
this._window.makeKeyAndVisible();
}
}
-
}
const iosApp = new IOSApplication();
diff --git a/tns-core-modules/trace/trace.d.ts b/tns-core-modules/trace/trace.d.ts
index 838a8729f..86e886e5b 100644
--- a/tns-core-modules/trace/trace.d.ts
+++ b/tns-core-modules/trace/trace.d.ts
@@ -100,10 +100,11 @@ export module categories {
export const Error: string;
export const Animation: string;
export const Transition: string;
-
- export const All: string;
+ export const Livesync: string;
export const separator: string;
+ export const All: string;
+
export function concat(...categories: string[]): string;
}
@@ -125,7 +126,7 @@ export interface TraceWriter {
}
/**
- * An interface used to trace information about specific event.
+ * An interface used to trace information about specific event.
*/
export interface EventListener {
filter: string;
@@ -133,7 +134,7 @@ export interface EventListener {
}
/**
- * An interface used to for handling trace error
+ * An interface used to for handling trace error
*/
export interface ErrorHandler {
handlerError(error: Error);
@@ -141,4 +142,4 @@ export interface ErrorHandler {
export class DefaultErrorHandler implements ErrorHandler {
handlerError(error);
-}
\ No newline at end of file
+}
diff --git a/tns-core-modules/trace/trace.ts b/tns-core-modules/trace/trace.ts
index 497fcf11e..2d54bf33e 100644
--- a/tns-core-modules/trace/trace.ts
+++ b/tns-core-modules/trace/trace.ts
@@ -129,9 +129,22 @@ export module categories {
export const Error = "Error";
export const Animation = "Animation";
export const Transition = "Transition";
- export const All = VisualTreeEvents + "," + Layout + "," + Style + "," + ViewHierarchy + "," + NativeLifecycle + "," + Debug + "," + Navigation + "," + Test + "," + Binding + "," + Error + "," + Animation + "," + Transition;
+ export const Livesync = "Livesync";
export const separator = ",";
+ export const All = VisualTreeEvents + separator
+ + Layout + separator
+ + Style + separator
+ + ViewHierarchy + separator
+ + NativeLifecycle + separator
+ + Debug + separator
+ + Navigation + separator
+ + Test + separator
+ + Binding + separator
+ + Error + separator
+ + Animation + separator
+ + Transition + separator
+ + Livesync;
export function concat(): string {
let result: string;
diff --git a/tns-core-modules/ui/core/view-base/view-base.d.ts b/tns-core-modules/ui/core/view-base/view-base.d.ts
index cbd910d46..b77b9e81f 100644
--- a/tns-core-modules/ui/core/view-base/view-base.d.ts
+++ b/tns-core-modules/ui/core/view-base/view-base.d.ts
@@ -160,7 +160,7 @@ export abstract class ViewBase extends Observable {
/**
* @deprecated use showModal with ShowModalOptions instead
- *
+ *
* Shows the View contained in moduleName as a modal view.
* @param moduleName - The name of the module to load starting from the application root.
* @param context - Any context you want to pass to the modally shown view.
@@ -175,7 +175,7 @@ export abstract class ViewBase extends Observable {
/**
* @deprecated use showModal with ShowModalOptions instead
- *
+ *
* Shows the view passed as parameter as a modal view.
* @param view - View instance to be shown modally.
* @param context - Any context you want to pass to the modally shown view. This same context will be available in the arguments of the shownModally event handler.
@@ -367,7 +367,7 @@ export abstract class ViewBase extends Observable {
public _goToVisualState(state: string): void;
/**
* @deprecated
- *
+ *
* This used to be the way to set attribute values in early {N} versions.
* Now attributes are expected to be set as plain properties on the view instances.
*/
diff --git a/tns-core-modules/ui/core/view/view-common.ts b/tns-core-modules/ui/core/view/view-common.ts
index 9dfab6815..a9c627a84 100644
--- a/tns-core-modules/ui/core/view/view-common.ts
+++ b/tns-core-modules/ui/core/view/view-common.ts
@@ -7,8 +7,7 @@ import {
import {
ViewBase, Property, booleanConverter, eachDescendant, EventData, layout,
getEventOrGestureName, traceEnabled, traceWrite, traceCategories,
- InheritedProperty,
- ShowModalOptions
+ InheritedProperty, ShowModalOptions
} from "../view-base";
import { HorizontalAlignment, VerticalAlignment, Visibility, Length, PercentLength } from "../../styling/style-properties";
@@ -38,6 +37,12 @@ function ensureAnimationModule() {
}
}
+export enum ModuleType {
+ markup = "markup",
+ script = "script",
+ style = "style"
+}
+
export function CSSType(type: string): ClassDecorator {
return (cls) => {
cls.prototype.cssType = type;
@@ -138,12 +143,22 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
public _onLivesync(context?: ModuleContext): boolean {
+ if (traceEnabled()) {
+ traceWrite(`${this}._onLivesync(${JSON.stringify(context)})`, traceCategories.Livesync);
+ }
+
_rootModalViews.forEach(v => v.closeModal());
_rootModalViews.length = 0;
- // Currently, we pass `context` only for style modules
- if (context && context.path) {
- return this.changeLocalStyles(context.path);
+ if (context && context.type && context.path) {
+ // Handle local styles
+ if (context.type === ModuleType.style) {
+ return this.changeLocalStyles(context.path);
+ }
+ // Handle module markup and script changes
+ else {
+ return this.changeModule(context);
+ }
}
return false;
@@ -156,11 +171,16 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return true;
});
}
- // Do not execute frame navigation for a change in styles
+
+ // Do not reset activity/window content for local styles changes
return true;
}
private changeStyles(view: ViewBase, contextPath: string): boolean {
+ if (traceEnabled()) {
+ traceWrite(`${view}.${view._moduleName}`, traceCategories.Livesync);
+ }
+
if (view._moduleName && contextPath.includes(view._moduleName)) {
(view).changeCssFile(contextPath);
return true;
@@ -168,6 +188,23 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return false;
}
+ private changeModule(context: ModuleContext): boolean {
+ eachDescendant(this, (child: ViewBase) => {
+ if (traceEnabled()) {
+ traceWrite(`${child}.${child._moduleName}`, traceCategories.Livesync);
+ }
+
+ // Handle changes in module's Page
+ if (child._moduleName && context.path.includes(child._moduleName) && child.page) {
+ child.page._onLivesync(context);
+ }
+ return true;
+ });
+
+ // Do not reset activity/window content for module changes
+ return true;
+ }
+
_setupAsRootView(context: any): void {
super._setupAsRootView(context);
if (!this._styleScope) {
diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts
index d192705b8..70f1fba7b 100644
--- a/tns-core-modules/ui/frame/fragment.transitions.android.ts
+++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts
@@ -232,22 +232,22 @@ export function _getAnimatedEntries(frameId: number): Set {
export function _updateTransitions(entry: ExpandedEntry): void {
const fragment = entry.fragment;
const enterTransitionListener = entry.enterTransitionListener;
- if (enterTransitionListener) {
+ if (enterTransitionListener && fragment) {
fragment.setEnterTransition(enterTransitionListener.transition);
}
const exitTransitionListener = entry.exitTransitionListener;
- if (exitTransitionListener) {
+ if (exitTransitionListener && fragment) {
fragment.setExitTransition(exitTransitionListener.transition);
}
const reenterTransitionListener = entry.reenterTransitionListener;
- if (reenterTransitionListener) {
+ if (reenterTransitionListener && fragment) {
fragment.setReenterTransition(reenterTransitionListener.transition);
}
const returnTransitionListener = entry.returnTransitionListener;
- if (returnTransitionListener) {
+ if (returnTransitionListener && fragment) {
fragment.setReturnTransition(returnTransitionListener.transition);
}
}
@@ -374,7 +374,7 @@ function getAnimationListener(): android.animation.Animator.AnimatorListener {
return AnimationListener;
}
-
+
function addToWaitingQueue(entry: ExpandedEntry): void {
const frameId = entry.frameId;
let entries = waitingQueue.get(frameId);
@@ -659,7 +659,7 @@ function setupAllAnimation(entry: ExpandedEntry, transition: Transition): void {
setupExitAndPopEnterAnimation(entry, transition);
const listener = getAnimationListener();
- // setupAllAnimation is called only for new fragments so we don't
+ // setupAllAnimation is called only for new fragments so we don't
// need to clearAnimationListener for enter & popExit animators.
const enterAnimator = transition.createAndroidAnimator(AndroidTransitionType.enter);
enterAnimator.transitionType = AndroidTransitionType.enter;
@@ -720,7 +720,7 @@ function transitionOrAnimationCompleted(entry: ExpandedEntry): void {
if (entries.size === 0) {
const frame = entry.resolvedPage.frame;
// We have 0 or 1 entry per frameId in completedEntries
- // So there is no need to make it to Set like waitingQueue
+ // So there is no need to make it to Set like waitingQueue
const previousCompletedAnimationEntry = completedEntries.get(frameId);
completedEntries.delete(frameId);
waitingQueue.delete(frameId);
@@ -730,8 +730,8 @@ function transitionOrAnimationCompleted(entry: ExpandedEntry): void {
// Will be null if Frame is shown modally...
// transitionOrAnimationCompleted fires again (probably bug in android).
if (current) {
- const isBack = frame._isBack;
- setTimeout(() => frame.setCurrent(current, isBack));
+ const navType = frame.navigationType;
+ setTimeout(() => frame.setCurrent(current, navType));
}
} else {
completedEntries.set(frameId, entry);
diff --git a/tns-core-modules/ui/frame/frame-common.ts b/tns-core-modules/ui/frame/frame-common.ts
index 6c55fcdb2..1346385e0 100644
--- a/tns-core-modules/ui/frame/frame-common.ts
+++ b/tns-core-modules/ui/frame/frame-common.ts
@@ -11,6 +11,12 @@ import { profile } from "../../profiling";
import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack } from "./frame-stack";
export * from "../core/view";
+export enum NavigationType {
+ back,
+ forward,
+ replace
+}
+
function buildEntryFromArgs(arg: any): NavigationEntry {
let entry: NavigationEntry;
if (typeof arg === "string") {
@@ -48,6 +54,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
public _isInFrameStack = false;
public static defaultAnimatedNavigation = true;
public static defaultTransition: NavigationTransition;
+ public navigationType: NavigationType;
// TODO: Currently our navigation will not be synchronized in case users directly call native navigation methods like Activity.startActivity.
@@ -206,7 +213,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
return this._currentEntry === entry;
}
- public setCurrent(entry: BackstackEntry, isBack: boolean): void {
+ public setCurrent(entry: BackstackEntry, navigationType: NavigationType): void {
const newPage = entry.resolvedPage;
// In case we navigated forward to a page that was in the backstack
// with clearHistory: true
@@ -217,6 +224,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
this._currentEntry = entry;
+ const isBack = navigationType === NavigationType.back;
if (isBack) {
this._pushInFrameStack();
}
@@ -229,15 +237,18 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
this._executingEntry = null;
}
- public _updateBackstack(entry: BackstackEntry, isBack: boolean): void {
+ public _updateBackstack(entry: BackstackEntry, navigationType: NavigationType): void {
+ const isBack = navigationType === NavigationType.back;
+ const isReplace = navigationType === NavigationType.replace;
this.raiseCurrentPageNavigatedEvents(isBack);
const current = this._currentEntry;
+ // Do nothing for Hot Module Replacement
if (isBack) {
const index = this._backStack.indexOf(entry);
this._backStack.splice(index + 1).forEach(e => this._removeEntry(e));
this._backStack.pop();
- } else {
+ } else if (!isReplace) {
if (entry.entry.clearHistory) {
this._backStack.forEach(e => this._removeEntry(e));
this._backStack.length = 0;
@@ -345,7 +356,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
}
@profile
- private performNavigation(navigationContext: NavigationContext) {
+ public performNavigation(navigationContext: NavigationContext) {
const navContext = navigationContext.entry;
this._executingEntry = navContext;
this._onNavigatingTo(navContext, navigationContext.isBackNavigation);
@@ -563,35 +574,39 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
return result;
}
- public _onLivesync(context?: ModuleContext): boolean {
- // Execute a navigation if not handled on `View` level
- if (!super._onLivesync(context)) {
- if (!this._currentEntry || !this._currentEntry.entry) {
+ public _onLivesync(): boolean {
+ // Reset activity/window content when:
+ // + Changes are not handled on View
+ // + There is no ModuleContext
+ if (traceEnabled()) {
+ traceWrite(`${this}._onLivesync()`, traceCategories.Livesync);
+ }
+
+ if (!this._currentEntry || !this._currentEntry.entry) {
+ return false;
+ }
+
+ const currentEntry = this._currentEntry.entry;
+ const newEntry: NavigationEntry = {
+ animated: false,
+ clearHistory: true,
+ context: currentEntry.context,
+ create: currentEntry.create,
+ moduleName: currentEntry.moduleName,
+ backstackVisible: currentEntry.backstackVisible
+ }
+
+ // If create returns the same page instance we can't recreate it.
+ // Instead of navigation set activity content.
+ // This could happen if current page was set in XML as a Page instance.
+ if (newEntry.create) {
+ const page = newEntry.create();
+ if (page === this.currentPage) {
return false;
}
-
- const currentEntry = this._currentEntry.entry;
- const newEntry: NavigationEntry = {
- animated: false,
- clearHistory: true,
- context: currentEntry.context,
- create: currentEntry.create,
- moduleName: currentEntry.moduleName,
- backstackVisible: currentEntry.backstackVisible
- }
-
- // If create returns the same page instance we can't recreate it.
- // Instead of navigation set activity content.
- // This could happen if current page was set in XML as a Page instance.
- if (newEntry.create) {
- const page = newEntry.create();
- if (page === this.currentPage) {
- return false;
- }
- }
-
- this.navigate(newEntry);
}
+
+ this.navigate(newEntry);
return true;
}
}
diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts
index f878f1709..0192ec102 100644
--- a/tns-core-modules/ui/frame/frame.android.ts
+++ b/tns-core-modules/ui/frame/frame.android.ts
@@ -1,15 +1,16 @@
// Definitions.
import {
- AndroidFrame as AndroidFrameDefinition, BackstackEntry,
- NavigationTransition, AndroidFragmentCallbacks, AndroidActivityCallbacks
+ AndroidFrame as AndroidFrameDefinition, AndroidActivityCallbacks,
+ AndroidFragmentCallbacks, BackstackEntry, NavigationTransition
} from ".";
+import { ModuleType } from "../../ui/core/view/view-common";
import { Page } from "../page";
// Types.
import * as application from "../../application";
import {
- FrameBase, stack, goBack, View, Observable,
- traceEnabled, traceWrite, traceCategories, traceError
+ FrameBase, goBack, stack, NavigationContext, NavigationType,
+ Observable, View, traceCategories, traceEnabled, traceError, traceWrite
} from "./frame-common";
import {
@@ -21,6 +22,7 @@ import { profile } from "../../profiling";
// TODO: Remove this and get it from global to decouple builder for angular
import { createViewFromEntry } from "../builder";
+import { getModuleName } from "../../utils/utils";
export * from "./frame-common";
@@ -87,8 +89,16 @@ export function reloadPage(context?: ModuleContext): void {
const callbacks: AndroidActivityCallbacks = activity[CALLBACKS];
if (callbacks) {
const rootView: View = callbacks.getRootView();
+ // Handle application root module
+ const isAppRootModuleChanged = context && context.path && context.path.includes(application.getMainEntry().moduleName) && context.type !== ModuleType.style;
- if (!rootView || !rootView._onLivesync(context)) {
+ // Reset activity content when:
+ // + Application root module is changed
+ // + View did not handle the change
+ // Note:
+ // The case when neither app root module is changed, neighter livesync is handled on View,
+ // then changes will not apply until navigate forward to the module.
+ if (isAppRootModuleChanged || !rootView || !rootView._onLivesync(context)) {
callbacks.resetActivityContent(activity);
}
} else {
@@ -104,7 +114,6 @@ export class Frame extends FrameBase {
private _containerViewId: number = -1;
private _tearDownPending = false;
private _attachedToWindow = false;
- public _isBack: boolean = true;
private _cachedAnimatorState: AnimatorState;
constructor() {
@@ -263,11 +272,11 @@ export class Frame extends FrameBase {
return newFragment;
}
- public setCurrent(entry: BackstackEntry, isBack: boolean): void {
+ public setCurrent(entry: BackstackEntry, navigationType: NavigationType): void {
const current = this._currentEntry;
const currentEntryChanged = current !== entry;
if (currentEntryChanged) {
- this._updateBackstack(entry, isBack);
+ this._updateBackstack(entry, navigationType);
// If activity was destroyed we need to destroy fragment and UI
// of current and new entries.
@@ -296,7 +305,7 @@ export class Frame extends FrameBase {
}
}
- super.setCurrent(entry, isBack);
+ super.setCurrent(entry, navigationType);
// If we had real navigation process queue.
this._processNavigationQueue(entry.resolvedPage);
@@ -330,10 +339,48 @@ export class Frame extends FrameBase {
return false;
}
+ public _onLivesync(context?: ModuleContext): boolean {
+ if (traceEnabled()) {
+ traceWrite(`${this}._onLivesync(${JSON.stringify(context)})`, traceCategories.Livesync);
+ }
+
+ if (!this._currentEntry || !this._currentEntry.entry) {
+ return false;
+ }
+
+ if (context && context.type && context.path) {
+ // Set NavigationType.replace for HMR.
+ this.navigationType = NavigationType.replace;
+ const currentBackstackEntry = this._currentEntry;
+ const contextModuleName = getModuleName(context.path);
+
+ const newPage = createViewFromEntry({ moduleName: contextModuleName });
+ const newBackstackEntry: BackstackEntry = {
+ entry: currentBackstackEntry.entry,
+ resolvedPage: newPage,
+ navDepth: currentBackstackEntry.navDepth,
+ fragmentTag: currentBackstackEntry.fragmentTag,
+ frameId: currentBackstackEntry.frameId
+ };
+
+ const navContext: NavigationContext = { entry: newBackstackEntry, isBackNavigation: false };
+ this.performNavigation(navContext);
+ return true;
+ } else {
+ // Fallback
+ return super._onLivesync();
+ }
+ }
+
@profile
public _navigateCore(newEntry: BackstackEntry) {
super._navigateCore(newEntry);
- this._isBack = false;
+ // NavigationType.replace for HMR.
+ // Otherwise, default to NavigationType.forward.
+ const isReplace = this.navigationType === NavigationType.replace;
+ if (!isReplace) {
+ this.navigationType = NavigationType.forward;
+ }
// set frameId here so that we could use it in fragment.transitions
newEntry.frameId = this._android.frameId;
@@ -360,7 +407,10 @@ export class Frame extends FrameBase {
navDepth = -1;
}
- navDepth++;
+ if (!isReplace) {
+ navDepth++;
+ }
+
fragmentId++;
const newFragmentTag = `fragment${fragmentId}[${navDepth}]`;
const newFragment = this.createFragment(newEntry, newFragmentTag);
@@ -383,7 +433,7 @@ export class Frame extends FrameBase {
}
public _goBackCore(backstackEntry: BackstackEntry) {
- this._isBack = true;
+ this.navigationType = NavigationType.back;
super._goBackCore(backstackEntry);
navDepth = backstackEntry.navDepth;
@@ -1282,4 +1332,4 @@ export function setActivityCallbacks(activity: android.support.v7.app.AppCompatA
export function setFragmentCallbacks(fragment: android.support.v4.app.Fragment): void {
fragment[CALLBACKS] = new FragmentCallbacksImplementation();
-}
\ No newline at end of file
+}
diff --git a/tns-core-modules/ui/frame/frame.d.ts b/tns-core-modules/ui/frame/frame.d.ts
index 5983e9aa1..36fdc6b95 100644
--- a/tns-core-modules/ui/frame/frame.d.ts
+++ b/tns-core-modules/ui/frame/frame.d.ts
@@ -3,14 +3,14 @@
* @module "ui/frame"
*/ /** */
+import { NavigationType } from "./frame-common";
import { Page, View, Observable, EventData } from "../page";
import { Transition } from "../transition";
export * from "../page";
/**
- * Represents the logical View unit that is responsible for navigation withing an application.
- * Typically an application will have a Frame object at a root level.
+ * Represents the logical View unit that is responsible for navigation within an application.
* Nested frames are supported, enabling hierarchical navigation scenarios.
*/
export class Frame extends View {
@@ -113,12 +113,13 @@ export class Frame extends View {
* @param entry to check
*/
isCurrent(entry: BackstackEntry): boolean;
+
/**
* @private
* @param entry to set as current
- * @param isBack true when we set current because of back navigation.
+ * @param navigationType
*/
- setCurrent(entry: BackstackEntry, isBack: boolean): void;
+ setCurrent(entry: BackstackEntry, navigationType: NavigationType): void;
/**
* @private
*/
@@ -143,6 +144,11 @@ export class Frame extends View {
* @private
*/
_updateActionBar(page?: Page, disableNavBarAnimation?: boolean);
+ /**
+ * @private
+ * @param navigationContext
+ */
+ public performNavigation(navigationContext: NavigationContext): void;
/**
* @private
*/
@@ -154,7 +160,7 @@ export class Frame extends View {
/**
* @private
*/
- _updateBackstack(entry: BackstackEntry, isBack: boolean): void;
+ _updateBackstack(entry: BackstackEntry, navigationType: NavigationType): void;
/**
* @private
*/
@@ -167,10 +173,12 @@ export class Frame extends View {
* @private
*/
_removeFromFrameStack();
+
/**
* @private
+ * Represents the type of navigation.
*/
- _isBack?: boolean;
+ navigationType: NavigationType;
//@endprivate
/**
@@ -275,6 +283,14 @@ export interface NavigationEntry extends ViewEntry {
clearHistory?: boolean;
}
+/**
+ * Represents a context passed to navigation methods.
+ */
+export interface NavigationContext {
+ entry: BackstackEntry;
+ isBackNavigation: boolean;
+}
+
/**
* Represents an object specifying a page navigation transition.
*/
diff --git a/tns-core-modules/ui/frame/frame.ios.ts b/tns-core-modules/ui/frame/frame.ios.ts
index 4fcbf0902..d85cc5189 100644
--- a/tns-core-modules/ui/frame/frame.ios.ts
+++ b/tns-core-modules/ui/frame/frame.ios.ts
@@ -1,12 +1,18 @@
// Definitions.
-import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from ".";
+import {
+ iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition
+} from ".";
import { Page } from "../page";
import { profile } from "../../profiling";
//Types.
-import { FrameBase, View, layout, traceEnabled, traceWrite, traceCategories, isCategorySet } from "./frame-common";
+import {
+ FrameBase, View, isCategorySet, layout, NavigationContext,
+ NavigationType, traceCategories, traceEnabled, traceWrite
+} from "./frame-common";
import { _createIOSAnimatedTransitioning } from "./fragment.transitions";
+import { createViewFromEntry } from "../builder";
import * as utils from "../../utils/utils";
export * from "./frame-common";
@@ -14,9 +20,10 @@ export * from "./frame-common";
const majorVersion = utils.ios.MajorVersion;
const ENTRY = "_entry";
+const DELEGATE = "_delegate";
const NAV_DEPTH = "_navDepth";
const TRANSITION = "_transition";
-const DELEGATE = "_delegate";
+const NON_ANIMATED_TRANSITION = "non-animated";
let navDepth = -1;
@@ -46,18 +53,57 @@ export class Frame extends FrameBase {
return this._ios;
}
- public setCurrent(entry: BackstackEntry, isBack: boolean): void {
+ public setCurrent(entry: BackstackEntry, navigationType: NavigationType): void {
const current = this._currentEntry;
const currentEntryChanged = current !== entry;
if (currentEntryChanged) {
- this._updateBackstack(entry, isBack);
+ this._updateBackstack(entry, navigationType);
- super.setCurrent(entry, isBack);
+ super.setCurrent(entry, navigationType);
+ }
+ }
+
+ public _onLivesync(context?: ModuleContext): boolean {
+ if (traceEnabled()) {
+ traceWrite(`${this}._onLivesync(${JSON.stringify(context)})`, traceCategories.Livesync);
+ }
+
+ if (!this._currentEntry || !this._currentEntry.entry) {
+ return false;
+ }
+
+ if (context && context.type && context.path) {
+ // Set NavigationType.replace for HMR.
+ // When `viewDidAppear()` set to NavigationType.forward.
+ this.navigationType = NavigationType.replace;
+ const currentBackstackEntry = this._currentEntry;
+
+ const contextModuleName = utils.getModuleName(context.path);
+ const newPage = createViewFromEntry({ moduleName: contextModuleName });
+ const newBackstackEntry: BackstackEntry = {
+ entry: currentBackstackEntry.entry,
+ resolvedPage: newPage,
+ navDepth: currentBackstackEntry.navDepth,
+ fragmentTag: undefined
+ }
+
+ const navContext: NavigationContext = { entry: newBackstackEntry, isBackNavigation: false };
+ this.performNavigation(navContext);
+ return true;
+ } else {
+ // Fallback
+ return super._onLivesync();
}
}
@profile
public _navigateCore(backstackEntry: BackstackEntry) {
+ // NavigationType.replace for HMR.
+ // Otherwise, default to NavigationType.forward.
+ const isReplace = this.navigationType === NavigationType.replace;
+ if (!isReplace) {
+ this.navigationType = NavigationType.forward;
+ }
super._navigateCore(backstackEntry);
let viewController: UIViewController = backstackEntry.resolvedPage.ios;
@@ -69,7 +115,9 @@ export class Frame extends FrameBase {
if (clearHistory) {
navDepth = -1;
}
- navDepth++;
+ if (!isReplace) {
+ navDepth++;
+ }
let navigationTransition: NavigationTransition;
let animated = this.currentPage ? this._getIsAnimatedNavigation(backstackEntry.entry) : false;
@@ -81,7 +129,7 @@ export class Frame extends FrameBase {
}
else {
//https://github.com/NativeScript/NativeScript/issues/1787
- viewController[TRANSITION] = { name: "non-animated" };
+ viewController[TRANSITION] = { name: NON_ANIMATED_TRANSITION };
}
let nativeTransition = _getNativeTransition(navigationTransition, true);
@@ -136,7 +184,8 @@ export class Frame extends FrameBase {
}
// We should hide the current entry from the back stack.
- if (!Frame._isEntryBackstackVisible(this._currentEntry)) {
+ // This is the case for HMR when NavigationType.replace.
+ if (!Frame._isEntryBackstackVisible(this._currentEntry) || isReplace) {
let newControllers = NSMutableArray.alloc().initWithArray(this._ios.controller.viewControllers);
if (newControllers.count === 0) {
throw new Error("Wrong controllers count.");
@@ -168,6 +217,7 @@ export class Frame extends FrameBase {
}
public _goBackCore(backstackEntry: BackstackEntry) {
+ this.navigationType = NavigationType.back;
super._goBackCore(backstackEntry);
navDepth = backstackEntry[NAV_DEPTH];
@@ -469,7 +519,7 @@ class UINavigationControllerImpl extends UINavigationController {
traceWrite(`UINavigationControllerImpl.popViewControllerAnimated(${animated}); transition: ${JSON.stringify(navigationTransition)}`, traceCategories.NativeLifecycle);
}
- if (navigationTransition && navigationTransition.name === "non-animated") {
+ if (navigationTransition && navigationTransition.name === NON_ANIMATED_TRANSITION) {
//https://github.com/NativeScript/NativeScript/issues/1787
return super.popViewControllerAnimated(false);
}
@@ -493,7 +543,7 @@ class UINavigationControllerImpl extends UINavigationController {
traceWrite(`UINavigationControllerImpl.popToViewControllerAnimated(${viewController}, ${animated}); transition: ${JSON.stringify(navigationTransition)}`, traceCategories.NativeLifecycle);
}
- if (navigationTransition && navigationTransition.name === "non-animated") {
+ if (navigationTransition && navigationTransition.name === NON_ANIMATED_TRANSITION) {
//https://github.com/NativeScript/NativeScript/issues/1787
return super.popToViewControllerAnimated(viewController, false);
}
diff --git a/tns-core-modules/ui/page/page-common.ts b/tns-core-modules/ui/page/page-common.ts
index 28814e186..f8c152c71 100644
--- a/tns-core-modules/ui/page/page-common.ts
+++ b/tns-core-modules/ui/page/page-common.ts
@@ -99,6 +99,10 @@ export class PageBase extends ContentView implements PageDefinition {
};
}
+ public _onLivesync(context?: ModuleContext): boolean {
+ return this.frame ? this.frame._onLivesync(context) : false;
+ }
+
@profile
public onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any) {
this._navigationContext = context;
@@ -190,4 +194,4 @@ export const androidStatusBarBackgroundProperty = new CssProperty