Wednesday, January 26, 2011

Dojo Pre-Localization (faster to develop and load)

Embedding Dojo Resources During Early Development

Want to code for localization but don't want to take the time to build a resource file (or wait for the resource file to load)?


Declare your resources in code like this:


dojo.setObject("coretech.nls.res.en_us", {
  "R1": "HELLO",
  "R2": "WORLD"
});
dojo.provide("coretech.nls.res.en_us");

Then use the resources like normal:

dojo.requireLocalization("coretech", "res");
dojo.addOnLoad(function() {
    var res = dojo.i18n.getLocalization("coretech", "res", this.lang);
    alert(res.R1);
    alert(dojo.toJson(res, true));
});

How do I know?

The dojo.provide method (1.5) is implemented in loader.js and looks like this:

return (d._loadedModules[resourceName] = d.getObject(resourceName, true));

Tuesday, January 25, 2011

Dojo on my Kindle

Scraping dojo campus to build an eBook

While the second edition of Dojo: The Definitive Guide would be ideal, it hasn't been written. In the meantime I've decided to grab the raw text from docs.dojocampus.org to read on my kindle. In order to do that I needed to get the names of what I was interested in:

Evaluate this code in a dojo environment:

var p, l = [];
for (p in dojo) {
  if (dojo.isFunction(dojo[p])) {
    l.push(p);
    console.log(p);
  }
};
dojo.filter(l, function(a) {
  if ("_" == a[0]) return false;
  if (a[0].toUpperCase() == a[0]) return false;
  return true;
}).sort().join("\n");

With this result I went to LinqPad and came up with this C# script:

void Main()
{
 // http://docs.dojocampus.org/dojo/subscribe?action=raw
 var urlTemplate = "http://docs.dojocampus.org/dojo/{0}?action=raw";
 var mods = new string[] {
 "addClass","addOnLoad","addOnUnload","addOnWindowUnload","anim",
 "animateProperty","attr",
 "blendColors","body","byId","cache","clone",
 "colorFromArray","colorFromHex","colorFromRgb","colorFromString",
 "connect","connectPublisher","contentBox","cookie","coords","create"
 ,"declare","delegate",
 "deprecated","destroy","disconnect","empty","eval","every","exists",
 "experimental","extend","fadeIn","fadeOut","fieldToObject","filter",
 "fixEvent",
 "forEach","formToJson","formToObject","formToQuery","fromJson",
 "getComputedStyle",
 "getNodeProp","getObject","hasAttr","hasClass","hitch","indexOf",
 "isAlien","isArray","isArrayLike",
 "isCopyKey","isDescendant",
 "isFunction","isObject","isString","lastIndexOf",
 "loadInit","loaded",
 "map","marginBox","mixin","moduleUrl","objectToQuery","partial","place",
 "platformRequire",
 "position","provide","publish","query","queryToObject","rawXhrPost",
 "rawXhrPut","ready",
 "registerModulePath","removeAttr","removeClass","replace","require",
 "requireAfterIf","requireIf",
 "requireLocalization","safeMixin","setContext","setObject",
 "setSelectable","some","stopEvent",
 "style","subscribe","toJson","toggleClass","trim","unloaded",
 "unsubscribe","when","windowUnloaded",
 "withDoc","withGlobal","xdRequireLocalization","xhr","xhrDelete",
 "xhrGet","xhrPost","xhrPut"
 };
 var outputDir = new System.IO.DirectoryInfo(@"g:\docs\dojo\");
 var maxGets = 50;
 var sb = new System.Text.StringBuilder();
 for (var i = 0; i= --maxGets) break;
   } catch (WebException ex) {
    ex.Dump(mods[i]);
    if (ex.Message.Contains("(503)")) {
     System.Threading.Thread.Sleep(60 * 60 * 1000);
    }    
   }
   System.Threading.Thread.Sleep(5000);
  }
 }
 System.IO.File.WriteAllText(System.IO.Path.Combine(outputDir.FullName, "dojo.methods.txt"), sb.ToString());
}

And finally I compressed "dojo.methods.txt" into a zip file and emailed it to myself@kindle.com. Now I have something to read tonight.

Monday, January 17, 2011

Dojo Dijit.Tree

Building a Javascript Object Explorer using the dojo Tree dijit.


Chapter 15 of Dojo: The Definitive Guide give a nice description of the Tree dijit using a TreeStoreModel or ItemFileReadStore.

