Defining a configuration tree for building a user interface is one of the easiest and straightest ways of rapidly developing a modern web application.
JUL uses this configuration tree to create component instances and to assign parent-child membership in a controlled and automated sequence.
Recap of layout and logic separation
There are two properties of a configuration object that allow splitting the layout and the logic associated with that configuration:
- The id property – it uniquely identifies a component. Using this property, we can simplify a configuration tree into a layout tree-like configuration and a key-value mapping with additional configuration for the nodes of the tree. The keys in this second part (configuration logic) are the id values. Because the logic part is a keyed mapping object, all keys (i.e. component ids) must be unique inside it. It should be added that the id property will get passed to the actual configuration object when instantiating the component.
var oConfig = { xclass: 'MyApp.widgets.Avatar', id: 'avatar-one', children: [ {xclass: 'MyApp.widgets.Hat', id: 'hat-two'}, {xclass: 'MyApp.widgets.Suit', id: 'suit-three'} ] }; var oLogic = { 'avatar-one': { userId: 121, thumbnail: 'face1.png', onHover: function() { console.log('Avatar hint'); } }, 'hat-two': { color: 'blue', size: 4 }, 'suit-three': { color: 'green', match: /(office|meeting|trip)/ } }; var oParser = new JUL.UI.Parser(); var oAvatar = oParser.create(oConfig, oLogic); // the resulting runtime calls are the equivalent of: var oHat = new MyApp.widgets.Hat({ id: 'hat-two', color: 'blue', size: 4 }); var oSuit = new MyApp.widgets.Suit({ id: 'suit-three', color: 'green', match: /(office|meeting|trip)/ }); oAvatar = new MyApp.widgets.Avatar({ id: 'avatar-one', children: [oHat, oSuit], userId: 121, thumbnail: 'face1.png', onHover: function() { console.log('Avatar hint'); } });
- The binding id – it serves the same scope as the id property but with several differences. The binding property is deleted after aggregating the layout and the logic parts. This property may also be an array, in that case the additional configurations referred by its elements being applied sequentially in the final configuration. Such an array is built automatically if using JUL.UI.include() method (see the API reference).
var oConfig = { xclass: 'MyApp.widgets.Avatar', id: 'the-avatar', cid: 'c-avatar', children: [ {xclass: 'MyApp.widgets.Hat', cid: 'c-hat'}, {xclass: 'MyApp.widgets.Suit', cid: 'c-suit'} ] }; var oLogic = { 'c-avatar': { userId: 121, thumbnail: 'face1.png', onHover: function() { console.log('Avatar hint'); } }, 'c-hat': { color: 'blue', size: 4 }, 'c-suit': { color: 'green', match: /(office|meeting|trip)/ } }; var oParser = new JUL.UI.Parser(); var oAvatar = oParser.create(oConfig, oLogic); // the resulting runtime calls are the equivalent of: var oHat = new MyApp.widgets.Hat({ color: 'blue', size: 4 }); var oSuit = new MyApp.widgets.Suit({ color: 'green', match: /(office|meeting|trip)/ }); oAvatar = new MyApp.widgets.Avatar({ id: 'the-avata', children: [oHat, oSuit], userId: 121, thumbnail: 'face1.png', onHover: function() { console.log('Avatar hint'); } });
Advanced composition and inheritance
Putting configuration objects into the tree is the simplest form of building the UI layout. But, with the help of the binding id and of the JUL.UI.include() method, the parser can combine component configurations in advanced ways.
- The first method is including a simpler configuration object into a more complex one. The process is split into two parts: including the base config into the destination layout and including the logic part of the base config into the configuration logic of the current config. Then, we can repeat this process of augmenting (extending) a configuration object as many times as we need. An example of this method is given below.
var MyApp = { config: {}, objects: {}, version: '1.0' }; MyApp.config.shapeUi = { xclass: 'MyLib.Shape', cid: 'mylib.shape', origin: {x: 0, y: 0} }; MyApp.config.shapeLogic = { 'mylib.shape': { color: 'grey', bgColor: 'white', getOrigin: function() { console.log('Shape origin'); } } }; MyApp.config.polygonUi = { include: MyApp.config.shapeUi, xclass: 'MyLib.Polygon', cid: 'mylib.polygon', corners: 0, sizes: [] }; MyApp.config.polygonLogic = { include: MyApp.config.shapeLogic, 'mylib.polygon': { getArea: function() { console.log('Polygon area'); } } }; MyApp.config.triangleUi = { include: MyApp.config.polygonUi, xclass: 'MyLib.Triangle', cid: 'mylib.triangle', corners: 3, sizes: [3, 4, 5] }; MyApp.config.triangleLogic = { include: MyApp.config.polygonLogic, 'mylib.triangle': { draw: function() { console.log('Triangle draw'); }, getArea: function() { console.log('Triangle area'); } } }; var oParser = new JUL.UI.Parser(); MyApp.objects.triangle = oParser.create(MyApp.config.triangleUi, MyApp.config.triangleLogic); // the resulting runtime calls are the equivalent of: MyApp.objects.triangle = new MyLib.Triangle({ origin: {x: 0, y: 0}, corners: 3, sizes: [3, 4, 5], color: 'grey', bgColor: 'white', getOrigin: function() { console.log('Shape origin'); }, getArea: function() { console.log('Triangle area'); }, draw: function() { console.log('Triangle draw'); } });
This kind of augmentation is called explicit inheritance as opposed to the prototypal or class based inheritance which we call implicit inheritance. Although we could use an implicit inheritance for extending a component configuration, JUL uses explicit inheritance in order to ensure a reliable serialization of the configuration objects. Because JUL.UI.obj2str() uses JSON internally to serialize the objects, a prototypal based inheritance would complicate the serialization process, given the differences in object prototypes across different runtime environments.
- The second method is inserting several base configurations in various points into the configuration tree, i.e. the component composition. The process consists in referring the base configs with include property of the given node and adding the base config logic to the include array of the destination logic mapping. An example of such a process is given below.
var MyApp = { config: {}, objects: {}, version: '1.0' }; MyApp.config.circleUi = { xclass: 'MyLib.Circle', cid: 'mylib.circle', origin: {x: 0, y: 0}, diameter: 1 }; MyApp.config.circleLogic = { 'mylib.circle': { fillColor: 'white', draw: function() { console.log('Draw circle'); } } }; MyApp.config.rectangleUi = { xclass: 'MyLib.Rectangle', cid: 'mylib.rectangle', origin: {x: 0, y: 0}, dimensions: {w: 0, h: 0}, }; MyApp.config.rectangleLogic = { 'mylib.rectangle': { fillColor: 'blue', draw: function() { console.log('Draw rectangle'); } } }; MyApp.config.logoUi = { include: MyApp.config.rectangleUi, id: 'the-logo', dimensions: {w: 10, h: 7}, children: [ {include: MyApp.config.rectangleUi, id: 'inner-area', origin: {x: 1, y: 1}, dimensions: {w: 8, h: 5}, children: [ {include: MyApp.config.circleUi, id: 'left-disc', origin: {x: 2, y: 2}, diameter: 3}, {include: MyApp.config.circleUi, id: 'right-disc', origin: {x: 2, y: 5}, diameter: 3} ]} ] }; MyApp.config.logoLogic = { include: [MyApp.config.circleLogic, MyApp.config.rectangleLogic], 'the-logo': { draw: function() { console.log('Draw logo'); } }, 'inner-area': { fillColor: 'yellow' }, 'left-disc': { fillColor: 'red' }, 'right-disc': { fillColor: 'green' } }; var oParser = new JUL.UI.Parser(); MyApp.objects.logo = oParser.create(MyApp.config.logoUi, MyApp.config.logoLogic); // the resulting runtime calls are the equivalent of: var oLeft = new MyLib.Circle({ id: 'left-disc', origin: {x: 2, y: 2}, diameter: 3, fillColor: 'red', draw: function() { console.log('Draw circle'); } }); var oRight = new MyLib.Circle({ id: 'right-disc', origin: {x: 2, y: 5}, diameter: 3, fillColor: 'green', draw: function() { console.log('Draw circle'); } }); var oInner = new MyLib.Rectangle({ id: 'inner-area', origin: {x: 1, y: 1}, dimensions: {w: 8, h: 5}, children: [oLeft, oRight], fillColor: 'yellow', draw: function() { console.log('Draw rectangle'); } }); MyApp.objects.logo = new MyLib.Rectangle({ id: 'the-logo', origin: {x: 0, y: 0}, dimensions: {w: 10, h: 7}, children: [oInner], fillColor: 'blue', draw: function() { console.log('Draw logo'); } });
The component composition may be combined with the explicit inheritance to get complex inclusions of the configuration trees.
- The third method is building a new component using a constructor function to aggregate the component configuration into a component instance. This method may use the previous two methods to build the component configuration and then it calls the JUL parser to create the component instance inside the constructor function. An example is given below.
var MyApp = { config: {}, objects: {}, widgets: {}, vesrsion: '1.0' }; MyApp.config.logoUi = { xclass: 'MyLib.Ellipse', cid: 'c-logo', dimensions: {dx: 18, dy: 12}, children: [ {xclass: 'MyLib.Triangle', cid: 'c-inner', origin: {x: 8, y: 1}, sizes: [7, 7, 7], children: [ {xclass: 'MyLib.Circle', cid: 'c-center', origin: {x: 4, y: 2}, diamerer: 4} ]} ] }; MyApp.config.logoLogic = { 'c-logo': { fillColor: 'green', Draw: function() { console.log('Draw logo'); } }, 'c-inner': { fillColor: 'red', draw: function() { console.log('draw inner'); } }, 'c-center': { fillColor: 'blue', draw: function() { console.log('Draw center'); } } }; MyApp.parser = new JUL.UI.Parser(); MyApp.widgets.Logo = function(oConfig) { var oApplied = {include: MyApp.config.logoUi}; JUL.apply(oApplied, oConfig || {}); var oAppliedLogic = {include: MyApp.config.logoLogic}; var sId = oApplied.id || oApplied.cid; if (!sId) { sId = 'a-random-id'; oApplied.cid = sId; } oAppliedLogic[sId] = {}; JUL.apply(oAppliedLogic[sId], oConfig || {}); delete oAppliedLogic[sId].xclass; delete oAppliedLogic[sId].id; delete oAppliedLogic[sId].cid; oApplied.xclass = MyApp.config.logoUi.xclass || MyApp.parser.defaultClass; return MyApp.parser.create(oApplied, oAppliedLogic); }; MyApp.config.ui = { xclass: 'MyLib.Rectangle', id: 'the-ui', dimensions: {w: 22, h: 16}, children: [ {xclass: 'MyApp.widgets.Logo', id: 'the-logo', origin: {x: 2, y: 2}} ] }; MyApp.config.logic = { 'the-ui': { fillColor: 'green', show: function() { console.log('Show UI'); } }, 'the-logo': { fillColor: 'yellow' } }; MyApp.objects.ui = MyApp.parser.create(MyApp.config.ui, MyApp.config.logic); // the resulting runtime calls are the equivalent of: var oCenter = new MyLib.Circle({ origin: {x: 4, y: 2}, diamerer: 4, fillColor: 'blue', draw: function() { console.log('Draw center'); } }); var oInner = new MyLib.Triangle({ origin: {x: 8, y: 1}, sizes: [7, 7, 7], children: [oCenter], fillColor: 'red', draw: function() { console.log('draw inner'); } }); var oLogo = new MyLib.Ellipse({ id: 'the-logo', origin: {x: 2, y: 2}, dimensions: {dx: 18, dy: 12}, children: [oInner], fillColor: 'yellow', Draw: function() { console.log('Draw logo'); } }); // next call will return the previous oLogo object new MyApp.widgets.Logo({ id: 'the-logo', origin: {x: 2, y: 2}, fillColor: 'yellow' }); MyApp.objects.ui = new MyLib.Rectangle({ id: 'the-ui', dimensions: {w: 22, h: 16}, children: [oLogo], fillColor: 'green', show: function() { console.log('Show UI'); } });
For a standard way to encapsulate complex elements into a component, please consult the JWL Library project page.
Parser circular inheritance and meta-initialization
A JUL parser is an instance of JUL.UI.Parser() and inherits automatically all JUL.UI members. But the parser itself has a method called Parser() which can be used to build a new parser derived from the current one. The Parser() method accepts as a parameter a configuration object whose members may override the inherited members of the parser. Furthermore, changing an inherited member from one of the ancestor parents (e.g. JUL.UI object) will reflect in all descendant parsers where that member is not overridden. We call that circular inheritance.
The JUL parser is set in action when calling its create() method, which in turn uses the configuration layout and logic objects to build a component tree. Typically, for each node in the layout config, the parser creates a component using the parser’s members like the class property, the children property, the id property, and so on, as meta-information. But if the currently processed node has a special property – called by default ‘parserConfig’, the parser starts a new derived parser based on that property to build that branch of the configuration tree. We call that parser meta-initialization.
// derive a parser from the base class var oXmlParser = new JUL.UI.Parser({ defaultClass: 'xml', useTags: true, topDown: true }); // derive a parser from the previous one var oHtmlParser = new oXmlParser.Parser({ defaultClass: 'html', customFactory: 'JUL.UI.createDom' }); // set a property for the base class and all the parsers where the property is not overridden JUL.UI.childrenProperty = 'childNodes'; // create a configuration for a mixed DOM tree var oConfig = { tag: 'div', css: 'svg-wrap', childNodes: [ {tag: 'h3', html: 'A SVG image'}, {tag: 'div', css: 'svg-img', childNodes: [ { // the next meta-info property will auto-derive a new parser for this config branch parserConfig: {defaultClass: 'svg', childrenProperty: 'nodes'}, tag: 'svg', width: 100, height: 100, viewBox: '0 0 100 100', nodes: [ {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'} ] } ]} ] }; // create the DOM tree and attach it to the body element oHtmlParser.create(oConfig, null, document.body);
<!-- generated DOM -->
<div class="svg-wrap">
<h3>A SVG image</h3>
<div class="svg-img">
<svg:svg width="100" height="100" viewBox="0 0 100 100">
<svg:circle cx="50" cy="50" r="48" fill="none" stroke="#000"/>
<svg:path d="M50,2a48,48 0 1 1 0,96a24 24 0 1 1 0-48a24 24 0 1 0 0-48"/>
<svg:circle cx="50" cy="26" r="6"/>
<svg:circle cx="50" cy="74" r="6" fill="#FFF"/>
</svg:svg>
</div>
</div>
Reliable serialization and XML to JUL conversion
Other than building components from a configuration tree, JUL aims to persistently store and retrieve the configuration objects (i.e. serialization). The safest way to do this is to produce the JavaScript code that will build the desired config object at runtime. To fulfill this goal, JUL uses an extended serialization based on the JSON standard. In addition to the JSON rules, JUL is able to serialize several native JavaScript types like Function, RegExp, Date, or custom types with the help of the JUL.UI._jsonReplacer() utility. The target of the serialization can be either JavaScript or JSON code, but the goal is to produce equivalent JavaScript code no matter where the serialization is run. We call this reliable serialization, i.e. the process where the conversion of the configuration object produces the same runtime effect (equivalent code) when the resulting code is executed. The actual code may differ in line breaks, white spaces, floats or date formatting, but it will always get the same effects at runtime. As a side note, the serialization doesn’t imply that the serialized form should be the same as the object being serialized, although that is the goal for the configuration objects. Keep in mind that the runtime object may have different prototypal members or enhanced functionality depending on the environment the code is run in. But, JUL config objects don’t depend on prototypes and are used just to read the configuration stored in their members. More details about the serialization can be found in the JUL.UI.obj2str() method reference.
var oConfig = { firstString: 'First\t\x22string\x22\t', secondString: "Second 'string'\n", firstNumber: parseFloat(Math.PI.toFixed(4)), secondNumber: 1.2e4, dates: { first: new Date(Date.UTC(2020, 5, 1)), second: '2030-02-10T15:00:30Z' }, matches: [ /(one|two|three)\s+times/i, new RegExp('<h1[\\s\\S]+?\/h1>', 'g') ], events: [ {name: 'click', handler: "function(oEvent) {\n\tconsole.log(oEvent.type + ' triggered');\n}"}, {name: 'change', handler: [ function(oEvent) { console.log('Before ' + oEvent.type + ' triggered'); }, function(oEvent) { console.log('After ' + oEvent.type + ' triggered'); } ]} ] }; var oParser = new JUL.UI.Parser(); console.log(oParser.obj2str(oConfig)); // previous line produces the following JavaScript code in all major runtime environments: oConfig = { firstString: 'First\t"string"\t', secondString: 'Second \'string\'\n', firstNumber: 3.1416, secondNumber: 12000, dates: { first: new Date(/*Mon, 01 Jun 2020 00:00:00 GMT*/1590969600000), second: new Date(/*Sun, 10 Feb 2030 15:00:30 GMT*/1896966030000) }, matches: [/(one|two|three)\s+times/i, /<h1[\s\S]+?\/h1>/g], events: [ {name: 'click', handler: function(oEvent) { console.log(oEvent.type + ' triggered'); }}, {name: 'change', handler: [function(oEvent) { console.log('Before ' + oEvent.type + ' triggered'); }, function(oEvent) { console.log('After ' + oEvent.type + ' triggered'); }]} ] };
Another feature of JUL is the conversion from XML based languages to JUL config objects. This is done by the JUL.UI.xml2jul() method which transforms a XML tree (or source code) into a JUL config tree (or the equivalent source code). The conversion takes into account the meta-information of the current parser. To see it in action, please visit the XML2JUL online example.
The custom factory
When building components from the configuration object, JUL aggregates the layout and the logic configs into a runtime configuration object for each component to be built. By default, the runtime config is passed to a constrictor function located by the dotted path stored in the class property of that config. There is also a default class property which applies for the nodes where the class property is not specified.
But the user can change this behavior by setting a custom factory callback at the JUL.UI.customFactory property which will receive the computed runtime configuration. A typical example of such a callback is JUL.UI.createDom() method that can build several types of DOM elements (HTML, SVG, XUL, etc.) depending on the browser support.
// using JUL custom factory to build a XUL window var oConfig = { tag: 'window', id: 'window-main', height: 450, hidden: true, title: 'JUL News Reader', width: 720, children: [ {tag: 'toolbox', children: [ {tag: 'menubar', children: [ {tag: 'toolbargrippy'}, {tag: 'menu', label: 'File', children: [ {tag: 'menupopup', children: [ {tag: 'menuitem', id: 'menuitem-exit', label: 'Exit'} ]} ]}, {tag: 'menu', label: 'View', children: [ {tag: 'menupopup', children: [ {tag: 'menuitem', id: 'menuitem-show-articles', checked: true, label: 'Show articles', type: 'checkbox'} ]} ]}, {tag: 'menu', label: 'Options', children: [ {tag: 'menupopup', children: [ {tag: 'menuitem', id: 'menuitem-autorefresh', label: 'Autorefresh every minute', type: 'checkbox'} ]} ]} ]} ]}, {tag: 'hbox', children: [ {tag: 'spacer', width: 7}, {tag: 'label', control: 'textbox-url', value: 'Address'}, {tag: 'spacer', width: 7}, {tag: 'textbox', id: 'textbox-url', flex: 1, value: 'http://feeds.skynews.com/feeds/rss/technology.xml'}, {tag: 'spacer', width: 5}, {tag: 'button', id: 'button-go', label: 'Go', tooltiptext: 'Get the news', width: 50} ]}, {tag: 'hbox', flex: 1, children: [ {tag: 'vbox', id: 'vbox-articles', width: 260, children: [ {tag: 'listbox', id: 'listbox-articles', flex: 1, children: [ {tag: 'listhead', children: [ {tag: 'listheader', label: 'Article', width: 500} ]}, {tag: 'listbody'} ]} ]}, {tag: 'splitter'}, {tag: 'vbox', width: '100%', children: [ {tag: 'description', id: 'description-title'}, {tag: 'description', id: 'description-date'}, {tag: 'hbox', id: 'hbox-image', hidden: true, pack: 'center', children: [ {tag: 'image', id: 'image-article', width: 200} ]}, {tag: 'description', id: 'description-content'} ]} ]} ] }; var oParser = new JUL.UI.Parser({ customFactory: 'JUL.UI.createDom', defaultClass: 'xul', topDown: true, useTags: true }); var oWindow = oParser.create(oConfig); oWindow.show();
To be continued
These were some advanced concepts of using JUL – The JavaScript UI Language. Other concepts like JSON custom replacers, serialization prefixes, code decorators, callback retrieving and scope binding, dotted path escaping, and so on, will be discussed in a future article.