diff --git a/tns-core-modules/ui/core/properties/properties.ts b/tns-core-modules/ui/core/properties/properties.ts index bf6c7ba0d..4734b693a 100644 --- a/tns-core-modules/ui/core/properties/properties.ts +++ b/tns-core-modules/ui/core/properties/properties.ts @@ -4,6 +4,12 @@ import { ViewBase } from "../view-base"; // Types. import { WrappedValue, PropertyChangeData } from "../../../data/observable"; +import { + write as traceWrite, + categories as traceCategories, + messageType as traceMessageType, +} from "../../../trace"; + import { Style } from "../../styling/style"; import { profile } from "../../../profiling"; @@ -125,7 +131,7 @@ export class Property implements TypedPropertyDescriptor< if (affectsLayout) { this.requestLayout(); } - + if (reset) { delete this[key]; if (valueChanged) { @@ -466,6 +472,13 @@ export class CssProperty implements definitions.CssProperty< const property = this; function setLocalValue(this: T, newValue: U | string): void { + const view = this.viewRef.get(); + if (!view) { + traceWrite(`${newValue} not set to view because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + + return; + } + const reset = newValue === unsetValue || newValue === ""; let value: U; if (reset) { @@ -482,7 +495,6 @@ export class CssProperty implements definitions.CssProperty< const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (changed) { - const view = this.view; if (reset) { delete this[key]; if (valueChanged) { @@ -534,6 +546,13 @@ export class CssProperty implements definitions.CssProperty< } function setCssValue(this: T, newValue: U | string): void { + const view = this.viewRef.get(); + if (!view) { + traceWrite(`${newValue} not set to view because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + + return; + } + const currentValueSource: number = this[sourceKey] || ValueSource.Default; // We have localValueSource - NOOP. @@ -557,7 +576,6 @@ export class CssProperty implements definitions.CssProperty< const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (changed) { - const view = this.view; if (reset) { delete this[key]; if (valueChanged) { @@ -718,12 +736,18 @@ export class CssAnimationProperty implements definitions.Css enumerable, configurable, get: getsComputed ? function (this: T) { return this[computedValue]; } : function (this: T) { return this[symbol]; }, set(this: T, boxedValue: U | string) { + const view = this.viewRef.get(); + if (!view) { + traceWrite(`${boxedValue} not set to view because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); + + return; + } const oldValue = this[computedValue]; const oldSource = this[computedSource]; const wasSet = oldSource !== ValueSource.Default; const reset = boxedValue === unsetValue || boxedValue === ""; - + if (reset) { this[symbol] = unsetValue; if (this[computedSource] === propertySource) { @@ -760,7 +784,6 @@ export class CssAnimationProperty implements definitions.Css valueChanged(this, oldValue, value); } - const view = this.view; if (view[setNative] && (computedValueChanged || isSet !== wasSet)) { if (view._suspendNativeUpdatesCount) { if (view._suspendedUpdates) { @@ -816,10 +839,16 @@ export class CssAnimationProperty implements definitions.Css } public _initDefaultNativeValue(target: T): void { + const view = target.viewRef.get(); + if (!view) { + traceWrite(`_initDefaultNativeValue not executed to view because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); + + return; + } + const defaultValueKey = this.defaultValueKey; if (!(defaultValueKey in target)) { - const view = target.view; const getDefault = this.getDefault; target[defaultValueKey] = view[getDefault] ? view[getDefault]() : this.defaultValue; } @@ -862,6 +891,13 @@ export class InheritedCssProperty extends CssProperty const property = this; const setFunc = (valueSource: ValueSource) => function (this: T, boxedValue: any): void { + const view = this.viewRef.get(); + if (!view) { + traceWrite(`${boxedValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + + return; + } + const reset = boxedValue === unsetValue || boxedValue === ""; const currentValueSource: number = this[sourceKey] || ValueSource.Default; if (reset) { @@ -876,7 +912,6 @@ export class InheritedCssProperty extends CssProperty } const oldValue: U = key in this ? this[key] : defaultValue; - const view = this.view; let value: U; let unsetNativeValue = false; if (reset) { @@ -907,7 +942,6 @@ export class InheritedCssProperty extends CssProperty const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; if (changed) { - const view = this.view; if (valueChanged) { valueChanged(this, oldValue, value); } @@ -997,7 +1031,14 @@ export class ShorthandProperty implements definitions.Shorth const converter = options.converter; function setLocalValue(this: T, value: string | P): void { - this.view._batchUpdate(() => { + const view = this.viewRef.get(); + if (!view) { + traceWrite(`setLocalValue not executed to view because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); + + return; + } + + view._batchUpdate(() => { for (let [p, v] of converter(value)) { this[p.name] = v; } @@ -1005,7 +1046,14 @@ export class ShorthandProperty implements definitions.Shorth } function setCssValue(this: T, value: string): void { - this.view._batchUpdate(() => { + const view = this.viewRef.get(); + if (!view) { + traceWrite(`setCssValue not executed to view because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); + + return; + } + + view._batchUpdate(() => { for (let [p, v] of converter(value)) { this[p.cssName] = v; } diff --git a/tns-core-modules/ui/core/view-base/view-base.ts b/tns-core-modules/ui/core/view-base/view-base.ts index caf868b0c..283de2df7 100644 --- a/tns-core-modules/ui/core/view-base/view-base.ts +++ b/tns-core-modules/ui/core/view-base/view-base.ts @@ -192,7 +192,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition public _domId: number; public _context: any; public _isAddedToNativeVisualTree: boolean; - public _cssState: ssm.CssState = new ssm.CssState(this); + public _cssState: ssm.CssState = new ssm.CssState(new WeakRef(this)); public _styleScope: ssm.StyleScope; public _suspendedUpdates: { [propertyName: string]: Property | CssProperty | CssAnimationProperty }; public _suspendNativeUpdatesCount: SuspendType; @@ -249,7 +249,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition constructor() { super(); this._domId = viewIdCounter++; - this._style = new Style(this); + this._style = new Style(new WeakRef(this)); } // Used in Angular. diff --git a/tns-core-modules/ui/frame/frame-common.ts b/tns-core-modules/ui/frame/frame-common.ts index b88d79c56..7c777efab 100644 --- a/tns-core-modules/ui/frame/frame-common.ts +++ b/tns-core-modules/ui/frame/frame-common.ts @@ -154,6 +154,8 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { } else { page._tearDownUI(true); } + + removed.resolvedPage = null; } // Attempts to implement https://github.com/NativeScript/NativeScript/issues/1311 diff --git a/tns-core-modules/ui/styling/style-properties.ts b/tns-core-modules/ui/styling/style-properties.ts index 0963cf353..c285d2950 100644 --- a/tns-core-modules/ui/styling/style-properties.ts +++ b/tns-core-modules/ui/styling/style-properties.ts @@ -25,6 +25,11 @@ import { matrixArrayToCssMatrix, multiplyAffine2d, } from "../../matrix"; +import { + write as traceWrite, + categories as traceCategories, + messageType as traceMessageType, +} from "../../trace"; import * as parser from "../../css/parser"; import { LinearGradient } from "./linear-gradient"; @@ -175,7 +180,12 @@ export const zeroLength: Length = { value: 0, unit: "px" }; export const minWidthProperty = new CssProperty({ name: "minWidth", cssName: "min-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueChanged: (target, oldValue, newValue) => { - target.view.effectiveMinWidth = Length.toDevicePixels(newValue, 0); + const view = target.viewRef.get(); + if (view) { + view.effectiveMinWidth = Length.toDevicePixels(newValue, 0); + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } }, valueConverter: Length.parse }); minWidthProperty.register(Style); @@ -183,7 +193,12 @@ minWidthProperty.register(Style); export const minHeightProperty = new CssProperty({ name: "minHeight", cssName: "min-height", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueChanged: (target, oldValue, newValue) => { - target.view.effectiveMinHeight = Length.toDevicePixels(newValue, 0); + const view = target.viewRef.get(); + if (view) { + view.effectiveMinHeight = Length.toDevicePixels(newValue, 0); + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } }, valueConverter: Length.parse }); minHeightProperty.register(Style); @@ -237,7 +252,12 @@ paddingProperty.register(Style); export const paddingLeftProperty = new CssProperty({ name: "paddingLeft", cssName: "padding-left", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueChanged: (target, oldValue, newValue) => { - target.view.effectivePaddingLeft = Length.toDevicePixels(newValue, 0); + const view = target.viewRef.get(); + if (view) { + view.effectivePaddingLeft = Length.toDevicePixels(newValue, 0); + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } }, valueConverter: Length.parse }); paddingLeftProperty.register(Style); @@ -245,7 +265,12 @@ paddingLeftProperty.register(Style); export const paddingRightProperty = new CssProperty({ name: "paddingRight", cssName: "padding-right", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueChanged: (target, oldValue, newValue) => { - target.view.effectivePaddingRight = Length.toDevicePixels(newValue, 0); + const view = target.viewRef.get(); + if (view) { + view.effectivePaddingRight = Length.toDevicePixels(newValue, 0); + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } }, valueConverter: Length.parse }); paddingRightProperty.register(Style); @@ -253,7 +278,12 @@ paddingRightProperty.register(Style); export const paddingTopProperty = new CssProperty({ name: "paddingTop", cssName: "padding-top", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueChanged: (target, oldValue, newValue) => { - target.view.effectivePaddingTop = Length.toDevicePixels(newValue, 0); + const view = target.viewRef.get(); + if (view) { + view.effectivePaddingTop = Length.toDevicePixels(newValue, 0); + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } }, valueConverter: Length.parse }); paddingTopProperty.register(Style); @@ -261,7 +291,12 @@ paddingTopProperty.register(Style); export const paddingBottomProperty = new CssProperty({ name: "paddingBottom", cssName: "padding-bottom", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueChanged: (target, oldValue, newValue) => { - target.view.effectivePaddingBottom = Length.toDevicePixels(newValue, 0); + const view = target.viewRef.get(); + if (view) { + view.effectivePaddingBottom = Length.toDevicePixels(newValue, 0); + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } }, valueConverter: Length.parse }); paddingBottomProperty.register(Style); @@ -822,7 +857,12 @@ export const borderTopWidthProperty = new CssProperty({ throw new Error(`border-top-width should be Non-Negative Finite number. Value: ${value}`); } - target.view.effectiveBorderTopWidth = value; + const view = target.viewRef.get(); + if (view) { + view.effectiveBorderTopWidth = value; + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } const background = target.backgroundInternal.withBorderTopWidth(value); target.backgroundInternal = background; }, valueConverter: Length.parse @@ -837,7 +877,12 @@ export const borderRightWidthProperty = new CssProperty({ throw new Error(`border-right-width should be Non-Negative Finite number. Value: ${value}`); } - target.view.effectiveBorderRightWidth = value; + const view = target.viewRef.get(); + if (view) { + view.effectiveBorderRightWidth = value; + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } const background = target.backgroundInternal.withBorderRightWidth(value); target.backgroundInternal = background; }, valueConverter: Length.parse @@ -852,7 +897,12 @@ export const borderBottomWidthProperty = new CssProperty({ throw new Error(`border-bottom-width should be Non-Negative Finite number. Value: ${value}`); } - target.view.effectiveBorderBottomWidth = value; + const view = target.viewRef.get(); + if (view) { + view.effectiveBorderBottomWidth = value; + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } const background = target.backgroundInternal.withBorderBottomWidth(value); target.backgroundInternal = background; }, valueConverter: Length.parse @@ -867,7 +917,12 @@ export const borderLeftWidthProperty = new CssProperty({ throw new Error(`border-left-width should be Non-Negative Finite number. Value: ${value}`); } - target.view.effectiveBorderLeftWidth = value; + const view = target.viewRef.get(); + if (view) { + view.effectiveBorderLeftWidth = value; + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } const background = target.backgroundInternal.withBorderLeftWidth(value); target.backgroundInternal = background; }, valueConverter: Length.parse @@ -1095,7 +1150,12 @@ export namespace Visibility { export const visibilityProperty = new CssProperty({ name: "visibility", cssName: "visibility", defaultValue: Visibility.VISIBLE, affectsLayout: isIOS, valueConverter: Visibility.parse, valueChanged: (target, oldValue, newValue) => { - target.view.isCollapsed = (newValue === Visibility.COLLAPSE); + const view = target.viewRef.get(); + if (view) { + view.isCollapsed = (newValue === Visibility.COLLAPSE); + } else { + traceWrite(`${newValue} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + } } }); visibilityProperty.register(Style); diff --git a/tns-core-modules/ui/styling/style-scope.ts b/tns-core-modules/ui/styling/style-scope.ts index 74a5262f1..2f216cb01 100644 --- a/tns-core-modules/ui/styling/style-scope.ts +++ b/tns-core-modules/ui/styling/style-scope.ts @@ -350,7 +350,7 @@ export class CssState { _matchInvalid: boolean; _playsKeyframeAnimations: boolean; - constructor(private view: ViewBase) { + constructor(private viewRef: WeakRef) { this._onDynamicStateChangeHandler = () => this.updateDynamicState(); } @@ -359,7 +359,8 @@ export class CssState { * As a result, at some point in time, the selectors matched have to be requerried from the style scope and applied to the view. */ public onChange(): void { - if (this.view && this.view.isLoaded) { + const view = this.viewRef.get(); + if (view && view.isLoaded) { this.unsubscribeFromDynamicUpdates(); this.updateMatch(); this.subscribeForDynamicUpdates(); @@ -370,7 +371,14 @@ export class CssState { } public isSelectorsLatestVersionApplied(): boolean { - return this.view._styleScope._getSelectorsVersion() === this._appliedSelectorsVersion; + const view = this.viewRef.get(); + if (!view) { + traceWrite(`isSelectorsLatestVersionApplied returns default value "false" because "this.viewRef" cleared.`, traceCategories.Style, traceMessageType.warn); + + return false; + } + + return this.viewRef.get()._styleScope._getSelectorsVersion() === this._appliedSelectorsVersion; } public onLoaded(): void { @@ -387,20 +395,28 @@ export class CssState { @profile private updateMatch() { - if (this.view._styleScope) { - this._appliedSelectorsVersion = this.view._styleScope._getSelectorsVersion(); - this._match = this.view._styleScope.matchSelectors(this.view); + const view = this.viewRef.get(); + if (view && view._styleScope) { + this._appliedSelectorsVersion = view._styleScope._getSelectorsVersion(); + this._match = view._styleScope.matchSelectors(view); } else { this._match = CssState.emptyMatch; } + this._matchInvalid = false; } @profile private updateDynamicState(): void { - const matchingSelectors = this._match.selectors.filter(sel => sel.dynamic ? sel.match(this.view) : true); + const view = this.viewRef.get(); + if (!view) { + traceWrite(`updateDynamicState not executed to view because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); - this.view._batchUpdate(() => { + return; + } + + const matchingSelectors = this._match.selectors.filter(sel => sel.dynamic ? sel.match(view) : true); + view._batchUpdate(() => { this.stopKeyframeAnimations(); this.setPropertyValues(matchingSelectors); this.playKeyframeAnimations(matchingSelectors); @@ -424,7 +440,14 @@ export class CssState { }); if (this._playsKeyframeAnimations = animations.length > 0) { - animations.map(animation => animation.play(this.view)); + const view = this.viewRef.get(); + if (!view) { + traceWrite(`KeyframeAnimations cannot play because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); + + return; + } + + animations.map(animation => animation.play(view)); Object.freeze(animations); this._appliedAnimations = animations; } @@ -440,13 +463,19 @@ export class CssState { .forEach(animation => animation.cancel()); this._appliedAnimations = CssState.emptyAnimationArray; - this.view.style["keyframe:rotate"] = unsetValue; - this.view.style["keyframe:scaleX"] = unsetValue; - this.view.style["keyframe:scaleY"] = unsetValue; - this.view.style["keyframe:translateX"] = unsetValue; - this.view.style["keyframe:translateY"] = unsetValue; - this.view.style["keyframe:backgroundColor"] = unsetValue; - this.view.style["keyframe:opacity"] = unsetValue; + const view = this.viewRef.get(); + if (view) { + view.style["keyframe:rotate"] = unsetValue; + view.style["keyframe:scaleX"] = unsetValue; + view.style["keyframe:scaleY"] = unsetValue; + view.style["keyframe:translateX"] = unsetValue; + view.style["keyframe:translateY"] = unsetValue; + view.style["keyframe:backgroundColor"] = unsetValue; + view.style["keyframe:opacity"] = unsetValue; + } else { + traceWrite(`KeyframeAnimations cannot be stopped because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); + } + this._playsKeyframeAnimations = false; } @@ -457,7 +486,14 @@ export class CssState { * @param matchingSelectors */ private setPropertyValues(matchingSelectors: SelectorCore[]): void { - const newPropertyValues = new this.view.style.PropertyBag(); + const view = this.viewRef.get(); + if (!view) { + traceWrite(`${matchingSelectors} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.warn); + + return; + } + + const newPropertyValues = new view.style.PropertyBag(); matchingSelectors.forEach(selector => selector.ruleset.declarations.forEach(declaration => newPropertyValues[declaration.property] = declaration.value)); @@ -466,8 +502,8 @@ export class CssState { const oldProperties = this._appliedPropertyValues; for (const key in oldProperties) { if (!(key in newPropertyValues)) { - if (key in this.view.style) { - this.view.style[`css:${key}`] = unsetValue; + if (key in view.style) { + view.style[`css:${key}`] = unsetValue; } else { // TRICKY: How do we unset local value? } @@ -479,14 +515,14 @@ export class CssState { } const value = newPropertyValues[property]; try { - if (property in this.view.style) { - this.view.style[`css:${property}`] = value; + if (property in view.style) { + view.style[`css:${property}`] = value; } else { const camelCasedProperty = property.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); - this.view[camelCasedProperty] = value; + view[camelCasedProperty] = value; } } catch (e) { - traceWrite(`Failed to apply property [${property}] with value [${value}] to ${this.view}. ${e}`, traceCategories.Error, traceMessageType.error); + traceWrite(`Failed to apply property [${property}] with value [${value}] to ${view}. ${e}`, traceCategories.Error, traceMessageType.error); } } @@ -535,7 +571,14 @@ export class CssState { } toString(): string { - return `${this.view}._cssState`; + const view = this.viewRef.get(); + if (!view) { + traceWrite(`toString() of CssState cannot execute correctly because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); + + return ""; + } + + return `${view}._cssState`; } } CssState.prototype._appliedChangeMap = CssState.emptyChangeMap; diff --git a/tns-core-modules/ui/styling/style/style.d.ts b/tns-core-modules/ui/styling/style/style.d.ts index a337088d8..03e002bef 100644 --- a/tns-core-modules/ui/styling/style/style.d.ts +++ b/tns-core-modules/ui/styling/style/style.d.ts @@ -139,7 +139,14 @@ export class Style extends Observable { public statusBarStyle: "light" | "dark"; public androidStatusBarBackground: Color; - constructor(ownerView: ViewBase); + constructor(ownerView: ViewBase | WeakRef); + public viewRef: WeakRef; + + /** + * @deprecated use `viewRef` instead. + * + * The `ViewBase` object associated with the Style! + */ public view: ViewBase; //flexbox layout properties diff --git a/tns-core-modules/ui/styling/style/style.ts b/tns-core-modules/ui/styling/style/style.ts index a16d83e95..2823fadd2 100644 --- a/tns-core-modules/ui/styling/style/style.ts +++ b/tns-core-modules/ui/styling/style/style.ts @@ -10,16 +10,34 @@ import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from "../../layouts/flexbox-layout"; - +import { + write as traceWrite, + categories as traceCategories, + messageType as traceMessageType, +} from "../../../trace"; import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "../../text-base"; export class Style extends Observable implements StyleDefinition { - constructor(public view: ViewBase) { + constructor(ownerView: ViewBase | WeakRef) { super(); + + // HACK: Could not find better way for cross platform WeakRef type checking. + if (ownerView.constructor.toString().indexOf("[native code]") !== -1) { + this.viewRef = >ownerView; + } else { + this.viewRef = new WeakRef(ownerView); + } } toString() { - return `${this.view}.style`; + const view = this.viewRef.get(); + if (!view) { + traceWrite(`toString() of Style cannot execute correctly because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn); + + return ""; + } + + return `${view}.style`; } public fontInternal: Font; @@ -125,5 +143,15 @@ export class Style extends Observable implements StyleDefinition { public alignSelf: AlignSelf; public PropertyBag: { new(): { [property: string]: string }, prototype: { [property: string]: string } }; + + public viewRef: WeakRef; + + public get view(): ViewBase { + if (this.viewRef) { + return this.viewRef.get(); + } + + return undefined; + } } Style.prototype.PropertyBag = class { [property: string]: string; }