mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 19:26:42 +08:00
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:
@ -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.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
25
tns-core-modules/module.d.ts
vendored
25
tns-core-modules/module.d.ts
vendored
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
8
tns-core-modules/ui/styling/style-scope.d.ts
vendored
8
tns-core-modules/ui/styling/style-scope.d.ts
vendored
@ -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[];
|
||||||
|
|
||||||
|
@ -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[][] = [];
|
||||||
|
@ -26,4 +26,4 @@
|
|||||||
"tns-core-modules/*": ["tns-core-modules/*"]
|
"tns-core-modules/*": ["tns-core-modules/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user