Cookie Consent by FreePrivacyPolicy.com TIBET
TIBET Logo

TIBET

Framework

Desktop Harness (Cookbook)


Getting Started

Development

Main and Renderer

Framing

Dialogs & Notifications

File Operations

Getting Started

Creating A TIBET Desktop Project

The tibet clone command is used to create all new TIBET projects.

For an Electron project add --dna electron to your clone command so your new project is based on our pre-built Electron 'dna':

tibet clone {{appname}} --dna electron
cd {{appname}}
tibet init

Running Your App

The tibet start command can be used to start any TIBET project, regardless of the project dna.

Navigate to your project directory and enter tibet start:

$ cd {{appname}}
$ tibet start
...

NOTE: tibet start actually delegates to tibet electron however we recommend you use tibet start for consistency across your TIBET-based projects.

NOTE: TIBET Desktop projects include package.json entries to ensure tibet start is set as the start script. This ensures your packaged applications operate correctly out-of-the-box.

Development

Basic TIBET Desktop Development

TIBET development for Electron-based projects is consistent with TIBET development on other supported platforms. This guide covers a few things unique to Electron (such as main vs. renderer processes) however the rest of your development process is the same.

To get a general feel for TIBET we recommend you spend a few minutes with the TIBET Quickstart and TIBET Essentials guides.

Each of our guides work with TIBET Desktop projects. Simply add --dna electron to the tibet clone command you use to create your hello project.

All of TIBET's command line tools (init, start, lint, test, build, release, deploy, version, etc.) work with TIBET Desktop.

Linting Your Application

TIBET's built-in JavaScript, CSS, and XML/XHTML linting tools work as expected with Desktop projects, ensuring all your "source" is lint-free.

Within your project directory tree enter tibet lint:

$ tibet lint

checked 0 of 22 total files
(8 filtered, 14 unchanged)
0 errors, 0 warnings. Clean!

Lint checks keep track of file changes so that only recently changed files are tested. You can also focus on just your JavaScript, CSS, or markup if you like.

See the tibet lint manpage for more details.

Testing Your Application

Your project is testable using the built-in tibet test command:

$ tibet test
# Loading TIBET platform at 2019-08-02T01:53:29.910Z
# TIBET reflection suite loaded and active in 6469ms
# TIBET starting test run
# 2 suite(s) found.
1..3
#
# tibet test APP --suite='APP'
ok - Has a namespace.
ok - Has an application type.
# pass: 2 total, 2 pass, 0 fail, 0 error, 0 skip, 0 todo, 0 only.
#
# tibet test APP.hello.app.Type --suite='APP.hello:app'
ok - Is a templated tag.
# pass: 1 total, 1 pass, 0 fail, 0 error, 0 skip, 0 todo, 0 only.
#
# PASS: 3 total, 3 pass, 0 fail, 0 error, 0 skip, 0 todo, 0 only.

TIBET's test harness launches your application in a headless fashion, loads any tests it finds, and executes them. This approach can be used effectively to test your rendering process. It can also be used to integration test your main process logic.

NOTE: Unit testing the main process is also possible, however it requires a little more effort. Stay tuned for more on this topic.

See the tibet test manpage and our TIBET Testing documentation for details on how to fully test your TIBET applications.

Main & Renderer

Adding Logic To The Main Process

TIBET Deskto projects use an electron.js file which loads your TIBET application along with optional plugins.

You can add logic to the Electron main process via this plugin mechanism - a dynamically configurable set of modules that TIBET loads into Electron when the application starts.

An excellent example can be found in the preload.js module supplied with every TIBET Desktop project in the plugins directory.

Let's modify a copy of that module to see how the process works.

  1. cd into the plugins directory and copy preload.js to tester.js.

  2. Edit tester.js and empty the body of the main function from the Requires block to the end of the function.

  3. Insert the following code:

logger.warn('This is only a warning');

  1. Edit the tibet.json file and add tester to the Array of entries under electron.plugins.core.

  2. Start your application:

tibet start

Describing the details of functionality available to you in the 'main process' (aka the "server") is best left to the Electron Documentation.

Adding Logic To The Renderer Process

To add logic to the renderer process follow the same process you'd use to add functionality to any TIBET client process. In other words, follow the same approach outlined in the TIBET Quickstart and TIBET Essentials guides.

Communicating Between Main and Renderer

TIBET makes it easy to communicate between Electron's two primary processes by leveraging the same communication model it uses for everything else - signaling.

