Saving Modifications to Station

This tutorial follows from Getting
Started - MyFirstWidget
.

We're going to modify MyFirstWidget to enable a user to make changes to the
Ramp Component on the Station through your Widget.

We will also introduce Templates, Lexicons, and CSS to simplify your modular
HTML design.

Outline

+- myFirstModule
   +- myFirstModule-ux
      +- src
      |  +- com
      |  |  +- companyname
      |  |     +- myFirstModule
      |  |        +- ux
      |  |           +- BMyFirstWidget.java
      |  +- rc
      |     +- templates
      |     |  +- MyFirstWidget.hbs -------------------------------------- *NEW*
      |     +- myFirstModule.css ----------------------------------------- *NEW*
      |     +- MyFirstWidget.js --------------------------------------*MODIFIED*
      +- module.lexicon -------------------------------------------------- *NEW*
      +- module-include.xml
      +- myFirstModule-ux.gradle
  • MyFirstWidget.hbs: a template file holding the HTML for MyFirstWidget.
  • myFirstModule.css: a file holding all of the module's CSS.
  • module.lexicon: a file holding the user-facing text elements of all of the
    module's Widgets.

Templates

Since our HTML will be getting more complicated, we're going to show you a
better way to add it to your Widget. Piecing it together within the JavaScript
itself makes it harder to follow and much more difficult to edit in the future.

In the first tutorial, we had a simple HTML input DOM element that was updated
every time the out Property of a Ramp Component changed on the server. Now
we're going to add two additional form elements: a text input and check box to
view and edit a Ramp's amplitude and enabled Properties respectively. To
do this, our Widget is going to use a client side JavaScript template library
called Handlebars. This allows us to keep our HTML
in a separate template file and then 'require' it into our Widget.

MyFirstWidget.hbs

<div class='my-first-widget'>
  <div>
    <label>{{value}}: <label>
    <input class='my-first-widget-value' type='text' value='{{loading}}' readonly='readonly'/>
  </div>
  <div>
    {{changeValues}}
  </div>
  <div>
    <label>{{amplitude}}: <label>
    <input class='my-first-widget-amplitude' type='text' value='{{loading}}' />
  </div>
  <div>
    <label>{{enabled}}: <label>
    <input class='my-first-widget-enabled' type='checkbox' value='Enabled' />
  </div>
</div>

The file name does not need to match, but this convention helps easily
distinguish which templates belong to which JavaScript files.

{{variables}} within double brackets are filled in by MyFirstWidget.js.

Modifying, Reading, and Saving a Widget

We can now add some additional methods to our JavaScript Widget to handle DOM
modification, reading and saving the data back to the Server.

Workflow

Firstly, it's important to understand the workflow. When a view with a Widget
is loaded, the Widget is...

  • Initialized: The DOM for the Widget is created and attached.

  • Loaded: A value is loaded in and the widget updates the DOM accordingly.

At this point the page is loaded, and the Widget is loaded with a value and
waiting for user interaction...

  • Modified: We want the the Widget to be notified when certain parts of it
    are changed by the user. Once a Widget is marked as modified, changes are not
    saved immediately back to the station. The user may choose to save, or they will
    be asked if they want to save changes when navigating away from the view.

    • setModified(true) is called to modify the
      Widget when one of the DOM elements change.
  • Read: The doRead method is overridden. Data is
    extracted back out of the DOM. Please note, a BajaScript Ramp Component
    doesn't have to be created to capture this data. It just reads out the data
    into a plain old JavaScript object that can be validated and saved.

  • Validated: The data is validated. For this tutorial we're not using any
    validator functions but it's still important to point them out. The validator
    functions will receive the output of doRead().

  • Saved: The doSave method is overridden so the data
    can be saved back to the Server. In this case, the BajaScript Ramp Component
    will have its Properties updated.

MyFirstWidget.js - modified

/**
 * A module defining `MyFirstWidget`.
 *
 * @module nmodule/myFirstModule/rc/MyFirstWidget
 *
 * Note that certain module IDs include an exclamation point (!). The '!'
 * character indicates the use of a RequireJS plugin. The plugins used for this
 * module include:
 *
 * - BajaScript (baja!): ensures that BajaScript has fully started
 * - Handlebars (hbs!): import Handlebars templates
 * - CSS (css!): import a CSS stylesheet for this `Widget`
 */
