feat(HMR): apply changes in application styles at runtime

Expose `HmrContext` interface.
Apply changes in `app.css` instantly.
Avoid navigation on livesync when changes in `app.css` have been made.
Apply changes in `app.css` on back navigation.
This commit is contained in:
Vasil Chimev
2018-10-04 19:20:13 +03:00
parent 60957799ad
commit 42a1491e6e
9 changed files with 110 additions and 34 deletions

View File

@ -70,11 +70,11 @@ export function setApplication(instance: iOSApplication | AndroidApplication): v
app = instance; app = instance;
} }
export function livesync() { export function livesync(context?: HmrContext) {
events.notify(<EventData>{ eventName: "livesync", object: app }); events.notify(<EventData>{ eventName: "livesync", object: app });
const liveSyncCore = global.__onLiveSyncCore; const liveSyncCore = global.__onLiveSyncCore;
if (liveSyncCore) { if (liveSyncCore) {
liveSyncCore(); liveSyncCore(context);
} }
} }
@ -92,7 +92,7 @@ export function loadAppCss(): void {
events.notify(<LoadAppCSSEventData>{ eventName: "loadAppCss", object: app, cssFile: getCssFileName() }); events.notify(<LoadAppCSSEventData>{ eventName: "loadAppCss", object: app, cssFile: getCssFileName() });
} catch (e) { } catch (e) {
throw new Error(`The file ${getCssFileName()} couldn't be loaded! ` + throw new Error(`The file ${getCssFileName()} couldn't be loaded! ` +
`You may need to register it inside ./app/vendor.ts.`); `You may need to register it inside ./app/vendor.ts.`);
} }
} }

View File

@ -212,12 +212,12 @@ export function getNativeApplication(): android.app.Application {
return nativeApp; return nativeApp;
} }
global.__onLiveSync = function () { global.__onLiveSync = function __onLiveSync(context?: HmrContext) {
if (androidApp && androidApp.paused) { if (androidApp && androidApp.paused) {
return; return;
} }
livesync(); livesync(context);
}; };
function initLifecycleCallbacks() { function initLifecycleCallbacks() {

View File

@ -18,6 +18,7 @@ export * from "./application-common";
import { createViewFromEntry } from "../ui/builder"; import { createViewFromEntry } from "../ui/builder";
import { ios as iosView, View } from "../ui/core/view"; import { ios as iosView, View } from "../ui/core/view";
import { Frame, NavigationEntry } from "../ui/frame"; import { Frame, NavigationEntry } from "../ui/frame";
import { loadCss } from "../ui/styling/style-scope";
import * as utils from "../utils/utils"; import * as utils from "../utils/utils";
import { profile, level as profilingLevel, Level } from "../profiling"; import { profile, level as profilingLevel, Level } from "../profiling";
@ -225,10 +226,21 @@ class IOSApplication implements IOSApplicationDefinition {
} }
} }
public _onLivesync(): void { public _onLivesync(context?: HmrContext): void {
// If view can't handle livesync set window controller. let executeLivesync = true;
if (!this._rootView._onLivesync()) { // HMR has context, livesync does not
this.setWindowContent(); if (context) {
if (context.module === getCssFileName()) {
loadCss(context.module);
this._rootView._onCssStateChange();
executeLivesync = false;
}
}
if (executeLivesync) {
// If view can't handle livesync set window controller.
if (!this._rootView._onLivesync()) {
this.setWindowContent();
}
} }
} }
@ -264,8 +276,8 @@ exports.ios = iosApp;
setApplication(iosApp); setApplication(iosApp);
// attach on global, so it can be overwritten in NativeScript Angular // attach on global, so it can be overwritten in NativeScript Angular
(<any>global).__onLiveSyncCore = function () { (<any>global).__onLiveSyncCore = function __onLiveSyncCore(context?: HmrContext) {
iosApp._onLivesync(); iosApp._onLivesync(context);
} }
let mainEntry: NavigationEntry; let mainEntry: NavigationEntry;
@ -373,10 +385,10 @@ function setViewControllerView(view: View): void {
} }
} }
global.__onLiveSync = function () { global.__onLiveSync = function __onLiveSync(context?: HmrContext) {
if (!started) { if (!started) {
return; return;
} }
livesync(); livesync(context);
} }

