NativeScript Core

Observable

Generally, almost every UI control could be bound to a data object (all NativeScript controls are created with data binding in mind). After the code has met the following requirements, data-binding can be used out of the box.

  • The target object has to be a successor of the Bindable class. All NativeScript UI controls inherit from Bindable class.
  • For one-way binding, using a plain property is sufficient.
  • For two-way data binding, the target property should be a dependency property.
  • The data object should raise a propertyChange event for every change in the value of its property in order to notify all of the listeners interested in the change.

The article will show basic and advanced binding techniques including the architectural pattern used in NativeScript framework - MVVM (Model-View-ViewModel).

Basics

Creating and using Observable objects requires the tns-core-modules/data/observable module.

const Observable = require("tns-core-modules/data/observable").Observable;
const fromObject = require("tns-core-modules/data/observable").fromObject;
const fromObjectRecursive = require("tns-core-modules/data/observable").fromObjectRecursive;
import { fromObject, fromObjectRecursive, Observable, PropertyChangeData } from "tns-core-modules/data/observable";

Observable Class

Observable is used when you want to be notified when a change occurs. Use on/off methods to add/remove listener.

<!-- Using basic string binding and tap event callback binding-->
<Label text="{{ clientName }}" tap="{{ onLabelTap }}" textWrap="true" class="h2" color="red"/>

<!-- Example for using binding with concatenation (text prop) and for using binding to change font-size -->
<Label text="{{ 'font-size:' + mySize }}" textWrap="true" fontSize="{{ mySize }}"/>

<!-- Example demonstrating the boolean property usage with visibility and ternary expression-->
<Label text="{{ isVisible }}" textWrap="true" visibility="{{ isItemVisible, isItemVisible ? 'visible' : 'collapsed' }}"/>
// creating an Observable and setting title propertu with a string value
const page = args.object;
const viewModel = new Observable();

// String binding using set with key-value
viewModel.set("clientName", "Jonh Doe");

// Number binding using set with key-value
viewModel.set("mySize", 26);

// Boolean binding using set with key-value
viewModel.set("isVisible", true);

// Binding event callback using set with key-value
viewModel.set("onLabelTap", (args) => {
    // args is of type EventData
    console.log("Tapped on", args.object); // <Label>
    console.log("Name: ", args.object.text); // The text value
});

// using get to obtain the value of specific key
console.log(viewModel.get("clientName")); // Jonh Doe
console.log(viewModel.get("mySize")); // 42
console.log(viewModel.get("isVisible")); // true

// bind the view-model to the view's bindingContext property (e.g. the curent page or  view from navigatingTo or loaded event)
page.bindingContext = viewModel;
// creating an Observable and setting title propertu with a string value
const viewModel = new Observable();

// String binding using set with key-value
viewModel.set("clientName", "Jonh Doe");

// Number binding using set with key-value
viewModel.set("mySize", 24);

// Boolean binding using set with key-value
viewModel.set("isVisible", true);

// Binding event callback using set with key-value
viewModel.set("onLabelTap", (args) => {
    // args is of type EventData
    console.log("Tapped on", args.object); // <Label>
    console.log("Name: ", args.object.text); // The text value
});

// using get to obtain the value of specific key
console.log(viewModel.get("clientName")); // Jonh Doe
console.log(viewModel.get("mySize")); // 42
console.log(viewModel.get("isVisible")); // true

// bind the view-model to the view's bindingContext property (e.g. the curent view from loaded event)
const view = <Page>data.object;
view.bindingContext = viewModel;

Creating Observable with fromObject Method

The fromObject method creates an Observable instance and sets its properties according to the supplied JavaScript object.

// fromObject creates an Observable instance and sets its properties according to the supplied JS object
const newViewModel = fromObject({ "myColor": "Lightgray" });
// the above is equal to
/*
    let newViewModel = new Observable();
    newViewModel.set("myColor", "Lightgray");
*/
// fromObject creates an Observable instance and sets its properties according to the supplied JS object
const newViewModel = fromObject({ "myColor": "Lightgray" });
// the above is equal to
/*
    let newViewModel = new Observable();
    newViewModel.set("myColor", "Lightgray");
*/

Creating Observable with fromObjectRecursive Method

