Understanding The Parser

One of the most common things to see in a Dojo page is a djConfig block like /* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .html4strict .imp {font-weight: bold; color: red;} .html4strict .kw1 {color: #b1b100;} .html4strict .kw2 {color: #000000; font-weight: bold;} .html4strict .kw3 {color: #000066;} .html4strict .coMULTI {color: #808080; font-style: italic;} .html4strict .es0 {color: #000099; font-weight: bold;} .html4strict .br0 {color: #66cc66;} .html4strict .st0 {color: #ff0000;} .html4strict .nu0 {color: #cc66cc;} .html4strict .sc0 {color: #00bbdd;} .html4strict .sc1 {color: #ddbb00;} .html4strict .sc2 {color: #009900;} djConfig="parseOnLoad: true" and later /* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .javascript .imp {font-weight: bold; color: red;} .javascript .kw1 {color: #000066; font-weight: bold;} .javascript .kw2 {color: #003366; font-weight: bold;} .javascript .kw3 {color: #000066;} .javascript .co1 {color: #009900; font-style: italic;} .javascript .coMULTI {color: #009900; font-style: italic;} .javascript .es0 {color: #000099; font-weight: bold;} .javascript .br0 {color: #66cc66;} .javascript .st0 {color: #3366CC;} .javascript .nu0 {color: #CC0000;} .javascript .me1 {color: #006600;} .javascript .re0 {color: #0066FF;} dojo.require("dojo.parser");. Together these lines include and enable Dojo's page parsing infrastructure. This machinery layers on top of dojo.query() to provide a way to declare instances of any class via markup in your page. To understand what we mean by that, let's take a simple example page which consists of a single tree backed by a JSON data store:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<script type="text/javascript" src="http://o.aolcdn.com/dojo/0.9.0/dojo/dojo.xd.js"
        djConfig="parseOnLoad: true">
</script>
<script type="text/javascript">
        dojo.require("dojo.data.JsonItemStore");
        dojo.require("dijit.Tree");
        dojo.require("dojo.parser");
        var countries = new dojo.data.JsonItemStore({ url: "countries.json" });
</script>
</head>
<body class="tundra">
        <div dojoType="dijit.Tree" store="countries" labelAttr="name" typeAttr="type"
           query="{type:'continent'}" >
</div>

In this case, we see the familiar use of the dojoType attribute to denote where an instance of our widget should be created. This is the functional equivalent of writing:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<script type="text/javascript" src="http://o.aolcdn.com/dojo/0.9.0/dojo/dojo.xd.js"
       djConfig="parseOnLoad: true">
</script>
<script type="text/javascript">
        dojo.require("dojo.data.JsonItemStore");
        dojo.require("dijit.Tree");
        dojo.require("dojo.parser");
    dojo.addOnLoad(
        var countries = new dojo.data.JsonItemStore({ url: "countries.json" });
        var tree = new dijit.Tree({
            store: countries,
            labelAttr: "name",
            typeAttr: "type",
            query: {type: "continent"}
        }, dojo.byId("treePlaceHolder"));
    });
</script>
</head>
<body class="tundra">
        <div id="treePlaceHolder"></div>

In fact, they're identical. The only difference is that in the first example, the work of locating and creating the widget instance is handed off to the Dojo parser. Fundamentally, this means that the parser is:

  • locating the nodes with dojoType attributes in the page
  • taking the attributes assigned to them and passing them into the constructor as properties on the configuration object
  • passing the source node for the widget as the second parameter to the constructor

There's nothing about this process (except perhaps the passing of the node) which should be specific to widgets, and indeed the Dojo parser is equipped to create instances of any class. That means that we can revise our first example to be fully markup-driven:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<script type="text/javascript" src="http://o.aolcdn.com/dojo/0.9.0/dojo/dojo.xd.js"
        djConfig="parseOnLoad: true">
</script>
<script type="text/javascript">
        dojo.require("dojo.data.JsonItemStore");
        dojo.require("dijit.Tree");
        dojo.require("dojo.parser");
</script>
</head>
<body class="tundra">
        <div dojoType="dojo.data.JsonItemStore" url="countries.json" jsId="countries"></div>
        <div dojoType="dijit.Tree" store="countries" labelAttr="name" typeAttr="type"
           query="{type:'continent'}" >
</div>

So how does this work? What happens to the source node in the resulting page when what we're creating isn't a widget? And what about the seemingly magical properties dojoType and jsId?

The Parsing Algorithm

To fully understand the parser, it's important to understand it's operation in broad terms. The parser operates by:

  • Searching the document for elements with a dojoType attribute. This search is linear and nodes are returned in document order.
  • Iterating over all matching nodes, attempting to match the declared type with an available class to instantiate.
    • If a class is found, the parser iterates over attributes of the class's prototype and populates the arguments object from the values of attributes on the source node of the same name. Lightweight type conversion is performed.
    • If a markup factory is found for the class, it is used to create a new instance to return
    • If no markup factory is found, the class is constructed using the new operator. The arguments to the constructor are assumed to be in the form /* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .javascript .imp {font-weight: bold; color: red;} .javascript .kw1 {color: #000066; font-weight: bold;} .javascript .kw2 {color: #003366; font-weight: bold;} .javascript .kw3 {color: #000066;} .javascript .co1 {color: #009900; font-style: italic;} .javascript .coMULTI {color: #009900; font-style: italic;} .javascript .es0 {color: #000099; font-weight: bold;} .javascript .br0 {color: #66cc66;} .javascript .st0 {color: #3366CC;} .javascript .nu0 {color: #CC0000;} .javascript .me1 {color: #006600;} .javascript .re0 {color: #0066FF;} new someClass(argumentsObj, sourceNode);
  • Events from markup are attached (although some may have been handled earlier)

The above process alludes to some features of the parser which we haven't seen in action yet. Here's a more sophisticated example. We create a class called "example.Class". The parser runs and creates an instance of this class (the div dojoType="example.Class"), which you can then access through the global variable "thinger."

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<script type="text/javascript" src="http://o.aolcdn.com/dojo/0.9.0/dojo/dojo.xd.js"
        djConfig="parseOnLoad: true">
</script>
<script type="text/javascript">
        dojo.require("dojo.parser");
        dojo.declare("example.Class", null, {
                constructor: function(args, node){
                        //  this class constructor is designed for the
                        // parser's "args, node" convention
                        dojo.mixin(this, args);
                },
        });
</script>
</head>
<body class="tundra">
        <div dojoType="example.Class" foo="bar" jsId="thinger">
                <script type="dojo/method">
                        // this block is executed as the class is created but
                        // after the class constructor is finished
                        console.debug(this.foo);   // Prints "bar"
                </script>
        </div>

Each script of type "dojo/method" is executed after the constructor runs. We saw examples of this in Part 1, Example 2.. Finally, the class constructor uses a mixin to copy the attributes from tag to properties in the instance. Thus thinger is created by calling the constructor with args = {foo: "bar"} and node as the div node itself. Foo is created as a property by mixin.

Type Conversions

Lightweight type conversions are done on the attribute values. The types are based on the property types used in the definition of the class. For example:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<script type="text/javascript" src="http://o.aolcdn.com/dojo/0.9.0/dojo/dojo.xd.js"
        djConfig="parseOnLoad: true">
</script>
<script type="text/javascript">
        dojo.require("dojo.parser");
        dojo.declare("example.Class", null, {
                constructor: function(args, node){
                        //  this class constructor is designed for the
                        // parser's "args, node" convention
                        dojo.mixin(this, args);
                },
                title: "Hello",
                isEnabled: true,
                dayCount: 45,
                onClick: function(){},
                names: ["Monday", "Tuesday", "Wednesday"],
                startDate: new Date()
        });
</script>
</head>
<body class="tundra">
        <div dojoType="example.Class" title="Good Morning" isEnabled="false" dayCount="4" onClick="alert(thinger.dayCount)" names="Thursday, Friday" startDate="2008-01-01" jsId="thinger">
                <script type="dojo/method">
                        // this block is executed as the class is created but
                        // after the class constructor is finished
                        console.debug(this.foo);   // Prints "bar"
                </script>
        </div>

In this example, the attributes will be converted to their correponding types that were used in the definition of example.Class:

  • title: String
  • isEnabled: Boolean
  • dayCount: Number
  • onClick: Function. Specify the function body in the attribute.
  • names: Array. Separate the array members by commas. Array members are assumed to be strings.
  • startDate: Date. "now" can be used to get the current date, otherwise, dojo.date.stamp.fromISOString() will be used to convert the text string to a Date object.

If the property type does not match one of the types listed above, then dojo.fromJson() will be used to convert the attribute value.

Markup Factory

If the class declares a method with the name markupFactory, that function will be used to create the object instance, instead of the constructor. This is useful if the class has special initialization for instances created via markup, versus instances created in script via the class constructor. An example class that defines a markupFactory method:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<script type="text/javascript" src="http://o.aolcdn.com/dojo/0.9.0/dojo/dojo.xd.js"
        djConfig="parseOnLoad: true">
</script>
<script type="text/javascript">
        dojo.require("dojo.parser");
        dojo.declare("example.Class", null, {
                constructor: function(args, node){
                        //  this class constructor is designed for the
                        // parser's "args, node" convention
                        dojo.mixin(this, args);
                },
                title: "Hello",
                isEnabled: true,
                dayCount: 45,
                onClick: function(){},
                names: ["Monday", "Tuesday", "Wednesday"],
                startDate: new Date(),
                markupFactory: function(params, domNode, constructorFunction){
                        //params: object that contains the markup attribute values,
                        //with type conversion already completed.
                        //domNode: the DOM node (the div in the code below)
                        //constructorFunction: The constructor function matching
                        //the dojoType in markup. In this example, example.Class
                        var instance = new constructorFunction(params, domNode);
                        //Do special initialization intialization here
                        return instance;
                }
        });
</script>
</head>
<body class="tundra">
        <div dojoType="example.Class" title="Good Morning" isEnabled="false" dayCount="4" onClick="alert(thinger.dayCount)" names="Thursday, Friday" startDate="2008-01-01" jsId="thinger">
                <script type="dojo/method">
                        // this block is executed as the class is created but
                        // after the class constructor is finished
                        console.debug(this.foo);   // Prints "bar"
                </script>
        </div>