Regarding the TreeStoreModel, Matthew Russell notes that "anything that presents this interface is just a valid model as the TreeStoreModel".


His comment give the answer to how one would build an object explorer.


The interface methods identified for readonly viewing are:

  • getRoot
  • mayHaveChildren
  • getChildren
  • getIdentity
  • getLabel


The interface was documented in the book but can also be found in the source code:

  • getRoot: function(onItem, onError)
  • mayHaveChildren: function(/*dojo.data.Item*/ item)
  • getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError)
  • getIdentity: function(/* item */ item)
  • getLabel: function(/*dojo.data.Item*/ item)


Armed with this information is was just a matter of writing the code:


var model = (function (args) {
    if (dojo.isString(args)) args = { root: args };
    args.root = args.root || "window";
    args.identifier = args.identifier || "id";
    args.label = args.label || "name";
    if (dojo.isString(args.root)) {
        var root = { parent: null };
        root[args.identifier] = args.root;
        root[args.label] = args.root;
        args.root = root;
    }
    if (!args.root.parent) {
        var parentPath = args.root[args.identifier].split(".");
        parentPath = parentPath.splice(0, parentPath.length - 1);
        parentPath = parentPath.join(".") || "dojo.global";
        args.root.parent = dojo.getObject(parentPath);
    }
    var result = {
        args: args,
        _getValue: function (item) {
            if (!item) return undefined;
            return dojo.getObject(item[args.identifier], false, args.root.parent);
        },
        getRoot: function (f1, f2) {
            f1(args.root);
        },
        mayHaveChildren: function (item) {
            var value = this._getValue(item);
            return dojo.isObject(value);
        },
        getIdentity: function (item) {
            return item[args.identifier];
        },
        getLabel: function (item) {
            return item[args.label];
        },
        getChildren: function (parentItem, callback) {
            var p = this._getValue(parentItem), children = [], itemName, child;
            for (itemName in p) {
                try {
                    child = { };
                    child[args.label] = itemName;
                    child[args.identifier] = parentItem[args.identifier] + '.' + itemName;
                    children.push(child);
                } catch (ex) {
                    console.log(ex);
                }
            }
            callback(children);
        }
    }
    return result;
} ("window"));

The args.identifier and args.label are in the interest of generality and don't add much value. Likewise _getValue requires the root parent and fully qualified identifiers. The alternative is to capture the value in the getChildren. Eliminate these features and the code is cut in half.


Links:
Nightly Build Tree Tests
Dojo Dictionary

Friday, January 14, 2011

ArcGIS Online Javascript API - Compact Build vs Standard Build

ESRI has (at least) two editions of the ArcGIS Online Javascript API.  One of those is the "compact" edition:

  1. http://serverapi.arcgisonline.com/jsapi/arcgis/?v=2.1 (550k)
  2. http://serverapi.arcgisonline.com/jsapi/arcgis/?v=2.1compact (355k)

Other than 200k, what is the difference?

The compact edition exclusively contains "esri.dijit.InfoWindowLite" whereas the basic edition contains (exclusively) the following:

"dijit._base", "dijit._base.focus", "dijit._base.place", "dijit._base.popup", "dijit._base.scroll", "dijit._base.sniff", "dijit._base.typematic", "dijit._base.wai", "dijit._base.window", "dijit._Container", "dijit._CssStateMixin", "dijit._HasDropDown", "dijit._Templated", "dijit._Widget", "dijit.form._FormWidget", "dijit.form.Button", "dijit.form.HorizontalRule", "dijit.form.HorizontalRuleLabels", "dijit.form.HorizontalSlider", "dijit.form.VerticalRule", "dijit.form.VerticalRuleLabels", "dijit.form.VerticalSlider", "dojo.AdapterRegistry", "dojo.cache", "dojo.cldr.nls.gregorian", "dojo.cldr.nls.gregorian.en_us", "dojo.cldr.nls.number", "dojo.cldr.nls.number.en_us", "dojo.cldr.supplemental", "dojo.date.locale", "dojo.date.stamp", "dojo.dnd.autoscroll", "dojo.dnd.common", "dojo.dnd.move", "dojo.dnd.Moveable", "dojo.dnd.Mover", "dojo.number", "dojo.parser", "dojo.regexp", "dojo.uacss", "dojo.window", "esri.dijit.InfoWindow", "esri.layers.agsimageservice", "esri.tasks._task", "esri.tasks.find", "esri.tasks.geometry", "esri.tasks.gp", "esri.tasks.identify", "esri.tasks.locator", "esri.tasks.na", "esri.tasks.query", "esri.tasks.route", "esri.toolbars._toolbar", "esri.toolbars.draw", "esri.toolbars.navigation", "esri.virtualearth.VEGeocoder", "esri.virtualearth.VETiledLayer"

