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)' />
);

: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();
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})

:point_right: 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.