This tutorial follows from Saving Modifications to Station.
We're going to modify MyFirstWidget to make it dashboardable.
Outline
+- myFirstModule
+- myFirstModule-ux
+- src
| +- com
| | +- companyname
| | +- myFirstModule
| | +- ux
| | +- BMyFirstWidget.java
| +- rc
| +- templates
| | +- MyFirstWidget.hbs ----------------------------------*MODIFIED*
| +- myFirstModule.css
| +- MyFirstWidget.js --------------------------------------*MODIFIED*
+- module.lexicon ----------------------------------------------*MODIFIED*
+- module-include.xml
+- myFirstModule-ux.gradle
Drag and Drop
One of the most useful features of dashboards is to allow users to drag components from the Nav tree and drop them onto a Widget, instantly loading the new component so the user can interact with it without navigating away from that page. On top of that, users can save the dashboard so that they always see their preferred component in that View.
In this example, we're adding drag-and-drop functionality to MyFirstWidget. When MyFirstWidget is shown in a dashboard, the user can drag a different Ramp onto it directly to bind the widget to the Ramp of their choice, instead. The ORD to that Ramp will be persisted in the user's dashboard data automatically.
The added lines of code related to this functionality have been marked as *DRAG & DROP* in the modified MyFirstWidget.js file.
Dashboard-only Features
Sometimes a user may want to show a feature of their Widget only when it is contained within a dashboard. We've added a private note box at the bottom of the Widget to demonstrate this.
The added lines of code related to this functionality have been marked as *DASHBOARD-ONLY NOTE* in the modified MyFirstWidget.js file.
There are a lot of changes in this example - again, just skim over the code to get a feel for what's going on, and then we'll discuss in more detail.
MyFirstWidget.js - modified
/**
* A module defining `MyFirstWidget`.
*
* @module nmodule/myFirstModule/rc/MyFirstWidget
*/
define([
'baja!',
'hbs!nmodule/myFirstModule/rc/templates/MyFirstWidget',
'lex!myFirstModule',
'log!nmodule.myFirstModule.rc.MyFirstWidget',
'bajaux/Widget',
'bajaux/dragdrop/dragDropUtils', //----------------------- *DRAG & DROP*
'bajaux/mixin/subscriberMixIn',
'bajaux/util/SaveCommand',
'jquery',
'Promise',
'css!nmodule/myFirstModule/rc/myFirstModule' ], function (
baja,
template,
lexs,
log,
Widget,
dragDropUtils,
subscriberMixIn,
SaveCommand,
$,
Promise) {
'use strict';
const [ myFirstModuleLex ] = lexs;
const logSevere = log.severe.bind(log);
// When the dashboard is saved, dashboardable property information will be saved away
// to the user's station, and is accessible by that user alone. Also note
// that these dashboardable properties are not attached to the `Widget's` value, but the
// `Widget` itself!
const widgetDefaults = () => ({
properties: {
rootCssClass: 'MyFirstWidget',
// This is where we will save the override ORD.
overrideOrd: { //---------------------------------- *DRAG & DROP*
value: '',
dashboard: true,
// Use hidden and readonly to hide this property from the user in an editor.
hidden: true,
readonly: true
},
// This is where we will save the contents of the private note unique to each user.
privateNote: { //-------------------------- *DASHBOARD-ONLY NOTE*
value: '',
dashboard: true,
hidden: true,
readonly: true
},
// This is a special optional property that can be added to see if a
// `Widget` is currently running on a dashboard. You do not need it to make
// your `Widget` dashboardable. This is how we will decide whether or not to
// show the private note textbox.
dashboard: {
value: false,
hidden: true,
readonly: true,
transient: true
}
}
});
/**
* An editor for working with `kitControl:Ramp` instances.
*
* This editor allows a user to drag and drop a new Ramp component onto it to
* be viewed or modified instantly. This `Widget` is also dashboardable, which
* allows a user to save the dropped override Ramp as the default one loaded.
* Dashboarding also provides a private note text box, which only that user
* can see and modify.
*
* @class
* @extends module:bajaux/Widget
* @alias module:nmodule/myFirstModule/rc/MyFirstWidget
*/
return class MyFirstWidget extends Widget {
constructor(params) {
super({ params, defaults: widgetDefaults() });
subscriberMixIn(this);
this.getCommandGroup().add(new SaveCommand());
}
doInitialize(dom) {
dom.html(template({
valueLabel: myFirstModuleLex.get('MyFirstWidget.value'),
changeValuesText: myFirstModuleLex.get('MyFirstWidget.changeValues'),
amplitudeLabel: myFirstModuleLex.get('MyFirstWidget.amplitude'),
enabledLabel: myFirstModuleLex.get('MyFirstWidget.enabled'),
loadingPlaceholder: myFirstModuleLex.get('MyFirstWidget.loading'),
noteLabel: myFirstModuleLex.get('MyFirstWidget.note'), //--------------------- *DASHBOARD-ONLY NOTE*
// Hide private note if widget is not dashboarded.
noteVisible: this.properties().getValue('dashboard'), // *DASHBOARD-ONLY NOTE*
privateNote: this.properties().getValue('privateNote'), // *DASHBOARD-ONLY NOTE*
dashboardPlaceholder: myFirstModuleLex.get('MyFirstWidget.dashboardPlaceholder') //-------------- *DASHBOARD-ONLY NOTE*
}));
dom.on('input', '.MyFirstWidget-amplitude', () => {
this.setModified(true);
});
dom.on('change', '.MyFirstWidget-enabled', () => {
this.setModified(true);
});
// Copy private note text to `Widget` when modified. - *DASHBOARD-ONLY NOTE*
dom.on('input', '.MyFirstWidget-noteText', () => {
this.properties().setValue('privateNote', this.$getNoteTextarea().val());
});
// Set up drag and drop. --------------------------------- *DRAG & DROP ORD*
dom.on('dragover', (e) => {
e.preventDefault();
});
dom.on('drop', (e) => {
this.$updateFromDrop(e.originalEvent.dataTransfer).catch(logSevere);
e.preventDefault();
e.stopPropagation();
});
this.getSubscriber().attach('changed', () => {
// Use override value if available. -------------------- *DRAG & DROP ORD*
const rampToUpdate = this.$overrideVal || this.value();
this.$updateDom(rampToUpdate);
});
// Set up the override ORD if it exists. ----------------- *DRAG & DROP ORD*
return this.$resolveOverrideOrd();
}
/**
* Update the DOM to show the ramp's current values.
*
* @param {baja.Component} ramp - an instance of `kitControl:Ramp`
*/
doLoad(ramp) {
// Use override value if available. -------------------- *DRAG & DROP ORD*
const rampToUpdate = this.$overrideVal || ramp;
this.$updateDom(rampToUpdate);
}
/**
* Update the DOM to reflect the given Ramp's current values.
* @private
* @param {baja.Component} ramp a `kitControl:Ramp`
*/
$updateDom(ramp) {
this.$getValueDisplay().val(ramp.getOutDisplay());
// Only update the editable DOM if the user hasn't made unsaved changes.
if (!this.isModified()) {
const amplitudeInput = this.$getAmplitudeInput();
// 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 (!amplitudeInput.is(':focus')) {
amplitudeInput.val(ramp.getAmplitudeDisplay());
}
this.$getEnabledCheckbox().prop('checked', ramp.getEnabled());
}
}
/**
* Reads out the `enabled` and `amplitude` values that the user has currently entered.
*
* @returns {module:nmodule/myFirstModule/rc/MyFirstWidget~RampProperties}
*/
doRead() {
return {
enabled: this.$getEnabledCheckbox().is(':checked'),
amplitude: parseFloat(this.$getAmplitudeInput().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().
*
* @param {module:nmodule/myFirstModule/rc/MyFirstWidget~RampProperties} readValue
* @returns {Promise}
*/
doSave(readValue) {
// Use override value if available. ------------------ *DRAG & DROP ORD*
const ramp = this.$overrideVal || this.value();
const { enabled, amplitude } = readValue;
return Promise.all([
ramp.set({ slot: 'enabled', value: enabled }),
ramp.set({ slot: 'amplitude', value: amplitude })
]);
}
/**
* Called when a new data value is dragged and dropped onto the `Widget`.
* Resolves a promise once the drag and drop operation has completed.
*
* @private
* @param {DataTransfer} dataTransfer - The data dropped onto the Widget.
* @returns {Promise}
*/
$updateFromDrop(dataTransfer) { //-------------- *DRAG & DROP*
return dragDropUtils.fromClipboard(dataTransfer)
.then((envelope) => {
if (envelope.getMimeType() !== 'niagara/navnodes') {
return;
}
return envelope.toJson()
.then((json) => {
// Although multiple items could have been dragged and dropped
// on, we're just going to use the first item in the list.
const obj = json && json[0];
const ord = obj && obj.ord;
if (ord) {
if (obj.typeSpec === 'kitControl:Ramp') {
return this.$setOverrideOrd(ord);
} else {
throw new Error('Override component must be a `kitControl:Ramp`.');
}
}
});
});
}
/**
* Sets up a new override ORD and unsubscribes the old one.
*
* @private
* @param {string} newOverrideOrd - The new override ORD.
* @returns {Promise}
*/
$setOverrideOrd(newOverrideOrd) { //------------- *DRAG & DROP*
// Record any old ORD value so we can unsubscribe it.
const oldOverrideOrd = this.properties().getValue('overrideOrd');
// There is no need to manually mark the `Widget` as modified here. When the
// widget is in a dashboard, a change to widget.properties() automatically
// does this.
this.properties().setValue('overrideOrd', newOverrideOrd);
return Promise.all([
this.$resolveOverrideOrd(),
oldOverrideOrd && baja.Ord.make(oldOverrideOrd).get()
.then((comp) => this.getSubscriber().unsubscribe(comp))
]);
}
/**
* Resolves any override ORD.
*
* @private
* @returns {Promise}
*/
$resolveOverrideOrd() { //------------------------ *DRAG & DROP*
const overrideOrd = this.properties().getValue('overrideOrd');
if (overrideOrd) {
return baja.Ord.make(overrideOrd).get({ subscriber: this.getSubscriber() })
.then((value) => {
this.$overrideVal = value;
this.$updateDom(value);
});
} else {
delete this.$overrideVal;
}
}
/**
* @private
* @returns {JQuery} the text box for the `amplitude` property
*/
$getAmplitudeInput() {
return this.jq().find('.MyFirstWidget-amplitude');
}
/**
* @private
* @returns {JQuery} the checkbox for the `enabled` property
*/
$getEnabledCheckbox() {
return this.jq().find('.MyFirstWidget-enabled');
}
/**
* @private
* @returns {JQuery} the readonly text box showing the Ramp's value
*/
$getValueDisplay() {
return this.jq().find('.MyFirstWidget-value');
}
/**
* @private
* @returns {JQuery} the label showing the Widget's override ORD
*/
$getOverrideOrdDisplay() { // ------------------ *DRAG & DROP ORD*
return this.jq().find('.MyFirstWidget-overrideOrd');
}
/**
* @private
* @returns {JQuery} the textarea containing the private note
*/
$getNoteTextarea() { //----------------------- *DASHBOARD-ONLY NOTE*
return this.jq().find('.MyFirstWidget-noteText');
}
};
});
First off, we're using the log! plugin for browser-based logging. This provides more features than just console.log() and can log up to the Application Director. See the log JSDoc for more details.
In doInitialize(), we've added some more properties relating to the override ORD and the dashboardable note to the Handlebars template. The contents of the dashboardable note are stored in the dashboardable privateNote Property. They are written to the DOM in doInitialize(), and as the user types, they are written back onto the privateNote Property. Because privateNote is a dashboardable Property, saving the Widget when it's on a dashboard will also save that Property.
We also arm new handlers for drag/drop. When we drop from the nav tree onto the Widget, the profile itself (either Workbench or HTML5 profile) will provide a specific data structure. The dragDropUtils module knows how to consume this data structure into an envelope which provides more details about what you're dropping. See the JSDoc for that module for more details, and inspect the $updateFromDrop() method here for an example. When the user drops a Ramp onto the Widget, it will record that Ramp's ORD as its overrideOrd. Again, because overrideOrd is a dashboardable Property, saving the Widget when it's on a dashboard will save overrideOrd as well.
$setOverrideOrd() and $resolveOverrideOrd() handle the storage and resolution of an existing override ORD.
MyFirstWidget.hbs - modified
<div class='MyFirstWidget'>
<div>
<label>{{overrideOrdLabel}} <span class='MyFirstWidget-overrideOrd'>{{overrideOrd}}</span></label>
</div>
<div>
<label>{{valueLabel}}: <label>
<input class='MyFirstWidget-value' type='text' value='{{loadingPlaceholder}}' readonly='readonly'/>
</div>
<div>
{{changeValuesText}}
</div>
<div>
<label>{{amplitudeLabel}}: <label>
<input class='MyFirstWidget-amplitude' type='text' value='{{loading}}' />
</div>
<div>
<label>{{enabledLabel}}: <label>
<input class='MyFirstWidget-enabled' type='checkbox' value='Enabled' />
</div>
<div class='MyFirstWidget-note'{{#unless noteVisible}} style='display: none;'{{/unless}}' >
<div>
{{noteLabel}}
</div>
<div>
<textarea class='MyFirstWidget-noteText' rows='4' cols='50' placeholder='{{dashboardPlaceholder}}' >
{{privateNote}}
</textarea>
</div>
</div>
</div>
module.lexicon - modified
#
# 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...
MyFirstWidget.note=Private note:
MyFirstWidget.overrideOrd=Override ORD:
MyFirstWidget.dashboardPlaceholder=Thanks to the dashboard, no other users will see this text when looking at this View.
Next
There's a lot more to learn about. To explore, we're going to switch from creating a Niagara Module that embeds our JavaScript Widget to using some JavaScript Playground Components. These are not something we would distribute to customers for production, but offer another way to experiment with bajaux.
In the playground examples, you can learn about...
- Using Commands.
- Using Properties.
- DOM event handling.
- Hyperlinking.
- Ensure you have the BoxService from the box palette added to your Station's Service Container.
- Open the
docDeveloperpalette and add theBajauxExamplesfolder to the root of your Station. - Make sure you're logged on as user with admin read, write and invoke privileges.
- Using Workbench or a browser, click on the first exercise to get started.