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:

```jsx
tabris.contentView.append(
  <tabris.Button>
    Hello World
  </tabris.Button>
);

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.
    <tabris.Button> creates the actual button via an JSX expression, similar to an HTML element. This is only supported in .jsx or .tsx files in a compiled project setup.
        Hello World is the text that the button displays. Most widgets that can display text allow defining it that way, but you can also set the text attribute.
    </tabris.Button> closes the Button element and ends the JSX expression.
); ends the append call.

Variations

You can also explicitly import any member of the tabris namespace from the tabris module, allowing you to omit it in the code:

```jsx
import {contentView, Button};

contentView.append(
  <Button>Hello World</Button>
);

The text content of the button can also be set as an attribute:

```jsx
import {contentView, Button};

contentView.append(
  <Button text='Hello World' />
);

And if you prefer pure JavaScript/TypeScript, equivalent code would be:

```js
import {contentView, Button} from 'tabris';

contentView.append(
  new Button({text: 'Hello World!'})
);

Finally, if you use a “Vanilla” JS project setup without cross-compilation you can use neither JSX nor the ES6 import syntax:

```js
const {contentView, Button} = require('tabris');

contentView.append(
  new Button({text: 'Hello World!'})
);

Widget Properties

Every widget supports a fixed set of properties, like text or background, that can be specified in the JSX element or constructor call.

contentView.append(
  <Button text='Hello World' background={[255, 128, 0]} />
);
contentView.append(
  new Button({text: 'Hello World!', background: [255, 128, 0]})
);

:point_right: 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(); // alternatively: $(Button).first();
const oldText = button.text;
button.text = 'New Text';

Using the 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:

contentView.find('*').set({background: 'red', opacity: 0.5})

:point_right: Some properties can only be set on creation and not be changed later (like textInput#type), while others (like widget#bounds) can not bet set at all.

Composition

Widgets can contain other widgets to form complex user interfaces. This hierarchy needs to start with an instance of ContentView (e.g. tabris.contentView) as a root element, since 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.

With JSX it is possible to create and insert an entire ui fragment in one call. In this example we create a page with content and add it to the appropriate parent:

navigationView.append(
  <Page>
    <TextView>
    <Button>
  </Page>
);

In pure JavaScript this requires multiple append calls:

navigationView.append(
  new Page().append(
    new TextView(),
    new 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.

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 attribute (JSX) and method (JS) 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(
  <Button onSelect={listener} />
);

// or ...

contentView.append(
  new Button()
    .onSelect(listener)
);

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 Listeners type that provides additional API for event handling. This is how you de-register a listener again:

button.onSelect.removeListener(listener);

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.on('dispose', () => 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.