View File

@ -51,8 +51,8 @@ declare namespace NodeJS {
__native?: any; __native?: any;
__inspector?: any; __inspector?: any;
__extends: any; __extends: any;
__onLiveSync: () => void; __onLiveSync: (context?: { type: string, module: string }) => void;
__onLiveSyncCore: () => void; __onLiveSyncCore: (context?: { type: string, module: string }) => void;
__onUncaughtError: (error: NativeScriptError) => void; __onUncaughtError: (error: NativeScriptError) => void;
TNS_WEBPACK?: boolean; TNS_WEBPACK?: boolean;
__requireOverride?: (name: string, dir: string) => any; __requireOverride?: (name: string, dir: string) => any;
@ -64,6 +64,27 @@ declare function clearTimeout(timeoutId: number): void;
declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): number; declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): number;
declare function clearInterval(intervalId: number): void; declare function clearInterval(intervalId: number): void;
declare enum HmrType {
markup = "markup",
script = "script",
style = "style"
}
/**
* Define a context for Hot Module Replacement.
*/
interface HmrContext {
/**
* The type of module for replacement.
*/
type: HmrType;
/**
* The module for replacement.
*/
module: string;
}
/** /**
* An extended JavaScript Error which will have the nativeError property initialized in case the error is caused by executing platform-specific code. * An extended JavaScript Error which will have the nativeError property initialized in case the error is caused by executing platform-specific code.
*/ */

View File

@ -17,6 +17,7 @@ import {
_updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, AnimationType _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, AnimationType
} from "./fragment.transitions"; } from "./fragment.transitions";
import { loadCss } from "../styling/style-scope";
import { profile } from "../../profiling"; import { profile } from "../../profiling";
// TODO: Remove this and get it from global to decouple builder for angular // TODO: Remove this and get it from global to decouple builder for angular
@ -82,13 +83,24 @@ function getAttachListener(): android.view.View.OnAttachStateChangeListener {
return attachStateChangeListener; return attachStateChangeListener;
} }
export function reloadPage(): void { export function reloadPage(context?: HmrContext): void {
const activity = application.android.foregroundActivity; const activity = application.android.foregroundActivity;
const callbacks: AndroidActivityCallbacks = activity[CALLBACKS]; const callbacks: AndroidActivityCallbacks = activity[CALLBACKS];
const rootView: View = callbacks.getRootView(); const rootView: View = callbacks.getRootView();
if (!rootView || !rootView._onLivesync()) { let executeLivesync = true;
callbacks.resetActivityContent(activity); // HMR has context, livesync does not
if (context) {
if (context.module === application.getCssFileName()) {
loadCss(context.module);
rootView._onCssStateChange();
executeLivesync = false;
}
}
if (executeLivesync) {
if (!rootView || !rootView._onLivesync()) {
callbacks.resetActivityContent(activity);
}
} }
} }
@ -469,19 +481,19 @@ export class Frame extends FrameBase {
switch (this.actionBarVisibility) { switch (this.actionBarVisibility) {
case "never": case "never":
return false; return false;
case "always": case "always":
return true; return true;
default: default:
if (page.actionBarHidden !== undefined) { if (page.actionBarHidden !== undefined) {
return !page.actionBarHidden; return !page.actionBarHidden;
} }
if (this._android && this._android.showActionBar !== undefined) { if (this._android && this._android.showActionBar !== undefined) {
return this._android.showActionBar; return this._android.showActionBar;
} }
return true; return true;
} }
} }
@ -846,14 +858,14 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
// parent while its supposed parent believes it properly removed its children; in order to "force" the child to // parent while its supposed parent believes it properly removed its children; in order to "force" the child to
// lose its parent we temporarily add it to the parent, and then remove it (addViewInLayout doesn't trigger layout pass) // lose its parent we temporarily add it to the parent, and then remove it (addViewInLayout doesn't trigger layout pass)
const nativeView = page.nativeViewProtected; const nativeView = page.nativeViewProtected;
if (nativeView != null) { if (nativeView != null) {
const parentView = nativeView.getParent(); const parentView = nativeView.getParent();
if (parentView instanceof android.view.ViewGroup) { if (parentView instanceof android.view.ViewGroup) {
if (parentView.getChildCount() === 0) { if (parentView.getChildCount() === 0) {
parentView.addViewInLayout(nativeView, -1, new org.nativescript.widgets.CommonLayoutParams()); parentView.addViewInLayout(nativeView, -1, new org.nativescript.widgets.CommonLayoutParams());
} }
parentView.removeView(nativeView); parentView.removeView(nativeView);
} }
} }