define(['baja!', //------------------------------------------------------- *NEW*
        'bajaux/mixin/subscriberMixIn',
        'bajaux/util/SaveCommand', //------------------------------------- *NEW*
        'bajaux/Widget',
        'css!nmodule/myFirstModule/rc/myFirstModule', //------------------ *NEW*
        'hbs!nmodule/myFirstModule/rc/templates/MyFirstWidget', //-------- *NEW*
        'jquery', //------------------------------------------------------ *NEW*
        'lex!myFirstModule', //------------------------------------------- *NEW*
        'Promise'], function ( //----------------------------------------- *NEW*
        baja,
        subscriberMixIn,
        SaveCommand,
        Widget,
        css,
        template,
        $,
        lexs, //---------------------------------------------------------- *NEW*
        Promise) {

  'use strict';

  var myFirstModuleLex = lexs[0]; //-------------------------------------- *NEW*

  function getAmpDom(dom) { //-------------------------------------------- *NEW*
    return dom.find('.my-first-widget-amplitude');
  }

  function getEnabledDom(dom) { //---------------------------------------- *NEW*
    return dom.find('.my-first-widget-enabled');
  }

  function getValueDom(dom) { //------------------------------------------ *NEW*
    return dom.find('.my-first-widget-value');
  }

  /**
   * An editor for working with `kitControl:Ramp` instances.
   *
   * @class
   * @extends module:bajaux/Widget
   * @alias module:nmodule/myFirstModule/rc/MyFirstWidget
   */
  var MyFirstWidget = function () {
      Widget.apply(this, arguments);
      subscriberMixIn(this);

    // Add a Save Command to allow the user to save the `Widget`.
    this.getCommandGroup().add(new SaveCommand()); //--------------------- *NEW*
  };
  MyFirstWidget.prototype = Object.create(Widget.prototype);
  MyFirstWidget.prototype.constructor = MyFirstWidget;

  /**
   * Initialize the `MyFirstWidget`.
   *
   * Update the contents of the DOM in which the `Widget` is initialized. This
   * function uses the Handlebars template we imported to generate the HTML.
   *
   * This function also sets up jQuery event handlers. By default, handlers
   * registered on the `dom` parameter, like then ones we arm in this function,
   * will be automatically cleaned up when the `Widget` is destroyed. Any
   * additional handlers (on child elements of the `dom` parameter, say, or on
   * elements outside of this `Widget`) would need to be cleaned up in
   * `doDestroy()` in order to prevent memory leaks.
   *
   * @param {jQuery} dom - The DOM element into which to load this `Widget`
   */
  MyFirstWidget.prototype.doInitialize = function (dom) {
    var widget = this;

    // The template function returns the contents of MyFirstWidget.hbs, but with
    // variables like {{value}} filled in using the properties of the object
    // argument.
    dom.html(template({ //------------------------------------------------ *NEW*
      value: myFirstModuleLex.get('MyFirstWidget.value'),
      changeValues: myFirstModuleLex.get('MyFirstWidget.changeValues'),
      amplitude: myFirstModuleLex.get('MyFirstWidget.amplitude'),
      enabled: myFirstModuleLex.get('MyFirstWidget.enabled'),
      loading: myFirstModuleLex.get('MyFirstWidget.loading')
    }));

    // When the user makes a change, mark the `Widget` as modified using
    // setModified(true).
    dom.on('input', '.my-first-widget-amplitude', function () { //-------- *NEW*
      widget.setModified(true);
    });
    dom.on('change', '.my-first-widget-enabled', function () { //--------- *NEW*
      widget.setModified(true);
    });
  };

  /**
   * Load a `kitControl:Ramp`.
   *
   * @param {baja.Component} ramp - an instance of `kitControl:Ramp`
   */
  MyFirstWidget.prototype.doLoad = function (ramp) {
    var widget = this,
        dom = widget.jq(),
        valueDom = getValueDom(dom),
        ampDom = getAmpDom(dom),
        enabledDom = getEnabledDom(dom);

    // Update the DOM to reflect the Ramp's current values.
    function update() { //------------------------------------------- *MODIFIED*
      valueDom.val(ramp.getOutDisplay());

      // Only update the editable DOM if the `Widget` isn't modified.
      if (!widget.isModified()) {

        // Don't reset the user's cursor every time the value refreshes if the
        // input box has focus. They may be trying to select or edit the
        // contents.
        if (!ampDom.is(':focus')) {
          ampDom.val(ramp.getAmplitudeDisplay());
        }
        enabledDom.prop('checked', ramp.getEnabled());
      }
    }

    this.getSubscriber().attach('changed', update);
    update();
  };

  /**
   * Read a new object with the current state of `enabled` and `amplitude`
   * being extracted from the dom.
   *
   * @returns {Object}
   */
  MyFirstWidget.prototype.doRead = function () { //----------------------- *NEW*
    var dom = this.jq();

    return {
      enabled: getEnabledDom(dom).is(':checked'),
      amplitude: parseFloat(getAmpDom(dom).val())
    };
  };

  /**
   * Save the user-entered changes to the loaded `kitControl:Ramp`.
   *
   * Note that the parameter to this function is the same as that resolved by
   * doRead().
   *
   * @returns {Promise}
   */
  MyFirstWidget.prototype.doSave = function (readValue) { //-------------- *NEW*

    // The Widget.value() method call is used to access the currently loaded
    // value. This is the same value that's passed into the Widget.doLoad()
    // method.
    var ramp = this.value(),

        // For the sake of efficiency, a BajaScript Batch object is used to
        // write the changes back in one network call.
        batch = new baja.comm.Batch(),

        promises = [
          ramp.set({
            slot: 'enabled',
            value: readValue.enabled,
            batch: batch 
          }),
          ramp.set({
            slot: 'amplitude',
            value: readValue.amplitude,
            batch: batch 
          })
        ];

    // Commit the changes in one network call.
    batch.commit();

    // Return a Promise so that the framework knows when the save has completed.
    return Promise.all(promises);
  };

  return MyFirstWidget;
});

