Talk to your Tabris App
If you’re a seasoned Tabris developer you’re likely familiar with the helpful shortcuts built into the Tabris CLI. After all, every time a Tabris app is side-loaded you get this message:
What you may not know is that you can do a lot more with the CLI if you enable interactive mode. To do so, simply add the --interactive
(or -i
) option to the tabris serve
command. Assuming you use npm start
to sideload your app, the package.json
script section may look like this:
1 2 3 4 5 |
//... "scripts": { "start": "tabris serve -w -a -i", //... } |
Now, once connected to a device, you can use the CLI like a regular JavaScript console, like with the node
REPL or a browser’s developer tools.
But what can you do with it? All text entered into the console will be evaluated as JavaScript expressions in the context of the global scope of your running Tabris application. Note that this feature requires a constant stable connection to the device. If the prompt becomes unresponsive, try restarting the application and/or the CLI.
There are a few useful objects and functions you can access immediately: console
(obviously), tabris
, require()
, $()
, localStorage
, secureStorage
, cordova
, navigator
and all namespaces contributed by UMD modules. If you want to see exactly what is available, type Reflect.ownKeys(global).join()
.
console
For simple expressions you will not need the console
since the return value is printed out anyway. You can use the dirxml
method to print out a summary of the UI state or localStorage, but that’s the same as using the CTRL + P
shortcut. However, if your command results in a promise you may pass it the console.log
function to see the result. See “Asynchronous commands” below.
tabris
Via the
tabris
object you have access to all the exports of the 'tabris'
module, for example contentView
, drawer
, app
, device
and fs
. It will be used in several examples below.
require
The require
function allows you to obtain the exports of any module in your application. However, this only works if you do not use a packager like Webpack. Also, if you use TypeScript consider that the runtime location of a module is in the dist/
directory, not src/
. If you want to see what exactly is available, here is a line to view the internal module registry of Tabris:
1 |
tabris.Module.root._cache |
Keep in mind that this is not an official API and may not work in future versions.
$
The $()
function gets you access to arbirary widget objects that are currently in your UI tree. Given a selector string it searches contentView for matches. So if you have a custom component called MainView
attached to contentView
, and you want to inspect its model
property, you could do it like this:
However, keep in mind that $(selector)
respects component encapsulation and it may therefore not be possible to match every widget in contentView
. It also doesn’t search the drawer
, and in some cases a widget may not have an id that makes it easy to match just the desired instance. In all of those cases print out the UI tree and use the displayed cid
to obtain the instance. Do not put the cid
in quotes, it’s a number and not a string.
localStorage and secureStorage
The CLI shortcuts already allow you to print, save, restore and clear the entire content of localStorage
. However, with direct access in the console you can be more precise and read, set or delete individual keys as you would in code.
cordova and navigator
Tabris apps always include the runtime parts of cordova responsible for loading cordova/phonegap compatible plug-ins. Some of these plug-ins publish API on either the navigator
and/or cordova
global objects. You can also see what plug-ins are loaded by typing:
1 |
cordova.require('cordova/plugin_list') |
Publishing other objects
Likely, your application’s most interesting objects aren’t easily acessible via the interactive console. After all, the entire point of a module system (such as commonJS used by tabris) is that you don’t have tons of variables floating around in the global scope where they can easily clash. But for debugging purposes you may want to put some of your key values in global scope temporarily. Let’s say you have an object that provides access to data that is used across your application, and the state of which may be useful to inspect via the CLI. Maybe it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
export class AppState { constructor() { this.people = [ {firstName: 'Joe', lastName: 'Smith'}, {firstName: 'Sam', lastName: 'Miller'} ]; } } export const appState = new AppState(); |
We want to put this object in global scope, but only during development. A good way to do this is to check if the developer toolbar is visible:
1 2 3 4 5 6 7 8 9 |
import {devTools} from 'tabris'; //... export const appState = new AppState(); if (devTools.isUiVisible()) { // @ts-ignore global.appState = appState; } |
Now you can check the state of people
at runtime:
It can be a cleaner alternative to putting “console.log()” commands all around your code.
Custom commands
There is even more you can do with this approach. Let’s say you have a longer command you regularily use:
1 |
localStorage.getItem('tabris-js-app:recent-urls') |
Of course you can take advantage of the built-in history feature of the CLI using the up and down arrow keys to bring back previously entered commands, but if it doesn’t happen to be within the last few, that’s not very convinient either. Instead you could add this piece of code somewhere in your app:
1 2 3 4 5 6 |
if (devTools.isUiVisible()) { // @ts-ignore Object.defineProperty(global, 'urls', { get: () => localStorage.getItem('tabris-js-app:recent-urls') }); } |
Now entering urls
returns exactly the same value as typing the entire line.
Asynchronous commands
The interactive console does not support the await
keyword. Therefore you need to deal with asynchroneous APIs (those returning promises) directly. For example, listing files in the on-device application directory could look like this:
1 |
tabris.fs.readDir(tabris.fs.filesDir).then(console.log) |
This is rather long, yet the output is not very pretty. Therefore this is a good candidate for a custom command:
1 2 3 4 5 6 7 8 9 10 |
if (devTools.isUiVisible()) { // @ts-ignore Object.defineProperty(global, 'files', { get: () => { const path = fs.filesDir; fs.readDir(path).then(files => console.log(files.join('\n'))); return path + ':'; } }); } |
Result:
Bonus: Listing all application files
This custom command will list all application files recursively, including the file size:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
if (devTools.isUiVisible()) { // @ts-ignore Object.defineProperty(global, 'files', { get: () => { const path = tabris.fs.filesDir; void listDir(path); return path + ':'; } }); async function listDir(path) { (await fs.readDir(path)).forEach(async fileOrDir => { const entry = path + '/' + fileOrDir; if (fs.isDir(entry)) { await listDir(entry); } else { console.log(`${entry} (${(await (fs.readFile(entry))).byteLength} bytes)`); } }); } } |
Feedback is welcome!
Want to join the discussion?Feel free to contribute!
Leave a Reply
Want to join the discussion?Feel free to contribute!