Practice creating your custom Tabris.js component
Tabris.js offers a number of built-in components, which allow developers to construct the UI for their native mobile applications. In some cases, you may feel the need for a reusable, custom look and feel component such as the following button with several states:
The custom button above has four states: action
, progress
, success
, and error
. Each state represents a different view with relevant styles. When we update the state of the button, relevant views are moved up or down with an animation.
There are several ways to create custom components, but one of the easiest methods is to combine simple built-in widgets into the container you prefer. First, let’s start with the following snippet that shows how the suggested class structure of a custom component looks like in general:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { Composite, EventObject, Listeners, Properties } from 'tabris'; import { event, property } from 'tabris-decorators'; export default class CustomButton extends Composite { @event onBaz: Listeners<EventObject<this>>; @property foo: string; @property bar: string; constructor(properties: Properties<CustomButton>) { super(properties); this.createUi(); } private createUi() { this.append(/* Create Relevant Widgets */); } } |
The CustomButton
class above has the following characteristics:
- Extends from
Composite
Container. - Defines events by using
@event
decorator oftabris-decorators
. - Defines properties by using
@property
decorator oftabris-decorators
. - Accepts property values in the constructor as well.
- Creates relevant widgets in the constructor.
The structure is a recommendation only, and you are welcome to update it for your own use case.
Now it is time to improve the above class in order to get the operational custom button with different states:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
import { Color, Composite, EventObject, Listeners, Properties } from 'tabris'; import { component, event, getById, property } from 'tabris-decorators'; import { StateView } from './StateView'; const BUTTON_HEIGHT = 56; @component export class CustomButton extends Composite { @event onSelect: Listeners<EventObject<this>>; @property actionText: string; @property progressText: string; @property successText: string; @property errorText: string; @getById resultView: StateView; @getById progressView: StateView; @getById actionView: StateView; private _state: StateType; constructor(properties: Properties<CustomButton>) { super({ height: BUTTON_HEIGHT, cornerRadius: BUTTON_HEIGHT / 2, ...properties }); this.createUi(); this.state = 'action'; } public set state(value: StateType) { this._state = value; this.updateStateViews(); } public get state(): StateType { return this._state; } private createUi(): void { this.append( new StateView({ id: 'resultView' }), new StateView({ id: 'progressView', text: this.progressText, textColor: '#F57F17', background: '#FFFDE7', renderActivityIndicator: true }), new StateView({ id: 'actionView', text: this.actionText, textColor: Color.white, background: '#03A9F4', highlightOnTouch: true }).onTap(() => this.onSelect.trigger()) ); } private updateStateViews(): void { switch (this.state) { case 'action': this.applyActionState(); break; case 'progress': this.applyProgressState(); break; case 'success': this.applyResultState('success'); break; case 'error': this.applyResultState('error'); break; } } private async applyActionState(): Promise<void> { await this.animateStateView(this.actionView, 0); this.animateStateView(this.progressView, -BUTTON_HEIGHT); this.animateStateView(this.resultView, - BUTTON_HEIGHT); } private applyProgressState(): void { this.animateStateView(this.actionView, BUTTON_HEIGHT); this.animateStateView(this.progressView, 0); this.animateStateView(this.resultView, -BUTTON_HEIGHT); } private applyResultState(result: 'success' | 'error'): void { this.animateStateView(this.actionView, BUTTON_HEIGHT); this.animateStateView(this.progressView, BUTTON_HEIGHT); this.animateStateView(this.resultView, 0); this.resultView.set({ text: result === 'success' ? this.successText : this.errorText, background: result === 'success' ? '#E8F5E9' : '#FFEBEE', textColor: result === 'success' ? '#4CAF50' : '#F44336' }); } private async animateStateView(view: StateView, translationY: number): Promise<void> { return view.animate( { transform: { translationY } }, { easing: 'linear', duration: 150 } ).catch(error => console.error(error)); } } type StateType = 'action' | 'progress' | 'success' | 'error'; |
As you can see, the updated class uses the additional features and sources, which we will briefly mention below:
- The updated class contains the
@component
decorator at the top of the class declaration in order to use the@getById
for direct child access. - Defines
state
accessor property that executes the functionupdateStateViews
when we set a value on it:
12345678910private _state: StateType;public set state(value: StateType) {this._state = value;this.updateStateViews();}public get state(): StateType {return this._state;}
This is a different approach than@property
and is useful when you are trying to perform some operations during a variable set or get. - Passes some property updates to the
super
call, which is not expected to be updated anymore. - Uses the additional custom component
StateView
in UI creation. It is convenient to create a custom component (view) and reuse it for repeating groups of views. Here is the source of the customStateView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import { ActivityIndicator, ColorValue, Composite, Font, LayoutData, Properties, TextView, Widget } from 'tabris'; import { bind, component, property } from 'tabris-decorators'; const CONTENT_FONT = Font.from({ size: 18, weight: 'medium' }); @component export class StateView extends Composite { @property @bind('TextView.text') text: string; @property @bind('TextView.textColor') textColor: ColorValue; @property({ default: false }) renderActivityIndicator: boolean; constructor(properties: Properties<StateView>) { super({ layoutData: LayoutData.stretch, ...properties }); this.append(...this.createComponents()); } private createComponents(): Widget[] { if (this.renderActivityIndicator) { return [ new TextView({ top: 16, bottom: 16, centerX: 0, font: CONTENT_FONT }), new ActivityIndicator({ left: [LayoutData.prev, 16], width: 24, height: 24, centerY: 2, tintColor: this.textColor }) ]; } return [ new TextView({ left: 16, top: 16, right: 16, bottom: 16, alignment: 'centerX', font: CONTENT_FONT }) ]; } } |
The only unmentioned feature is the @bind
decorator in the StateView
component, which creates two-way bindings between relevant widgets.
Now, we can use the custom button and update its state as shown in the following sample snippet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import { contentView } from 'tabris'; import { CustomButton } from './CustomButton'; export class App { constructor() { contentView.background = '#E8EAF6'; } public start(): void { contentView.append( new CustomButton({ left: 16, right: 16, centerY: 0, actionText: 'Active State', progressText: 'Progress State', successText: 'Success State', errorText: 'Error State' }).onSelect(async ({ target }) => this.handleButtonSelect(target).catch(error => console.error(error))) ); } private async handleButtonSelect(button: CustomButton): Promise<void> { button.state = 'progress'; await new Promise(resolve => setTimeout(resolve, 2000)); button.state = 'success'; await new Promise(resolve => setTimeout(resolve, 2000)); button.state = 'action'; await new Promise(resolve => setTimeout(resolve, 2000)); button.state = 'progress'; await new Promise(resolve => setTimeout(resolve, 2000)); button.state = 'error'; } } |
That is all. Hopefully, this blog post encourages more people to create their own custom components. If you have a question or recommendation, feel free to leave a comment below.
You can download the complete project from GitHub.
Feedback is welcome!
Want to join the discussion?Feel free to contribute!