Lexicons

Lexicons allow the user to easily modify any text elements within your Widget
that you choose, all in one place, without ever touching your code. This helps
your end user customize your Widget to fit their particular region or special
needs.

This works very well in conjunction with using variables in your HTML templates.

module.lexicon

#
# Lexicon for the my first module ux.
#

MyFirstWidget.value=Value
MyFirstWidget.changeValues=Changing these inputs modifies the Widget, not the component. You must Save to push the changes to the station.
MyFirstWidget.amplitude=Amplitude
MyFirstWidget.enabled=Enabled
MyFirstWidget.loading=Loading...

Default Values

You can also set default values for lexicon entries within a JavaScript
lex.get() function call. This will help if module.lexicon is ever
unavailable. Even though this code allows the Widget to function without a
module.lexicon file, you should still create one so that the user can easily
find and customize the Widget text, all in one place.

dom.html(template({
  value: lex.get({
    key: "MyFirstWidget.value",
    def: "Value"
  }),
  changeValues: lex.get({
    key: "MyFirstWidget.changeValues",
    def: 'Changing these inputs modifies the Widget, not the ' +
    'component. You must Save to push the changes to the station.'
  }),
  amplitude: lex.get({
    key: "MyFirstWidget.amplitude",
    def: "Amplitude"
  }),
  enabled: lex.get({
    key: "MyFirstWidget.enabled",
    def: "Enabled"
  }),
  loading: lex.get({
    key: "MyFirstWidget.loading",
    def: "Loading..."
  })
}));

CSS

The CSS RequireJS plug-in is being used to import a style sheet for the Widget.
Please note, since there may be other bajaux Widgets running along side yours
in the same view importing their own CSS, you must make your CSS selectors
unique. An easy way to do this is to include your Widget's name in the
selector, since that must also be unique already (e.g. my-first-widget).

MyFirstModule.css

.my-first-widget {
  background-color: #E8E8E8;
  padding: 5px;
}

.my-first-widget > div {
  padding: 5px;
}

Next

See our Making your Widget Dashboardable
tutorial!