See TIBET Signaling for details on TIBET signaling.

Signaling From Renderer To Main

To communicate with the main process from a TIBET application running in an Electron renderer process use the following outline.

In the renderer process you'll want to use signalMain on the TP.electron.ElectronMain type:

TP.electron.ElectronMain.signalMain('TP.sig.SampleSignal', {"foo":"bar"});

In the Main process you set up a handler for that signal:

ipcMain.handle('TP.sig.SampleSignal',
    function(event, payload) {
        //  event contains native Electron Event object.
        //  payload contains a POJO: {"foo":"bar"}
    });

You can return a Promise from the main process using the following pattern.

In the renderer process:

TP.electron.ElectronMain.signalMain('TP.sig.SampleSignal2', {"foo":"bar"}).then(
function() {...});

In the main process:

ipcMain.handle('TP.sig.SampleSignal',
    function(event, payload) {
        return await dialog.showMessageBox(...);
    });

Signaling From Main To Renderer

Communicating from the Electron main process to a TIBET application running in a renderer process also uses TIBET's signaling mechanism.

In the renderer process subscribe to the signal with TP.electron.ElectronMain. This is typically done in the init instance method TIBET uses to initialize a new instance:

APP.MyApp.MyType.Inst.defineMethod('init',
function() {
    //  The TP.sig is optional here.
    this.observe(TP.electron.ElectronMain, 'TestSignal');
});

Also in the renderer process, define a handler for the target signal:

APP.MyApp.MyType.Inst.defineHandler('TestSignal',
function(aSignal) {
    //  The payload contains {"foo":"bar"} that we sent from the main process.
    APP.info('Got to the TestSignal handler: ' + aSignal.getPayload());

    return this;
});

Last, add code to the main process to send the signal:

mainWindow.webContents.send('TP.sig.TestSignal', {"foo":"bar"});

That's all there is to it.

Logging Activity In The Main Process

When you define a module to be loaded into the main process TIBET will provide tha function with an options parameter. A 'logger' object is provided in the options which allows you access to a common logging module from TIBET.

module.exports = function(options) {
    var logger,
        meta;

    logger = options.logger;
    meta = {
        type: 'plugin',
        name: 'preload'
    };
    logger.system('preloading utilities', meta);

    ...
};

TIBET's default logger object has the following methods for level-based logging:

logger.trace();
logger.debug();
logger.info();
logger.warn();
logger.error();
logger.fatal();
logger.system();

Logging Activity In The Renderer Process

To log data in the renderer process leverage TIBET's lgging functionality available as documented in TIBET Logging.

Framing

Automatic Application Versioning and Updates

For deploying automatically versioned TIBET applications, see Deploying via electron-builder

Automatic Profile Load and Save

TIBET Desktop will automatically save any data that is registered under the top-level profile key in TIBET's configuration system. Putting the following code in the Application object's AppDidStart signal handler method would cause this value to be saved:

APP.MyApp.Application.Inst.defineHandler('AppDidStart',
function(aSignal) {
    ...
    TP.sys.setcfg('profile.favoritecolor', 'red');
});

This data is written to the tibet.json file under the profile key when a 'proper shutdown' (i.e. via the quit menu item) is initiated in the TIBET Desktop app.

For now, the only data that is tracked by TIBET itself in the profile system are the user's windows size and position.

Adding A Native Menu To Your App

NOTE: This feature is currently being reworked to be more powerful and is missing from the current TIBET release. This section is representative of the functionality that will be available in a future release.

To function the tag must be rendered in a TIBET application. Usually you'll use your application's app tag to render top-level shared components such as dialogs. You'll find a project's app tag in {{project}}/src/tags/APP.{{appname}}.app/. For this example we'll add the dialog to the APP.{{appname}}.app.xhtml file.

Rendering The Tag

<electron:menu label="MyMenu">
    <electron:menuitem label="DoThing1" on:click="DoFirstThing"/>
    <electron:menu label="MySubmenu">
        <electron:menuitem label="DoSubmenu1" on:click="DoSubFirstThingFirst"/>
        <electron:menuitem label="DoSubmenu2" on:click="DoSubFirstThingSecond"/>
    </electron:menu>
    <electron:menuitem label="DoThing2" on:click="DoSecondThing"/>
</electron:menu>

Handling the menu triggers

