mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat(HMR): style views at runtime (#7012)
* 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
This commit is contained in:
3
tests/app/app/main-page.css
Normal file
3
tests/app/app/main-page.css
Normal file
@@ -0,0 +1,3 @@
|
||||
Button {
|
||||
color: green;
|
||||
}
|
||||
@@ -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 = `
|
||||
@@ -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() {
|
||||
@@ -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!");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
26
tests/app/ui/builder/builder-tests.ts
Normal file
26
tests/app/ui/builder/builder-tests.ts
Normal file
@@ -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
tests/app/ui/builder/component-module.xml
Normal file
3
tests/app/ui/builder/component-module.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<StackLayout>
|
||||
<Label id="label"></Label>
|
||||
</StackLayout>
|
||||
@@ -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 => {
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -128,7 +130,7 @@ 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);
|
||||
@@ -136,6 +138,12 @@ const moduleCreateView = profile("module.createView", (moduleNamePath: string, m
|
||||
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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++;
|
||||
@@ -651,7 +653,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
}
|
||||
|
||||
public resetNativeView(): void {
|
||||
//
|
||||
//
|
||||
}
|
||||
|
||||
private resetNativeViewInternal(): void {
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from ".";
|
||||
|
||||
import {
|
||||
ViewBase, Property, booleanConverter, EventData, layout,
|
||||
ViewBase, Property, booleanConverter, eachDescendant, EventData, layout,
|
||||
getEventOrGestureName, traceEnabled, traceWrite, traceCategories,
|
||||
InheritedProperty,
|
||||
ShowModalOptions
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Execute a navigation if not handled on `View` level
|
||||
if (!super._onLivesync(context)) {
|
||||
if (!this._currentEntry || !this._currentEntry.entry) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.navigate(newEntry);
|
||||
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);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user