Tabris Decorators Part 4: Event Handling
NOTE: This article is about Tabris 2.x. Some information is outdated when targeting Tabris 3.x.
In this article, we will cover the new event handling API of the Tabris.js framework provided by the tabris-decorators Tabris.js. It is a part of a blog post series dedicated to that extension. Tabris-decorators make developing mobile apps with Tabris.js 2.x and TypeScript more convenient. It features data binding capabilities, enhanced event handling, and dependency injection, all explicitly designed for the Tabris.js 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.
- Tabris Decorators Part 1: Intro to TypeScript Decorators
- Tabris Decorators Part 2: Introducing Tabris Components
- Tabris Decorators Part 3: Data Binding Basics
In this chapter we take a look at the event handling features. Unlike data binding, event handling is already built into Tabris.js. However, this API was designed with JavaScript in mind and is a bit clumsy to use in TypeScript. With tabris-decorators we offer an abstraction layer that is not only TypeScript optimized, but also adds a lot of convenient features on top of it.
For the most part this improved event system is independent from decorators and TypeScript. For that reason it will (with minor changes) be moved to the
tabriscore module in Tabris.js 3.0. In fact, it is already available in the beta 1 release. The old event API will still be supported.
Previously…
We continue with our example component LabeledInput. As of now it consists of a TextView bound to the labelText property of LabeledInput and a TextInput bound do text:
|
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> ); } } |
What we are still missing is the application’s ability to react to user inputs.
Introducing @event
With the @event decorator this is just one line:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { Composite } from 'tabris'; import { bind, ChangeListeners, component, event, property } from 'tabris-decorators'; @component export default class LabeledInput extends Composite { @bind('#input.text') public text: string; @event public readonly onTextChanged: ChangeListeners<string>; @property public labelText: string; private jsxProperties: ComponentJSX<this>; // remaining class... } |
And now we can do this:
|
1 2 3 |
labeledInput.onTextChanged(ev => { console.log(ev.value); }); |
Or this:
|
1 2 3 4 |
<LabeledInput id='input' labelText='First Name:' text='Jane' onTextChanged={ev => console.log(ev.value)}/> |
How it works
The @event decorator creates a function of the type ChangeListeners and attaches it to the component instance. This function allows registering event listeners in a type-safe way. The type of the event that the listeners are registered for is derived from the name of the property the @event is attached to:
onTextChangedregisters listeners fortextChangedevents.onFooChangedregisters listeners forfooChangedevents.onBarregisters listeners forbarevents.
The change event itself is triggered by the property setter that the @bind decorator generated. It too derives the matching event name from the name of the property. So to listen to change events of a two-way binding you only need to have the @event decorator on a field with a name and type that match that of the bound property:

Properties decorated with @property also generate change events whenever their value changes. Therefore we could also add the @event for labelText:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@component export default class LabeledInput extends Composite { @bind('#input.text') public text: string; @event public readonly onTextChanged: ChangeListeners<string>; @property public labelText: string; @event public onLabelTextChanged: ChangeListeners<string>; private jsxProperties: ComponentJSX<this>; // remaining class... } |
Again the even type is determined by the names of the two properties:

Event Triggering
But @event can do more ChangeListeners actually extends the Listeners type, which is not just a function but also an object providing new API to manage listeners and dispatch events. It is an abstraction of the built-in Tabris event system (i.e. on, off and trigger methods) and can be used interchangeably.
Let’s assume another component that has the @event on onFoo:
|
1 2 3 4 |
class MyWidget extends Composite { @event public readonly onFoo: Listeners<{bar: string}>; //... remaining class... } |
This creates the public API for event registration of foo events. The generic notation <{bar: string}> indicates that the event object that the listeners receive has a field bar of the type string. This is in addition to target, type, and timeStamp that are available on every EventObject.
Note that we don’t need
@componentfor@eventto work. The event target does not even have to be a widget.
To trigger an event we can use the trigger method of Listeners available directly on onFoo.
Example:
|
1 2 |
myWidget.onFoo(listner); myWidget.onFoo.trigger({bar: 'Hello World!'}); |
This is the same as doing…
|
1 2 |
myWidget.on('foo', listner); myWidget.trigger('foo', Object.assign(new EventObject(), {bar: 'Hello World!'})); |
…except that the former is type-safe and the latter isn’t.
As you can see the new API also does not require us to create an EventObject instance, the new trigger method will do that for us and copy the given data to it.
Event Forwarding
Back to LabeledInput. In addition to change event we also want to be notified when the user presses enter while TextInput has the focus. The goal would be to enable the following code:
|
1 2 3 |
<LabeledInput labelText='First Name:' onAccept={ev => console.log(`Your name is ${ev.text}`)}/> |
We will call the event accept, just like it does on TextInput. Lets start by adding the @event:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@component export default class LabeledInput extends Composite { @bind('#input.text') public text: string; @event public readonly onTextChanged: ChangeListeners<string>; @property public labelText: string; @event public readonly onLabelTextChanged: ChangeListeners<string>; @event public readonly onAccept: Listeners<{text: string}>; private jsxProperties: ComponentJSX<this>; // remaining class... } |
Now to forward the accept event from TextInput to LabeledInput we can simply do this:
|
1 2 3 4 |
<textInput id='input' left='prev() 12' width={250} font='20px' onAccept={ev => this.onAccept.trigger(ev)}/> |
But it can be even shorter:
|
1 2 3 4 |
<textInput id='input' left='prev() 12' width={250} font='20px' onAccept={this.onAccept.trigger}/> |
This is possible because the trigger method of Listeners is permanently bound to event type and target, no matter how it is called. The event object that is passed on will not be re-used, instead trigger will copy the text property to a new EventObject instance with the correct type and target fields.
Putting it all together
Now LabeledInput looks 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 27 28 29 30 |
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; @event public readonly onTextChanged: ChangeListeners<string>; @property public labelText: string; @event public readonly onLabelTextChanged: ChangeListeners<string>; @event public readonly onAccept: Listeners<{text: 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' onAccept={this.onAccept.trigger}/> </widgetCollection> ); } } |
And now we can update the demo snippet from last time to use the new accept event:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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:' onAccept={showName}/> </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(); } |
For now this completes our LabeledInput example component. For more example code using the Tabris decorators, have a look at the reddit_viewer project here.
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 initin an empty directory – this will create a simple example app - Type
tabris serveand 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!





