RenderJS

RenderJS is a fully promise-based JavaScript library for building single-page web applications from reusable components, called gadgets. It is developed and maintained by Nexedi and used for the responsive ERP5 interface and as a basis for applications in app stores such as OfficeJS.

What are Gadgets?

Gadgets consist of HTML, JavaScript, and CSS files. They should function standalone or along with their parent and children inside a tree of gadgets. Gadgets can be sandboxed inside an inline frame, or placed directly in the DOM. They communicate with each other by declaring methods, which their parent can access, and publishing methods, which their children can access by acquiring the published methods. At Nexedi, RenderJS is usually used in combination with jIO, which allows us to build complex applications that connect with multiple storages in a non-obtrusive, verbose and maintainable way.

Promises

RenderJS is fully asynchronous and uses an implementation based on RSVP.js. However, in order to safely cancel chains of promises, we had to create a custom version of RSVP.js, linked to further down, that does not use .then to chain promises. Instead, promise chains are written as RSVP.Queue().push(function () {...}).push(function (result) {...}).push(undefined, function (error) {...});

Another JavaScript Framework? Why use RenderJS?

Nexedi's free software products and the custom solutions developed from them must normally run for many years. As the complexity of our projects is usually very high, redevelopments to follow the current trending JavaScript framework or to replace legacy frameworks is out of our scope. Hence, we created RenderJS and jIO, two no-frills libraries that are:

sturdy, small API, easy to use once understood.

maintainable, small number of reusable components.

controllable, the full app is a single, cancellable chain of gadgets and promises.

configurable, build anything, wrap anything, promisify anything.

cross-domain, embed 3rd-party gadgets within any application

Getting Started

RenderJS is easy to set up and get working.

Source Code

The RenderJS source code is available on GitLab, with a mirror on GitHub. To build,

Open the HTML file in a web browser and it should display the Hello, world! text. You are now using RenderJS! Follow the OfficeJS Application Tutorial for a gentle introduction to application development using RenderJS and jIO, or keep on reading to dive straight into the full RenderJS API.

API - Quickguide

Below is a list of all methods provided by RenderJS, followed by a more detailed explanation in later sections.

Only data-gadget-url is required. Set data-gadget-scope to access the gadget by that scope name in JavaScript. Set data-gadget-sandbox to be public, to wrap the gadget in a <div> directly in the DOM, or or iframe, to wrap the gadget in an <iframe>.

[returns Promise]. The parameters exactly correspond to those when declaring the gadget in HTML, with the addition of element, to specify an element to wrap the gadget in, rather than the default auto-generated <div>.

Set Initial State

[rJS].setState({key: "value"})

[returns Promise]. The gadget's state should be set once when initialising the gadget. The state should contain key/value pairs, but the state is just an ordinary JavaScript object with no hard restrictions.

Change State

[rJS].changeState({"key": 123});

[returns Promise]. Change the state by passing in a new key-value pair, which only overwrites the keys provided in the changeState call, and only if the current and new values are different. All other keys remain unchanged.

[returns Promise]. Trigger fired after changeState whenever the gadget state changes. Pushing to a list or modifying an object do not count, but reassigning keys and values is a change. The modification_dict only contains all the modified state parameters.

[returns Promise]. The render handler must be called manually, usually from a parent gadget or in declareService. It is not necessary to have a render handler; however, it is good practice so that other gadgets have a standard method to manuallyinitialise a child gadget.

[returns Promise]. Declaring methods is the most common way of adding functionality to a gadget. Only declare methods which require the this context or which should be accessible by other gadgets.

Declare Service

[rJS].declareService(function (param_dict) {
// method code
})

[returns Promise]. Services automatically trigger as soon as the gadget is loaded into the DOM, and are usually used for event binding. There can be multiple declareService handlers, which all trigger simultaneously.

[returns Promise]. Create an event listener for the given event on the gadget wrapper element, usually the <div>, with the additional boolean parameters being useCapture and preventDefault, respectively. Alternatively, you can use the loopEventListener and promiseEventListener definded in gadget_global.js and explained later on.

Loop

[rJS].onLoop(function () {
// method code
}, delay)

When the gadget is displayed, loop on the callback method in a service.
A delay can be configured between each loop execution.

Note: all gadgets must explicitly declare all their dependencies, such as RSVP.js and RenderJS, because gadgets should work standalone. RenderJS prevents dependencies from being loaded multiple times.

The above gadget uses gadget_global.js, which contains common methods for promisifying event bindings and file readers. The gadget_example.js file is discussed in detail below.

In this example there is a single child gadget defined, called child_gadget. You can define as many gadgets as you like, and the scope is used to reference different child gadgets in JavaScript. If you don't need to reference a gadget, you can just omit the scope and let RenderJS assign one for internal use only.

