10 Best Practices for Developing JavaScript Widgets

I’d like to offer updated, practical recommendations to developers who are building widgets. Widgets are essentially a mini-application within your web app that provides specific functionality, for example the map switcher widget included as a screenshot in this post. In May 2010, the OpenAjax Alliance published the Metadata 1.0 specification to define a set of standards for widgets. This was a great start, however in the last two years there have been some significant advancements which render some potions of this spec aged and in need of updating. And, since I’ve run across quite a few widgets in the last several years that seem to consistently have certain problems, this post attempts to provide guidelines for anyone looking to make their widgets significantly more re-usable.

State and Display Properties. All widgets need to make their State accessible via public setters and getters which, at a minimum, includes visible/not visible. Getter properties simply offer developers a way to determine whether the widget is visible or not. So, I add that there also needs to be a way to set the State as visible/not visible. Advanced widgets also need control over inline and block positioning.

Persistence. Widgets need the ability to maintain certain aspects of State and persist between app or browser restarts. For example, primary text fields should be persistent after an end user restarts the browser. The corollary is that there also needs to be functionality to clear persisted data.

View Transitions.Widgets must also persist fully and correctly through view transitions. This has to do with mobile web apps designed to mimic native app functionality that occurs when a user switches between different views within the same app.

Auto-resize. Widgets must auto-resize to their container. The sheer popularity of smartphones and tablets is driving this requirement. For example, when a phone is turned on its side, a widget should automatically fill the new dimensions of the container. If your widget doesn’t auto-resize it will significant limit it’s marketability for mobile devices.

Events. Widgets must have a clearly defined life cycle that is accessible via events. At a minimum this includes when the widget is initialized, after specific actions, and upon an errors. Events need to fire when they are expected to and not before or after. This is especially important for mobile apps where event bubbling, or even CPU/Memory overload, may cause delays in the callback. A properly wired event will fire as expected 100% of the time.

Namespace. Widgets must use name space separation so as to not cause variable and function conflicts. For example, don’t name your widget Class “AppWidget”, a best practice alternative is to call it “com.mycompany.AppWidget”. This also includes protecting your global variables to reduce “leakage” outside of your Classes.

Public Properties and Methods. Only properties and methods that are internal should be kept private. All other properties need to be public.

Documentation and Code Comments. This is an age-old problem. Here’s another way to look at it: in many cases, lack of documentation costs other developers significant lost time, effort and money. The one to two minutes a developer saves by not writing a comment could cost another developer hours, days or weeks of frustration.

Cross-platform compatibility. Clearly document which platforms the widget was tested on. If you haven’t been testing your widgets cross-platform then you need to be. If you only designed your widget for a specific browser, that’s okay as long as it’s documented. At a minimum, truly cross-platform widgets will have been tested on the latest production versions of Firefox, Chrome, iPad/iPhone (Safari), Android (native) and IE.

Debugging. If your widget uses an obfuscated JavaScript library, consider providing either a debug version, or a version that includes public (debuggable) methods alongside the private and obfuscated ones.

Auto-resize Dojo Mobile Charts on Orientation Change

The best I can tell, Dojo’s dojox.mobile.Charts2D do not auto-resize on their own when the phone’s orientation changes. I posted a question on how to get around this on the Dojo Community Forum and never got an answer. So, I had to cobble together my own solution.

I have to point out that the functionality I built by hand is inherent in Flex and Silverlight, and you wouldn’t even bat an eyelash thinking about this. So, from a productivity standpoint I spent about double and maybe even triple the time I should have needed in order to sort through why things weren’t working as they should, and to build my own best practice for handling it. 

I do consider what I built as a hack, so caveat emptor. It should at least give you a good starting point to improve on what I’ve already done. There are some important things to note.

  • Here’s a sample demonstrating the functionality: https://andygup.net/samples/realestate/
  • Dojo does not provide any State properties on the View. So, I had to build that.
  • Dojo does not provide any way to bind a dijit to a mobile View. In other words, this enables the Chart to take action automatically when something happens in the View. Check…yep, I bolted that in.
  • Dojo, as far as I know, does not provide a way to detect when the phone’s orientation changes. So you have to listen for that at the window object level. I’m fairly certain that the pattern I used is not completely reliable across all platforms, but it’s what I had to work with. So, I built that too.
  • I also had to detect if there was no orientation change prior to a View transition. This was so that I didn’t unnecessarily redraw the chart and make it appear to flicker. This check was important because my chart is in a secondary View. There seems to be a bug in charts redraw() function in that the chart may self destruct if you try to redraw it from a different View.
  • There’s a bug in the Android native browser that passes the previous orientation event object to the listener. You actually have to set an event timer so that you retrieve the final, and most recent, orientation event object.
Here’s how you initialize the chart. In this case, I’m using a pieChart. This snippet also includes the html markup:
pieChart = new com.agup.PieChart("chart1","statsView").pieChart;

<div id="chart1ParentDiv" dojoType="dojox.mobile.RoundRect">
        <div id="chart1" style="width:100%; height: 350px;"></div>
</div>

Here’s the PieChart Class that I built to encapsulate the functionality I described above:

dojo.declare("com.agup.PieChart",null,{
    pieChart:null,
    orientationChanged:null,
    constructor:function(chartDiv,chartView){
        this.pieChart = this._createChart(chartDiv);
        this.orientationChanged = false;
        if(chartView)this._setTransitionListener(chartView);
        if(chartView)this._setOrientationListener();
    },
    _createChart:function(chartDiv){
        //create the chart
        //Had problems with using just HTML markup, so creating it here and piping to DIV
        var pieChart = new dojox.charting.Chart2D(chartDiv);
        //set the theme
        pieChart.setTheme(dojox.charting.themes.PlotKit.blue);
        //add plot
        pieChart.addPlot("default", {
            type: "Pie",
            radius: 100,
            fontColor: "black",
            labelOffset: "-20"
        });

        pieChart.isVisible = false; //NOTE: this is a new public property that we inject

        return pieChart;
    },
    _setTransitionListener:function(/* DIV of dojox.mobile.View where chart resides - typeof String  */view){
        var test = dijit.byId(view);
        var pieChart = this.pieChart;
        dojo.connect(test, "onAfterTransitionIn",null,
                dojo.hitch(this,function(){
                    pieChart.isVisible = true;
                    if(pieChart != null && this.orientationChanged == true)var time = setTimeout(function(){pieChart.resize()},700);
                })
        );

        dojo.connect(test, "onAfterTransitionOut",null,
                function(){
                    pieChart.isVisible = false;
                }
        );
    },
    _setOrientationListener:function(){
        var supportsOrientationChange = "onorientationchange" in window,
                orientationEvent = supportsOrientationChange ? "orientationchange" : "resize";

        window.addEventListener(orientationEvent,
            dojo.hitch(this,function(){
                var pieChart = this.pieChart;
                var orientationChanged = this.orientationChanged;
                if(pieChart != null && pieChart.isVisible == false){
                    orientationChanged = true;
                }
                if(pieChart != null && pieChart.isVisible == true){
                    orientationChanged = false;
                    var time = setTimeout(function(){pieChart.resize()},700);
                }
        }), false);
    }
});