To handle the menu triggers for the menu items, simply implement standard TIBET handlers on the Application object. You'll find a project's Application subtype in {{project}}/src/APP.{{appname}}.Application.js.

APP.MyApp.Application.Inst.defineHandler('DoFirstThing',
function(aSignal) {
    //  Handle the 'DoFirstThing' menu item here.
    return this;
});

Dialogs And Notifications

Showing a Native Dialog

Use an electron:dialog tag with no type.

<electron:dialog ... />

The electron:dialog tag gives you markup-driven access to Electron's native dialogs without the need for coding. For regular dialog boxes, we do not specify a type.

Rendering The Tag

To function the tag must be rendered in a TIBET application. Usually you'll use your application's app tag to render top-level shared components such as dialogs. You'll find a project's app tag in {{project}}/src/tags/APP.{{appname}}.app/. For this example we'll add the dialog to the APP.{{appname}}.app.xhtml file.

<electron:dialog title="Warn User">
    <label>Please don't do that</label>
    <button>OK, I won't</button>
    <button>No, I will anyway!</button>
</electron:dialog>

Activating The Dialog

To trigger the dialog to open you'll need to "activate" it. You can do this in a number of ways. The simplest is to target it with a signal. In the example below we'll add an id to the dialog so we can target it and we'll include a button to fire the activation signal we want:

<electron:dialog id="MyWarnDialog" title="Warn User">
    <label>Please don't do that</label>
    <button>OK, I won't</button>
    <button>No, I will anyway!</button>
</electron:dialog>
<button on:click="{signal: UIActivate, origin: 'MyWarnDialog'}">Warn User</button>

Getting the results

To get the results of the dialog, simply use TIBET Data Binding to bind the results to a URN data container:

<electron:dialog id="MyWarnDialog" title="Warn User" bind:out="urn:tibet:warnresponse">
    <label>Please don't do that</label>
    <button>OK, I won't</button>
    <button>No, I will anyway!</button>
</electron:dialog>
<button on:click="{signal: UIActivate, origin: 'MyWarnDialog'}">Warn User</button>

Viewing the results

Then, to see the result that was clicked (the index of the button that was clicked), bind the URN to a <textarea/>:

<textarea bind:in="urn:tibet:warnresponse"/>

To view and manipulate the results programmatically, peek inside the URN container:

response = TP.uc('urn:tibet:warnresponse').getContent();

Taking action based on response

To take an action based on the button that the user clicked, simply grab the result and switch on its value:

//  Create a Number here via the TP.nc() call
response = TP.nc(TP.uc('urn:tibet:warnresponse').getContent());
switch(response) {
    case 0:
        //  The user clicked 'OK, I won't'
    break;
    case 1:
        //  The user clicked 'No, I will anyway!'
    break;
}

Showing a Native Error Dialog

Use an electron:dialog tag with a type of error.

<electron:dialog type="error" ... />

The electron:dialog tag gives you markup-driven access to Electron's native error dialogs without the need for coding.

Rendering The Tag

To function the tag must be rendered in a TIBET application. Usually you'll use your application's app tag to render top-level shared components such as dialogs. You'll find a project's app tag in {{project}}/src/tags/APP.{{appname}}.app/. For this example we'll add the dialog to the APP.{{appname}}.app.xhtml file.

<electron:dialog type="error" title="Error Condition">
    <label>There was an error condition!</label>
</electron:dialog>

Activating The Dialog

To trigger the dialog to open you'll need to "activate" it. You can do this in a number of ways. The simplest is to target it with a signal. In the example below we'll add an id to the dialog so we can target it and we'll include a button to fire the activation signal we want:

<electron:dialog type="error" id="MyErrorDialog" title="Error Condition">
    <label>There was an error condition!</label>
</electron:dialog>
<button on:click="{signal: UIActivate, origin: 'MyErrorDialog'}">Pop an error
dialog</button>

An error dialog box has no return value, so there are no further actions we can take with it.

Showing a Native Notification

Use an electron:notification tag.

<electron:notification ... />

The electron:notification tag gives you markup-driven access to Electron's native notification system without the need for coding.

Rendering The Tag

To function the tag must be rendered in a TIBET application. Usually you'll use your application's app tag to render top-level shared components such as notifications. You'll find a project's app tag in {{project}}/src/tags/APP.{{appname}}.app/. For this example we'll add the dialog to the APP.{{appname}}.app.xhtml file.

<electron:notification title="Hi there">
    <body>Just wanted to say "Hi there"!</body>