The sandbox parameter can be set to public to wrap the gadget in a div tag directly in the DOM, or iframe to wrap the gadget in an inline frame. This is used to restrict the access of embedded third-party gadgets.

The example above includes some HTML content. This is not necessary, since usually the child gadgets add most of the content to the DOM, which is done automatically by RenderJS as the gadget is being rendered.

JavaScript Gadget

Below is a gadget using the full API, explained step-by-step. The full code is available at the end.

Every gadget usually starts with some variable declarations and methods which do not have to be published on the gadget itself, similar to private or internal parameters and methods. The method checkChange above is the callback run on detection of input events explained below. It retrieves a text input's value and triggers a state change with this value. States are explained in detail below as well. createForm assembles a HTML form element using the parameter provided.

The last part in this snippet is the RenderJS object rJS(window). It is only used internally but has to be at the start of every gadget chain. Of course, the whole gadget must always be wrapped in a closure passing the globals being accessed.

Gadgets are "state-able". The state is a dictionary of parameters, which is initialized using setState. The state above is initialized with just a single key, which is updated by the checkChange method shown in the previous snippet.

You can change the state using changeState, which only updates the parameters passed to it. State changes can be handled using onStateChange, which passes a dictionary with only the updated state parameters. Let's continue.

As soon as all the gadgets declared in the HTML have loaded in the DOM, as well as all their dependencies, ready fires. You can think of it as similar to jQuery's $(document).ready. You can have multiple ready calls, but each fires only after the previous returns, so you might as well put everything into one ready method.

Older examples of RenderJS may still use the getElement method to retrieve the gadget element and store it in the gadget state. This is now automated, and you can access the gadget wrapper element using gadget.element directly.

The above snippet shows the use of the changeState method by adding another key to the state, called counter. This will trigger the onStateChange method described further down. The ready handler ends by showing how to access a gadget's state and wrapper element.

Gadgets can access methods published by any parent gadget using declareAcquiredMethod. Imagine you have a gadget that handles access to a storage or server. Only this gadget should interact with the server directly, so it should publish its methods so that all its child gadgets can access the server through it. Method publication and acquisition encourages a clear separation of concerns, so that changing the storage would mean only having to modify the storage gadget.

Note: methods can only be acquired from gadgets higher up in the gadget tree. Let's assume the method above just increments the parameter passed by 1.

A method must be published by the parent gadget for the method to be acquired by child gadgets. Continuing with the storage example, it would be wise to place a storage gadget relatively high up in the gadget tree in order to make sure that all child gadgets who need access can actually access the storage.

As mentioned above, any change in state will trigger the onStateChange handler with the modification_dict that includes just the state parameters that have been changed. In this example, the changeState which introduced the counter key will trigger the onStateChange handler, but the method only looks for the key key, which has not changed, so it will not be in the modification_dict and the method will do nothing.

Declared methods contain all methods a gadget is using or wants to expose to other gadgets. Other methods can remain private and are placed outside of the rJS chain. A gadget should always declare a render method, which allows a parent gadget to trigger rendering of this gadget without knowing what the gadget actually does while also being able to pass in parameters through option_dict. If all gadgets have a render method, then it would be easy to access a gadget and, for example, make it expose its API. The idea is also that gadgets need not know about each other and their functionalities, if render is the common entry point all gadgets share.

In this section we start a RSVP.Queue() chain which is what most gadgets contain. It is possible to originate queues from gadget based methods directly, so the separate call to RSVP.Queue().push(function () {return first_method();})... is not necessary, because you can push() on first_method directly.

The chain above shows how to access a gadget declared in HTML. Doing it this way requires the gadget declared in the HTML file to have an explicit scope so it is possible to reference this gadget from the gadget JavaScript. As described earlier, declaring in HTML means all dependencies will have been loaded when the .ready event fires. You can also declare gadgets directly inside JavaScript. This is shown further down. Once the gadget is available, we call its render method passing in the configuration received. This is a common pattern of handing things such as configuration information from gadget to gadget.

Note that RSVP.all([...]) is only used for demonstration purpose here, as there is only a single Promise to be returned. But you could also do the same process for multiple independent gadgets in parallel by adding their respective getDeclaredGadget and render calls to the RSVP.all([...]) promise list.

Next we call the method we set to be acquired from a parent gadget calling it with one of the parameters defined on the gadget state dict. Acquired methods can be called directly on the gadget context and will always return a promise. In case the acquisition failed for some reason, an error will be thrown. In the snippet above we assume the acquired method to increase the counter passed in by 1 and returning it to the next step.

In this step, the result of the called method is used in a method this gadget publishes. If the gadget itself only exposes a method to other gadgets, the method can directly be added to the allowPublicAcquisition handler like so:

Of course, this method will then not be available on the gadget itself.

In the above example the returned counter is passed into the published method which returns an HTML string containing a form. This is added to the DOM in the next step before another changeState is triggered, updating our state value with the value passed in from the parent gadget upon initialization. Note, that this time onStateChange will actually try to do something as the modification dict will include the changed value parameter.