The fromObjectRecursive method creates an Observable instance and sets its properties according to the supplied JavaScript object. This function will create new Observable for each nested object (except arrays and functions) from supplied JavaScript object.

// fromObjectRecursive will create new Observable for each nested object (except arrays and functions)
const nestedViewModel = fromObjectRecursive({
    client: "John Doe",
    favoriteColor: { hisColor: "Green" } // hisColor is an Observable (using recursive creation of Observables)
});
// the above is equal to
/*
    let newViewModel2 = new Observable();
    newViewModel2.set("client", "John Doe");
    newViewModel2.set("favoriteColor", fromObject( {hisColor: "Green" }));
*/
// fromObjectRecursive will create new Observable for each nested object (except arrays and functions)
const nestedViewModel = fromObjectRecursive({
    client: "John Doe",
    favoriteColor: { hisColor: "Green" } // hisColor is an Observable (using recursive creation of Observables)
});
// the above is equal to
/*
    const newViewModel2 = new Observable();
    newViewModel2.set("client", "John Doe");
    newViewModel2.set("favoriteColor", fromObject( {hisColor: "Green" }));
*/

Adding Event Listener and Using propertyChangeEvent

Using propertyChangeEvent and responding to property changes with arguments of type PropertyChangeData.

const myListener = viewModel.addEventListener(Observable.propertyChangeEvent, (args) => {
    // args is of type PropertyChangeData
    console.log("propertyChangeEvent [eventName]: ", args.eventName);
    console.log("propertyChangeEvent [propertyName]: ", args.propertyName);
    console.log("propertyChangeEvent [value]: ", args.value);
    console.log("propertyChangeEvent [oldValue]: ", args.oldValue);
});
const myListener = viewModel.addEventListener(Observable.propertyChangeEvent, (args: PropertyChangeData) => {
    // args is of type PropertyChangeData
    console.log("propertyChangeEvent [eventName]: ", args.eventName);
    console.log("propertyChangeEvent [propertyName]: ", args.propertyName);
    console.log("propertyChangeEvent [value]: ", args.value);
    console.log("propertyChangeEvent [oldValue]: ", args.oldValue);
});

Removing Event Listener

The event listeners can be explicitly removed when no longer needed.

viewModel.removeEventListener(Observable.propertyChangeEvent, myListener);
viewModel.removeEventListener(Observable.propertyChangeEvent, myListener);

Improve this document

Demo Source


Mvvm Pattern

