Property System
NativeScript provides own property system based on a wrapper around the well known JavaScript's Object.defineProperty
. To deliver good developer experience in the context of mobile development with UI and CSS elements, we provided extended classes of the Property
class. This article will cover the provided property classes and the base techniques when working with views and properties including initialization, registering, views lifecycles and recycling and handling changed events. Some commonly used methods of View
are demonstrated as well.
Property System Classes
The implementation of all property classes can be found under tns-core-modules/ui/core/properties
module. Below, we are going to look at all exposed classes from that module.
Property class
Property
is a simple wrapper around Object.defineProperty
with some additional callbacks like valueChange
, valueConverter
and equalityComparer
. When you define property you specify the owning type and the type of the property:
export const textProperty = new Property<MyButtonBase, string>({
name: "text",
defaultValue: "",
affectsLayout: true
});
textProperty.register(MyButtonBase);
Looking at textProperty: Property<MyButtonBase, string>
- the owning type is MyButtonBase
meaning that this property will be defined on instances of MyButtonBase
. The type of the property is string
so it will accept any text.
The valueChange
event is executed when the value of a property has changed. If the type of the property isn't string
we will need to specify valueConverter
and equalityComparer
. The valueConverter
is called if a string value is set to this property (for example from XML or CSS) and there you will have to convert that string to meaningful value if possible or throw exception if you can't. If equalityComparer
is specified it will be called everytime a value is set to a property. There you can compare current and new value for equality. For example, if your property is of type Color
you can use Color.equals
as equalityComparer
function so even if new instance of Color
is set the comparer will return false
if current color and new color have the same argb
value.
There is one more option in the Property
constructor: affectsLayout: boolean
.
When set to true
setting new value to this property will trigger a new layout pass. This is done as performance optimization. Android has an integrated layout system so most of the time it will invalidate itself when needed. Thus we skip one native call by defining affectsLayout
as true
only for iOS for example using 'isIOS' boolean property. Because iOS doesn't have integrated layout system if you know that this property could affect the layout you should specify it in the Property
constructor.
The flag affectsLayout
should be true
(mainly for iOS) when setting that property will change the element size and/or position. For example in our case setting button text to something different will either widen or shorten the width of the button so this will affect the element dimension hence with specify it as affectsLayout: isIOS
. If this property won't change element position/size then you don't have to specify affectsLayout
at all. For example background-color
property doesn't change element position/size.
Note: In the platform specific implementation use
getDefault
andsetNative
symbols from the property object (example: textProperty), to define how this property is applied to native views. ThegetDefault
method is called just once before the first call tosetNative
so that we know what is the default native value for this property. The value that you return will be passed tosetNative
method when we decide to recycle the native view. Recycling the native view of control is done only ifrecycleNativeView
field is set to true.
CssProperty Class
The CssProperty
is very similar to Property
type with two small differences:
- you have to additionally specify
cssName
which will be used to set this property through CSS - its value can be set from inline styles, page CSS or application CSS
export const myOpacityProperty = new CssProperty<Style, number>({
name: "myOpacity",
cssName: "my-opacity",
defaultValue: 1,
valueConverter: (v) => {
const x = parseFloat(v);
if (x < 0 || x > 1) {
throw new Error(`opacity accepts values in the range [0, 1]. Value: ${v}`);
}
return x;
}
});
myOpacityProperty.register(Style);
Note: For CSS properties that could be animated via keyframe animations, you can use the extended
CssAnimationProperty
which comes with the optionalkeyframe
parameter.
InheritedCssProperty Class
The InheritedCssProperty
is a property defined on Style type. These are inheritable CSS properties that could be set in CSS and propagates value on its children. These are properties like FontSize, FontWeight, Color, etc.
export const selectedBackgroundColorProperty = new InheritedCssProperty<Style, Color>({
name: "selectedBackgroundColor",
cssName: "selected-background-color",
equalityComparer: Color.equals,
valueConverter: (v) => new Color(v)
});
selectedBackgroundColorProperty.register(Style);
ShorthandProperty Class
The shorthand property provides the capability to provide shorthand syntax rules for your CSS properties. For example, instead of the explicit side-by-side syntax for all four margins
margin-top: 0;
margin-right: 10;
margin-bottom: 0;
margin-left: 10;
The user would want to use the shorthand syntax for margin
as follows:
margin: 0 10 0 10;
Creating the shorthand margin
property would require to have all CSS properties defined. This way, you could use them to set the syntax rule in our shorthand property getter.
const marginProperty = new ShorthandProperty<Style, string | PercentLength>({
name: "margin",
cssName: "margin",
getter: function (this: Style) {
if (PercentLength.equals(this.marginTop, this.marginRight) &&
PercentLength.equals(this.marginTop, this.marginBottom) &&
PercentLength.equals(this.marginTop, this.marginLeft)) {
return this.marginTop;
}
return `${PercentLength.convertToString(this.marginTop)} ${PercentLength.convertToString(this.marginRight)} ${PercentLength.convertToString(this.marginBottom)} ${PercentLength.convertToString(this.marginLeft)}`;
},
converter: convertToMargins
});
marginProperty.register(Style);
export const marginLeftProperty = new CssProperty<Style, PercentLength>({ name: "marginLeft", cssName: "margin-left", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginLeftProperty.register(Style);
export const marginRightProperty = new CssProperty<Style, PercentLength>({ name: "marginRight", cssName: "margin-right", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginRightProperty.register(Style);
export const marginTopProperty = new CssProperty<Style, PercentLength>({ name: "marginTop", cssName: "margin-top", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginTopProperty.register(Style);
export const marginBottomProperty = new CssProperty<Style, PercentLength>({ name: "marginBottom", cssName: "margin-bottom", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginBottomProperty.register(Style);
CoercibleProperty Class
The CoercibleProperty
is a property that extends the base Property class by providing the capability to be coercible. For better illustration when a property might need to be coercible let's assume that we are working on selectedIndex
property of some UI element that can hold different number of items
. The base case would suggest that the selectedIndex
would vary within the number of items, but what would cover the case where the items are changed dynamically (and the selectedIndex
is not within the length range)? This is the case that can be handled by a property that can coerce the value.
Creating the selectedIndex
as coercible property dependent on the number of items
export const selectedIndexProperty = new CoercibleProperty<SegmentedBarBase, number>({
name: "selectedIndex", defaultValue: -1,
valueChanged: (target, oldValue, newValue) => {
target.notify(<SelectedIndexChangedEventData>{ eventName: SegmentedBarBase.selectedIndexChangedEvent, object: target, oldIndex: oldValue, newIndex: newValue });
},
// in this case the coerce value will change depending on whether the actual number of items
// is more or less than the value we want to apply for selectedIndex
coerceValue: (target, value) => {
let items = target.items;
if (items) {
let max = items.length - 1;
if (value < 0) {
value = 0;
}
if (value > max) {
value = max;
}
} else {
value = -1;
}
return value;
},
valueConverter: (v) => parseInt(v)
});
selectedIndexProperty.register(SegmentedBarBase);
When setting the items
property we will coerce the selectedIndex
[itemsProperty.setNative](value: SegmentedBarItem[]) {
this.nativeViewProtected.clearAllTabs();
const newItems = value;
if (newItems) {
newItems.forEach((item, i, arr) => this.insertTab(item, i));
}
selectedIndexProperty.coerce(this);
}
Registering the Property
After a property is defined it needs to be registered on a type like this:
textProperty.register(MyButtonBase);
The CssProperties
should be registered on the Style
class like this:
// Augmenting Style definition so it includes our myOpacity property
declare module "tns-core-modules/ui/styling/style" {
interface Style {
myOpacity: number;
}
}
// Defines 'myOpacity' property on Style class.
myOpacityProperty.register(Style);
The registration defines that property for the type passed on to register
method.
Note: Make sure that put your
register
call after your class definition or you will get an exception.
Value Change Event
To get notification when some property value change a propertyNameChange has to be specified as eventName to addEventListener
method.
textField.addEventListener('textChange', handler...)
NativeView Property
Use nativeView
instead of ios
and android
properties. The ios
and android
properties are left for compatibility. However, all view-lifecycle methods and native property callbacks (explained below) should work with the nativeView
property.
Views Lifecycle and Recycling
NativeScript 3.0 introduced nativeView recycling. With nativeView recycling is aimed to reduce instantiation of native views which is really expensive operation in Android. In order to be able to recycle it, we need all properties exposed from the View to be of our property system.
We have method that gets the default value for a property which is get the first time a property value is changed. Once we know that our View is not needed anymore we will reset the native view to its original state and put it in a map where some future Views of the same type could reuse it. There are 3 new important methods:
-
createNativeView
- you override this method, create and return your nativeView -
initNativeView
- in this method you setup listeners/handlers to the nativeView -
disposeNativeView
- in this method you clear the reference between nativeView and javascript object to avoid memory leaks as well as reset the native view to its initial state if you want to reuse that native view later.
In Android, avoid access to native types in the root of the module (note that ClickListener
is declared and implemented in a function which is called at runtime). This is specific for the V8 snapshot feature which is generated on a host machine where android runtime is not running. What is important is that if you access native types, methods, fields, namespaces, etc. at the root of your module (e.g. not in a function) your code won't be compatible with V8 snapshot feature. The easiest workaround is to wrap it in a function like in the above initializeClickListener
function.
In this implementation, we use singleton listener (for Android - clickListener
) and handler (for iOS - handler
) to reduce the need to instantiate native classes and to reduce memory usage. If possible, it is recommended to use such techniques to reduce native calls.
Iterating Over View Children
There are two methods that allow you to traverse view-hierarchy. Both of them accept a callback
function that is called for each child. The callback should return a boolean value - if it is falsy the iteration will break. This is particularly useful if you are searching for a specific view and you want to stop iterating as soon as you have found it.
For getting View children use:
public eachChildView(callback: (child: View) => boolean): void
This method was previously known as _eachChildView()
. It will return View
descendants only. For example TabView returns the view of each TabViewItem because is TabViewItem is of type ViewBase.
Getting ViewBase children use:
public eachChild(callback: (child: ViewBase) => boolean): void;
This method will return all views including ViewBase
. It is used by the property system to apply native setters, propagate inherited properties, apply styles, etc. In the case of TabView – this method will return TabViewItems as well so that they could be styled through CSS.
View Class Common Methods
Each UI element extends the View
class (e.g., like StackLayout
or Label
) and comes with a set of methods created to ease the UI development. Methods related to measuring and positioning should be called in navigatedTo
event of the current Page
to ensure that all layout measuring has passed.
-
getViewById
- Returns the child view with the specified id.<Page navigatedTo="onNavigatedTo"> <StackLayout id="myStack"> <Label text="Tap the button" /> <Button text="TAP" tap="" /> <Label id="myLabel" text="" /> </StackLayout> </Page>
export function onNavigatedTo(args: EventData) { const page = <Page>args.object; let stack = <StackLayout>page.getViewById("myStack"); // e.g. StackLayout<myStack>@file:///app/page.xml:2:5; let label = <Label>stack.getViewById("myLabel"); // e.g. Label<myLabel>@file:///app/main-page.xml:5:9; }
function onNavigatedTo(args) { const page = args.object; let stack = page.getViewById("myStack"); // e.g. StackLayout<myStack>@file:///app/page.xml:2:5; let label = stack.getViewById("myLabel"); // e.g. Label<myLabel>@file:///app/main-page.xml:5:9; } exports.onNavigatedTo = onNavigatedTo;
Angular Specific Note: In Angular to use
getViewById
for root search, we might need to inject nativePage
object. As an alternative, in Angular, we can get reference to the native elements using theViewChild
directive and thenativeElement
property.<StackLayout #myNgStack id="myStackId"> <Label text="Using ViewChild against getViewById" /> </StackLayout>
import { ViewChild, ElementRef } from "@angular/core"; export class MyComponent { @ViewChild("myNgStack") stackRef: ElementRef; myNativeStack: StackLayout; constructor(private _page: Page) { } ngAfterViewInit() { this._page.getViewById("myStackId"); this.myNativeStack = this.stackRef.nativeElement; // this._page.getViewById("myStack") === this.myNativeStack } }
-
getActualSize
- Returns the actual size of the view in device-independent pixels. The returned value is of typeSize
.let stackSize: Size = stack.getActualSize(); let stackWidth = stackSize.width; // e.g. 411.42857142857144 DIP let stackHeight = stackSize.height; // e.g. 603.4285714285714 DIP
let stackSize = stack.getActualSize(); let stackWidth = stackSize.width; let stackHeight = stackSize.height;
-
getLocationInWindow
- Returns the location of this view in the window coordinate system. The returned value is of typePoint
.let locationInWindow: Point = stack.getLocationInWindow(); let locationWindowX = locationInWindow.x; // e.g. 10 let locationWindowY = locationInWindow.y; // e.g. 80
let locationInWindow = stack.getLocationInWindow(); let locationWindowX = locationInWindow.x; let locationWindowY = locationInWindow.y;
-
getLocationOnScreen
- Returns the location of this view in the screen coordinate system. The returned value is of typePoint
.
let locationOnScreen : Point = stack.getLocationOnScreen(); let locScreenX = locationOnScreen.x; // e.g. 10 let locScreenY = locationOnScreen.y; // e.g. 67.42857142857143
var locationOnScreen = stack.getLocationOnScreen(); var locScreenX = locationOnScreen.x; var locScreenY = locationOnScreen.y;
-
getLocationRelativeTo
- Returns the location of this view against another view's coordinate system. The returned value is of typePoint
.
let labelLocationRelativeToStack: Point = label.getLocationRelativeTo(stack); let labelRelativeX = labelLocationRelativeToStack.x; let labelRelativeY = labelLocationRelativeToStack.y;
let labelLocationRelativeToStack = label.getLocationRelativeTo(stack); let labelRelativeX = labelLocationRelativeToStack.x; let labelRelativeY = labelLocationRelativeToStack.y;
-
View Properties module Property CssProperty CssAnimationProperty InheritedCssProperty ShorthandProperty CoercibleProperty isIOS