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