EcmaScript 6, TypeScript and JSX
EcmaScript 6
Tabris.js 2 supports all ES5 and most ES6/ES7 (aka ES2015/ES2016) features without transpilers like Babel. This includes:
- Arrow functions
- Classes
- const
- Default parameters (except iOS 9)
- Destructuring assignment
- Exponentiation operator(except iOS 9)
- for…of
- Generators (except iOS 9)
- Iterators
- let
- Map
- Methods
- Object property shorthands
- Promise
- Reflect (except iOS 9)
- Rest parameters (except iOS 9)
- Proxy (except iOS 9)
- set and get literals
- Set
- Spread operator
- Symbol
- Template literals
- Typed Arrays and ArrayBuffer
- WeakMap
- WeakSet
NOT supported (without transpiling) are:
- async/await: Use
then
instead. - import/export: Use the
require
function andexports
object instead.
TypeScript
Tabris.js 2 is optimized for use with TypeScript 2. TypeScript is a type-safe dialect of JavaScript/EcmaScript and also supports ES6 module syntax (import
and export
statements) and async
/await
. A complete guide to TypeScript can be found at typescriptlang.org. As an IDE we can recommend Visual Studio Code with the tslint extension, but there are many suitable TypeScript IDEs out there.
TypeScript files have to be “transpiled” to JavaScript before execution. The compiler is included when generating a new Tabris.js app using the tabris init
command, so no additional installation step is required. Simply choose TypeScript App
when given the option. After the app has been generated, type npm start
to serve the TypeScript code to your Tabris Developer App as JavaScript. In Visual Studio Code you can also use the preconfigured start
task instead.
As long as the task is still running, changes to your TypeScript code (any .ts
file in src
) will be detected by the TypeScript compiler automatically. No restart is needed.
Stay safe
In TypeScript not all APIs, not even all Tabris.js APIs, are perfectly type safe. It’s therefore recommended to follow these general guidelines:
Casting: Avoid explicit casting, as it can fail silently. Use type guards instead.
Implicit “any”: In TypeScript a value of the type any
is essentially the same as a JavaScript value. The compiler will accept any actions on this value, including assigning it to typed variables. An implicit any
may occur if
you do not give a type for a variable, field or parameter, and none can be inferred by assignment. Always give the type of function parameters. Fields and variables are safe only if they are assigned a value on declaration.
Widget event handling: Do not use widget.on(event, handler)
. Instead, use widget.on({event: handler})
.
Widget apply method: Use widget.apply
only to set properties of the base Widget
class, like layoutData
.
Selector API and WidgetCollection: By default the widget methods find
, children
, sibling
return a “mixed” WidgetCollection. This means you would have to do a type check and cast to safely retrieve a widget from the collection. However, you can also use widget classes (constructors) as selectors, which results in a collection that TypeScript “knows” to only have instances of that type. In that case no cast will be necessary. Example: widget.children(Button).first('.myButton')
returns a button (or null), but nothing else. It should be noted that the set
method of such a WidgetCollection is still not type-aware. You can use the forEach
method instead to safely set properties for all widgets in the collection.
NPM modules: The tabris
module is automatically type-safe, but the same is not true for all modules that can be installed via npm
. You may have to manually install declaration files for each installed npm module.
Interfaces
When used in TypeScript the tabris module exports the following interfaces used by the specific widget properties:
Image
Color
Font
LayoutData
Bounds
Transformation
margin
dimension
offset
BoxDimensions
ImageData
Selector
AnimationOptions
You may want to use these for your own custom UI component properties:
import {Composite, Color} from 'tabris';
class MyCustomButton extends Composite {
/* ... constructor, etc ... */
public set textColor(value: Color) {
this.internalLabel.textColor = value;
}
}
The tabris module also exports the following event object types:
-
EventObject<T>
class, whereT
is the type of thetarget
event property. Used for events that have no type-specific properties and also the basis for all other event interfaces. -
PropertyChangedEvent<T, U>
interface, whereT
is the type of thetarget
property andU
is the type of thevalue
property. Used for all events matching the naming scheme{propertyName}Changed
, e.g.BackgroundChanged
. -
Target-specific events following the naming scheme
{TargetType}{EventName}Event
, for examplePickerSelectEvent
.
These can be used to define listeners outside as class members:
import {Composite, Picker, PickerSelectEvent} from 'tabris';
class MyCustomForm extends Composite {
constructor() {
super();
/* .... */
new Picker()
.appendTo(this)
.on({select: this.handlePickerSelect});
}
private handlePickerSelect(ev: PickerSelectEvent) {
/* .... */
}
}
You can also directly create instances of EventObject
(since it’s a class, not an interface) and use them to trigger events that have no type-specific properties, and you can use it as a base for event objects that have additional properties, such as change events.
Interfaces relating to set
, get
and the constructor properties
parameters:
-
{TargetType}Properties
interface, e.g.CompositeProperties
Properties<T extends NativeObject>
Partial<T extends NativeObject, U extends keyof T>
These interfaces can be used to extend the properties accepted by the set
and get
methods, as well as those supported by the constructor of your own class. Let’s look at a simple constructor first:
import {Composite, CompositeProperties} from 'tabris';
class MyCustomForm extends Composite {
constructor(properties?: CompositeProperties) {
super(properties);
/* .... */
}
}
This just makes your class accept all properties of the class it extends (Composite
). For every built-in widget, there is a matching properties interface (CompositeProperties
in this case) that can be used in this way.
The special interface Properties
provides a generic way to reference the properties interface belonging to a widget. Thus, the above can also be written as:
import {Composite, Properties} from 'tabris';
class MyCustomForm extends Composite {
constructor(properties?: Properties<MyCustomForm>) {
super(properties);
/* .... */
}
}
In order to make set
, get
and the constructor (if declared as above) accept the properties added in your class, the properties interface of your class must be extended. This can be done by overriding a special property called tsProperties
and extending its type. For this we are using the properties interface for the super class and the Tabris’ version of the Partial
interface. For example, let’s add properties foo
and bar
to a custom component:
import {Composite, Partial, Properties} from 'tabris';
class MyCustomForm extends Composite {
public tsProperties: Properties<Composite> & Partial<this, 'foo' | 'bar'>;
// initializing plain properties is a must for "super" and "set" to work as a expected.
public foo: string = null;
public bar: number = null;
constructor(properties?: Properties<MyCustomForm>) {
super(properties);
}
}
Instead of
Properties<Composite>
you can also useCompositeProperties
in this example. Do NOT useProperties<MyCustomForm>
to definetsProperties
(it creates a circular reference).
If you add a property to your class, but not to
tsProperties
, you can still access it directly (i.e.instance.foo
), but not inset
,get
or the constructor. Also, you can of course extend (or completely exchange) the interface used by the constructor, for example to define constructor arguments that are not settable public properties. An example for this can be found in this snippet
How does this Work? The generic
Properties<T>
interface references the type oftsProperties
onT
. Theset
andget
methods use the interfaceProperties<this>
, thereby always referencingtsProperties
. (The actual value of thetsProperties
property is not relevant to this mechanism - it is alwaysundefined
.) In the above examplestsProperties
is overwritten using&
to extend the properties interface of the super class with selected properties of your own class. In thePartial<T, U>
interfaceU
is a TypeScript string union type that is used to filter the properties ofT
. Both of these “special” interfaces use a TypeScript technique known as “mapped types”.
Finally, there are the {TargetType}Events
interfaces, e.g. CompositeEvents
. These are used by the on
and off
methods. You may want to extend them to define your own on
/off
methods when extending widget classes.
JSX
JSX is an extension to the JavaScript/TypeScript syntax that allows mixing code with XML-like declarations. Tabris 2 supports type-safe JSX out of the box with any TypeScript based project. All you have to do is name your files .tsx
instead of .ts
. You can then use JSX expressions to create widgets. For example…
ui.contentView.append(
<composite left={0} top={0} right={0} bottom={0}>
<button centerX={0} top={100} text='Show Message' onSelect={handleButtonSelect}/>
<textView centerX={0} top='prev() 50' font='24px'/>
</composite>
);
…is the same as…
ui.contentView.append(
new Composite({left: 0, top: 0, right: 0, bottom: 0}).append(
new Button({centerX: 0, top: 100, text: 'Show Message'}).on({select: handleButtonSelect}),
new TextView({centerX: 0, top: 'prev() 50', font: '24px'})
)
);
JSX in Tabris.js TypeScript apps follows these specific rules:
- Every JSX element is a constructor call. If nested directly in code, they need to be separated from each other (see below).
- Element names starting lowercase are intrinsic elements. These include all instantiable widget build into Tabris.js, as well as
WidgetCollection
. The types of these elements don’t need to be explicitly imported. - Element names starting with uppercase are user defined elements, i.e. any class extending a Tabris.js widget. These do need to be imported.
- Attributes can be either strings (using single or double quotation marks) or JavaScript/TypeScript expressions (using curly braces).
- An attribute of the same name as a property is used to set that property via constructor.
- An attribute that follows the naming scheme
on{EventType}
is used to register a listener with that event. - Each element may have any number of child elements (if that type supports children), all of which are appended to their parent in the given order. An element that has a
text
attribute may also use plain text a child element, e.g.<textView>Hello</textView>
. A child can also be a JavaScript expression wrapped in{}
, just like attributes can be. The expression may result in an instance ofWidgetCollection
or an array of widgets. - While the JSX expressions themselves are type-safe, their return type is not (it’s
any
), so follow the instructions for casting above. It can be considered safe to use unchecked JSX expressions withinwidget.append()
, as all JSX elements are appendable types.
Note that this is not valid:
ui.contentView.append(
<button centerX={0} top={100} text='Show Message'/>
<textView centerX={0} top='prev() 50' font='24px'/>
);
JSX elements that are nested directly in code must be separated like any expression, in this case by a comma:
ui.contentView.append(
<button centerX={0} top={100} text='Show Message'/>,
<textView centerX={0} top='prev() 50' font='24px'/>
);
To avoid this, you may wrap your widgets in a WidgetCollection
. This example has the same effect as the previous:
ui.contentView.append(
<widgetCollection>
<button centerX={0} top={100} text='Show Message'/>
<textView centerX={0} top='prev() 50' font='24px'/>
</widgetCollection>
);
To support your own attributes on a user defined element, add a field on your custom widget called jsxProperties
. The type of the field defines what attributes are accepted. (The assigned value is irrelevant.) It should be an interface that includes some or all properties supported by the object. It can also include fields for listeners following the naming scheme on{EventType}
. To support all JSX attributes of the super class as well, extend the corresponding interface exported by the JSX namespace.
An example with a new property foo
and a matching change event would look like this:
import {Composite, CompositeProperties, EventObject, ui} from 'tabris';
type MyViewProperties = { foo?: string; };
class MyView extends Composite implements MyViewProperties {
private jsxProperties: JSX.CompositeProperties & MyViewProperties & {
onFooChanged?: (ev: EventObject<MyView> & {value: string}) => void;
};
private _foo: string = '';
constructor(properties: CompositeProperties & MyViewProperties) {
super(properties);
}
public set foo(value: string) {
if (this._foo !== value) {
this._foo = value;
this.trigger('fooChanged', Object.assign(new EventObject(), {value}));
}
}
public get foo() {
return this._foo;
}
}
The result can be used like this:
ui.contentView.append(
<MyView foo='Hello' onFooChanged={({value}) => console.log(value)} />
);
Another kind of supported user defined element are functions that return a WidgetCollection. As per convention their name has to start with an uppercase letter. The function is called with two arguments: The element’s attributes as an object and its children (if any) as an array. An example of this feature would be to call a given widget factory a given number of times:
function Repeat(properties: {times: number}, [callback]: [() => Widget]): WidgetCollection<Widget> {
let result = [];
for (let i = 0; i < properties.times; i++) {
result[i] = callback();
}
return new WidgetCollection(result);
}
It can then be used like a regular element:
ui.contentView.append(
<Repeat times={10}>{() => <textView top='prev() 10'>Hello Again!</textView>}</Repeat>
)
Note that this example assumes that the element has exactly one child (the callback
function), but the type and number of children are not checked at compile time. (The attributes are.) It would therefore be a good idea to check the type of the children at runtime.
JSX without TypeScript
If you want to use JSX without writing TypeScript, you can still use the TypeScript compiler to convert your .jsx
files to .js
. Simply generate a TypeScript app and add an entry "allowJs": true
in the compilerOptions
object of tsconfig.json
. Then change the filenames in the include
object from .ts
and .tsx
to .js
and .jsx
. You may also have to adjust your linter setup, if you use any.