</electron:notification>

Activating The Notification

To trigger the notification to open you'll need to "activate" it. You can do this in a number of ways. The simplest is to target it with a signal. In the example below we'll add an id to the notification so we can target it and we'll include a button to fire the activation signal we want:

<electron:notification id="MyNotification" title="Hi there">
    <body>Just wanted to say "Hi there"!</body>
</electron:notification>
<button on:click="{signal: UIActivate, origin: 'MyNotification'}">Send the user
a notification.</button>

Notifications have no return value, so there are no further actions we can take with them.

Showing the User a Dialog Before Quitting

TODO

File Operations

Opening A File By Using A Native Open Dialog

Use an electron:dialog tag with a type of open.

<electron:dialog type="open" ... />

The electron:dialog tag gives you markup-driven access to Electron's native open dialogs without the need for coding.

Rendering The Tag

To function the tag must be rendered in a TIBET application. Usually you'll use your application's app tag to render top-level shared components such as dialogs. You'll find a project's app tag in {{project}}/src/tags/APP.{{appname}}.app/. For this example we'll add the dialog to the APP.{{appname}}.app.xhtml file.

<electron:dialog type="open"/>

Activating The Dialog

To trigger the dialog to open you'll need to "activate" it. You can do this in a number of ways. The simplest is to target it with a signal. In the example below we'll add an id to the dialog so we can target it and we'll include a button to fire the activation signal we want:

<electron:dialog type="open" id="MyOpenDialog"/>
<button on:click="{signal: UIActivate, origin: 'MyOpenDialog'}">Open File</button>

Getting the results

To get the results of the open dialog, simply use TIBET Data Binding to bind the results to a URN data container:

<electron:dialog type="open" id="MyOpenDialog" bind:out="urn:tibet:openfilenames"/>
<button on:click="{signal: UIActivate, origin: 'MyOpenDialog'}">Open File</button>

Viewing the results

Then, to see the filename(s) that were selected, bind the URN to a <textarea/>:

<textarea bind:in="urn:tibet:openfilenames"/>

To view and manipulate the results programmatically, peek inside the URN container:

filenames = TP.uc('urn:tibet:openfilenames').getContent();
filenames.forEach(
    function(filename) {
        APP.info(filename);
    });

Viewing the file content

To view the file content, simply create a URL from the filenames and get their content:

filenames = TP.uc('urn:tibet:openfilenames').getContent();
filenames.forEach(
    function(fn) {
        //  Print the contents to the application log.
        APP.info(TP.uc(fn).getContent());
    });

Saving A File By Using A Native Save Dialog

Use an electron:dialog tag with a type of save.

<electron:dialog type="save" ... />

The electron:dialog tag gives you markup-driven access to Electron's native save dialogs without the need for coding.

Rendering The Tag

To function the tag must be rendered in a TIBET application. Usually you'll use your application's app tag to render top-level shared components such as dialogs. You'll find a project's app tag in {{project}}/src/tags/APP.{{appname}}.app/. For this example we'll add the dialog to the APP.{{appname}}.app.xhtml file.

<electron:dialog type="save"/>

Activating The Dialog

To trigger the dialog to open you'll need to "activate" it. You can do this in a number of ways. The simplest is to target it with a signal. In the example below we'll add an id to the dialog so we can target it and we'll include a button to fire the activation signal we want:

<electron:dialog type="save" id="MySaveDialog"/>
<button on:click="{signal: UIActivate, origin: 'MySaveDialog'}">Save File</button>

Getting the results

To get the results of the save dialog, simply use TIBET Data Binding to bind the results to a URN data container:

<electron:dialog type="save" id="MySaveDialog" bind:out="urn:tibet:savefilename"/>
<button on:click="{signal: UIActivate, origin: 'MySaveDialog'}">Save File</button>

Viewing the results

Then, to see the filename that was entered, bind the URN to a <textarea/>:

<textarea bind:in="urn:tibet:savefilename"/>

To view and manipulate the results programmatically, peek inside the URN container:

filename = TP.uc('urn:tibet:savefilename').getContent();

Saving the file content

To save the file content, simply create a URL from the filename, set its content and call save:

filename = TP.uc('urn:tibet:savefilename').getContent();
request = TP.request(TP.hc('refresh', true)));

TP.uc(filename).setContent('This is some content to save').save(request);

More…

For more Cookbook development recipes and TIBET development concepts see the documentation for the Client Stack and its individual layers.