Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3c2c1d9

Browse filesBrowse files
authored
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
1 parent 451026f commit 3c2c1d9
Copy full SHA for 3c2c1d9

File tree

10 files changed

+142
-50
lines changed
Filter options

10 files changed

+142
-50
lines changed

‎tests/app/app/main-page.css

Copy file name to clipboard
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Button {
2+
color: green;
3+
}

‎tests/app/livesync/livesync-tests.ts

Copy file name to clipboardExpand all lines: tests/app/livesync/livesync-tests.ts
+27-1Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const mainPageCssFileName = "./app/main-page.css";
1515
const mainPageHtmlFileName = "./app/main-page.html";
1616
const mainPageXmlFileName = "./app/main-page.xml";
1717

18+
const black = new Color("black");
1819
const green = new Color("green");
1920

2021
const mainPageTemplate = `
@@ -56,7 +57,7 @@ export function test_onLiveSync_ModuleContext_Script_AppTs() {
5657
}
5758

5859
export function test_onLiveSync_ModuleContext_Style_MainPageCss() {
59-
_test_onLiveSync_ModuleContext({ type: "style", path: mainPageCssFileName });
60+
_test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: mainPageCssFileName });
6061
}
6162

6263
export function test_onLiveSync_ModuleContext_Markup_MainPageHtml() {
@@ -110,4 +111,29 @@ function _test_onLiveSync_ModuleContext(context: { type, path }) {
110111
const topmostFrame = frame.topmost();
111112
TKUnit.waitUntilReady(() => topmostFrame.currentPage && topmostFrame.currentPage.isLoaded && !topmostFrame.canGoBack());
112113
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded);
114+
}
115+
116+
function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) {
117+
const pageBeforeNavigation = helper.getCurrentPage();
118+
119+
const page = <Page>parse(pageTemplate);
120+
helper.navigateWithHistory(() => page);
121+
122+
const pageBeforeLiveSync = helper.getCurrentPage();
123+
pageBeforeLiveSync._moduleName = "main-page";
124+
global.__onLiveSync({ type: context.type, path: context.path });
125+
126+
const pageAfterLiveSync = helper.getCurrentPage();
127+
TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString());
128+
129+
TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Local styles NOT applied - livesync navigation executed!");
130+
TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!");
131+
TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!");
132+
133+
helper.goBack();
134+
135+
const pageAfterNavigationBack = helper.getCurrentPage();
136+
TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, black, "App styles applied on back navigation!");
137+
TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!");
138+
TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
113139
}

‎tests/app/testRunner.ts

Copy file name to clipboardExpand all lines: tests/app/testRunner.ts
+6-3Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,15 @@ allTests["VISUAL-STATE"] = visualStateTests;
168168
import * as valueSourceTests from "./ui/styling/value-source-tests";
169169
allTests["VALUE-SOURCE"] = valueSourceTests;
170170

171-
import * as buttonTests from "./ui/button/button-tests";
172-
allTests["BUTTON"] = buttonTests;
173-
174171
import * as borderTests from "./ui/border/border-tests";
175172
allTests["BORDER"] = borderTests;
176173

174+
import * as builderTests from "./ui/builder/builder-tests";
175+
allTests["BUILDER"] = builderTests;
176+
177+
import * as buttonTests from "./ui/button/button-tests";
178+
allTests["BUTTON"] = buttonTests;
179+
177180
import * as labelTests from "./ui/label/label-tests";
178181
allTests["LABEL"] = labelTests;
179182

‎tests/app/ui/builder/builder-tests.ts

Copy file name to clipboard
+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { path } from "tns-core-modules/file-system";
2+
import { loadPage } from "tns-core-modules/ui/builder";
3+
import { assertEqual, assertNull } from "../../TKUnit";
4+
5+
const COMPONENT_MODULE = "component-module";
6+
const LABEL = "label";
7+
8+
function getViewComponent() {
9+
const moduleNamePath = path.join(__dirname, COMPONENT_MODULE);
10+
const fileName = path.join(__dirname, `${COMPONENT_MODULE}.xml`);
11+
const view = loadPage(moduleNamePath, fileName);
12+
return view;
13+
}
14+
15+
export function test_view_is_module_root_component() {
16+
const view = getViewComponent();
17+
const actualModule = view._moduleName;
18+
assertEqual(actualModule, COMPONENT_MODULE, `View<${view}> is NOT root component of module <${COMPONENT_MODULE}>.`);
19+
}
20+
21+
export function test_view_is_NOT_module_root_component() {
22+
const view = getViewComponent();
23+
const nestedView = view.getViewById(`${LABEL}`);
24+
const undefinedModule = nestedView._moduleName;
25+
assertNull(undefinedModule, `View<${nestedView}> should NOT be a root component of a module.`);
26+
}
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<StackLayout>
2+
<Label id="label"></Label>
3+
</StackLayout>

‎tns-core-modules/ui/builder/builder.ts

Copy file name to clipboardExpand all lines: tns-core-modules/ui/builder/builder.ts
+12-4Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export function load(pathOrOptions: string | LoadOptions, context?: any): View {
5959

6060
export function loadPage(moduleNamePath: string, fileName: string, context?: any): View {
6161
const componentModule = loadInternal(fileName, context, moduleNamePath);
62-
return componentModule && componentModule.component;
62+
const componentView = componentModule && componentModule.component;
63+
markAsModuleRoot(componentView, moduleNamePath);
64+
return componentView;
6365
}
6466

6567
const loadModule = profile("loadModule", (moduleNamePath: string, entry: ViewEntry): ModuleExports => {
@@ -96,7 +98,7 @@ export const createViewFromEntry = profile("createViewFromEntry", (entry: ViewEn
9698
} else if (entry.moduleName) {
9799
// Current app full path.
98100
const currentAppPath = knownFolders.currentApp().path;
99-
101+
100102
// Full path of the module = current app full path + module name.
101103
const moduleNamePath = path.join(currentAppPath, entry.moduleName);
102104
const moduleExports = loadModule(moduleNamePath, entry);
@@ -108,7 +110,7 @@ export const createViewFromEntry = profile("createViewFromEntry", (entry: ViewEn
108110
return viewFromBuilder(moduleNamePath, moduleExports);
109111
}
110112
}
111-
113+
112114
throw new Error("Failed to load page XML file for module: " + entry.moduleName);
113115
});
114116

@@ -128,14 +130,20 @@ interface ModuleExports {
128130
const moduleCreateView = profile("module.createView", (moduleNamePath: string, moduleExports: ModuleExports): View => {
129131
const view = moduleExports.createPage();
130132
const cssFileName = resolveFileName(moduleNamePath, "css");
131-
133+
132134
// If there is no cssFile only appCss will be applied at loaded.
133135
if (cssFileName) {
134136
view.addCssFile(cssFileName);
135137
}
136138
return view;
137139
});
138140

141+
function markAsModuleRoot(componentView: View, moduleNamePath: string): void {
142+
const lastIndexOfSeparator = moduleNamePath.lastIndexOf(path.separator);
143+
const moduleName = moduleNamePath.substring(lastIndexOfSeparator + 1);
144+
componentView._moduleName = moduleName;
145+
}
146+
139147
function loadInternal(fileName: string, context?: any, moduleNamePath?: string): ComponentModule {
140148
let componentModule: ComponentModule;
141149

‎tns-core-modules/ui/core/view-base/view-base.d.ts

Copy file name to clipboardExpand all lines: tns-core-modules/ui/core/view-base/view-base.d.ts
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ export abstract class ViewBase extends Observable {
108108
flexWrapBefore: FlexWrapBefore;
109109
alignSelf: AlignSelf;
110110

111+
/**
112+
* @private
113+
* Module name when the view is a module root. Otherwise, it is undefined.
114+
*/
115+
_moduleName?: string;
116+
111117
//@private
112118
/**
113119
* @private

‎tns-core-modules/ui/core/view-base/view-base.ts

Copy file name to clipboardExpand all lines: tns-core-modules/ui/core/view-base/view-base.ts
+4-2Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
244244
public _defaultPaddingLeft: number;
245245
public _isPaddingRelative: boolean;
246246

247+
public _moduleName: string;
248+
247249
constructor() {
248250
super();
249251
this._domId = viewIdCounter++;
@@ -651,7 +653,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
651653
}
652654

653655
public resetNativeView(): void {
654-
//
656+
//
655657
}
656658

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

689691
this._context = context;
690692

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

‎tns-core-modules/ui/core/view/view-common.ts

Copy file name to clipboardExpand all lines: tns-core-modules/ui/core/view/view-common.ts
+32-7Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from ".";
66

77
import {
8-
ViewBase, Property, booleanConverter, EventData, layout,
8+
ViewBase, Property, booleanConverter, eachDescendant, EventData, layout,
99
getEventOrGestureName, traceEnabled, traceWrite, traceCategories,
1010
InheritedProperty,
1111
ShowModalOptions
@@ -136,6 +136,37 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
136136
}
137137
}
138138

139+
public _onLivesync(context?: ModuleContext): boolean {
140+
_rootModalViews.forEach(v => v.closeModal());
141+
_rootModalViews.length = 0;
142+
143+
// Currently, we pass `context` only for style modules
144+
if (context && context.path) {
145+
return this.changeLocalStyles(context.path);
146+
}
147+
148+
return false;
149+
}
150+
151+
private changeLocalStyles(contextPath: string): boolean {
152+
if (!this.changeStyles(this, contextPath)) {
153+
eachDescendant(this, (child: ViewBase) => {
154+
this.changeStyles(child, contextPath);
155+
return true;
156+
});
157+
}
158+
// Do not execute frame navigation for a change in styles
159+
return true;
160+
}
161+
162+
private changeStyles(view: ViewBase, contextPath: string): boolean {
163+
if (view._moduleName && contextPath.includes(view._moduleName)) {
164+
(<this>view).changeCssFile(contextPath);
165+
return true;
166+
}
167+
return false;
168+
}
169+
139170
_setupAsRootView(context: any): void {
140171
super._setupAsRootView(context);
141172
if (!this._styleScope) {
@@ -210,12 +241,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
210241
}
211242
}
212243

213-
_onLivesync(): boolean {
214-
_rootModalViews.forEach(v => v.closeModal());
215-
_rootModalViews.length = 0;
216-
return false;
217-
}
218-
219244
public onBackPressed(): boolean {
220245
return false;
221246
}

‎tns-core-modules/ui/frame/frame-common.ts

Copy file name to clipboardExpand all lines: tns-core-modules/ui/frame/frame-common.ts
+23-33Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -564,44 +564,34 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
564564
}
565565

566566
public _onLivesync(context?: ModuleContext): boolean {
567-
super._onLivesync();
568-
569-
if (!this._currentEntry || !this._currentEntry.entry) {
570-
return false;
571-
}
572-
573-
const currentEntry = this._currentEntry.entry;
574-
if (context && context.path) {
575-
// Use topmost instead of this to cover nested frames scenario
576-
const topmostFrame = topmost();
577-
const moduleName = topmostFrame.currentEntry.moduleName;
578-
const reapplyStyles = context.path.includes(moduleName);
579-
if (reapplyStyles && moduleName) {
580-
topmostFrame.currentPage.changeCssFile(context.path);
581-
return true;
567+
// Execute a navigation if not handled on `View` level
568+
if (!super._onLivesync(context)) {
569+
if (!this._currentEntry || !this._currentEntry.entry) {
570+
return false;
582571
}
583-
}
584572

585-
const newEntry: NavigationEntry = {
586-
animated: false,
587-
clearHistory: true,
588-
context: currentEntry.context,
589-
create: currentEntry.create,
590-
moduleName: currentEntry.moduleName,
591-
backstackVisible: currentEntry.backstackVisible
592-
}
573+
const currentEntry = this._currentEntry.entry;
574+
const newEntry: NavigationEntry = {
575+
animated: false,
576+
clearHistory: true,
577+
context: currentEntry.context,
578+
create: currentEntry.create,
579+
moduleName: currentEntry.moduleName,
580+
backstackVisible: currentEntry.backstackVisible
581+
}
593582

594-
// If create returns the same page instance we can't recreate it.
595-
// Instead of navigation set activity content.
596-
// This could happen if current page was set in XML as a Page instance.
597-
if (newEntry.create) {
598-
const page = newEntry.create();
599-
if (page === this.currentPage) {
600-
return false;
583+
// If create returns the same page instance we can't recreate it.
584+
// Instead of navigation set activity content.
585+
// This could happen if current page was set in XML as a Page instance.
586+
if (newEntry.create) {
587+
const page = newEntry.create();
588+
if (page === this.currentPage) {
589+
return false;
590+
}
601591
}
602-
}
603592

604-
this.navigate(newEntry);
593+
this.navigate(newEntry);
594+
}
605595
return true;
606596
}
607597
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.