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.

Creating our color picker bundle

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

The 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"}]}}}}

The widgets extension key indicates that the package contains a UI widget extension. Under this key:

The color picker plugin

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.

The widget file

The `color-picker.js` file provides the glue between the SAFE X3 web framework and the `colorwheel` plugin:
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;},};}};

Requiring dependencies

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');

External modules are usually loaded with 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.

Loading CSS files

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");});

This code dynamically adds CSS <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.

Helper function

The `append` function that follows is just a little helper function to add elements to the DOM:
CODECODE CODE javascriptfunction append(parent, tag) {return parent.appendChild(document.createElement(tag));}

The create function

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...}}

This function is designed as an API or interface handshake: the 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:

The API returned by create may contain the following methods:

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';

Then we are calling the 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);

What follows is more interesting: we are trapping the 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();});

At this point the create function returns the widget's API:
CODECODE CODE javascriptreturn {setValue: function(value) {wheel.color(Raphael.getRGB(value));},getValue: function() {return wheel.color().hex;},};

The 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.

The code for the non-editable _sample_ case is simpler. As the sample is not editable, the UI framework will never need to retrieve a new value from the widget. So we do not need to provide any `getValue()` method in the returned API:
CODECODE CODE javascriptvar sample = append(container.div, 'div');sample.className = 'xa1-color-picker-sample';return {setValue: function(value) {sample.style['background-color'] = value;},};

Using the color picker from 4GL code

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.

Deploying our extension on the 'node.js' web server

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.

Links