Electron

TIBET + Electron

wins

  • Leverage full-stack library specifically designed for desktop apps.
  • Cross-platform desktop and web applications with a single library.
  • Use interactive development tools for faster desktop app development.

concepts

It was never about the web…

A long-standing challenge in the IT industry is developing cross-platform applications; applications that run well on Windows®, Mac OS®, and Linux® platforms.

The web, while not strictly an application platform, made it possible to leverage web browsers and web development skills as one way to address the cross-platform development problem. It's not a great solution, but frankly, there haven't been many solid alternatives. Until now.

Electron, a pairing of Chromium and NodeJS, provides a compelling platform for cross-platform application development, one that major companies are already leveraging.

Slack, Microsoft Teams, Visual Studio Code, Postman, Discord, Zoom, Signal, Wordpress, Skype, WhatsApp and dozens of other applications have taken advantage of Electron to create powerful desktop versions, versions that far surpass their browser-based variants, when such variants even exist.

Now it's your turn.

The TIBET solution

Designed from day one to run offline, TIBET is unique among web platforms both in scope and in our focus on desktop application development.

TIBET's client stack includes support for file operations, state and profile management, data localization, advanced keyboard shortcuts, drag-and-drop, context menus, and numerous other features common in desktop applications but rarely supported by web frameworks.

TIBET's server components and development tools help you manage mocking of local and remote service endpoints, unit and integration testing, build/release/deploy cycles, and more.

TIBET's low-code approach not only speeds development, it also features built-in debugger integration. The result is a development process that let's you get more done with less effort.

Perhaps most importantly, TIBET is a full-stack solution that's fully-supported. Instead of struggling to pull together dozens of a-la-carte parts and tooling elements, TIBET lets you work with a fully-integrated stack and toolchain that's backed by the vendor that built it… Us.

TIBET+Electron
TIBET+Electron

cookbook

Getting Started

Getting started is easy…

Install TIBET

For Windows, Mac OS, or Linux:

$ npm install -g tibet
$ tibet init

For Docker-based development see the full install guide.

Creating A TIBET+Electron 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 Electron 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+Electron 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+Electron 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+Electron 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 your Electron project.

Linting Your TIBET+Electron Application

TIBET's built-in JavaScript, CSS, and XML/XHTML linting tools work as expected with Electron 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 TIBET+Electron Application

Your TIBET+Electron 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+Electron 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 Electron 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 Electron TIBET applications, see Deploying via electron-builder

Automatic Profile Load and Save

TIBET in Electron 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 Electron 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 Electron 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);

code

The TIBET library dna includes a template specifically designed for creating Electron-based applications.

The TIBET Lama is currently available in a Technology Preview form and is fully compatible with running in an Electron environment.