How do I know this?

First, load the basic IDE:

(function() {
    var url = "http://serverapi.arcgisonline.com/jsapi/arcgis/?v=2.1",
        init = function() {
            console.log("Restore djConfig, AOL may need it, ESRI does not");
            djConfig = dojo.config;
            coretech.ext.inspect("esri21fDojo");
        };


        djConfig = {
            scopeMap: [
                ["dojo", "esri21fDojo"],
                ["dijit", "esri21fDijit"],
                ["dojox", "esri21fDojox"]
            ],
            addOnLoad: init
        };
        $LAB.script(url);
}());

Second, load the compact IDE:

(function() {
    var url = "http://serverapi.arcgisonline.com/jsapi/arcgis/?v=2.1compact",
        init = function() {
            console.log("Restore djConfig, AOL may need it, ESRI does not");
            djConfig = dojo.config;
            coretech.ext.inspect("esri21cDojo");
        };

        djConfig = {
            scopeMap: [
                ["dojo", "esri21cDojo"],
                ["dijit", "esri21cDijit"],
                ["dojox", "esri21cDojox"]
            ],
            addOnLoad: init
        };
        $LAB.script(url);
}());

Finally, compare the two:

var compact = esri21cDojo._loadedModules;var full = esri21Dojo._loadedModules;
var compactOnly = [];
for (var c in compact) {
  if (undefined === full[c]) compactOnly.push(c);
}
compactOnly.push("---------------------------------");
for (var f in full) {
  if (undefined === compact[f]) compactOnly.push(f);
}
dojo.toJson(compactOnly, true);

