Widget Basics
In the context of Tabris.js a widget is a native UI element that can be freely arranged, composed and configured in application code. In JavaScript these elements are represented by subclasses of tabris.Widget
.
See also “UI Architecture” for a technical overview of all UI-related Tabris.js API including non-widget API.
Hello World
This is a complete Tabris.js app to create an onscreen button:
```js
tabris.contentView.append(
new tabris.Button({text: 'Hello World!'})
);
This gets us a push button that looks and behaves like a typical Android button on an Android device, and like a typical iOS button on iOS devices.
Let’s look at each part of the app:
Code | Explanation |
---|---|
tabris.contentView |
uses the tabris namespace to access the contentView widget instance, which represents the main content of your app. |
.append( |
calls the append method to add something to that area. |
new tabris.Button(
|
creates the actual button. |
{text: 'Hello World'}
|
sets the text property which is the label of the button. |
)
|
end of constructor call |
); |
end of append call |
Variations
You can (and in most situations should) explicitly import any member of the tabris
namespace from the tabris module, allowing you to omit it in the code:
```js
import {contentView, Button} from 'tabris';
contentView.append(
new Button({text: 'Hello World!'})
);
If you use a “Vanilla” JS project setup without cross-compilation you must use require
instead of the ES6 import
syntax:
```js
const {contentView, Button} = require('tabris');
contentView.append(
new Button({text: 'Hello World!'})
);
In JavaScript/JSX or TypeScript/JSX compiled project setup you can also create the button via an JSX expression, similar to an HTML element. This is only supported in .jsx
or .tsx
files.
```jsx
import {contentView, Button} from 'tabris';
contentView.append(
<Button text='Hello World' />
);
Most widgets that can display text allow to set it in the body of the element:
```jsx
import {contentView, Button} from 'tabris';
contentView.append(
<Button>Hello World</Button>
);
A vanilla JavaScript equivalent to JSX is also available as of Tabris 3.6:
```js
import {contentView, Button} from 'tabris';
contentView.append(
Button({text: 'Hello World!'})
);
This invokes Button
as a factory, not a constructor. At a first glance this simply omits new
, but this API also support additional attributes to register listener and add children. (It is the technical equivalent to using JSX syntax, but unlike JSX does not require a compiler setup.)
Widget Properties
Every widget supports a fixed set of properties, like text
or background
, that can be specified when the widget is created. Most properties support values multiple different types, but will always convert these to the same standard type.
contentView.append(
new Button({text: 'Hello World!', background: [255, 128, 0]})
);
contentView.append(
Button({text: 'Hello World!', background: new Color(255, 128, 0))
);
contentView.append(
<Button text='Hello World' background='rgb(255, 128, 0)' />
);
Details about the JSX syntax for attributes can be found here.
Modifying Widgets
To set or get a property on an existing widget you need a reference, which you can easily obtain using the selector API:
const button = contentView.find(Button).first();
const oldText = button.text;
button.text = 'New Text';
There is also a shorthand for contentView.find(selector)
: $(selector)
. You can use $
anywhere without importing it.
Using the widgets set()
method, it’s also possible to set multiple properties in one call:
button.set({
text: 'Hello New World',
background: 'blue'
});
With the selector API you can also set multiple properties of multiple widgets, for example using class selectors.
$('.label').set({background: 'red', opacity: 0.5})
Some properties can only be set on creation and not be changed later (e.g.
textInput#type
), and a few (e.g.widget#bounds
) can not bet set at all.
Composition
Widgets can contain other widgets to form complex user interfaces. This hierarchy always starts with an instance of ContentView
(e.g. tabris.contentView
) as the root element, all other widget types needs to have a parent to be visible.
All widgets that can contain child widgets are instances of Composite
or one of its subclasses - which include ContentView
. With some exceptions (e.g. NavigationView
) widgets can be freely arranged within their parent, which is the topic of this article.
In pure JavaScript this primarily done via append
calls. In this example we create a page with content and add it to the appropriate parent:
navigationView.append(
new Page().append(
new TextView(),
new Button()
)
);
With JSX child elements a placed within the body of their parent element:
navigationView.append(
<Page>
<TextView>
<Button>
</Page>
);
If you are calling the constructor without new
the children
attribute can be set to an array containing all children. Note that on the actual widget instance children
is a method, not an array.
navigationView.append(
Page({
children: [
TextView(),
Button()
]
})
);
If a widget that already has a parent is added to another, it is automatically removed from the old parent first.
The current parent of a widget is returned by the parent
method, and the children by the children
method. You can not set the parent or children via these methods, only get them.
Event Handling
Widgets can notify event callback functions (“listeners”) of events such as a user interaction or a property change. For each type of event supported by a widget there is a matching method (JS) and creation attribute (JSX/factories) to register a listener. These all start with an on
prefix, so the select
event is registered with onSelect
.
Example:
const listener = () => {
console.log('Button selected!');
}
contentView.append(
new Button().onSelect(listener)
);
// or ...
contentView.append(
<Button onSelect={listener} />
);
// or ...
contentView.append(
Button({onSelect: listener})
);
Only JSX element and widget factories accept listeners this way, onSelect
is NOT an instance property that a listener can be assigned to:
button.onSelect = ev => handleEvent(ev); // WRONG!!
Nor can one be passed to a constructor when called with a new
call:
new Button({
onSelect: ev => handleEvent(ev) // WRONG!!
});
Always favor arrow functions over function
to create listeners, it avoids issues with this
having unexpected values. A simple wrapper can suffice:
button.onSelect((ev) => this.doSomething(ev));
The listener function is called with an instance of EventObject that may include a number of additional properties depending on the event type.
The listener registration method is also an object of the type Listeners
which provides additional API for event handling. This is how you de-register a listener again:
button.onSelect.removeListener(listener);
It also implements the Observable
API:
const subscription = button.onSelect.subscribe(observer);
RxJS recognizes it as a generic Subscribable
:
rxjs.from(button.onSelect)
.pipe(debounceTime(100))
.subscribe(ev => ...);
Change Events
All widgets support property change events that are fired when a property value changes. All change events are named after the property with Changed
as a postfix, e.g. myValue
fires myValueChanged
, so listeners can be registered via onMyValueChanged
.
In addition to the common event properties, change events have a property value
that contains the new value of the property.
Example:
new TextInput().onTextChanged(event => {
console.log('The text has changed to: ' + event.value);
})
Animations
All widgets have the method animate(properties, options)
. It expects a map of properties to animate (akin to the set
method), and a set of options for the animation itself.
All animated properties are set to their target value as soon as the animation starts. Therefore, reading the property value will always result in either the start or target value, never one in between.
Only the properties transform
and opacity
can be animated.
The animate
method returns a Promise that is resolved once the animation is completed. If the animation is aborted, e.g. by disposing the widget, the promise is rejected.
In this example we use the async/await
syntax to wait for the animation to finish, then dispose the widget.
async function fadeOut(widget) {
await widget.animate(
{opacity: 0}
{duration: 1000, easing: 'ease-out'}
);
widget.dispose());
}
fadeOut(myLabel);
Disposing of a Widget
When a widget instance is no longer needed in the application it should be disposed to free up the resources it uses. This is done using the dispose
method, which disposes of the widget and all of its children. It triggers a removeChild event on the parent and a dispose event on itself.
Example:
button.onDispose(() => console.log('Button disposed!'));
button.dispose();
After a widget has been disposed isDisposed()
returns true
while almost all other methods will throw an exception if called.
Custom Components
Custom components are user-defined subclasses of Composite
(or other Component
subclasses such as Page
and Tab
). They are composed of built-in widgets (or other custom components) to form a discrete part of the UI which can then be used (and re-used) to form the application UI as a whole. Custom components can define new properties and events for interaction with other parts of the application.
Typically a custom component creates its own children in its constructor and “hides” them from the public API by overriding the children
method or applying the @component
decorator (TypeScript only). The children are then modified using the protected selector API (e.g. _find
) as a reaction to property changes.
A custom component with a custom text
property and custom event myEvent
may look like this in plain JavaScript:
class ExampleComponent extends Composite {
/** @param {tabris.Properties<ExampleComponent>=} properties */
constructor(properties) {
super();
/** @type {string} */
this._text = '';
/** @type {tabris.Listeners<{target: ExampleComponent}>} */
this.onMyEvent = new Listeners(this, 'myEvent');
this.append(this._createContent()).set(properties); // The order is important!
this.children = () => new WidgetCollection([]);
}
set text(value) {
this._text = value;
this._find('#message').only(TextView).text = value;
}
get text() {
return this._text;
}
_createContent() {
return Stack({spacing: 23, padding: 23, children: [
TextView({font: '18px', text: 'The message:'}),
TextView({font: '18px', id: 'message', background: 'yellow'}),
Button({
top: 24, text: 'Push me',
onSelect: this.onMyEvent.trigger
})
]});
}
}
Or like this in TypeScript/JSX using one-way data binding:
@component
export class ExampleComponent extends Composite {
@prop text: string;
@event onMyEvent: Listeners<{target: ExampleComponent}>;
constructor(properties: Properties<ExampleComponent>) {
super();
this.append(
<Stack spacing={12} padding={12}>
<TextView font='18px'>The message:</TextView>
<TextView font='18px' background='yellow' bind-text='text'/>
<CheckBox top={24} onSelect={this.onMyEvent.trigger}>
Push Me
</CheckBox>
</Stack>
).set(properties);
}
}
And in JavaScript/JSX it’s only slightly different:
@component
export class ExampleComponent extends Composite {
/** @type {string} */
@prop(String) text;
/** @type {tabris.Listeners<{target: ExampleComponent}>} */
@event onMyEvent;
/**
* @param {Properties<ExampleComponent>} properties
*/
constructor(properties) {
super();
this.append(
<Stack spacing={12} padding={12}>
<TextView font='18px'>The message:</TextView>
<TextView font='18px' bind-text='text'/>
<CheckBox top={24} onSelect={this.onMyEvent.trigger}>
Push Me
</CheckBox>
</Stack>
).set(properties);
}
}
Functional Components
A functional component is the term used for functions that return a widget or WidgetCollection
, a.k.a a widget factory. Its name typically starts with an upper case, and it should take a single properties
or attributes
parameter object. In the latter case it needs to create widgets using declarative UI syntax.
A common use case for this is to create widgets that have different default values, such as a different background or text color.
A very simple functional component could look like this:
/** @param {tabris.Properties<TextView>} properties */
function StyledComponent(properties) {
return new TextView({textColor: 'red', background: 'yellow', ...properties});
}
Functional components can also work as selectors:
$(StyledComponent).only().text = 'Hello WOrld';
For more advanced examples, read this section in the “Declarative UI” article.