MVVM ((Model-View-ViewModel) is the base pattern on which NativeScript framework is build. MVVM facilitates a separation of development of the graphical user interface from development of the business logic or back-end logic (the data model).

  • Model: The model defines and represents the data. Separating the model from the various views that might use it allows for code reuse.
  • View: The view represents the UI, which in NativeScript is written in XML. The view is often data-bound to the view model so that changes made to the view model in JavaScript instantly trigger visual changes to UI components.
  • View Model: The view model contains the application logic (often including the model), and exposes the data to the view. NativeScript provides a module called Observable, which facilitates creating a view model object that can be bound to the view.

The biggest benefit of separating models, views, and view models, is that you are able to use two-way data binding; that is, changes to data in the model get instantly reflected on the view, and vice versa. The other big benefit is code reuse, as you're often able to reuse models and view models across views.

Complete example demonstrating MVVM pattern in NativeScript application with Plaing JavaScript and with TypeScript (using Class).

Plain JavaScript

const Observable = require("tns-core-modules/data/observable").Observable;

function getMessage(counter) {
    if (counter <= 0) {
        return "Hoorraaay! You unlocked the NativeScript clicker achievement!";
    } else {
        return `${counter} taps left`;
    }
}

function createViewModel() {
    const viewModel = new Observable();

    viewModel.set("counter", 42);
    viewModel.set("message", getMessage(viewModel.counter));

    viewModel.onTap = function () {
        this.set("message", getMessage(--this.counter));
    };

    return viewModel;
}
exports.createViewModel = createViewModel;
const createViewModel = require("./main-view-model").createViewModel;

function onNavigatingTo(args) {
    const page = args.object;

    // using the view model as binding context for the current page
    const mainViewModel = createViewModel();
    page.bindingContext = mainViewModel;
}
exports.onNavigatingTo = onNavigatingTo;
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="onNavigatingTo" class="page">
    <Page.actionBar>
        <ActionBar title="MVVM Pattern"/>
    </Page.actionBar>
    <StackLayout class="p-20">
        <Label text="Tap the button" class="h1 text-center"/>
        <!-- using the view model method `onTap` and property `message` -->
        <Button text="TAP" tap="{{ onTap }}" class="btn btn-primary btn-active"/>
        <Label text="{{ message }}" class="h2 text-center" textWrap="true"/>
    </StackLayout>
</Page>

TypeScript

import { Observable } from "tns-core-modules/data/observable";

export class HelloWorldModel extends Observable {

    private _counter: number;
    private _message: string;

    constructor() {
        super();

        // Initialize default values.
        this._counter = 42;
        this.updateMessage();
    }

    get message(): string {
        return this._message;
    }

    set message(value: string) {
        if (this._message !== value) {
            this._message = value;
            this.notifyPropertyChange("message", value);
        }
    }

    public onTap() {
        this._counter--;
        this.updateMessage();
    }

    private updateMessage() {
        if (this._counter <= 0) {
            this.message = "Hoorraaay! You unlocked the NativeScript clicker achievement!";
        } else {
            this.message = `${this._counter} taps left`;
        }
    }
}
import { HelloWorldModel } from "./main-view-ts-model";
import { EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";

export function onNavigatingTo(args: EventData) {
    const page = <Page>args.object;

    // using the view model as binding context for the current page
    const mainViewModel = new HelloWorldModel();
    page.bindingContext = mainViewModel;
}
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="onNavigatingTo" class="page">
    <Page.actionBar>
        <ActionBar title="MVVM Pattern"/>
    </Page.actionBar>
    <StackLayout class="p-20">
        <Label text="Tap the button" class="h1 text-center"/>
        <!-- using the view model method `onTap` and property `message` -->
        <Button text="TAP" tap="{{ onTap }}" class="btn btn-primary btn-active"/>
        <Label text="{{ message }}" class="h2 text-center" textWrap="true"/>
    </StackLayout>
</Page>

Improve this document

Demo Source


Parent Binding

Another common case in working with bindings is requesting access to the parent binding context. It is because it might be different from the bindingContext of the child and might contain information, which the child has to use. Generally, the bindingContext is inheritable, but not when the elements (items) are created dynamically based on some data source. For example, ListView creates its child items based on an itemТemplate, which describes what the ListView element will look like. When this element is added to the visual tree, it gets for binding context an element from a ListView items array (with the corresponding index). This process creates a new binding context chain for the child item and its inner UI elements. So, the inner UI element cannot access the binding context of the ListView. In order to solve this problem, NativeScript binding infrastructure has two special keywords: $parent and $parents. While the first one denotes the binding context of the direct parent visual element, the second one can be used as an array (with a number or string index). This gives you the option to choose either N levels of UI nesting or get a parent UI element with a given type. Let's see how this works in a realistic example.

<Page navigatingTo="onNavigatingTo" xmlns="http://schemas.nativescript.org/tns.xsd">
    <Page.actionBar>
        <ActionBar title="Parents Binding"/>
    </Page.actionBar>
    <GridLayout rows="*" >
        <ListView items="{{ items }}">
            <!--Describing how the element will look like-->
            <ListView.itemTemplate>
                <GridLayout columns="auto, *">
                    <Label text="{{ $value }}" col="0"/>
                    <!--The TextField has a different bindingCotnext from the ListView, but has to use its properties. Thus the parents['ListView'] has to be used.-->
                    <TextField text="{{ $parents['ListView'].test, $parents['ListView'].test }}" col="1"/>
                </GridLayout>
            </ListView.itemTemplate>
        </ListView>
    </GridLayout>
</Page>
const fromObject = require("data/observable").fromObject;

function onNavigatingTo(args) {
    const page = args.object;
    const viewModel = fromObject({
        items: [1, 2, 3],
        test: "Parent binding! (the value came from the `test` property )"
    });

    page.bindingContext = viewModel;
}
exports.onNavigatingTo = onNavigatingTo;
import { fromObject, EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";

export function onNavigatingTo(args: EventData) {
    const page = <Page>args.object;
    const viewModel = fromObject({
        items: [1, 2, 3],
        test: "Parent binding! (the value came from the `test` property )"
    });

    page.bindingContext = viewModel;
}

Improve this document

Demo Source


Plain Object Binding

A very common case is to provide a list (array) of plain elements (numbers, dates, strings) to a ListView items collection. All examples above demonstrate how to bind a UI element to a property of the bindingContext. If there is only plain data, there is no property to bind, so the binding should be to the entire object. Here comes another feature of NativeScript binding - object or value binding. To refer to the entire object, which is Date() in the example, the keyword $value should be used.

<ListView items="{{ items }}" class="list-group">
    <ListView.itemTemplate>
        <StackLayout class="list-group-item">
            <Label text="Date" class="list-group-item-heading" />
            <!-- use $value to bind plain objects (e.g. number, string, Date)-->
            <Label text="{{ $value }}" class="list-group-item-text" />
        </StackLayout>
    </ListView.itemTemplate>
</ListView>
const fromObject = require("tns-core-modules/data/observable").fromObject;

function onNavigatingTo(args) {
    const page = args.object;

    const list = [];
    for (let i = 0; i < 15; i++) {
        list.push(new Date());
    }

    const viewModel = fromObject({
        items: list
    });

    page.bindingContext = viewModel;
}
exports.onNavigatingTo = onNavigatingTo;
import { fromObject, EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";

export function onNavigatingTo(args: EventData) {
    const page = <Page>args.object;

    const list = [];
    for (let i = 0; i < 15; i++) {
        list.push(new Date());
    }

    const viewModel = fromObject({
        items: list
    });

    page.bindingContext = viewModel;
}

Improve this document

Demo Source


Two Way

Two-way data binding is a way of binding that combines binding from and to the application UI (binding to model and binding to UI) A typical example is a TextField that reads its value from the Model but also changes the Model based on user input.

The example is demonstrating two-way binding via code-behind. The TextField accepts an empty string as initial value (the same binding is used for the Label element). Then when the user inputs new string into the TextField, the two-way binding will update the label's text property simultaneously.

const observableSource = fromObject({
    myTextSource: "" // initial binding value (in this case empty string)
});

// create the TextField
const targetTextField = new TextField();
// create the Label
const targetLabel = new Label();
stackLayout.addChild(targetTextField);
stackLayout.addChild(targetLabel);

// binding the TextField with BindingOptions
const textFieldBindingOptions = {
    sourceProperty: "myTextSource",
    targetProperty: "text",
    twoWay: true
};
targetTextField.bind(textFieldBindingOptions, observableSource);

// binding the Label with BindingOptions
const labelBindingOptions = {
    sourceProperty: "myTextSource",
    targetProperty: "text",
    twoWay: false // we don't need two-way for the Label as it can not accept user input
};
targetLabel.bind(labelBindingOptions, observableSource);
const observableSource = fromObject({
    myTextSource: "" // initial binding value (in this case empty string)
});

// create the TextField
const targetTextField = new TextField();
// create the Label
const targetLabel = new Label();
stackLayout.addChild(targetTextField);
stackLayout.addChild(targetLabel);

// binding the TextField with BindingOptions
const textFieldBindingOptions = {
    sourceProperty: "myTextSource",
    targetProperty: "text",
    twoWay: true
};
targetTextField.bind(textFieldBindingOptions, observableSource);

// binding the Label with BindingOptions
const labelBindingOptions = {
    sourceProperty: "myTextSource",
    targetProperty: "text",
    twoWay: false // we don't need two-way for the Label as it can not accept user input
};
targetLabel.bind(labelBindingOptions, observableSource);

To create a binding in XML, a source object is needed, which will be created the same way, as in the example above. Then the binding is described in the XML (using a mustache syntax). With an XML declaration, only the names of the properties are set - for the target: text, and for source: textSource. The interesting thing here is that the source of the binding is not specified explicitly. More about this topic will be discussed in the Binding source article.

<Page xmlns="http://schemas.nativescript.org/tns.xsd">
    <StackLayout>
        <TextField text="" />
    </StackLayout>
</Page>

Note: When creating UI elements with an XML declaration, the data binding is two-way by default.

Improve this document

Demo Source


See Also Data Binding Concepts in NativeScript