UI extensibility example: color picker bundle
This documentation shows how to use extensibility scenarios that are experimentally deployed in update 9. The APIs described here are minimal and will evolve in the future. They are manually deployed, which currently prevents installation in the Cloud. They are usable only in an Early adopter program context.
This article explains how you create a custom widget and associate it to a SAFE X3 datatype. All properties that carry this datatype will be rendered and edited with your custom widget.
We will demonstrate it with a color picker. This tutorial shows you how you can add an RGB color datatype to your SAFE X3 application and register a widget that renders all RGB color properties as a color sample that the user can modify with a color picker.
Before writing our JavaScript code we must create a bundle directory and a 'package.json' file.
Our bundle will have the following structure:
xa1-calculator/package.json public/deps/colorwheel/colorwheel.jsraphael.jscolor-picker.csscolor-picker.js
package.json
file contains:CODECODE CODE json{"name": "xa1-color-picker","description": "Color picker widget extension","version": "1.0.0","author": "ACME Inc.","private": true,"sage": {"x3": {"extensions": {"widgets": [{"module": "xa1-color-picker/public/color-picker","type": "application/x-xa1-color"}]}}}}
widgets
extension key indicates that the package contains a UI widget extension. Under this key:module
value gives the path to the JavaScript module that implements the extension.type
value is the media type for properties that will is bound to this widget.We will not write the color picker ourselves. Instead, we will use the colorwheel jquery plugin.
This plugin is built with the raphael vector graphics library. So we need to include the raphael.js
source file in our package. We have put the colorwheel
plugin and the raphael
library inside the public/deps
directory, to keep them well separated from our widget.
CODECODE CODE javascript"use strict";// load colorwheel component and its dependencies.require.shallow('./deps/raphael');require.shallow('./deps/colorwheel/colorwheel');// load CSS files - none in this case['/xa1-color-picker/public/color-picker.css'].forEach(function(href) {$("<link/>", {rel: "stylesheet",type: "text/css",href: href,}).appendTo("head");});// small helper functionfunction append(parent, tag) {return parent.appendChild(document.createElement(tag));}// add a 'color-picker' widget to the container // returns our color widget's APIexports.create = function(container) {if (container.editable) {// create two divs, one for the wheel and one for the inputvar wheelDiv = append(container.div, 'div');wheelDiv.className = 'xa1-color-picker-wheel';var input = append(container.div, 'input');input.className = 'xa1-color-picker-input';// create the wheel and link it to the input fieldvar wheel = Raphael.colorwheel(wheelDiv, 150);wheel.input(input);// handle the change eventwheel.onchange(function() {container.setDirty();});// return the widget's APIreturn {setValue: function(value) {wheel.color(Raphael.getRGB(value));},getValue: function() {return wheel.color().hex;},};} else {// create a div for the color samplevar sample = append(container.div, 'div');sample.className = 'xa1-color-picker-sample';// return the widget's APIreturn {setValue: function(value) {sample.style['background-color'] = value;},};}};
Let us walk through this file. First we load the JavaScript source for the plugin:
CODECODE CODE javascriptrequire.shallow('./deps/raphael');require('./deps/colorwheel/colorwheel');
require
, but we cannot load raphael.js
with the vanilla require
function because this file contains a require
call that references a non-existent module (eve
, line 398). By using require.shallow
instead, we tell the server to ignore require
directives found in this file, and we avoid the problem.After the 'require' calls, we load our extension's CSS file:
CODECODE CODE javascript['/xa1-color-picker/public/color-picker.css'].forEach(function(href) {$("<link/>", {rel: "stylesheet",type: "text/css",href: href,}).appendTo("head");});
<link>
elements to the head of our document. It is written with a forEach
loop so that we can easily modify it if we have to load several CSS files later.CODECODE CODE javascriptfunction append(parent, tag) {return parent.appendChild(document.createElement(tag));}
Then we have the most important function of our widget module: the create
function that the UI framework calls to create our widget:
CODECODE CODE javascriptexports.create = function(container) {// create the widget...return {// our widget's interface...}}
container
parameter provides the API that the widget uses to access its container, and the create
function returns the API that the widget exposes to its container.The container
parameter is an object with the following properties and methods:
container.div
: the <div>
DOM element that will contain the widget.container.editable
: a boolean flag that indicates if the widget should be configured to edit the property, or just to display it.container.facet
: a string that identifies the type of page in which the widget is created: $details
, $edit
, $query
, $lookup
, $summary
, ... This string may be used to adapt the widget to the type of page.container.title
: the property's title.container.setDirty()
: a method to notify the container that the value has been modified in the widget.The API returned by create
may contain the following methods:
setValue(val)
: method that the container calls in order to fill the widget with a new value.getValue()
: method that the container calls in order to retrieve the widget's value.The create
method tests the container.editable
flag. If the response is 'true', then it creates a color wheel widget; otherwise it returns a much simpler sample widget that only displays a rectangle filled with the color. Let us take a more detailed look at the editable case. The first part is DOM code that creates two <div>
elements: one for the wheel and one for an edit field:
CODECODE CODE javascriptvar wheelDiv = append(container.div, 'div');wheelDiv.className = 'xa1-color-picker-wheel';var input = append(container.div, 'input');input.className = 'xa1-color-picker-input';
colorwheel
plugin to create the wheel and to link it to the input field:CODECODE CODE javascriptvar wheel = Raphael.colorwheel(wheelDiv, 150);wheel.input(input);
onchange
event and calling container.setDirty()
. This call notifies the page controller that the resource has been modified, which enables the "Save" button on the form.CODECODE CODE javascriptwheel.onchange(function() {container.setDirty();});
create
function returns the widget's API:CODECODE CODE javascriptreturn {setValue: function(value) {wheel.color(Raphael.getRGB(value));},getValue: function() {return wheel.color().hex;},};
setValue
function is called when the form is filled. We implement it by converting the string value to an RGB color that we pass to the colorwheel
component.The getValue
function is called by the framework to retrieve the value from the widget. This happens when we tab through the form and also when the user clicks on the "Save" button. We implement it by extracting the hex string value from the colorwheel's current color value.
CODECODE CODE javascriptvar sample = append(container.div, 'div');sample.className = 'xa1-color-picker-sample';return {setValue: function(value) {sample.style['background-color'] = value;},};
At this point we have a fully functional widget which is automatically registered by the framework via the 'package.json' file. Now we will see how we can tie this widget to a property of a SAFE X3 class.
The way the integration is done is through the data type. In the 'package.json' configuration file, the type associated to the widget is application/x-xa1-color
. We need therefore to define:
Once this is done, any representation having properties that refer to the corresponding data type will be entered or displayed with the color picker.
Deployment is easy: you need to add your extension directory under the node_modules
directory of the SAFE X3 'node.js' server, and restart the 'node.js' server.
Do not forget to restart the 'node.js' server everytime you make a change to your bundle. Otherwise it will not pick up the latest code.