RenderJS Home RenderJS

    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,

    > git clone https://lab.nexedi.com/nexedi/renderjs.git
    > npm install
    > grunt server
    

    or just download the files directly:

    The following files might also be useful:

    Hello World

    Create an HTML and JavaScript file, with the following contents:

    <!-- gadget_hello.html -->
    <!doctype html>
    <html>
      <head>
        <title>Hello World Gadget</title>
        <script type="text/javascript" src="rsvp.js"></script>
        <script type="text/javascript" src="renderjs.js"></script>
        <script type="text/javascript" src="gadget_hello.js"></script>
      </head>
      <body>
      </body>
    </html>
    
    /* gadget_hello.js */
    /*jslint nomen: true, indent: 2, maxerr: 3 */
    /*global window, rJS, RSVP */
    (function (window, rJS, RSVP) {
      "use strict";
    
      rJS(window)
    
        .declareMethod("render", function () {
          this.element.textContent = "Hello, world!";
        });
    
    }(window, rJS, RSVP));
    

    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.

    Do what? Do this! Explanation
    Declare Gadget (HTML)
    <div data-gadget-url="gadget_example.html"
      data-gadget-scope="example"
      data-gadget-sandbox="public">
    </div>
    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>.
    Declare Gadget (JS)
    [rJS].declareGadget("gadget_example.html", {
      "scope": "example",
      "sandbox": "public",
      "element": this.element.querySelector(".gadget-div")
    });
    [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>.
    Get Declared Gadget
    [rJS].getDeclaredGadget("scope");
    [returns Promise] Retrieve.a previously declared gadget by its scope name. .
    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.
    Change State Callback
    [rJS].onStateChange(function (modification_dict) {
      if (modification_dict.hasOwnProperty("key") {
        // do something
      }
    })
    [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.
    Ready Handler
    [rJS].ready(function () {
    // all dependencies loaded, initialise automatically
    })
    The ready handler is triggered automatically when all gadget dependencies have loaded.
    Render Handler
    [rJS].declareMethod("render", function (param_dict) {
    // initialise manually
    })
    [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.
    Declare Method
    [rJS].declareMethod("methodName", function (param_dict) {
      // method code
    })
    [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.
    Declare Job
    [rJS].declareJob(function (param_dict) {
      // method code
    })
    [returns Promise]. Jobs manually trigger by being called, like an ordinary RenderJS method. However, calling a job cancels the last call of the job if it hasn't finished.
    Bind Event
    [rJS].onEvent("submit", function (event) {
      // method code
    }, false, true)
    [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.
    Publish Method
    [rJS].allowPublicAcquisition("methodName", function (param) {
      // method code
    })
    Publish a method to allow children to acquire it. Only methods passed into allowPublicAcquisition in a parent gadget can be acquired using declareAcquiredMethod in a child gadget.
    Acquire Method
    [rJS].declareAcquiredMethod("methodName", "methodName")
    Acquire a method from a parent gadget, by passing the name of the published method as the first parameter and the name to call it locally as the second parameter.

    API - Detailed Explanation

    The following section explains the RenderJS API in depth, using two more detailed files.

    HTML Gadget

    A typical gadget looks something like this.

    <!doctype html>
    <html>
      <head>
        <title>Example Gadget</title>
        <link rel="stylesheet" href="gadget_example.css" />
        <script type="text/javascript" src="rsvp.js"></script>
        <script type="text/javascript" src="renderjs.js"></script>
        <script type="text/javascript" src="gadget_global.js"></script>
        <script type="text/javascript" src="gadget_example.js"></script>
      </head>
      <body>
        <h1>Example Title</h1>
        <section></section>
        <div data-gadget-url="gadget_child.html"
             data-gadget-scope="child_gadget"
             data-gadget-sandbox="public">
        </div>
      </body>
    </html>
    

    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.

    /*jslint nomen: true, indent: 2, maxerr: 3 */
    /*global window, document, rJS, RSVP, loopEventListener, promiseEventListener */
    (function (window, document, rJS, RSVP, loopEventListener, promiseEventListener) {
      "use strict";
    
      /////////////////////////////
      // some variables
      /////////////////////////////
      var NOT_USED = "abc123";
    
      /////////////////////////////
      // some methods 
      /////////////////////////////
      function checkChange() {
        var gadget = this;
        return gadget.changeState({
          key: gadget.element.querySelector("input[type='text']").value
        });
      }
      
      function createForm(param) {
        var fragment = document.createDocumentFragment(),
          form = document.createElement("form"),
          input = document.createElement("input"),
          submit = document.createElement("input");
        
        form.setAttribute("name", "foo");
        input.setAttribute("type", "text);
        input.setAttribute("value", param);
        submit.setAttribute("type", "submit");
        submit.setAttribute("value="Submit");
        form.appendChild(input);
        form.appendChild(submit);
        fragment.appendChild(form);
        return fragment;
      }
    
      rJS(window)
    
    

    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.

        /////////////////////////////
        // state
        /////////////////////////////
        .setState({key: ""})
    

    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.

        /////////////////////////////
        // ready
        /////////////////////////////
        .ready(function () {
          var gadget = this;
    
          console.log("READY - dependencies loaded");
          
          return new RSVP.Queue()
            .push(function () {
              return gadget.changeState({"counter": 123});
            })
            .push(function () 
              console.log("READY - gadget configuration");
              console.log(gadget.state);
              console.log(gadget.element);
            ]);
         })
    

    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.

        /////////////////////////////
        // acquired methods (from parent gadgets)
        /////////////////////////////
        .declareAcquiredMethod("methodThisGadgetWantsFromAParent", "methodThisGadgetWantsFromAParent")
    

    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.

        /////////////////////////////
        // published methods (to child gadgets)
        /////////////////////////////
        .allowPublicAcquisition("methodThisGadgetWantsFromAParentToChildren", function (param) {
          return createForm(param);
        })
    

    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.

        /////////////////////////////
        // state change
        /////////////////////////////
        .onStateChange(function (modification_dict) {
          var gadget = this,
            input = gadget.element.querySelector("input[type='text']"");
            
          console.log("state change, modifcation = ", modification_dict);
    
          if (modification_dict.hasOwnProperty("key") {
            // input.value = this.state.key;
            input.value = modification_dict.key;
          }
        })
    

    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
        /////////////////////////////
        .declareMethod("render", function (option_dict) {
          var gadget = this;
          
          console.log("RENDER - called manually by the parent gadget");
    
    

    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.

          return new RSVP.Queue()
            
            // initialize the gadget already declared in HTML
            .push(function () {
              // only one promise in parallel execution, could be more
              return RSVP.all([
                gadget.getDeclaredGadget("child_gadget")
              ]);
            })
            .push(function (my_gadget_list) {
              return RSVP.all([
                my_gadget_list[0].render(option_dict}),
              ]);
            })
    

    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.

            // call the method needed and pass the counter
            // assume it increases the counter by 1
            .push(function () {
              return gadget.methodThisGadgetWantsFromAParent(gadget.state.counter);
            ))
    
            // call gadget internal (and published) method with the result
            .push(function (result) {
              return gadget.methodThisGadgetWantsFromAParentToChildren(result);
            })
            
            // add the form to the DOM
            .push(function (content) {
              var div = document.createElement("div");
              div.appendChild(content);
              gadget.element.appendChild(div);
              
              // change state to update input field in case a key was passed in option_dict
              return gadget.changeState({key: option_dict.value});
            })
    

    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:

        /*
        /////////////////////////////
        // published methods (to child gadgets)
        /////////////////////////////
        .allowPublicAcquistion("methodThisGadgetWantsFromAParentToChildren", function (my_parameter) {
          return my_parameter;
        }
        */
    

    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.

            // declare another gadget dynamically, using declareJob
            .push(function () {
              return gadget.deferRenderGadget();
            })
    

    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.

            .push(undefined, function (my_error) {
              console.log(my_error);
              throw my_error;
            });
        })
    

    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.

        /////////////////////////////
        // gadget event binding
        /////////////////////////////
        .onEvent("change", checkChange, false, true)
        .onEvent("input", checkChange, false, true)
    

    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.

        /////////////////////////////
        // declareJob
        /////////////////////////////
        .declareJob("deferDeclareGadget", function () {
          var gadget = this,
            element = gadget.element,
            div = document.createElement("div");
          
          element.appendChild(div);
    
          return new RSVP.Queue()
            .push(function () {
              return gadget.declareGadget("gadget_third.html", {
                scope: "third_gadget",
                element: div
              })
            })
            .push(function () {
              return gadget.deferRenderGadget();
            });
        })
    
        .declareJob("deferRenderGadget", function () {
          var gadget = this;
          return gadget.getDeclaredGadget("third_gadget")
            .push(function (my_gadget) {
              return my_gadget.render({
                "other": "parameters",
                "to": "pass"
              });
            });
        })   
    

    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.

        /////////////////////////////
        // DOM element event binding
        /////////////////////////////
        .declareService(function () {
          var gadget = this,
            props = gadget.property_dict,
            form = props.element.querySelector("form");
    
          console.log("DECLARESERVICE - content available in DOM");
    

    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:

    <div data-gadget-url="gadget_jio.html"
             data-gadget-scope="jio_gadget_localstorage"
             data-gadget-sandbox="public">
        </div>
        <div data-gadget-url="gadget_jio.html"
             data-gadget-scope="jio_gadget_webdav"
             data-gadget-sandbox="public">
        </div>
    

    The gadget itself will only be loaded once, but two instances of the gadget will be available and addressable by their respective scope.

          function callback(my_event) {
            console.log("form submit registered");
            my_event.preventDefault();
            return false;
          }
          
          // form submit binding
          if (form) {
            return loopEventListener(form, "submit", false, callback);
          
          // example showing single-use promiseEventListener
          } else {
            return new RSVP.Queue()
              .push(function () {
                var button = "<button>Single Use Button</button>";
                props.element.appendChild(button);
                return promiseEventListener(props.elemnt.querySelector("button", "click", true);
              })
              .push(function (my_event) {
                alert(my_event);
              });
          }
        });
    

    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.

    }(window, document, rJS, RSVP, loopEventListener, promiseEventListener));
    

    Close by passing in the necessary global parameters.

    This example covers everything you can do with RenderJS. The full code can be found below as well as further information and examples.

    /*jslint nomen: true, indent: 2, maxerr: 3 */
    /*global window, document, rJS, RSVP, loopEventListener, promiseEventListener */
    (function (window, document, rJS, RSVP, loopEventListener, promiseEventListener) {
      "use strict";
    
      /////////////////////////////
      // some variables
      /////////////////////////////
      var NOT_USED = "abc123";
    
      /////////////////////////////
      // some methods 
      /////////////////////////////
      function checkChange() {
        var gadget = this;
        return gadget.changeState({
          key: gadget.element.querySelector("input[type='text']").value
        });
      }
      
      function createForm(param) {
        var fragment = document.createDocumentFragment(),
          form = document.createElement("form"),
          input = document.createElement("input"),
          submit = document.createElement("input");
        
        form.setAttribute("name", "foo");
        input.setAttribute("type", "text);
        input.setAttribute("value", parameter);
        submit.setAttribute("type", "submit");
        submit.setAttribute("value", "Submit");
        form.appendChild(input);
        form.appendChild(submit);
        fragment.appendChild(form);
        return fragment;
      }
    
      rJS(window)
        
        /////////////////////////////
        // state
        /////////////////////////////
        .setState({key: ""})
    
        /////////////////////////////
        // ready
        /////////////////////////////
        .ready(function () {
          var gadget = this;
    
          console.log("READY - dependencies loaded");
          
          return new RSVP.Queue()
            .push(function () {
              return gadget.changeState({"counter": 123});
            })
            .push(function () 
              console.log("READY - gadget configuration");
              console.log(gadget.state);
              console.log(gadget.element);
            ]);
         })
    
        /////////////////////////////
        // acquired methods (from parent gadgets)
        /////////////////////////////
        .declareAcquiredMethod("methodThisGadgetWantsFromAParentToChildren", "methodThisGadgetWantsFromAParent")
    
        /////////////////////////////
        // published methods (to child gadgets)
        /////////////////////////////
        .allowPublicAcquistion("methodThisGadgetWantsFromAParentToChildren", function (param) {
          return createForm(param);
        })
        
        /////////////////////////////
        // state change
        /////////////////////////////
        .onStateChange(function (modification_dict) {
          var gadget = this,
            input = gadget.element.querySelector("input[type='text']");
            
          console.log("state change, modification = ", modification_dict);
    
          if (modification_dict.hasOwnProperty("key") {
            // input.value = this.state.key;
            input.value = modification_dict.key;
          }
        })
        
        /////////////////////////////
        // declared methods
        /////////////////////////////
        .declareMethod("render", function (option_dict) {
          var gadget = this;
    
          console.log("RENDER - called manually by the parent gadget");
    
          return new RSVP.Queue()
            
            // initialize the gadget already declared in HTML
            .push(function () {
              // only one promise in parallel execution, could be more
              return RSVP.all([
                gadget.getDeclaredGadget("child_gadget")
              ]);
            })
            .push(function (my_gadget_list) {
              return RSVP.all([
                my_gadget_list[0].render(option_dict}),
              ]);
            })
    
            // call the method needed and pass the counter
            // assume it increase the counter by 1
            .push(function () {
              return gadget.methodThisGadgetWantsFromAParent(gadget.state.counter);
            ))
    
            // call gadget internal (and published) method with the result
            .push(function (result) {
              return gadget.methodThisGadgetWantsFromAParentToChildren(result);
            })
            
            // add the form to the DOM
            .push(function (content) {
              var div = document.createElement("div");
              div.appendChild(content);
              gadget.element.appendChild(div);
              
              // change state to update input field in case a value was passed in option_dict
              return gadget.changeState({key: option_dict.value});
            })
            
            // declare another gadget dynamically, using declareJob
            .push(function () {
              return gadget.deferRenderGadget();
            })
            
            // capture errors
            .push(undefined, function (my_error) {
              console.log(my_error);
              throw my_error;
            });
        })
    
        /////////////////////////////
        // gadget event binding
        /////////////////////////////
        .onEvent("change", checkChange, false, true)
        .onEvent("input", checkChange, false, true)
    
        /////////////////////////////
        // declareJob
        /////////////////////////////
        .declareJob("deferDeclareGadget", function () {
          var gadget = this,
            element = gadget.element,
            div = document.createElement("div");
          
          element.appendChild(div);
    
          return new RSVP.Queue()
            .push(function () {
              return gadget.declareGadget("gadget_third.html", {
                scope: "third_gadget",
                element: div
              })
            })
            .push(function () {
              return gadget.deferRenderGadget();
            });
        })
    
        .declareJob("deferRenderGadget", function () {
          var gadget = this;
          return gadget.getDeclaredGadget("third_gadget")
            .push(function (my_gadget) {
              return my_gadget.render({
                "other": "parameters",
                "to": "pass"
              });
            });
        })            
    
        /////////////////////////////
        // DOM element event binding
        /////////////////////////////
        .declareService(function () {
          var gadget = this,
            props = gadget.property_dict,
            form = props.element.querySelector("form");
    
          console.log("DECLARESERVICE - content available in DOM");
    
          function callback(my_event) {
            console.log("form submit registered");
            my_event.preventDefault();
            return false;
          }
          
          // form submit binding
          if (form) {
            return loopEventListener(form, "submit", false, callback);
          
          // example showing single-use promiseEventListener
          } else {
            return new RSVP.Queue()
              .push(function () {
                var button = "<button>Single Use Button</button>";
                props.element.appendChild(button);
                return promiseEventListener(props.elemnt.querySelector("button", "click", true);
              })
              .push(function (my_event) {
                alert(my_event);
              });
          }
        });
    
    }(window, document, rJS, RSVP, loopEventListener, promiseEventListener));
    

    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:

    /* Bad example */
      return new RSVP.Queue()
        .push(function () {
          return gadget.changeState({"counter": 123});
        })
        .push(function () 
          console.log("READY - gadget configuration");
          console.log(gadget.state);
          console.log(gadget.element);
        ]);
        
      /* Good example */
      return gadget.changeState({"counter": 123})
        .push(function () {
          console.log("READY - gadget configuration");
          console.log(gadget.state);
          console.log(gadget.element);
        });

    allowing to write more concise and maintainable code.

    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:

          .onEvent("submit", function (event) {
            var target = event.target[0];
            if (target.getAttribute("name") === "some_form") {
              return handleThisSpecificForm(event);
            }
          }, false, true);
    

    Using this way of setting bindings on the gadget instead of the nested DOM element makes writing code much easier and bundles all bindings on the respective gadget.

    Non-Bubbling Events

    You can also use onEvent (set on the gadget wrapping <div> element) for non-bubbling events like so:

    //someEventThatDoesNotBubble
    .onEvent("invalid", function (my_event) {
      return callbackFunction();
    })
    

    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:

          return new RSVP.Queue()
            .push(function () {...})
            .push(undefined, function (my_error) {
              console.log(my_error);
              throw my_error;
            });
        })
    

    In addition RenderJS can trap service errors as well as errors resulting from malformed HTML and dependency loading. To also be able to handle these types of errors occurring outside JavaScript, add:

      .allowPublicAcquisition("reportGadgetDeclarationError", function (argument_list, scope) {
          // Do not crash the UI in case of wrongly configured gadget,
          // bad network, loading bug.
          this.state.rejected_dict[scope] = null;
          console.log(argument_list[0]);
        })
    
      .allowPublicAcquisition("reportServiceError", function (argument_list) {
          // Do not crash the UI in case of gadget service error.
          // do something, for example
          console.log(argument_list[0]);
        })
    

    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 Free Software, licensed under the terms of the GNU GPL v3 (or later). For details, please see Nexedi licensing.

    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:

    • OfficeJS - Office Productivity App Store (Chat client, task managers, various editors).