feat(HMR): apply changes in page styles at runtime when app root is a frame (#6857)

* feat(HMR): apply changes in page styles at runtime

* fix: livesync tests

* test: changeCssFile method

* refactor: address comments

Add a comment.
Update `let` to `const`.
Update `changesCssFile` test.

* test: add an assert
This commit is contained in:
Vasil Chimev
2019-02-14 14:03:13 +02:00
committed by GitHub
parent 8e9a13c705
commit 44b8acd79c
10 changed files with 120 additions and 59 deletions

View File

@ -674,6 +674,23 @@ export function test_CSS_isAppliedOnPage_From_addCssFile() {
}); });
} }
export function test_CSS_isAppliedOnPage_From_changeCssFile() {
const testButton = new buttonModule.Button();
testButton.text = "Test";
const testCss = "button { color: blue; }";
const testFunc = function (views: Array<viewModule.View>) {
helper.assertViewColor(testButton, "#0000FF");
const page: pageModule.Page = <pageModule.Page>views[1];
page.changeCssFile("~/ui/styling/test.css");
helper.assertViewBackgroundColor(page, "#FF0000");
TKUnit.assert(testButton.style.color === undefined, "Color should not have a value");
}
helper.buildUIAndRunTest(testButton, testFunc, { pageCss: testCss });
}
const invalidCSS = ".invalid { " + const invalidCSS = ".invalid { " +
"color: invalidValue; " + "color: invalidValue; " +
"background-color: invalidValue; " + "background-color: invalidValue; " +

View File

@ -82,19 +82,23 @@ export function setApplication(instance: iOSApplication | AndroidApplication): v
export function livesync(rootView: View, context?: ModuleContext) { export function livesync(rootView: View, context?: ModuleContext) {
events.notify(<EventData>{ eventName: "livesync", object: app }); events.notify(<EventData>{ eventName: "livesync", object: app });
const liveSyncCore = global.__onLiveSyncCore; const liveSyncCore = global.__onLiveSyncCore;
let reapplyAppCss = false; let reapplyAppStyles = false;
let reapplyLocalStyles = false;
if (context) { if (context && context.path) {
const fullFileName = getCssFileName();
const fileName = fullFileName.substring(0, fullFileName.lastIndexOf(".") + 1);
const extensions = ["css", "scss"]; const extensions = ["css", "scss"];
reapplyAppCss = extensions.some(ext => context.path === fileName.concat(ext)); const appStylesFullFileName = getCssFileName();
const appStylesFileName = appStylesFullFileName.substring(0, appStylesFullFileName.lastIndexOf(".") + 1);
reapplyAppStyles = extensions.some(ext => context.path === appStylesFileName.concat(ext));
if (!reapplyAppStyles) {
reapplyLocalStyles = extensions.some(ext => context.path.endsWith(ext));
}
} }
if (reapplyAppCss && rootView) { if (reapplyAppStyles && rootView) {
rootView._onCssStateChange(); rootView._onCssStateChange();
} else if (liveSyncCore) { } else if (liveSyncCore) {
liveSyncCore(); reapplyLocalStyles ? liveSyncCore(context) : liveSyncCore();
} }
} }

View File

@ -225,9 +225,9 @@ class IOSApplication implements IOSApplicationDefinition {
} }
} }
public _onLivesync(): void { public _onLivesync(context?: ModuleContext): void {
// If view can't handle livesync set window controller. // If view can't handle livesync set window controller.
if (this._rootView && !this._rootView._onLivesync()) { if (this._rootView && !this._rootView._onLivesync(context)) {
this.setWindowContent(); this.setWindowContent();
} }
} }
@ -264,8 +264,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 (context?: ModuleContext) {
iosApp._onLivesync(); iosApp._onLivesync(context);
} }
let mainEntry: NavigationEntry; let mainEntry: NavigationEntry;

View File

@ -52,7 +52,7 @@ declare namespace NodeJS {
__inspector?: any; __inspector?: any;
__extends: any; __extends: any;
__onLiveSync: (context?: { type: string, path: string }) => void; __onLiveSync: (context?: { type: string, path: string }) => void;
__onLiveSyncCore: () => void; __onLiveSyncCore: (context?: { type: string, path: string }) => void;
__onUncaughtError: (error: NativeScriptError) => void; __onUncaughtError: (error: NativeScriptError) => void;
__onDiscardedError: (error: NativeScriptError) => void; __onDiscardedError: (error: NativeScriptError) => void;
TNS_WEBPACK?: boolean; TNS_WEBPACK?: boolean;

View File

@ -105,6 +105,14 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this._updateStyleScope(cssFileName); this._updateStyleScope(cssFileName);
} }
public changeCssFile(cssFileName: string): void {
const scope = this._styleScope;
if (scope && cssFileName) {
scope.changeCssFile(cssFileName);
this._onCssStateChange();
}
}
public _updateStyleScope(cssFileName?: string, cssString?: string, css?: string): void { public _updateStyleScope(cssFileName?: string, cssString?: string, css?: string): void {
let scope = this._styleScope; let scope = this._styleScope;
if (!scope) { if (!scope) {

View File

@ -18,14 +18,14 @@ export function PseudoClassHandler(...pseudoClasses: string[]): MethodDecorator;
/** /**
* Specifies the type name for the instances of this View class, * Specifies the type name for the instances of this View class,
* that is used when matching CSS type selectors. * that is used when matching CSS type selectors.
* *
* Usage: * Usage:
* ``` * ```
* @CSSType("Button") * @CSSType("Button")
* class Button extends View { * class Button extends View {
* } * }
* ``` * ```
* *
* Internally the decorator set `Button.prototype.cssType = "Button"`. * Internally the decorator set `Button.prototype.cssType = "Button"`.
* @param type The type name, e. g. "Button", "Label", etc. * @param type The type name, e. g. "Button", "Label", etc.
*/ */
@ -50,8 +50,8 @@ export type px = number;
export type percent = number; export type percent = number;
/** /**
* The Point interface describes a two dimensional location. * The Point interface describes a two dimensional location.
* It has two properties x and y, representing the x and y coordinate of the location. * It has two properties x and y, representing the x and y coordinate of the location.
*/ */
export interface Point { export interface Point {
/** /**
@ -66,8 +66,8 @@ export interface Point {
} }
/** /**
* The Size interface describes abstract dimensions in two dimensional space. * The Size interface describes abstract dimensions in two dimensional space.
* It has two properties width and height, representing the width and height values of the size. * It has two properties width and height, representing the width and height values of the size.
*/ */
export interface Size { export interface Size {
/** /**
@ -99,8 +99,8 @@ export interface ShownModallyData extends EventData {
} }
/** /**
* This class is the base class for all UI components. * This class is the base class for all UI components.
* A View occupies a rectangular area on the screen and is responsible for drawing and layouting of all UI components within. * A View occupies a rectangular area on the screen and is responsible for drawing and layouting of all UI components within.
*/ */
export abstract class View extends ViewBase { export abstract class View extends ViewBase {
/** /**
@ -475,13 +475,13 @@ export abstract class View extends ViewBase {
* [Deprecated. Please use the on() instead.] Adds a gesture observer. * [Deprecated. Please use the on() instead.] Adds a gesture observer.
* @param type - Type of the gesture. * @param type - Type of the gesture.
* @param callback - A function that will be executed when gesture is received. * @param callback - A function that will be executed when gesture is received.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution. * @param thisArg - An optional parameter which will be used as `this` context for callback execution.
*/ */
observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any); observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any);
/** /**
* A basic method signature to hook an event listener (shortcut alias to the addEventListener method). * A basic method signature to hook an event listener (shortcut alias to the addEventListener method).
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change") or you can use gesture types. * @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change") or you can use gesture types.
* @param callback - Callback function which will be executed when event is raised. * @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution. * @param thisArg - An optional parameter which will be used as `this` context for callback execution.
*/ */
@ -527,12 +527,12 @@ export abstract class View extends ViewBase {
modal: View; modal: View;
/** /**
* Animates one or more properties of the view based on the supplied options. * Animates one or more properties of the view based on the supplied options.
*/ */
public animate(options: AnimationDefinition): AnimationPromise; public animate(options: AnimationDefinition): AnimationPromise;
/** /**
* Creates an Animation object based on the supplied options. * Creates an Animation object based on the supplied options.
*/ */
public createAnimation(options: AnimationDefinition): Animation; public createAnimation(options: AnimationDefinition): Animation;
@ -562,7 +562,7 @@ export abstract class View extends ViewBase {
public getActualSize(): Size; public getActualSize(): Size;
/** /**
* Derived classes can override this method to handle Android back button press. * Derived classes can override this method to handle Android back button press.
*/ */
onBackPressed(): boolean; onBackPressed(): boolean;
@ -575,7 +575,7 @@ export abstract class View extends ViewBase {
/** /**
* @private * @private
* Adds a new values to current css. * Adds a new values to current css.
* @param cssString - A valid css which will be added to current css. * @param cssString - A valid css which will be added to current css.
*/ */
addCss(cssString: string): void; addCss(cssString: string): void;
@ -586,13 +586,20 @@ export abstract class View extends ViewBase {
*/ */
addCssFile(cssFileName: string): void; addCssFile(cssFileName: string): void;
/**
* @private
* Changes the current css to the content of the file.
* @param cssFileName - A valid file name (from the application root) which contains a valid css.
*/
changeCssFile(cssFileName: string): void;
// Lifecycle events // Lifecycle events
_getNativeViewsCount(): number; _getNativeViewsCount(): number;
_eachLayoutView(callback: (View) => void): void; _eachLayoutView(callback: (View) => void): void;
/** /**
* Iterates over children of type View. * Iterates over children of type View.
* @param callback Called for each child of type View. Iteration stops if this method returns falsy value. * @param callback Called for each child of type View. Iteration stops if this method returns falsy value.
*/ */
public eachChildView(callback: (view: View) => boolean): void; public eachChildView(callback: (view: View) => boolean): void;
@ -673,7 +680,7 @@ export abstract class View extends ViewBase {
/** /**
* @private * @private
*/ */
_onLivesync(): boolean; _onLivesync(context?: { type: string, path: string }): boolean;
/** /**
* @private * @private
*/ */
@ -681,9 +688,9 @@ export abstract class View extends ViewBase {
/** /**
* Updates styleScope or create new styleScope. * Updates styleScope or create new styleScope.
* @param cssFileName * @param cssFileName
* @param cssString * @param cssString
* @param css * @param css
*/ */
_updateStyleScope(cssFileName?: string, cssString?: string, css?: string): void; _updateStyleScope(cssFileName?: string, cssString?: string, css?: string): void;
@ -715,7 +722,7 @@ export abstract class View extends ViewBase {
} }
/** /**
* Base class for all UI components that are containers. * Base class for all UI components that are containers.
*/ */
export class ContainerView extends View { export class ContainerView extends View {
/** /**
@ -725,7 +732,7 @@ export class ContainerView extends View {
} }
/** /**
* Base class for all UI components that implement custom layouts. * Base class for all UI components that implement custom layouts.
*/ */
export class CustomLayoutView extends ContainerView { export class CustomLayoutView extends ContainerView {
//@private //@private

View File

@ -76,7 +76,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
if (backstackIndex !== -1) { if (backstackIndex !== -1) {
backstack = backstackIndex; backstack = backstackIndex;
} else { } else {
// NOTE: We don't search for entries in navigationQueue because there is no way for // NOTE: We don't search for entries in navigationQueue because there is no way for
// developer to get reference to BackstackEntry unless transition is completed. // developer to get reference to BackstackEntry unless transition is completed.
// At that point the entry is put in the backstack array. // At that point the entry is put in the backstack array.
// If we start to return Backstack entry from navigate method then // If we start to return Backstack entry from navigate method then
@ -153,7 +153,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
// } // }
// let currentPage = this._currentEntry.resolvedPage; // let currentPage = this._currentEntry.resolvedPage;
// let currentNavigationEntry = this._currentEntry.entry; // let currentNavigationEntry = this._currentEntry.entry;
// if (currentPage["isBiOrientational"] && currentNavigationEntry.moduleName) { // if (currentPage["isBiOrientational"] && currentNavigationEntry.moduleName) {
// if (this.canGoBack()){ // if (this.canGoBack()){
// this.goBack(); // this.goBack();
@ -162,7 +162,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
// currentNavigationEntry.backstackVisible = false; // currentNavigationEntry.backstackVisible = false;
// } // }
// // Re-navigate to the same page so the other (.port or .land) xml is loaded. // // Re-navigate to the same page so the other (.port or .land) xml is loaded.
// this.navigate(currentNavigationEntry); // this.navigate(currentNavigationEntry);
// } // }
// } // }
@ -224,7 +224,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
newPage.onNavigatedTo(isBack); newPage.onNavigatedTo(isBack);
// Reset executing entry after NavigatedTo is raised; // Reset executing entry after NavigatedTo is raised;
// we do not want to execute two navigations in parallel in case // we do not want to execute two navigations in parallel in case
// additional navigation is triggered from the NavigatedTo handler. // additional navigation is triggered from the NavigatedTo handler.
this._executingEntry = null; this._executingEntry = null;
} }
@ -259,7 +259,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
return true; return true;
} }
} }
return false; return false;
} }
@ -563,7 +563,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
return result; return result;
} }
public _onLivesync(): boolean { public _onLivesync(context?: ModuleContext): boolean {
super._onLivesync(); super._onLivesync();
if (!this._currentEntry || !this._currentEntry.entry) { if (!this._currentEntry || !this._currentEntry.entry) {
@ -571,6 +571,17 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
} }
const currentEntry = this._currentEntry.entry; 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 = { const newEntry: NavigationEntry = {
animated: false, animated: false,
clearHistory: true, clearHistory: true,

View File

@ -82,13 +82,13 @@ function getAttachListener(): android.view.View.OnAttachStateChangeListener {
return attachStateChangeListener; return attachStateChangeListener;
} }
export function reloadPage(): void { export function reloadPage(context?: ModuleContext): void {
const activity = application.android.foregroundActivity; const activity = application.android.foregroundActivity;
const callbacks: AndroidActivityCallbacks = activity[CALLBACKS]; const callbacks: AndroidActivityCallbacks = activity[CALLBACKS];
if (callbacks) { if (callbacks) {
const rootView: View = callbacks.getRootView(); const rootView: View = callbacks.getRootView();
if (!rootView || !rootView._onLivesync()) { if (!rootView || !rootView._onLivesync(context)) {
callbacks.resetActivityContent(activity); callbacks.resetActivityContent(activity);
} }
} else { } else {
@ -153,7 +153,7 @@ export class Frame extends FrameBase {
// In case activity was destroyed because of back button pressed (e.g. app exit) // In case activity was destroyed because of back button pressed (e.g. app exit)
// and application is restored from recent apps, current fragment isn't recreated. // and application is restored from recent apps, current fragment isn't recreated.
// In this case call _navigateCore in order to recreate the current fragment. // In this case call _navigateCore in order to recreate the current fragment.
// Don't call navigate because it will fire navigation events. // Don't call navigate because it will fire navigation events.
// As JS instances are alive it is already done for the current page. // As JS instances are alive it is already done for the current page.
if (!this.isLoaded || this._executingEntry || !this._attachedToWindow) { if (!this.isLoaded || this._executingEntry || !this._attachedToWindow) {
return; return;
@ -183,13 +183,13 @@ export class Frame extends FrameBase {
const entry = this._currentEntry; const entry = this._currentEntry;
if (entry && manager && !manager.findFragmentByTag(entry.fragmentTag)) { if (entry && manager && !manager.findFragmentByTag(entry.fragmentTag)) {
// Simulate first navigation (e.g. no animations or transitions) // Simulate first navigation (e.g. no animations or transitions)
// we need to cache the original animation settings so we can restore them later; otherwise as the // we need to cache the original animation settings so we can restore them later; otherwise as the
// simulated first navigation is not animated (it is actually a zero duration animator) the "popExit" animation // simulated first navigation is not animated (it is actually a zero duration animator) the "popExit" animation
// is broken when transaction.setCustomAnimations(...) is used in a scenario with: // is broken when transaction.setCustomAnimations(...) is used in a scenario with:
// 1) forward navigation // 1) forward navigation
// 2) suspend / resume app // 2) suspend / resume app
// 3) back navigation -- the exiting fragment is erroneously animated with the exit animator from the // 3) back navigation -- the exiting fragment is erroneously animated with the exit animator from the
// simulated navigation (NoTransition, zero duration animator) and thus the fragment immediately disappears; // simulated navigation (NoTransition, zero duration animator) and thus the fragment immediately disappears;
// the user only sees the animation of the entering fragment as per its specific enter animation settings. // the user only sees the animation of the entering fragment as per its specific enter animation settings.
// NOTE: we are restoring the animation settings in Frame.setCurrent(...) as navigation completes asynchronously // NOTE: we are restoring the animation settings in Frame.setCurrent(...) as navigation completes asynchronously
this._cachedAnimatorState = getAnimatorState(this._currentEntry); this._cachedAnimatorState = getAnimatorState(this._currentEntry);
@ -727,7 +727,7 @@ function ensureFragmentClass() {
return; return;
} }
// this require will apply the FragmentClass implementation // this require will apply the FragmentClass implementation
require("ui/frame/fragment"); require("ui/frame/fragment");
if (!fragmentClass) { if (!fragmentClass) {
@ -855,11 +855,11 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
entry.viewSavedState = null; entry.viewSavedState = null;
} }
// fixes 'java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first'. // fixes 'java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first'.
// on app resume in nested frame scenarios with support library version greater than 26.0.0 // on app resume in nested frame scenarios with support library version greater than 26.0.0
// HACK: this whole code block shouldn't be necessary as the native view is supposedly removed from its parent // HACK: this whole code block shouldn't be necessary as the native view is supposedly removed from its parent
// right after onDestroyView(...) is called but for some reason the fragment view (page) still thinks it has a // right after onDestroyView(...) is called but for some reason the fragment view (page) still thinks it has a
// 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) {
@ -908,8 +908,8 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
} }
// [nested frames / fragments] see https://github.com/NativeScript/NativeScript/issues/6629 // [nested frames / fragments] see https://github.com/NativeScript/NativeScript/issues/6629
// retaining reference to a destroyed fragment here somehow causes a cryptic // retaining reference to a destroyed fragment here somehow causes a cryptic
// "IllegalStateException: Failure saving state: active fragment has cleared index: -1" // "IllegalStateException: Failure saving state: active fragment has cleared index: -1"
// in a specific mixed parent / nested frame navigation scenario // in a specific mixed parent / nested frame navigation scenario
entry.fragment = null; entry.fragment = null;
@ -1019,15 +1019,15 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks {
} }
// NOTE: activity.onPostResume() is called when activity resume is complete and we can // NOTE: activity.onPostResume() is called when activity resume is complete and we can
// safely raise the application resume event; // safely raise the application resume event;
// onActivityResumed(...) lifecycle callback registered in application is called too early // onActivityResumed(...) lifecycle callback registered in application is called too early
// and raising the application resume event there causes issues like // and raising the application resume event there causes issues like
// https://github.com/NativeScript/NativeScript/issues/6708 // https://github.com/NativeScript/NativeScript/issues/6708
if ((<any>activity).isNativeScriptActivity) { if ((<any>activity).isNativeScriptActivity) {
const args = <application.ApplicationEventData>{ const args = <application.ApplicationEventData>{
eventName: application.resumeEvent, eventName: application.resumeEvent,
object: application.android, object: application.android,
android: activity android: activity
}; };
application.notify(args); application.notify(args);
application.android.paused = false; application.android.paused = false;
@ -1151,7 +1151,7 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks {
// Paths that go trough this method: // Paths that go trough this method:
// 1. Application initial start - there is no rootView in callbacks. // 1. Application initial start - there is no rootView in callbacks.
// 2. Application revived after Activity is destroyed. this._rootView should have been restored by id in onCreate. // 2. Application revived after Activity is destroyed. this._rootView should have been restored by id in onCreate.
// 3. Livesync if rootView has no custom _onLivesync. this._rootView should have been cleared upfront. Launch event should not fired // 3. Livesync if rootView has no custom _onLivesync. this._rootView should have been cleared upfront. Launch event should not fired
// 4. _resetRootView method. this._rootView should have been cleared upfront. Launch event should not fired // 4. _resetRootView method. this._rootView should have been cleared upfront. Launch event should not fired
private setActivityContent( private setActivityContent(

View File

@ -29,6 +29,7 @@ export class CssState {
export class StyleScope { export class StyleScope {
public css: string; public css: string;
public addCss(cssString: string, cssFileName: string): void; public addCss(cssString: string, cssFileName: string): void;
public changeCssFile(cssFileName: string): void;
public static createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): RuleSet[]; public static createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): RuleSet[];
public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[]; public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[];

View File

@ -568,6 +568,19 @@ export class StyleScope {
this.appendCss(null, cssFileName); this.appendCss(null, cssFileName);
} }
@profile
private changeCssFile(cssFileName: string): void {
if (!cssFileName) {
return;
}
const cssSelectors = CSSSource.fromURI(cssFileName, this._keyframes);
this._css = cssSelectors.source;
this._localCssSelectors = cssSelectors.selectors;
this._localCssSelectorVersion++;
this.ensureSelectors();
}
@profile @profile
private setCss(cssString: string, cssFileName?): void { private setCss(cssString: string, cssFileName?): void {
this._css = cssString; this._css = cssString;