JSX

JSX is a proprietary extension to the JavaScript/TypeScript syntax that allows mixing code with XML-like declarations. Tabris 3 supports JSX out of the box in .jsx and .tsx files with any TypeScript compiler based projects. The only requirement is that in tsconfig.json the jsx property is set to "react" and jsxFactory to "JSX.createElement".

Usage

In Tabris, JSX is used to create UI elements in a declarative manner. Every constructor for a Widget, WidgetCollection or Popup based class can be used as an JSX element.

Action JavaScript/TypeScript JSX
Create an instance new TextView() <TextView />
Set a string property* new TextView({text: 'foo'}) <TextView text='foo' />
Set non-string property* new TextView({elevation: 3}) <TextView elevation={3} />
Set property to true new TextView({markupEnabled: true}) <TextView markupEnabled />
Register listener new TextView().onResize(cb) <TextView onResize={cb} />
Set properties object new TextView(props) <TextView {...props} />
Mix in properties object new TextView({text: 'foo', ...props}) <TextView text='foo' {...props} />
Add new children new Composite().append(new TextView()) <Composite><TextView /></Composite>
Add existing children new Composite().append(children) <Composite>{children}</Composite>

Some properties can also be set by putting the value between the opening and closing tag, like children. The value must be put in curly braces, except strings and markup, which can be inlined directly.

Most commonly this sets the text property. Examples:

JavaScript/TypeScript JSX
new TextView({text: foo}) <TextView>{foo}</TextView>
new TextView({text: 'foo'}) <TextView>{'foo'}</TextView>
new TextView({text: 'foo'}) <TextView>foo</TextView>
new TextView({text: '<b>bar/b>'}) <TextView><b>bar</b></TextView>
new TextView({text: 'bar ' + foo}) <TextView>bar {foo}</TextView>

Properties that support this are always marked as such in the API reference.

:warning: In TypeScript JSX expressions themselves are type-safe, but their return type is any! So be extra careful when you assign them to a variable to give it the proper type.

To add multiple children to an existing parent you group them using WidgetCollection or $:

import {contentView, Button, TextView, WidgetCollection, $};

// JavaScript/TypeScript:
contentView.append(
  new Button(),
  new TextView()
);

// JSX:
contentView.append(
  <WidgetCollection>
    <Button />
    <TextView />
  </WidgetCollection>
);

// <$> can be used as a shortcut of <WidgetCollection>
contentView.append(
  <$>
    <Button />
    <TextView />
  </$>
);

This is not necessary inside JSX elements:

<Composite>
  <Button />
  <TextView />
</Composite>

The <$> element can also create multiline strings:

/** @type {string} **/
const str =
  <$>
    This is some very long text
    across multiple lines
  </$>
);
contentView.append(<TextView>{str}</TextView>);

The line breaks and indentions will not become part of the final string.

Custom JSX Elements

JSX becomes a lot more powerful when creating your own elements. There are two ways to do this.

Stateless Functional Components

A “stateless function component” (SFC) is essentially a factory that can be used as an JSX element. This can be any function that fits the following pattern:

  • Name starts with an upper case.
  • Returns a JSX supported value, e.g. a widget or array of widgets.
  • Takes a plain object as the first parameter. (This contains the attributes and children.)

A SFC that initializes an existing widget with new default values could look like this:

const StyledText = attributes => <TextView textColor='red' {...attributes} />;

// example usage:
contentView.append(<StyledText>Hello World!</StyledText>);

If the element has children (everything within the element’s body) they are mapped to the attribute “children”. Therefore <Foo><Bar/></Foo> is treated like <Foo children={<Bar/>}/>.

In TypeScript (.tsx files) you need to give the proper type of the attributes object:

const StyledText = (attributes: Attributes<TextView>) =>
  <TextView textColor='red' {...attributes} />;

:point-right: The Attributes interface needs to be imported from 'tabris'

If your IDE understands jsDocs wit TypeScript types you can also do this in .jsx files:

/** @param {Attributes<TextView>=} attributes */
const StyledText = attributes => <TextView textColor='red' {...attributes} />;

A function that was used as a SFC can also be used as a selector, as can its name:

console.log(contentView.find(StyledText).first() === contentView.find('StyledText').first());

Custom Components

Any custom component (a user-defined class extending a built-in widget) can be used as a JSX element right away. The only requirement is that the constructor takes the properties object and passes it on to the base class in a super(properties) or set(properties) call. All attributes are interpreted as either a property or a listener as you would expect.

In TypeScript the attributes that are available on the element are derived from the properties and events of the component:

  • All public, writable properties except functions (methods) are valid attributes.
  • All events defined via Listeners properties are also valid listener attributes.
  • All child types accepted by the super type are still accepted.

Data Binding

Declarative data binding via JSX is provided by the tabris-decorators extension. Once installed in your project, you can do one-way bindings between a property of your custom component and the property of a child like this:

@component
class CustomComponent extends Composite {

  @property public myText: string = 'foo';

  constructor(properties: CompositeProperties) {
    super(properties);
    this.append(
      <TextView bind-text='myText'/>
    );
  }

}

The tabris-decorators module also provides two-way bindings.

Adding Special Attributes

In the rare case that the above behavior needs to be modified, you can do so by declaring a special (TypeScript-only) property called jsxAttributes. The type of this property defines what JSX attributes are accepted. The value is irrelevant, it should not be assigned to anything.

The following example defines a JSX component that takes a “foo” attribute even though there is no matching property:

class CustomComponent extends tabris.Composite {

  public jsxAttributes: tabris.JSXAttributes<this> & {foo: number};

  constructor({foo, ...properties}: tabris.Properties<CustomComponent> & {foo: number}) {
    super(properties);
    console.log(foo);
  }

}

The type JSXAttributes<this> part provides the default behavior for JSX attributes as described above. The second part {foo: number} is the additional attribute. In this case it would be a required attribute, but it can be made optional like this: {foo?: number}. The constructor properties parameter type should be extended in exactly the same way.

Which elements are accepted as children is determined by a children entry of jsxAttributes. If your custom component does not accept children you could disallow them like this:

class CustomComponent extends tabris.Composite {

  public jsxAttributes: tabris.JSXAttributes<this> & {children?: never};

}