Ever wanted to build a great web application? And then searched for the right JavaScript framework to make the task easier?
When choosing a powerful framework to build with, we notice some things to be learned. It has a custom way to initialize the application. It has its own way to aggregate the components of the user interface. It has its own format to attach the interface logic including the event listeners. After mastering these steps we see that we’ve spent half of the time allocated for the application development. And we haven’t solved yet the aspect of code reusability. And last but not least, the best JavaScript framework to solve all these may not be so affordable.
What if I tell you that all you need to do for the previous task is writing a configuration file for your application? You will say that there are frameworks that allow this in the form of a generated XML file. But what I’m talking about applies to any framework, either old or new. And it’s written entirely in JavaScript.
Meet JUL – The JavaScript UI Language!
JUL + SmartClient Example
// application instance
APP = {
version: '1.0',
parser: new JUL.UI.Parser({
// SmartClient uses 'items' as container's 'children' property
childrenProperty: 'items',
// other properties which need to instantiate as components
membersProperties: ['members', 'tabs', 'pane', 'dataSource'],
// in SmartClient, IDs are automatically exposed as globals
idProperty: 'ID',
// simple factory to call 'create' utility if the component class is specified,
// or to return a config object otherwise
customFactory: function(oConfig) {
if (oConfig.xclass === 'Object') { return oConfig; }
else { return isc[oConfig.xclass].create(oConfig); }
}
}),
// initialization method that creates the UI
init: function() {
this.parser.create(this.ui, this.logic);
}
};
// layout configuration tree for the whole application
APP.ui = {
xclass: 'Dialog',
ID: 'mainDialog',
title:"JUL + SmartClient Dialog",
showShadow:true,
autoSize: true,
autoDraw: true,
buttons: [isc.Dialog.OK, isc.Dialog.CANCEL],
items: [
{xclass: 'TabSet', width: 680, height: 350, selectedTab: 0, tabs: [
{title: 'TreeGrid', pane: {
xclass: 'TreeGrid',
ID: 'supplyTree',
animateFolders: true,
selectionType: 'single'
}},
{title: 'ListGrid', pane: {
xclass: 'ListGrid',
ID: 'supplyList',
alternateRecordStyles: true,
selectionType: 'single'
}},
{title: 'DynamicForm', pane: {
xclass: 'DynamicForm',
ID: 'supplyForm',
numCols: 4,
colWidths: [100, 200, 100, 200],
margin: 25,
cellPadding: 5,
autoFocus: false
}}
]}
]
};
// interface logic bounded by component IDs
APP.logic = {
mainDialog: {
okClick: function() {
alert('Please fill the required info!');
},
cancelClick: function() {
alert('Won\'t close for now.');
},
onCloseClick: function() {
return false;
}
},
supplyTree: {
autoFetchData: true,
loadDataOnDemand: false,
dataSource: {
xclass: 'DataSource',
ID: 'supplyCategory',
clientOnly: true,
dataURL: '../data/supplyCategory.data.xml',
recordXPath: '//supplyCategory',
fields: [
{name: 'categoryName', title: 'Item', type: 'text', length: 128, required: true, primaryKey: true},
{name: 'parentID', hidden: true, type: 'text', required: true, foreignKey: 'supplyCategory.categoryName', rootValue: 'root'}
]
},
nodeClick: function(viewer, node, recordNum) {
supplyList.filterData({category: node.categoryName});
}
},
supplyList: {
autoFetchData: true,
showAllRecords: true,
dataSource: {
xclass: 'DataSource',
ID: 'supplyItem',
clientOnly: true,
dataURL: '../data/supplyItem.data.xml',
recordXPath: '//supplyItem',
fields: [
{name: 'itemID', type: 'sequence', hidden: true, primaryKey: true},
{name: 'itemName', type: 'text', title: 'Item', length: 128, required: true},
{name: 'SKU', type: 'text', title: 'SKU', length: 10, required: true},
{name: 'description', type: 'text', title: 'Description', length: 2000},
{name: 'category', type: 'text', title: 'Category', length: 128, required: true, foreignKey: 'supplyCategory.categoryName'},
{name: 'units', type: 'enum', title: 'Units', length: 5, valueMap: [
'Roll', 'Ea', 'Pkt', 'Set', 'Tube', 'Pad', 'Ream', 'Tin', 'Bag', 'Ctn', 'Box'
]},
{name: 'unitCost', type: 'float', title: 'Unit Cost', required: true, validators: [
{type: 'floatRange', min: 0, errorMessage: 'Please enter a valid (positive) cost'},
{type: 'floatPrecision', precision: 2, errorMessage: 'The maximum allowed precision is 2'}
]},
{name: 'inStock', type: 'boolean', title: 'In Stock'},
{name: 'nextShipment', type: 'date', title: 'Next Shipment'}
]
},
recordClick: function(viewer, record, recordNum, field, fieldNum, value, rawValue) {
supplyForm.setValues(record);
}
},
supplyForm: {
dataSource: 'supplyItem',
useAllDataSourceFields: true
}
};
// avoid automatic rendering if 'autoDraw' not specified
isc.setAutoDraw(false);
// start the application
APP.init();JUL + YUI Example
(function() {
// configuring YUI Loader to load dependencies from CDN
var oLoader = new YAHOO.util.YUILoader({
base: 'https://ajax.googleapis.com/ajax/libs/yui/2.9.0/build/',
require: ['reset-fonts', 'container', 'button', 'tabview', 'datatable', 'editor', 'calendar'],
loadOptional: true,
// will fire APP.init method on loading complete
onSuccess: function() { APP.init(); }
});
// application instance
APP = {
version: '1.0',
parser: new JUL.UI.Parser({
// besides 'children', all these properties are also treated as (arrays of) component configs
membersProperties: ['datasource', 'paginator'],
// YUI needs a heavy custom factory to instantiate a tree of component configs
customFactory: function(oConfig) {
var oComponent = null;
// YUI constructors have a variable number of required arguments
// some components need body initialization for complete rendering at initialization phase
switch (oConfig.xclass) {
case 'Dialog':
oComponent = new YAHOO.widget.Dialog(oConfig.id, oConfig);
oComponent.setHeader(oConfig.header || '');
oComponent.setBody(oConfig.body || '');
oComponent.render();
break;
case 'DataTable':
oComponent = new YAHOO.widget.DataTable(oConfig.id, oConfig.columns, oConfig.datasource, oConfig);
break;
case 'DataSource':
oComponent = new YAHOO.util.DataSource(oConfig.url, oConfig);
oComponent.responseType = YAHOO.util.DataSource.TYPE_JSON;
break;
case 'Editor':
oComponent = new YAHOO.widget.Editor(oConfig.id, oConfig);
oComponent.render();
break;
case 'Calendar':
oComponent = new YAHOO.widget.Calendar(oConfig.id, oConfig.container, oConfig);
oComponent.render();
break;
default:
oComponent = new YAHOO.widget[oConfig.xclass](oConfig);
}
// this part appends all children instances to the parent instance,
// when this action isn't triggered by the YUI constrictor
if (oConfig.children) {
var aChildren = [].concat(oConfig.children);
for (var i = 0; i < aChildren.length; i++) {
switch (oConfig.xclass) {
case 'Dialog':
aChildren[i].appendTo(oComponent.form);
break;
case 'TabView':
oComponent.addTab(aChildren[i]);
break;
case 'Tab':
if (aChildren[i].oDomContainer) {
oComponent.get('contentEl').appendChild(aChildren[i].oDomContainer);
}
else {
aChildren[i].appendTo(oComponent.get('contentEl'));
}
break;
default:
aChildren[i].appendTo(oComponent);
}
}
}
// here we treat Element hierarchy listeners
if (oConfig.listeners) {
for (var sItem in oConfig.listeners) {
oComponent.subscribe(sItem, oConfig.listeners[sItem], oComponent);
}
}
// these are Custom Events which have a different syntax of intercepting
if (oConfig.events) {
for (sItem in oConfig.events) {
oComponent[sItem].subscribe(oConfig.events[sItem], oComponent);
}
}
return oComponent;
}
}),
// this method creates the UI tree
init: function() {
var oMain = this.parser.create(this.ui, this.logic);
oMain.show();
}
};
// layout configuration tree for the whole application
APP.ui = {
xclass: 'Dialog',
id: 'dialog-main',
header: 'JUL + YUI Dialog',
width: '700px',
height: '450px',
fixedcenter: true,
postmethod: 'manual',
children: [
{xclass: 'TabView', children: [
{xclass: 'Tab', label: 'DataTable', active: true, children: [
{xclass: 'DataTable', id: 'datatable-json', columns: [
{key: 'id', label: 'ID', sortable: true},
{key: 'name', label: 'Name', width: 272, sortable: true},
{key: 'date', label: 'Date', width: 110, sortable: true, formatter: 'date'},
{key: 'price', label: 'Price', width: 90, sortable: true, formatter: 'currency'},
{key: 'number', label: 'Number', width: 72, sortable: true, formatter: 'number'}
], paginator: {
xclass: 'Paginator', rowsPerPage: 10
}}
]},
{xclass: 'Tab', label: 'Editor', children: [
{xclass: 'Editor', id: 'editor-test', width: '670px', height: '188px', animate: true, dompath: true}
]},
{xclass: 'Tab', label: 'Calendar', children: [
{xclass: 'Calendar', id: 'APP.calendar', container: 'calendar-container'},
{xclass: 'Button', id: 'button-test', label: 'Show selected date'}
]}
]}
]
};
// interface logic bounded by component IDs
APP.logic = {
'dialog-main': {
buttons: [
{text: 'OK', isDefault: true, handler: function() { this.submit(); }},
{text: 'Cancel', handler: function() { this.hide(); }}
],
events: {
beforeHideEvent: function() {
alert('Won\'t close for now.');
return false;
},
beforeSubmitEvent: function() {
alert('Please fill the required info!');
return false;
}
}
},
'datatable-json': {
datasource: {
xclass: 'DataSource',
url: '../data/data.json',
responseSchema: {resultsList: 'records', fields: [
{key: 'id', parser: 'number'}, 'name', {key: 'date', parser: 'date'},
{key: 'price', parser: 'number'}, {key: 'number', parser: 'number'}
]}
},
initialRequest: '?nc=' + (new Date()).getTime()
},
'button-test': {
listeners: {
click: function() {
var aDates = APP.calendar.getSelectedDates();
if (aDates.length) {
alert(aDates[0].toLocaleString());
}
else {
alert('Nothing selected!');
}
}
}
}
};
// we call the loader to do the jobs of loading the dependencies, and of starting the application
oLoader.insert();
})();JUL + AmpleSDK Example
// define application namespace
JUL.ns('APP.ui');
JUL.apply(APP, {
version: '1.0'
});
// build the config tree for the main XUL dialog
APP.ui.components = {
tag: 'dialog',
// a dotted ID means publishing the component under that namespace
id: 'APP.mainWindow',
title: 'JUL + Ample SDK Dialog',
css: 'main-dialog',
width: 610,
height: 333,
buttons: 'accept,cancel',
context: 'none',
hidden: true,
commandset: [
{tag: 'command', id: 'cmd-more-info', label: 'More info ...'}
],
children: [
{tag: 'tabbox', selectedIndex: 0, tabs: [
{tag: 'tab', label: 'XUL+SVG'},
{tag: 'tab', label: 'Chart'},
{tag: 'tab', label: 'Custom tab'}
]}
]
};
// preparing an array of items for the listbox config
var aItems = [];
for (var n = 1; n < 11; n++) {
aItems.push({tag: 'listitem', children: [
{tag: 'listcell', label: 'First ' + n},
{tag: 'listcell', label: 'Last ' + n}
]});
}
// add all tabpanel configs
APP.ui.components.children[0].tabpanels = [{
tag: 'tabpanel',
orient: 'vertical',
// using compact form for a DOM config tree - where container elements can be 'members' properties
hbox: [
{tag: 'image', src: '../media/shield.jpg'},
{tag: 'description', value: 'Register online!', style: 'font-weight:bold;padding:15px', flex: 1},
// container for the SVG tree
{xclass:'html', tag: 'div', id: 'APP.svgBox'}
],
// using default 'members' property i.e. 'children'
children: [
{tag: 'listbox', id: 'list-names', seltype: 'single', height: 99, listhead: [
{tag: 'listheader', label: 'First Name', width: 200, tooltiptext: 'First Name'},
{tag: 'listheader', label: 'Last Name', width: 200, tooltiptext: 'Last Name'}
],
// assign listbody to the array constructed before
listbody: aItems
}
],
groupbox: [
{tag: 'caption', label: 'Your information'},
{tag: 'vbox', children: [
{tag: 'hbox', children: [
{tag: 'label', control:'register-firstname', value: 'Enter your first name', flex: 1},
{tag: 'textbox', id: 'register-firstname', flex: 1}
]},
{tag: 'hbox', children: [
{tag: 'label', control: 'register-lastname', value: 'Enter your last name', flex: 1},
{tag: 'textbox', id: 'register-lastname', flex: 1}
]},
{tag: 'hbox', children: [
{tag: 'button', command: 'cmd-more-info', flex: 1}
]}
]}
]
}, {
tag: 'tabpanel',
orient: 'vertical',
children: [
// container for the Ample Chart tree
{xclass:'html', tag: 'div', id: 'APP.chartBox'}
]
}, {
tag: 'tabpanel',
// include a config specified by its namespace path
include: 'APP.ui.customTab',
// the properties below overwrite those included
id: 'custom-tab',
css: 'custom-class',
orient: 'vertical'
}];
// creating a config object used as included config
APP.ui.customTab = {
tag: 'tabpanel',
children: [
// another inclusion
{tag: 'description', include: 'APP.ui.copy'}
],
groupbox: [
{tag: 'caption', label:'Content'},
{include: 'APP.ui.hbox'},
// the binding property 'cid' is used to link UI with the logic config
{tag: 'button', cid: 'Button.test', label: 'Test me!'}
]
};
// this is a logic config - see the correspondence of the property 'Button.test'
APP.test = {
'Button.test': {
listeners: {
// the DOM creating factory also accepts listeners as namespace paths
click: 'APP.doTest'
}}
};
// define a listener for the 'Test me!' button
APP.doTest = function() {
alert('This is a test!');
};
// another member used as included config
APP.ui.hbox = {
tag: 'hbox',
children: [
{tag: 'label', value: 'Left line', flex: 1},
{tag: 'label', value: 'Right line', flex: 1}
]
};
// testing HTML entities
APP.ui.copy = {html: '© Copyright Note'};
// this is the logic config for the application UI - see the correspondence between its properties and the component IDs
APP.ui.bindings = {
// it also supports the inclusion of an array of logic configs
include: ['APP.test'],
'APP.mainWindow': {listeners: {
// inline listeners for the main dialog
dialogaccept: function() {
alert('Please fill the required info!');
return false;
},
dialogcancel: function() {
alert("Won't close for now.");
return false;
}
}},
// listener for the XUL command element
'cmd-more-info': {listeners: {
command: function() {
alert('JUL example with different DOM component trees (XUL, SVG, Ample Chart)');
}
}}
};
// SVG config tree
APP.ui.svg = {
tag: 'svg',
width: '100px',
height: '100px',
viewBox: '0 0 100 100',
g: [
{tag: 'circle', cx: 50, cy: 50, r: 48, fill: 'none', stroke: '#000'},
{tag: 'path', d: 'M50,2a48,48 0 1 1 0,96a24 24 0 1 1 0-48a24 24 0 1 0 0-48'},
{tag: 'circle', cx: 50, cy: 26, r: 6},
{tag: 'circle', cx: 50, cy: 74, r: 6, fill: '#FFF'}
]
};
// Ample Chart config tree
APP.ui.chart = {
tag: 'bar', title: 'Column chart', orient: 'vertical',
xAxisLabel: 'X Axis', yAxisLabel: 'Y Label',
xAxisValueLabels: '1999,2000,2001,2002,2003', children: [
{tag: 'group', label: 'Set 0', children: [
{tag: 'item', value: 10},
{tag: 'item', value: 20},
{tag: 'item', value: 30},
{tag: 'item', value: 40},
{tag: 'item', value: 50}
]},
{tag: 'group', label: 'Set 1', children: [
{tag: 'item', value: 20},
{tag: 'item', value: 10},
{tag: 'item', value: 25},
{tag: 'item', value: 45},
{tag: 'item', value: 15}
]},
{tag: 'group', label: 'Set 2', children: [
{tag: 'item', value: 30},
{tag: 'item', value: 30},
{tag: 'item', value: 5},
{tag: 'item', value: 10},
{tag: 'item', value: 40}
]},
{tag: 'group', label: 'Set 3', children: [
{tag: 'item', value: 15},
{tag: 'item', value: 25},
{tag: 'item', value: 35},
{tag: 'item', value: 30},
{tag: 'item', value: 10}
]}
]
};
// change defaults the will be inherited by the new parsers - all will treat configs as DOM element configs
JUL.UI.useTags = true;
// wait for Ample to be ready
ample.ready(function() {
// new parser for the XUL tree
var oParser = new JUL.UI.Parser({
// the 'JUL.UI.createDom' factory as the custom factory
customFactory: JUL.UI.createDom,
// all elements created with XUL XML namespace
defaultClass: 'xul',
// all of these properties are treated as container configs i.e. parsed for member configs
membersProperties: ['commandset', 'hbox', 'vbox',
'groupbox', 'tabs', 'tabpanels', 'listhead', 'listbody'],
// using the top-down instantiation instead of the default bottom-up one
topDown: true
});
// create the component tree with bounded logic, the root will be published as 'APP.mainWindow'
// when instantiating top-down, the 'APP.ui.components' needs to be normalized (expanded)
// in order for the 'JUL.UI.createDom' to create container elements for the compacted members
oParser.create(oParser.expand(APP.ui.components), APP.ui.bindings);
// add the DOM element to the Ample document element
ample.documentElement.appendChild(APP.mainWindow);
// new parser for the SVG tree
var oSvgParser = new JUL.UI.Parser({
customFactory: 'JUL.UI.createDom',
defaultClass: 'svg',
membersProperties: ['g']
});
var oSvg = oSvgParser.create(APP.ui.svg);
// 'APP.svgBox' element was published with the creation of the dialog
APP.svgBox.appendChild(oSvg);
// new parser for the Ample Chart tree
var oChartParser = new JUL.UI.Parser({
customFactory: JUL.UI.createDom,
defaultClass: 'chart'
});
var oChart = oChartParser.create(APP.ui.chart);
// 'APP.chartBox' element was published with the creation of the dialog
APP.chartBox.appendChild(oChart);
// show the main dialog
APP.mainWindow.show();
});These were some examples of JUL in action. It can do even more, as we will see in the future posts.
If you are interested, please visit the project page at Sourceforge.










The Zonebuilder