Skip to content

Commit

Permalink
feat(HMR): style views at runtime (#7012)
Browse files Browse the repository at this point in the history
* feat(HMR): style view at runtime

test: module root view component

test: update livesync tests

refactor: _onLivesync function

* style: remove a comment

* refactor: rename the property
  • Loading branch information
vchimev committed Mar 13, 2019
1 parent 451026f commit 3c2c1d9
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 50 deletions.
3 changes: 3 additions & 0 deletions tests/app/app/main-page.css
@@ -0,0 +1,3 @@
Button {
color: green;
}
28 changes: 27 additions & 1 deletion tests/app/livesync/livesync-tests.ts
Expand Up @@ -15,6 +15,7 @@ const mainPageCssFileName = "./app/main-page.css";
const mainPageHtmlFileName = "./app/main-page.html";
const mainPageXmlFileName = "./app/main-page.xml";

const black = new Color("black");
const green = new Color("green");

const mainPageTemplate = `
Expand Down Expand Up @@ -56,7 +57,7 @@ export function test_onLiveSync_ModuleContext_Script_AppTs() {
}

export function test_onLiveSync_ModuleContext_Style_MainPageCss() {
_test_onLiveSync_ModuleContext({ type: "style", path: mainPageCssFileName });
_test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: mainPageCssFileName });
}

export function test_onLiveSync_ModuleContext_Markup_MainPageHtml() {
Expand Down Expand Up @@ -110,4 +111,29 @@ function _test_onLiveSync_ModuleContext(context: { type, path }) {
const topmostFrame = frame.topmost();
TKUnit.waitUntilReady(() => topmostFrame.currentPage && topmostFrame.currentPage.isLoaded && !topmostFrame.canGoBack());
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded);
}

function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) {
const pageBeforeNavigation = helper.getCurrentPage();

const page = <Page>parse(pageTemplate);
helper.navigateWithHistory(() => page);

const pageBeforeLiveSync = helper.getCurrentPage();
pageBeforeLiveSync._moduleName = "main-page";
global.__onLiveSync({ type: context.type, path: context.path });

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!");

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.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
}
9 changes: 6 additions & 3 deletions tests/app/testRunner.ts
Expand Up @@ -168,12 +168,15 @@ allTests["VISUAL-STATE"] = visualStateTests;
import * as valueSourceTests from "./ui/styling/value-source-tests";
allTests["VALUE-SOURCE"] = valueSourceTests;

import * as buttonTests from "./ui/button/button-tests";
allTests["BUTTON"] = buttonTests;

import * as borderTests from "./ui/border/border-tests";
allTests["BORDER"] = borderTests;

import * as builderTests from "./ui/builder/builder-tests";
allTests["BUILDER"] = builderTests;

import * as buttonTests from "./ui/button/button-tests";
allTests["BUTTON"] = buttonTests;

import * as labelTests from "./ui/label/label-tests";
allTests["LABEL"] = labelTests;

Expand Down
26 changes: 26 additions & 0 deletions tests/app/ui/builder/builder-tests.ts
@@ -0,0 +1,26 @@
import { path } from "tns-core-modules/file-system";
import { loadPage } from "tns-core-modules/ui/builder";
import { assertEqual, assertNull } from "../../TKUnit";

const COMPONENT_MODULE = "component-module";
const LABEL = "label";

function getViewComponent() {
const moduleNamePath = path.join(__dirname, COMPONENT_MODULE);
const fileName = path.join(__dirname, `${COMPONENT_MODULE}.xml`);
const view = loadPage(moduleNamePath, fileName);
return view;
}

export function test_view_is_module_root_component() {
const view = getViewComponent();
const actualModule = view._moduleName;
assertEqual(actualModule, COMPONENT_MODULE, `View<${view}> is NOT root component of module <${COMPONENT_MODULE}>.`);
}

export function test_view_is_NOT_module_root_component() {
const view = getViewComponent();
const nestedView = view.getViewById(`${LABEL}`);
const undefinedModule = nestedView._moduleName;
assertNull(undefinedModule, `View<${nestedView}> should NOT be a root component of a module.`);
}
3 changes: 3 additions & 0 deletions tests/app/ui/builder/component-module.xml
@@ -0,0 +1,3 @@
<StackLayout>
<Label id="label"></Label>
</StackLayout>
16 changes: 12 additions & 4 deletions tns-core-modules/ui/builder/builder.ts
Expand Up @@ -59,7 +59,9 @@ export function load(pathOrOptions: string | LoadOptions, context?: any): View {

export function loadPage(moduleNamePath: string, fileName: string, context?: any): View {
const componentModule = loadInternal(fileName, context, moduleNamePath);
return componentModule && componentModule.component;
const componentView = componentModule && componentModule.component;
markAsModuleRoot(componentView, moduleNamePath);
return componentView;
}

const loadModule = profile("loadModule", (moduleNamePath: string, entry: ViewEntry): ModuleExports => {
Expand Down Expand Up @@ -96,7 +98,7 @@ export const createViewFromEntry = profile("createViewFromEntry", (entry: ViewEn
} else if (entry.moduleName) {
// Current app full path.
const currentAppPath = knownFolders.currentApp().path;

// Full path of the module = current app full path + module name.
const moduleNamePath = path.join(currentAppPath, entry.moduleName);
const moduleExports = loadModule(moduleNamePath, entry);
Expand All @@ -108,7 +110,7 @@ export const createViewFromEntry = profile("createViewFromEntry", (entry: ViewEn
return viewFromBuilder(moduleNamePath, moduleExports);
}
}

throw new Error("Failed to load page XML file for module: " + entry.moduleName);
});

Expand All @@ -128,14 +130,20 @@ interface ModuleExports {
const moduleCreateView = profile("module.createView", (moduleNamePath: string, moduleExports: ModuleExports): View => {
const view = moduleExports.createPage();
const cssFileName = resolveFileName(moduleNamePath, "css");

// If there is no cssFile only appCss will be applied at loaded.
if (cssFileName) {
view.addCssFile(cssFileName);
}
return view;
});

function markAsModuleRoot(componentView: View, moduleNamePath: string): void {
const lastIndexOfSeparator = moduleNamePath.lastIndexOf(path.separator);
const moduleName = moduleNamePath.substring(lastIndexOfSeparator + 1);
componentView._moduleName = moduleName;
}

function loadInternal(fileName: string, context?: any, moduleNamePath?: string): ComponentModule {
let componentModule: ComponentModule;

Expand Down
6 changes: 6 additions & 0 deletions tns-core-modules/ui/core/view-base/view-base.d.ts
Expand Up @@ -108,6 +108,12 @@ export abstract class ViewBase extends Observable {
flexWrapBefore: FlexWrapBefore;
alignSelf: AlignSelf;

/**
* @private
* Module name when the view is a module root. Otherwise, it is undefined.
*/
_moduleName?: string;

//@private
/**
* @private
Expand Down
6 changes: 4 additions & 2 deletions tns-core-modules/ui/core/view-base/view-base.ts
Expand Up @@ -244,6 +244,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public _defaultPaddingLeft: number;
public _isPaddingRelative: boolean;

public _moduleName: string;

constructor() {
super();
this._domId = viewIdCounter++;
Expand Down Expand Up @@ -651,7 +653,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}

public resetNativeView(): void {
//
//
}

private resetNativeViewInternal(): void {
Expand Down Expand Up @@ -688,7 +690,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition

this._context = context;

// This will account for nativeView that is created in createNativeView, recycled
// This will account for nativeView that is created in createNativeView, recycled
// or for backward compatability - set before _setupUI in iOS contructor.
let nativeView = this.nativeViewProtected;

Expand Down
39 changes: 32 additions & 7 deletions tns-core-modules/ui/core/view/view-common.ts
Expand Up @@ -5,7 +5,7 @@ import {
} from ".";

import {
ViewBase, Property, booleanConverter, EventData, layout,
ViewBase, Property, booleanConverter, eachDescendant, EventData, layout,
getEventOrGestureName, traceEnabled, traceWrite, traceCategories,
InheritedProperty,
ShowModalOptions
Expand Down Expand Up @@ -136,6 +136,37 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
}

public _onLivesync(context?: ModuleContext): boolean {
_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);
}

return false;
}

private changeLocalStyles(contextPath: string): boolean {
if (!this.changeStyles(this, contextPath)) {
eachDescendant(this, (child: ViewBase) => {
this.changeStyles(child, contextPath);
return true;
});
}
// Do not execute frame navigation for a change in styles
return true;
}

private changeStyles(view: ViewBase, contextPath: string): boolean {
if (view._moduleName && contextPath.includes(view._moduleName)) {
(<this>view).changeCssFile(contextPath);
return true;
}
return false;
}

_setupAsRootView(context: any): void {
super._setupAsRootView(context);
if (!this._styleScope) {
Expand Down Expand Up @@ -210,12 +241,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
}

_onLivesync(): boolean {
_rootModalViews.forEach(v => v.closeModal());
_rootModalViews.length = 0;
return false;
}

public onBackPressed(): boolean {
return false;
}
Expand Down
56 changes: 23 additions & 33 deletions tns-core-modules/ui/frame/frame-common.ts
Expand Up @@ -564,44 +564,34 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
}

public _onLivesync(context?: ModuleContext): boolean {
super._onLivesync();

if (!this._currentEntry || !this._currentEntry.entry) {
return false;
}

const currentEntry = this._currentEntry.entry;
if (context && context.path) {
// Use topmost instead of this to cover nested frames scenario
const topmostFrame = topmost();
const moduleName = topmostFrame.currentEntry.moduleName;
const reapplyStyles = context.path.includes(moduleName);
if (reapplyStyles && moduleName) {
topmostFrame.currentPage.changeCssFile(context.path);
return true;
// Execute a navigation if not handled on `View` level
if (!super._onLivesync(context)) {
if (!this._currentEntry || !this._currentEntry.entry) {
return false;
}
}

const newEntry: NavigationEntry = {
animated: false,
clearHistory: true,
context: currentEntry.context,
create: currentEntry.create,
moduleName: currentEntry.moduleName,
backstackVisible: currentEntry.backstackVisible
}
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;
// 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;
}
}
Expand Down

0 comments on commit 3c2c1d9

Please sign in to comment.