The next snippet will show how to declare a gadget dynamically from within JavaScript- The method deferRenderGadget was created using declareJob (shown further below). Jobs are a way to postpone something to the earliest possible moment. In previous version of renderJS this function was not available often causing many methods having to be called on declareService (imagine loading a table and having to wait for table headers to load table content). DeclareService will "prepone" the job to be run as soon as possible causing fewer interruptions in the UI.

Note that calling a job here will post it to be executed as soon as possible but NOT within this promise chain. The code will immediately jump to the next step and any errors caused from the job will not show up in the error handler of this chain. Instead, they will be thrown in the error handling set in declareService.

The last step in the chain traps any errors and throws them. In theory a single error handler in the initial gadget is enough if you can ensure a promise chain is never broken throughout an application and all event listeners are properly wrapped in promises. In this case, the error will be propagated to this top-most error handler. It is however good practice to add error handlers on important methods to ensure that errors are traceable even if a chain is broken.

There are two ways of event binding in RenderJS. You can bind to the gadget (similar to the document) or to elements directly (shown below). Binding to the gadget can be done using the onEvent handler, specifying the event to listen to, the callback declared initially and the useCapture and preventDefault parameters.

The next snippet specifies that jobs to run as fast as possible. In the above case the first job is declaring a gadget dynamically in JavaScript (Note that you can pass in an element which any HTML this gadget generates will be nested in). The job is finished by calling another job implying that this method will run as soon as the previous method has finished, in this case once the gadget and all dependencies have been loaded.

The second job retrieves the declared gadget, which is only possible once the previous job finishes and calls that gadgets render method, passing in a different set of parameters.

The next section is called declareService and handles DOM element binding. It triggers once the underlying gadgets DOM has been built (compared to ready firing once dependencies have been loaded and render being initialized through the parent gadget. Imagine a graph library requiring the available width on screen to render a graph - this cannot be done on ready or render, because the gadget is available only in memory at this time. Once the DOM is built, all declareService(s) trigger (there can be more than one, too). Another option of doing this would be declareJob of course.

Note that querySelector(All) is the preferred way of querying the gadget HTML, because it is also available while a gadget is still being rendered. Note also, that it is not allowed to use id attributes anywhere in a gadget, because you cannot prevent multiple gadgets from existing on the same page. The gadget scope parameter must instead be used to access a gadget. For example, if you want to create two storage instance from the same gadget, you can do so like this:

This section shows the principles of wrapping event bindings and form events inside promises. The gadget_global.js provides the underlying methods, called loopEventListener (can trigger multiple times) and promiseEventListener (triggers a single time). Note the loopEventListener is also used in the onEvent handler.

The loopEventListener is set on the form submit and calls the defined callback. The promiseEventListener does not have a callback, instead it just jumps to the next step in the promise chain. It is a single-use promise, so it should not be used to bind to interactive elements such as buttons.

Tips and Tricks

Skip Queues

It is not necessary to explicitly start all chains with a call to return new RSVP.Queue() as all methods available on gadget are queue-able. In the example it is done for demonstration purposes but the following is also possible:

DeclareService Not Firing

While you should use declareJob or .onEvent in favor of declareService sometimes it is still useful when manually building parts of the DOM. As explained the declareService handlers will trigger when the gadgets DOM has been placed on the page. Note that gadget DOM means refers to the full gadget, including its <div> wrapper. Just appending parts built inside the gadget will not trigger declareService

Only Use onEvent

The DOM element binding using declareService is not really necessary, as you can easily capture events bubbling up from a DOM element to the containing gadget and handle them there. For example:

Non-Bubbling Events

Ready is tricky

It is good practice to not do any gadget operations such as calling render or loading and working with sub-gadgets within ready. Do this when calling render manually. Also note you can have multiple .ready handlers but they will all fire in parallel, so on .ready handler cannot depend on the outcome of another.0

Error handling

The advantage of running an application as a single chain of promises is the ability to capture and handle errors. Imagine an application crashing for some reason, RenderJS being able to capture the error and sending an Ajax request with an error report to a log instead of an app just breaking. Basic error handling within JavaScript is already possible using the above error handler at the end of promise queues:

Tests

You can run tests after installing and building RenderJS by opening the /test/ folder.

FAQ

Q: What browsers does RenderJS support?

A: RenderJS will work on fully html5 compliant browsers. Thus, RenderJS should work well with the latest version of Chrome and Firefox. IE is a stretch and Safari as well. Run the tests to find out if your browser is supported.

Licence

RenderJS is an open-source library and is licensed under the LGPL license. More information on LGPL can be found here.

Examples

Most of the front end solutions created by Nexedi are based on RenderJS and jIO. For ideas and inspiration check out the following examples: