Tabris Decorators Part 2: Introducing Tabris Components
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 create 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.
If you want to know more about TypeScript Decorators in general, what they are, and what benefits they bring to your work, please see our Introduction blog post here.
What is Tabris Component?
A Tabris component, for the purpose of this text, is any TypeScript class that extends Composite
, Page
or Tab
and uses the @component
decorator. To write your first component, set up a Tabris 2.x project with tabris-decorators
as described here.
For our main example, we will write a simple LabeledInput
view that consists of a TextView
(the label) and a TextInput
described by it. That may be useful in forms where you would need this combination over and over again.
Let’s start just with the visuals:
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> ); } } |
This creates just children with same placeholder text. Notice the @component
decorator above the class definition.
The
jsxProperties
field is required for the compiler to know which attributes are valid when using this widget as a JSX element. It’ll become relevant when we discuss event handling.
Okay, so this should already work to some degree. Import the component in another module and add it to the main content view:
1 2 3 4 5 6 7 8 9 |
import LabeledInput from './ui/LabeledInput'; ui.contentView.append( <widgetCollection> <LabeledInput id='input'/> <LabeledInput id='anotherInput'/> <LabeledInput id='lastInput'/> </widgetCollection> ).children().set({left: 12, top: 'prev() 12'}); |
Okay, but…
What does the @component decorator actually do?
You can find the full documentation here, but for now, we are looking at how it encapsulates the component.
Consider this code:
1 |
ui.find('#input').set({visible: false}); // hides all matching widget |
The intend here is to hide the first LabeledInput
, but without @component
it would actually also hide all the textInput
elements inside all the other LabeledInput
instances. The decorator makes the component behave to the outside as though it had no children, a bit like the shadow DOM in HTML. Here’s the proof:
1 |
console.log(ui.find(LabeledInput).children().length); // "0" |
Of course we still want to be able to access and manipulate the children from the inside of the component, which is possible via protected API dedicated to this use case:
1 |
this._find('#input').set({visible: false}); // available inside LabeledInput only |
Aside from _find
there is also _apply
and _children
. However, you will rarely need any of those, as we will discover in the chapters on data binding. For now, just remember to always use the protected API instead of the public one and you will be fine.
Coming up…
The encapsulation aspect of @component
is a convenient safeguard against accidental name collisions of id
or class
, but it’s not its main purpose. What we really need it for, is data binding which we will discuss in the next chapter. See you then!
Feedback is welcome!
Want to join the discussion?Feel free to contribute!