A page widgetization practice

A page widgetization practice

I was working on the page reconstruction recently, and here is how I divide a page into widgets and how do they interacts in this new attempt.

Core Concepts

Page and widget: A page is composed by several widgets, and a widget is the minimum unit for reuse.

Widget controller: Accepts multiple widget data requirements and deduplicates their data types, sends Ajax request to server side, receives bound data of different types in JSON format, and deliver to each widget to render. It works as the core for page aggregation. The reason a centralized controller on the page sends request to server side rather than widgets do themselves is to reduce the Ajax request count and improve b/s interaction efficiency.

Data type, data format and data: A widget defines a way of data visualization, which requires the data in one certain data format, but in different data types. Say, a 2-dimension chart widget visualizes the data in the format of 2-d number array. Data type is a business aware concept, meaning what the data stand for.

A page widgetization practice

Folder structure

ICanHaz.js
jquery.js
require.js
css.js  // RequireJS plugin
text.js // RequireJS plugin
widget-controller.js
date    // widget folder, contains:
-- template.html  // widget template
-- widget.css     // widget styles
-- widget.js      // widget javascript
-- test.html      // test page
-- test.js        // test page javascript
-- test.json      // test page data

The widget named “date” is self-contained, specifically with its test cases included. Simple, decoupled and reusable, the whole mechanism is composed by:

controller / widget / widget styles / widget template / widget test suite

Code

The code of the controller (widget-controller.js):

/**
 * Core controller for widget aggregation.
 */
define([],
    function() {
        return function(){
            // key: data type, value: widget
            var mappings = {};
            var widgets = [];

            var preCallback = function(){};
            var postCallback = function(){};

            /**
             * add a widget to page
             * @return this
             */
            this.add = function(widget) {
                widgets.push(widget);

                if (widget.dataTypes) {
                    $.each(widget.dataTypes, function(index, dataType) {
                        if (!mappings[dataType])
                            mappings[dataType] = [];

                        mappings[dataType].push(widget);
                    });
                }
                
                return this;
            };

            /**
             * commit data and send an Ajax request, parse and deliver responded data to each widget
             * @param params page level parameters
             * @return this
             */
            this.commit = function(params) {
                // TODO, sending request to server side, deliver the result data to widgets according to mappings, and call this.render to finalize the widget visualization
                return this;
            };

            /**
             * render widget with data, exposing this method for easier local test
             * @param widget
             * @param data
             * @return this
             */
            this.render = function(widget, data) {
                widget.render.call(widget, data);
                return this;
            };

            /**
             * set page level callback when data retrieved, but before any widget rendering:
             * function(data) {} // returned value will be treated as the data updated for rendering
             * @param func
             * @return this
             */
            this.setPreCallback = function(func) {
                preCallback = func;
                return this;
            };

            /**
             * set page level post callback after the rendering of all widgets, specifically for customized interactions between widgets:
             * function(data) {}
             * @param func
             * @return this
             */
            this.setPostCallback = function(func) {
                postCallback = func;
                return this;
            };
        };
    }
);

Test page (test.html) is the entrance for local test. Considering the legacy code, I didn’t import JQuery and ICanHaz by RequireJS:

<html>
<head>
	<script type="text/javascript" src="../jquery.js"></script>
	<script type="text/javascript" src="../ICanHaz.js"></script>
	
	<script data-main="test" src="../require.js"></script>
</head>
<body>
	<div>
		<widget name="birth-date" />
	</div>
	<div>
		<widget name="death-date" />
	</div>
</body>
</html>

Please note the local test can only be run on FireFox or Chrome because of this limitation.

Javascript for test page (test.js). Two instances (birthDateWidget and deathDateWidget) of the same widget date:

require.config({
    baseUrl: '../',
    paths: {
        'jquery' : 'jquery',
        'icanhaz' : 'ICanHaz',
        'text' : 'text',
        'css' : 'css',
        'domReady' : 'domReady',

        'widget-controller' : 'widget-controller',
        'widget-date' : 'date/widget'
    }
});

require(['widget-controller', 'widget-date', 'text!date/test.json'], function (WidgetController, DateWidget, json){
    var controller = new WidgetController();

    var birthDateWidget = new DateWidget({name : 'birth-date'});
    var deathDateWidget = new DateWidget({name : 'death-date'});
    
    controller.add(birthDateWidget).add(deathDateWidget);
    //controller.commit({...}); sending ajax request

    var dataObj = eval ("(" + json + ")");
    controller.render(birthDateWidget, dataObj.birth).render(deathDateWidget, dataObj.death);
});

Widget javascript (widget.js) loads its styles and visualize the data to actual DOMs:

define(['text!date/template.html', 'css!date/widget'],
    function(template) {
        ich.addTemplate('widget-date-template', template);
        
        return function(attrs){
            this.render = function(data){
                var html = ich['widget-date-template'](data);
                $("widget[name=" + attrs.name + "]").replaceWith(html);
            };
        };
    }
);

Widget styles (widget.css) are only be imported when the widget is actually used on the page:

div .widget-date {
	margin-top: 10px;
	border: 1px solid #000000;
}

ICanHaz is used to decouple data and widget template, and the two are finally aggregated on a widget.

Widget template (template.html):

<div class="widget-date">
	type: {{name}}, date: {{date}}
</div>

Data (test.json), which is hard coded for widget test, but should be retrieved from server side in reality:

{
	birth : {name: 'birth', date : '2015-02-14'},
	death : {name: 'death', date : '2015-03-14'}
}

The prototype code can be download here: prototype.zip.

====================================================================

[2/17/2015] Updated: create “template controller” to move the template logic out of widget.

Template (template-controller.js):

/**
 * Template controller.
 * @module
 */
define([],
    function() {
        /**
         * Template controller constructor.
         * @class
         * @param name, template controller name, used as the key for compiled template storage
         * @param template
         */
        return function(name, template) {
            // template should be compiled only once
            ich.addTemplate(name, template);
            
            /**
             * Render the data on widget.
             * @param attrs, widget level attributes, specifically, attrs.id should be the exactly same as the attribute id of that widget DOM
             * @param data, mutable business aware data, data has higher priority than attrs when they have the same key
             */
            this.render = function(attrs, data) {
                    // merge objects, deep copy
                    var merged = {};
                    $.extend(true, merged, attrs, data);

                    var html = ich[name](merged);
                    $("widget[name=" + attrs.id + "]").replaceWith(html);
            };
        };
    }
);

So the widget (widget.js) javascript is much simpler now:

/**
 * DateWidget module.
 * @module
 */
define(['template-controller', 'text!date/template.html', 'css!date/widget'], // template and css
    function(TemplateController, template) {
        var templateController = new TemplateController('widget-date', template);

        /**
         * Class DateWidget constructor. Method render is mandatory for any widget.
         * @class
         * @param attrs, widget level attributes, specifically, attrs.id should be the exactly same as the attribute id of that widget DOM
         * @return the concrete widget class
         */
        return function(attrs) {
            /**
             * Render the widget.
             * @param data, attrs will be merged into data and sent to template controller to finalize the rendering
             */
            this.render = function(data) {
                templateController.render(attrs, data);
            };
        };
    }
);

New code download link: widgets.zip.

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:

One comment

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Preview on Feedage: