Fundamental Concepts
Data Binding in NativeScript
Data Binding refers to a connection (binding) and data flow between ViewModel (Model) and User Interface (UI).
It gets activated through three steps:
- Create a ViewModel(let's call it DataModel) class extending the
Observable
class - Make DataModel available to the UI by setting
page.bindingContext
=new DataModel()
- Using the mustach syntax
, bind the UI components properties to the members of the
DataModel
instance.
When you look at a new project, you see an example of those steps applied.
There are various ways to manage data flow, often referred to as data bindings:
- One-way (to UI) data binding - This is the most common form of data binding. An example would be binding a string property in the Model to a Label component.
<Label text="{{ name }}" />
export class HelloWorldModel extends Observable {
name = 'John Doe'
}
- One-way (to Model) data binding - Binding which updates the model in relation to some action in the UI. The best example for this is a tap event. Static and data bound arguments can be passed using function currying. Here's a working example.
<Button text="Submit" tap="{{ onTap }}"/>
<Button text="Button 1" tap="{{ onTap2('Button 1') }}" />
<Button text="Button 2" tap="{{ onTap2('Button 2') }}" />
<Button text="Button 3" tap="{{ onTap3('Button 3', counter) }}" />
export class HelloWorldModel extends Observable {
public counter: number = 42
onTap(args: EventData) {
//
}
onTap2(source: string) {
return function fnOnTap2(args: EventData) {
console.log('onTap2.source =', source)
}
}
onTap3(source: string, num: number) {
return (args: EventData) => {
console.log('onTap3.source =', source)
console.log('onTap3.num =', num)
this.counter--
}
}
}
- Two-way data binding - The data flows from the Model to the UI and vice versa. A typical example is a
TextField
that reads its value from a Model, and also changes the Model based on user input.
<TextField text="{{ name }}"/>
export class HelloWorldModel extends Observable {
name = 'John Doe'
}
Accessing parent bindingContext
A parent and a child UI components can have different binding contexts and the child component might need to bind its property to a source property in its parent's bindingContext.
Generally, the binding context is inheritable, but not when the components 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 component will look like. When this component is added to the visual tree, for binding context it gets 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 components. So, the inner UI component cannot access the binding context of the ListView
. In order to solve this problem, NativeScript binding infrastructure has two special keywords:
$parent
: denotes the binding context of the direct parent visual component$parents
: 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 component with a given type.
Note
If the value of the items
property of the ListView
is an array of plain elements(numbers,string, dates) as in the preceeding example, you use the $value
variable to access the current item of the array.
If it is an array of objects,you use the current object property name as the variable name.
Using binding expressions
NativeScript supports the following polymorphic binding expressions:
Property access
To access a value stored in an object property of the bindingContext, use the propert access expression:
user = {
names: {
first: 'Sara',
},
}
<Label text="{{ user.name.first }}" textWrap="true" />
array access
<Label text="{{ fruits[0] }}" textWrap="true" />
logical operators
You can use the not(!
) operator to reverse the logical state of a binding context property.
<Label text="{{ !isUserLoggedIn }}" textWrap="true" />
Supported operators: &&
, ||
and !
.
unary operators
<Label text="{{ +age }}" textWrap="true" />
age = '33'
Converts a property value to a number
. To convert a property to a number
and negate it, use the -
operator.
comparison operators
<Label text="{{ prop1 > prop2 }}" textWrap="true" />
Supported operators: >
,<
, <=
, >=
, ==
, !=
, ===
, !==
.
ternary operator
<Label text="{{ prop1 ? prop2 : prop3 }}" textWrap="true" />
grouping parenthesis
<Label text="{{ prop1*(prop2 + prop3) }}" textWrap="true" />
function calls
<Label text="{{ someMethod(p1,p2,...,pN) }}" textWrap="true" />
comparison operators
<Label text="{{ property1 > property2 }}" textWrap="true" />
Other supported operators are: <
, <=
, >=
, ==
, !=
, ===
, !==
.
Note
Special characters need to be escaped as follows:
- double quotes:
"
→"
- single quote:
'
→'
- less than:
<
→<
- greater than:
>
→>
- ampersand:
&
→&
Using data converters
Often data within the Model is stored in a way that is optimized for best performance regarding tasks such as search, replace etc. Unfortunately, the way computers store data differs significantly from human-readable formats. One fitting example is the Date object. In JavaScript, Date actually is a very big number that represents milliseconds from 01.01.1970
which does not speak much to any human. This is where data converters come into play. Data converters are essentially functions that format the data from the Model into a human-readable format for display in the UI.
<StackLayout class="p-20 bg">
<Label text="{{ date | dateConverter('dd-mm-yyyy') }}" textWrap="true" />
<Label text="{{ name | toUpperCaseConverter }}" textWrap="true" />
<Label text="{{ title | toTitle }}" textWrap="true" />
</StackLayout>
export class HelloWorldModel extends Observable {
name = 'nandee'
title = 'hello world!'
date = Date.now() // number
dateConverter = this.formatDate()
toUpperCaseConverter = this.toUpperCase()
toTitle = this.convertToTitle()
formatDate() {
return {
toView(value: number, format: string) {
const date = new Date(value)
const day = date.getDate().toString().padStart(2, '0')
const month = (date.getMonth() + 1).toString().padStart(2, '0') // months are zero based in JS.
const year = date.getFullYear().toString()
return format
.replace('dd', day)
.replace('mm', month)
.replace('yyyy', year)
},
}
}
toUpperCase() {
return {
toView(value: string) {
return value.toUpperCase()
},
}
}
convertToTitle() {
return {
toView(str: string) {
return str.replace(/(^|\s)\S/g, function (t) {
return t.toUpperCase()
})
},
}
}
}
Stop binding
Generally there is no need to stop binding explicitly since the Observable object uses weak references, which prevents any memory leaks. However, if you need to data unbind a view, call the unbind() method with the target property name as the argument.
targetTextField.unbind('text')
- Previous
- XmlParser
- Next
- Error Handling