Tabris Decorators Part 3: Data Binding Basics
NOTE: This article is about Tabris 2.x. Some information is outdated when targeting Tabris 3.x.
In this article, we will cover how TypeScript decorators are used in the Tabris Framework to enable data binding for UI Components. It is a part of the blog post series dedicated to the Tabris.js extension called tabris-decorators, which makes developing mobile apps with Tabris.js 2.x and TypeScript more convenient. The extension features data binding capabilities, enhanced event handling, and dependency injection, explicitly designed for the Tabris mobile framework. To use it the npm module must be installed separately.
It’s recommended to read the previous posts in this series, though you may skip the first one.
In this chapter, we take a first look at data binding. The @component decorator
allows for simple one-way and two-way bindings between a property of a custom component instance and a property of one of its children. This also involves the @property
and @bind
decorators and new JSX syntax, all provided by the tabris-decorators
module
The story so far…
We will continue from where we left off in the previous chapter with the LabeledInput
example. The current state is that we have a simple component containing a TextView
and a TextInput
. Except for the constructer it does not provide any API of its own:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import { Composite } from 'tabris'; import { component, ComponentJSX } from 'tabris-decorators'; @component export default class LabeledInput extends Composite { private jsxProperties: ComponentJSX<this>; constructor(properties: Partial<LabeledInput>) { super(properties); this.append( <widgetCollection> <textView id='label' height={32} centerY={0} text='Label:' font='20px'/> <textInput id='input' left='prev() 12' width={250} font='20px'/> </widgetCollection> ); } } |
It can be used like this:
1 |
<LabeledInput /> |
However, right now the text
property of the TextView
is hard-coded to the 'Label:'
. Next we will add a labelText
property to the LabeledInput
that will allow setting its value as a JSX attribute:
1 |
<LabeledInput labelText='First Name:'/> |
If the labelText
property was final (cannot change after widget construction) it could be set via constructor parameter.
Simply add a new property to the class:
1 |
public labelText: string; |
That makes the labelText
part of the properties
parameter of the LabeledInput
constructor. Now we can set its value directly on the child element:
1 2 3 4 |
<textView id='label' height={32} centerY={0} text={properties.labelText} font='20px'/> |
But that would mean that any changes to the labelText
after a widget construction, would have no effect on the actual textView
element. Data-binding solves this problem.
One-way bindings
In essence, we want to be able to do this:
1 2 |
const labeledInput = ui.find(LabeledInput).first(); labeledInput.labelText = 'Your Name:'; |
What we need here, is a one-way data binding which applies changes of the labelText
property (the “base property”) to the text
property on the “label” TextView
(the “target property”). Since the TextView
is not interactive, there is no need to have a “reverse” data flow that reflects changes on the target property back to the base.
One-way bindings first require a base property decorated with a @property
, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Composite } from 'tabris'; import { component, property } from 'tabris-decorators'; @component export default class LabeledInput extends Composite { @property public labelText: string; private jsxProperties: ComponentJSX<this>; constructor(properties: Partial<LabeledInput>) {… } } |
This makes the property eligible for one-way bindings. We could also initialize the property with the declaration, but in this case, we don’t really have a useful default value for it.
Next we establish the actual binding. The @component
decorator enables a new JSX attribute syntax where an existing attribute of a child is prefixed with a bind-
. The value of that attribute is the name of a base property to which it shall be bound. So in our case this becomes:
1 2 3 |
<textView id='label' bind-text='labelText' height={32} centerY={0} font='20px'/> |
And voilà: All changes to the labeledInput.text
will from now on be applied to the “label” TextView
.
This is not all the
@property
decorator can do. It will pop up again in the next chapter on an event handling and in the second chapter on data binding.
Two-way bindings
Next we will add a property representing the actual text entered by the user. We will call it a text
:
1 |
<LabeledInput labelText='First Name:' text='Jane'/> |
It differs from the labelText
in that the value can change by itself due to user interactions. If we just used a one-way binding as above, this would not work as expected:
1 |
const inputText = labeledInput.text; |
The text
property would always provide the same value, even if the user has edited the text in the TextInput
widget.
So here is where we need a two-way binding that keeps both base and target properties in sync with each other. This is accomplished by attaching the @bind
decorator to the declaration of the base property:
1 |
@bind('#input.text') public text: string; |
The string given to the decorator takes a binding path which follows the pattern '#
.
As a result we end up with a class like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import { Composite } from 'tabris'; import { bind, component, property, ComponentJSX } from 'tabris-decorators'; @component export default class LabeledInput extends Composite { @bind('#input.text') public text: string; @property public labelText: string; private jsxProperties: ComponentJSX<this>; constructor(properties: Partial<LabeledInput>) { super(properties); this.append( <widgetCollection> <textView id='label' height={32} centerY={0} bind-text='labelText' font='20px'/> <textInput id='input' left='prev() 12' width={250} font='20px'/> </widgetCollection> ); } } |
Now, to prove that everything actually works, let’s write a mini form for entering your name. The result of the user input will be shown in a dialog.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { ui } from 'tabris'; import LabeledInput from './ui/LabeledInput'; ui.contentView.append( <widgetCollection> <LabeledInput id='firstname' labelText='First name:'/> <LabeledInput id='lastname' labelText='Last name:'/> <button onSelect={showName} text='OK'/> </widgetCollection> ).children().set({left: 12, top: 'prev() 12'}); function showName() { const firstName = ui.find('#firstname').first(LabeledInput).text; const lastName = ui.find('#lastname').first(LabeledInput).text; new AlertDialog({message: `Hello ${firstName} ${lastName}!`}).open(); } |
Some additional remarks for Data Binding Basics:
- Components are initialized when
append
is called the first time. All bindings of any kind (i.e. one- and two-way bindings) are established at that moment, and appending any more widgets later, has no effect. This may change in Tabris.js 3. - The IDE / TypeScript compiler can not check that the bindings will be valid, so no auto completion is available for this syntax. However, at runtime any invalid bindings will throw errors, they will not just fail silently.
- Binding to non-components (i.e. models or other widgets) is not fully implemented yet.
- We will discuss edge-cases and limitations in another blog post further down the road.
That’s it with the basics of the tabris-decorators
data binding features . The next chapter will deal with an event handling, continuing with the same example class.
Get Started with the Tabris Framework
To try out Tabris.js,
- Install the new Tabris.js 2 developer app on your device
- Try out the examples bundled in this app
- Run your own code snippets from the playground, our online Tabris.js editor
To start developing real apps,
- Install the latest Tabris CLI on your machine:
npm install -g tabris-cli
- Type
tabris init
in an empty directory – this will create a simple example app - Type
tabris serve
and load it in the developer app
The documentation contains everything you need to know (tip: try the doc search). Beginners find a step-by-step guide in this ebook. If you have questions or comments, you’re also invited to join the community chat.
Happy coding!
Feedback is welcome!
Want to join the discussion?Feel free to contribute!