feat(hmr): preserve navigation history on applying changes (#7146)

This commit is contained in:
Vasil Chimev
2019-04-23 17:47:29 +03:00
committed by GitHub
parent 4e56c89f7d
commit d35e14ed0f
23 changed files with 414 additions and 168 deletions

View File

@ -0,0 +1,5 @@
<Page loaded="onLoaded">
<StackLayout>
<Button id="button" text="button"></Button>
</StackLayout>
</Page>

View File

@ -0,0 +1,3 @@
export function onLoaded() {
console.log("Button page loaded!");
}

View File

@ -0,0 +1,5 @@
<Page loaded="onLoaded">
<StackLayout>
<Button id="button" text="button"></Button>
</StackLayout>
</Page>

View File

@ -0,0 +1,3 @@
export function onLoaded() {
console.log("Label page loaded!");
}

View File

@ -0,0 +1,5 @@
<Page loaded="onLoaded">
<StackLayout>
<Label id="label" text="label"></Label>
</StackLayout>
</Page>

View File

@ -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 = `
<Page>
<StackLayout>
<Label id="label" text="label"></Label>
</StackLayout>
</Page>`;
const pageTemplate = `
<Page>
<StackLayout>
<Button id="button" text="button"></Button>
</StackLayout>
</Page>`;
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 = <Page>parse(mainPageTemplate);
helper.navigate(() => mainPage);
export function setUp() {
const labelPage = <Page>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 = <Page>createViewFromEntry(({ moduleName: buttonPageModuleName }));
helper.navigateWithHistory(() => buttonPage);
const page = <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 = <Page>parse(pageTemplate);
helper.navigateWithHistory(() => page);
const buttonPage = <Page>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 = <Page>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 = <Page>parse(pageTemplate);
helper.navigateWithHistory(() => page);
const buttonPage = <Page>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!");
}
}
function waitUntilLivesyncComplete(frame: Frame) {
if (isAndroid) {
TKUnit.waitUntilReady(() => frame._executingEntry === null);
} else {
TKUnit.waitUntilReady(() => frame.currentPage.isLoaded);
}
}

View File

@ -5,11 +5,11 @@
"repository": "<fill-your-repository-here>",
"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": {

View File

@ -83,22 +83,20 @@ export function livesync(rootView: View, context?: ModuleContext) {
events.notify(<EventData>{ 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);
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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.
*/

View File

@ -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)) {
(<this>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) {

View File

@ -232,22 +232,22 @@ export function _getAnimatedEntries(frameId: number): Set<BackstackEntry> {
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 = <ExpandedAnimator>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);

View File

@ -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;
}
}

View File

@ -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 = <Page>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();
}
}

View File

@ -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.
*/

View File

@ -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 = <Page>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<UIViewController>().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);
}

View File

@ -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<Style, Color>(
name: "androidStatusBarBackground", cssName: "android-status-bar-background",
equalityComparer: Color.equals, valueConverter: (v) => new Color(v)
});
androidStatusBarBackgroundProperty.register(Style);
androidStatusBarBackgroundProperty.register(Style);

View File

@ -1,11 +1,11 @@
// Definitions.
import { Frame } from "../frame";
import { NavigationType } from "../frame/frame-common";
// Types.
import { ios as iosView } from "../core/view";
import {
PageBase, View, layout,
actionBarHiddenProperty, statusBarStyleProperty, Color
PageBase, View, layout, actionBarHiddenProperty, statusBarStyleProperty, Color
} from "./page-common";
import { profile } from "../../profiling";
@ -20,7 +20,7 @@ const majorVersion = iosUtils.MajorVersion;
function isBackNavigationTo(page: Page, entry): boolean {
const frame = page.frame;
if (!frame) {
if (!frame || frame.navigationType === NavigationType.replace) {
return false;
}
@ -133,14 +133,20 @@ class UIViewControllerImpl extends UIViewController {
const newEntry = this[ENTRY];
let isBack: boolean;
let navType = frame.navigationType;
// We are on the current page which happens when navigation is canceled so isBack should be false.
if (frame.currentPage === owner && frame._navigationQueue.length === 0) {
if (navType !== NavigationType.replace && frame.currentPage === owner && frame._navigationQueue.length === 0) {
isBack = false;
navType = NavigationType.forward;
} else {
isBack = isBackNavigationTo(owner, newEntry);
if (isBack) {
navType = NavigationType.back;
}
}
frame.setCurrent(newEntry, isBack);
frame.setCurrent(newEntry, navType);
frame.navigationType = isBack ? NavigationType.back : NavigationType.forward;
// If page was shown with custom animation - we need to set the navigationController.delegate to the animatedDelegate.
frame.ios.controller.delegate = this[DELEGATE];
@ -182,7 +188,7 @@ class UIViewControllerImpl extends UIViewController {
const frame = owner.frame;
// Skip navigation events if we are hiding because we are about to show a modal page,
// or because we are closing a modal page,
// or because we are closing a modal page,
// or because we are in tab and another controller is selected.
const tab = this.tabBarController;
if (owner.onNavigatingFrom && !owner._presentedViewController && !this.presentingViewController && frame && frame.currentPage === owner) {

View File

@ -28,6 +28,11 @@ export function convertString(value: any): any {
return result;
}
export function getModuleName(path: string): string {
let moduleName = path.replace("./", "");
return moduleName.substring(0, moduleName.lastIndexOf("."));
}
export module layout {
const MODE_SHIFT = 30;
const MODE_MASK = 0x3 << MODE_SHIFT;
@ -148,4 +153,4 @@ export function hasDuplicates(arr: Array<any>): boolean {
export function eliminateDuplicates(arr: Array<any>): Array<any> {
return Array.from(new Set(arr));
}
}

View File

@ -185,13 +185,13 @@ export module ad {
export module ios {
/**
* @deprecated use the respective native property directly
*
*
* Checks if the property is a function and if it is, calls it on this.
* Designed to support backward compatibility for methods that became properties.
* Will not work on delegates since it checks if the propertyValue is a function, and delegates are marshalled as functions.
* Example: getter(NSRunLoop, NSRunLoop.currentRunLoop).runUntilDate(NSDate.dateWithTimeIntervalSinceNow(waitTime));
*/
export function getter<T>(_this: any, propertyValue: T | {(): T}): T;
export function getter<T>(_this: any, propertyValue: T | { (): T }): T;
// Common properties between UILabel, UITextView and UITextField
export interface TextUIView {
@ -255,7 +255,7 @@ export module ios {
* @param rootViewController The root UIViewController instance to start searching from (normally window.rootViewController).
* Returns the visible UIViewController.
*/
export function getVisibleViewController(rootViewController: any/* UIViewController*/ ): any/* UIViewController*/;
export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/;
}
/**
@ -305,6 +305,12 @@ export function escapeRegexSymbols(source: string): string
*/
export function convertString(value: any): any
/**
* Gets module name from path.
* @param path The module path.
*/
export function getModuleName(path: string): string
/**
* Sorts an array by using merge sort algorithm (which ensures stable sort since the built-in Array.sort() does not promise a stable sort).
* @param arr - array to be sorted