This tutorial follows from Getting
Started - MyFirstWidget.
We're going to modify MyFirstWidget
to enable a user to make changes to theRamp
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 forMyFirstWidget
.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'sWidgets
.
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 aWidget
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.
- setModified(true) is called to modify the
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 ofdoRead()
.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 JavaScriptlex.get()
function call. This will help if module.lexicon
is ever
unavailable. Even though this code allows the Widget
to function without amodule.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!