If you run the compact JS code through a beautifier you can see that InfoWindowLite declares a InfoWindow, presumably customized for smaller screens but also to avoid any dijit dependencies (there is still a dependency on dijit._base.manager):

    if (!dojo._hasResource["esri.dijit.InfoWindowLite"]) {
        dojo._hasResource["esri.dijit.InfoWindowLite"] = true;
        dojo.provide("esri.dijit.InfoWindowLite");
        dojo.declare("esri.dijit.InfoWindow", null, {...


Links


Wednesday, January 12, 2011

Using AOL Dojo CDN with ArcGIS Javascript API

There are at least two problems with mixing a CDN with the ArcGIS Javascript API that may be mentioned in these posts:

1. You cannot close the window without getting errors.

2. You cannot add the map to a widget created from the CDN.


This snippet address #1 by wrapping the esriDijit.findWidgets to only return defined widgets:

(function () {
  var findWidgets = esriDijit.findWidgets;
  esriDijit.findWidgets = function (x) {
    var r1 = findWidgets(x), r2 = [];
    for (var i = 0; i < r1.length; i++) {
      if (r1[i]) r2.push(r1[i]);
    }
    return r2;
  };
})();

Widgets created outside of the esriWidget scope are null in the original result.  Without this patch the calling code will fail when attempting to operate on the null values.  

I don't have a clean solution to the second problem but it is possible as long as you clean-up the ESRI widgets before the CDN widgets.  In my case I added the map to layout control:

  var mapNode = esriDojo.doc.createElement("div");
  mapNode.id = "map";
  var containerNode = new esriDijit.layout.ContentPane({ title: "ESRI Map" });
  containerNode.setContent(mapNode);
  dijit.byId("tabs").addChild(containerNode);
  var esriMap = new esri.Map("map", { extent: extent });

To avoid errors on closing the window clean up the widgets:

  esriMap.destroy();
  dijit.byId("tabs").removeChild(containerNode);
  containerNode.destroy();

Related links:

Tuesday, January 4, 2011

Building Dojo Localization Scripts

Building jsDojo Localization Scripts

It seems the only way to compile localization scripts is to require them in a module and then build the module:

Define a module:

dojo.provide("happy.BirthdayWidget");
dojo.requireLocalization("happy", "birthday");
dojo.declare("happy.BirthdayWidget", null, {
    constructor: function () {
        // do something with HappyBirthday
        var nls = dojo.i18n.getLocalization("happy", "birthday");
        this._text = nls.HappyBirthday;
        alert(this._text);
    }

});

Create a profile referencing this module:

dependencies = {
layers: [
{
name: "dojolab.js", 
dependencies: ["dijit.Dialog"]
},
{
name: "happy.js", 
dependencies: ["happy.BirthdayWidget"],
layerDependencies: ["dojolab.js"]
}
],
prefixes: 
[
["dijit", "../dijit"],
["happy", "C:/Users/calix/Documents/Visual Studio 2010/Projects/DojoLab/DojoLab/happy"]
]
};

Build the layers in this profile:

build profile=dojolab action=release

Move the product files from dojo\release\dojo\dojo\ and dojo\release\dojo\dojo\nls into a "scripts" folder:

    <script src="scripts/dojolab.js.uncompressed.js" type="text/javascript"></script>
    <script src="scripts/happy.js.uncompressed.js" type="text/javascript"></script>

Tell dojo where to find the localization files:

    <script type="text/javascript">
        var djConfig = {
            useXDomain: false,
            baseUrl: "http://localhost:52987",
            modulePaths: {
                "happy": "/happy",
                "dojo.nls.happy_en-us": "/scripts/happy_en-us",
                "dojo.nls.dojolab_en-us": "/scripts/dojolab_en-us"
            }
        };
    </script>

The former development code which invoked requireLocalization and require is now essentially ignored because the modules are already loaded:

    <script type="text/javascript">
        dojo.require("dojo.i18n");
        dojo.requireLocalization("happy", "birthday");
        dojo.require("dijit.Dialog");
        dojo.require("happy.BirthdayWidget");
        dojo.addOnLoad(function () {
            var nls = dojo.i18n.getLocalization("happy", "birthday");
            var text = nls.HappyBirthday;
            var dialog = new dijit.Dialog({ content: text });
            dialog.show();
            var w = new happy.BirthdayWidget();
            dialog.setContent("w: " + w._text);
        });
    </script>


Dissecting Dojo Localization

Dissecting jsDojo Localization

Consider dojo.requireLocalization(moduleName, bundleName, locale, availableFlatLocales).

What does dojo.requireLocalization("happy", "birthday") do?  It depends...

In a development environment there will be no parameter provided for availableFlatLocales.  The moduleName is resolved against the module prefix provide via djConfig.modulePaths.

Oddly a "/" is expected to terminate the path, with code explicitly adds one if it's missing.  In this case "/happy" become "/happy/".  But providing "/happy/" causes an extra "/", which causes failure with some servers.  Specifically, i18n._requireLocalization invokes i18n._searchLocalePath with a search function which contains this code:


var syms = dojo._getModuleSymbols(moduleName);
var modpath = syms.concat("nls").join("/");

Don't provide a trailing slash in your modulePaths definitions:


        var djConfig = {
            useXDomain: false,
            baseUrl: "http://localhost:52987",
            modulePaths: {
                "happy": "/happy", // do not add a "/" to the end
                "dojo.nls.dojolab_en-us": "/scripts/dojolab_en-us"
            }
        };

If the path does not begin with a "/" the djConfig.baseUrl is prepended.  If the resulting path contains "http://" it is considered an xdomain path.  Otherwise, if the baseUrl contains "http://" and the baseUrl is not in the host domain it is again considered an xdomain path.

If it *is not* considered an xdomain path...

dojo._xdRealRequireLocalization is a pointer to the original implementation of dojo.requireLocalization, which is later replaced with a wrapper.  When not an xdomain path the original/real code is executed, which itself is a thin wrapper for i18n._requireLocalization.

Consider i18n._requireLocalization(moduleName, bundleName, locale, availableFlatLocales).

If no locale is provided dojo.locale is assumed.  A bundle is constructed with the name moduleName.nls.bundleName.  In this case the bundle is "happy.nls.birthday".

In a development environment availableFlatLocales is not provided.  When availableFlatLocales is provided a "best locale" calculation finds the closest match to the locale (en-us may resolve to en), otherwise the actual locale is used.

If the bundle is not found in the list of loaded modules, the bundle is added to the list of loaded modules via dojo.provide.  A module path is calculated by appending a "nls" after the symbol path.  In this case that would be "/happy/nls" (or "/happy//nls" if you did not heed my warning).

It now attempts to get the localization files for the locale.  If the locale is "en-us" this means searching for "ROOT", "en", "en-us".

If "happy.nls.birthday.ROOT" is not already registered as a loaded module it is registered and the associated script is loaded.  In this case it looks for "/happy/nls/birthday.js".  It repeats for en and en-us, loading "/happy/nls/en/birthday.js", "/happy/nls/en-us/birthday.js".  The resulting bundle contains a property for each of ROOT, en, en-us.  In this case the bundle has no ROOT and no en values because resources were only provided for en-us:

{"ROOT":undefined,"en":undefined,"en_us":{"HappyBirthday":"Happy Birthday!"}}

Requesting localization for "en-us" results in three trips to the server to construct a bundle containing resources for ROOT, en, en-us.

Is it possible to do this in one request?

Consider i18n._preloadLocalizations(bundlePrefix, localesGenerated).

This method also invokes _searchLocalePath, but with down=true so instead of looking for ROOT, en, en-us it reverse the search order.  It first looks for and loads the "dojo.nls.dojolab_en-us" module.  If no module path is provided for "dojo.nls.dojolab_en-us" or "dojo.nls" it finds "dojo" and resolves to the CDN, in this case "http://o.aolcdn.com/dojo/1.5.0/dojo/nls/dojolab_en-us.js".

Provide a module path for "dojo.nls" or "dojo.nls.dojolab_en-us" or explicitly include the localization files with a <script>.

If i18n._preloadLocalizations finds a localization file it immediately stops searching for more.  The assumption being that the en-us file must contain the full spectrum of resource text and does not need to get any data from en or ROOT.  This happens because i18n._preloadLocalizations assumes the most specific localization file contains all the resources.  This method is reserved for compiled code and rightly so.  The specific localization file contains resources for multiple namespaces when generated via the build:


dojo.provide("dojo.nls.dojolab_en-us");


dojo.provide("dijit.nls.loading");
dijit.nls.loading._built = true;
dojo.provide("dijit.nls.loading.en_us");
dijit.nls.loading.en_us = {
    "loadingState": "Loading...",
    "errorState": "Sorry, an error occurred"
};


dojo.provide("dijit.nls.common");
dijit.nls.common._built = true;
dojo.provide("dijit.nls.common.en_us");
dijit.nls.common.en_us = {
    "buttonOk": "OK",
    "buttonCancel": "Cancel",
    "buttonSave": "Save",
    "itemClose": "Close"
};


When dojo.requireLocalization is invoked a bundle is created via a callback to dojo._loadPath, which eval's the localization file and passes the result to the callback.   The callback adds this result to the bundle.  That doesn't make sense when resources are bundled together, as with the build resources.  In that case dojo._loadPath is invoked indirectly from _preloadLocalizations via dojo.require and no callback is provided.

Observation -- compiled resources are modules and are loaded via dojo.require and not dojo.requireLocalization.

When not in an xdomain environment it appears to be the case that the only way to eliminate multiple trips to the server for resources is to perform a custom build.  I suspect the same is true for xdomain loading, in which case the conclusion is that in order to avoid multiple trips to the server for a resource it is necessary to perform a custom build.

If it *is* an xdomain path...

not sure yet.

Sunday, January 2, 2011

Using Dojo with Blogspot

I loaded dojo into this blogspot posting by editing the HTML directly and adding a <script> tag to load the AOL CDN of dojo.  The script editor did not allow me to include a <link> so I added it dynamically via a script I found here.

Using Dojo with BlogSpot -- Dojo Injection Example

Expand "Source Code" to view how this was done
The source for this posting is this posting!  I'm not sure what view source is going to look like so the basic technique is here:


<div class="claro">
  <div dojotype="dijit.layout.AccordionContainer">
    <div dojotype="dijit.layout.ContentPane" title="Section One">Section One</div>
    <div dojotype="dijit.layout.ContentPane" title="Section Two">Section Two</div>
  </div>
</div>
<script>
  var djConfig={addOnLoad: dojoReady, parseOnLoad: true};
  var COREBLOG = COREBLOG || {};
  var COREBLOG.injectCss = function (filename){
    var fileref=document.createElement("link");
    fileref.setAttribute("rel", "stylesheet");
    fileref.setAttribute("type", "text/css");
    fileref.setAttribute("href", filename);
    document.getElementsByTagName("head")[0].appendChild(fileref);
  };
  COREBLOG.injectCss("http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css");
</script>
<script src="http://o.aolcdn.com/dojo/1.5/dojo/dojo.xd.js"></script>
<script>
dojo.require("dijit.layout.AccordionContainer");
dojo.require("dijit.layout.ContentPane");
</script>