View File

@ -17,17 +17,17 @@ export class PageBase extends ContentView implements PageDefinition {
public static navigatedToEvent = "navigatedTo"; public static navigatedToEvent = "navigatedTo";
public static navigatingFromEvent = "navigatingFrom"; public static navigatingFromEvent = "navigatingFrom";
public static navigatedFromEvent = "navigatedFrom"; public static navigatedFromEvent = "navigatedFrom";
private _navigationContext: any; private _navigationContext: any;
private _actionBar: ActionBar; private _actionBar: ActionBar;
public _frame: Frame; public _frame: Frame;
public actionBarHidden: boolean; public actionBarHidden: boolean;
public enableSwipeBackNavigation: boolean; public enableSwipeBackNavigation: boolean;
public backgroundSpanUnderStatusBar: boolean; public backgroundSpanUnderStatusBar: boolean;
public hasActionBar: boolean; public hasActionBar: boolean;
get navigationContext(): any { get navigationContext(): any {
return this._navigationContext; return this._navigationContext;
} }
@ -89,7 +89,7 @@ export class PageBase extends ContentView implements PageDefinition {
const frame = this.parent; const frame = this.parent;
return frame instanceof Frame ? frame : undefined; return frame instanceof Frame ? frame : undefined;
} }
private createNavigatedData(eventName: string, isBackNavigation: boolean): NavigatedData { private createNavigatedData(eventName: string, isBackNavigation: boolean): NavigatedData {
return { return {
eventName: eventName, eventName: eventName,
@ -103,6 +103,10 @@ export class PageBase extends ContentView implements PageDefinition {
public onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any) { public onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any) {
this._navigationContext = context; this._navigationContext = context;
if (!this._cssState.isSelectorsLatestVersionApplied()) {
this._onCssStateChange();
}
//https://github.com/NativeScript/NativeScript/issues/731 //https://github.com/NativeScript/NativeScript/issues/731
if (!isBackNavigation && bindingContext !== undefined && bindingContext !== null) { if (!isBackNavigation && bindingContext !== undefined && bindingContext !== null) {
this.bindingContext = bindingContext; this.bindingContext = bindingContext;

View File

@ -19,6 +19,11 @@ export class CssState {
* Gets the static selectors that match the view and the dynamic selectors that may potentially match the view. * Gets the static selectors that match the view and the dynamic selectors that may potentially match the view.
*/ */
public changeMap: ChangeMap<ViewBase>; public changeMap: ChangeMap<ViewBase>;
/**
* Checks whether style scope and CSS state selectors are in sync.
*/
public isSelectorsLatestVersionApplied(): boolean
} }
export class StyleScope { export class StyleScope {
@ -29,6 +34,9 @@ export class StyleScope {
public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[]; public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[];
public ensureSelectors(): number; public ensureSelectors(): number;
public isApplicationCssSelectorsLatestVersionApplied(): boolean;
public isLocalCssSelectorsLatestVersionApplied(): boolean;
public applySelectors(view: ViewBase): void public applySelectors(view: ViewBase): void
public query(options: Node): SelectorCore[]; public query(options: Node): SelectorCore[];

View File

@ -271,7 +271,7 @@ export function removeTaggedAdditionalCSS(tag: String | Number): Boolean {
changed = true; changed = true;
} }
} }
if (changed) { mergeCssSelectors(); } if (changed) { mergeCssSelectors(); }
return changed; return changed;
} }
@ -307,7 +307,7 @@ function onLiveSync(args: applicationCommon.CssChangedEventData): void {
loadCss(applicationCommon.getCssFileName()); loadCss(applicationCommon.getCssFileName());
} }
const loadCss = profile(`"style-scope".loadCss`, (cssFile: string) => { export const loadCss = profile(`"style-scope".loadCss`, (cssFile: string) => {
if (!cssFile) { if (!cssFile) {
return undefined; return undefined;
} }
@ -343,6 +343,7 @@ export class CssState {
_appliedChangeMap: Readonly<ChangeMap<ViewBase>>; _appliedChangeMap: Readonly<ChangeMap<ViewBase>>;
_appliedPropertyValues: Readonly<{}>; _appliedPropertyValues: Readonly<{}>;
_appliedAnimations: ReadonlyArray<kam.KeyframeAnimation>; _appliedAnimations: ReadonlyArray<kam.KeyframeAnimation>;
_appliedSelectorsVersion: number;
_match: SelectorsMatch<ViewBase>; _match: SelectorsMatch<ViewBase>;
_matchInvalid: boolean; _matchInvalid: boolean;
@ -367,6 +368,15 @@ export class CssState {
} }
} }
public isSelectorsLatestVersionApplied(): boolean {
if (this._appliedSelectorsVersion && this.view._styleScope) {
this.view._styleScope.ensureSelectors();
return this.view._styleScope._getSelectorsVersion() === this._appliedSelectorsVersion;
} else {
return true;
}
}
public onLoaded(): void { public onLoaded(): void {
if (this._matchInvalid) { if (this._matchInvalid) {
this.updateMatch(); this.updateMatch();
@ -381,6 +391,7 @@ export class CssState {
@profile @profile
private updateMatch() { private updateMatch() {
this._appliedSelectorsVersion = this.view._styleScope._getSelectorsVersion();
this._match = this.view._styleScope ? this.view._styleScope.matchSelectors(this.view) : CssState.emptyMatch; this._match = this.view._styleScope ? this.view._styleScope.matchSelectors(this.view) : CssState.emptyMatch;
this._matchInvalid = false; this._matchInvalid = false;
} }
@ -597,8 +608,8 @@ export class StyleScope {
} }
public ensureSelectors(): number { public ensureSelectors(): number {
if (this._applicationCssSelectorsAppliedVersion !== applicationCssSelectorVersion || if (!this.isApplicationCssSelectorsLatestVersionApplied() ||
this._localCssSelectorVersion !== this._localCssSelectorsAppliedVersion || !this.isLocalCssSelectorsLatestVersionApplied() ||
!this._mergedCssSelectors) { !this._mergedCssSelectors) {
this._createSelectors(); this._createSelectors();
@ -607,6 +618,14 @@ export class StyleScope {
return this._getSelectorsVersion(); return this._getSelectorsVersion();
} }
public isApplicationCssSelectorsLatestVersionApplied(): boolean {
return this._applicationCssSelectorsAppliedVersion === applicationCssSelectorVersion;
}
public isLocalCssSelectorsLatestVersionApplied(): boolean {
return this._localCssSelectorsAppliedVersion === this._localCssSelectorVersion;
}
@profile @profile
private _createSelectors() { private _createSelectors() {
let toMerge: RuleSet[][] = []; let toMerge: RuleSet[][] = [];

View File

@ -26,4 +26,4 @@
"tns-core-modules/*": ["tns-core-modules/*"] "tns-core-modules/*": ["tns-core-modules/*"]
} }
} }
} }