Andy Tso gets a call from Regina Opulence, the head of the new Internet Startup clickdammit.com. "I need to have a mail web site," she said, "one like Google Mail. Can you make one up in a week?"
"Hmmmm." said Andy, "What's your business plan, if I may ask?"
"It's brilliant." she said. "Our backend servers will read the mail they send back and forth, then send them ads based on the things they talk about!"
Andy rolled his eyes to the ceiling. It would do no good to talk about privacy issues, legality, or all that other nonsense. He was just a tech guy, and Regina wanted him to do techie-type things. At least Clickdammit.com paid well. "Lemme give it a whirl. I think I can get you a pretty good demo in a week. "
He hung up the phone and wondered ... how could he get this demo done and propose to his longtime girlfriend on Friday night? He still had to write the speech, after all.
Fortunately there's Dojo!
So here's the cocktail napkin view of the email client:
Like most Outlook-inspired email programs, it will have a three pane, split-screen layout with a toolbar on top and status bar on the bottom. The left hand side will use accordion panes that flip up and down like a window blinds. Fortunately, all the layout tools you need are already in Dijit.
Andy likes to build the layout from the inside-out best, so he'll tackle the steps in this order:
start with the message list and message area first. In between these two is the movable divider bar, called the sizer. In Dijit, you can use the dijit.layout.SplitContainer widget to model this like so:
/* 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;}Usually the innermost children in a layout are dijit.layout.ContentPane's. You can place arbitrary HTML in the pane, as we've done here, or lazy load the content from other URL's, like a server-side include.
The dijit.layout.SplitContainer itself can be oriented horiztonally or vertically, as the "orientation" attribute shows. A vertically oriented SplitContainer means the components are stacked vertically. Note that the orientation is opposite of the sizer bar orientation - i.e. if the SplitContainer is vertical, as it is in this case, the sizer orientation is horizontal. The sizer bar here is 5 pixels and changing it does not change the content immediately, as the activeSizing attribute shows.
The left hand bar is an Accordion Container, which is easier to show than to describe. In Dijit, the dijit.layout.AccordionContainer holds dijit.layout.AccordionPane's, a special kind of ContentPane.
/* 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;}The title attributes give the label for the top of the AccordionPane. When the user clicks on this title or the arrow icon next to it, the pane will slide into view.
Now we can glue these two pieces together with another SplitContainer, this one oriented horizontally:
/* 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;}The whole thing will be situated in a tab named Inbox. Though our cocktail napkin drawing doesn't show other tabs, the idea is to open them for new email composition. This way you can work on many emails at once, clicking on the tabs to switch between them. Dijit has a widget dijit.layout.TabContainer, and like the Split and Accordion containers, it's a container for subscontainers and panes.
/* 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;}The dijit.Layout.TabContainer can hold as many tabs as you want, each marked with a title attribute used to title the tab. This illustrates a familiar pattern in Dijit. Sometimes a widget contains attributes that don't really deal with the widget itself - title, for instance, or sizeMin and sizeShare in earlier examples. They only make sense when that widget is used within other widgets. So an AccordionContainer ignores a sizeMin attribute, but because it's a child of a SplitContainer, the SplitContainer uses it for sizing. Title is similar. In a normal HTML tag, title would display a tooltip, but because the SpilitContainer is inside a TabContainer, the title is used specially.
What is that jsId attribute? jsId sets a global JavaScript variable for the widget, which will make it easy to script widget actions later. For example:
// Note used for step 1 - this is a preview tabs.closeChild(tabs.selectedChildWidget);
closes the currently selected tab in the tab container. Without even knowing the Dijit API's, it's easy to understand what's going on here. That's object-oriented magic at work.
To top it all off, we smush these tabs into a dijit.layout.LayoutContainer. A LayoutContainer was pioneered by Borland Delphi and copied in Java AWT and other toolkits. The idea is to split a box into five areas: top, bottom, left, right, and client (the middle). If the box is resized, the client area takes most of the growth or shrinkage - it's the document part of the app.
In our email client, the toolbar will occupy the top, the status bar the bottom, and the TabContainer the client portion:
/* 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;}The layoutAlign attributes tell LayoutContainer where they should be placed.
Now Andy just adds the standard Dojo headers and the skeleton is complete! /* 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;}
There's no JavaScript at all beyond the dojo.require statements. And because he's using CDN, he didn't even have to install Dojo on the server. While Andy takes a moment to write more of his empassioned speech, you can download the code in step1.html below and try it out.
Ready? On to the next step...
Andy thinks, "That was easy. Now what's the next easiest thing to do?" The left-hand side panes, Address Book and Folders, will each have a tree. The Folders tree will have a heirarchy - folders within folders - but the Address Book is nice and flat. Andy figures he'll use the Address Book to learn Dijit Trees, then use his knowledge on the more complex Folders pane later. Here's the cocktail napkin view:
[inline:mail_step2.png]Back in the old days (i.e. 5 minutes ago), Andy would have drawn up a PHP file that sent all the markup for the Address Book tree - the folder IMG tags, the labels, etc. - over the wire. While this approach worked fine in the old days, he has learned it doesn't work well for Web 2.0 applications like this one. Why?
He decides to leave the architecture questions alone for a moment, and looks at the Dijit catalog entry for Tree. The easiest data-driven way to approach this uses dojo.data. Dojo.data reminds Andy of ODBC in the Windows world, JDBC in Java, and DBD in PHP (Andy gets around.) Though dojo.data can talk to many different kinds of backing stores formatted in CSV, XML or HTML, Andy picks JavaScript Object Notation, or JSON. A JSON representation of the address book would look like this:
{
identifier: 'id',
label: 'label',
items: [
{ type: 'address', id: 'adam', label: "Adam Arlen" },
{ type: 'address', id: 'bob', label: "Bob Baxter" },
{ type: 'address', id: 'carrie', label: "Carrie Crow" }
]
}
JSON nicely represents JavaScript objects, bracketed by { and }, and arrays bracketed by [ and ]. The attributes identifier and label conform to dojo.data's metadata specifications. Dojo.data uses the drivers dojo.data.ItemFileReadStore and dojo.data.ItemFileWriteStore to read JSON data in this format.
Andy thinks, "it'll be a snap to write a PHP for that format!" For the demo, he will simply use the above file as mail/mail.json. If the project moves past the demo phase, he can replace it with a PHP that reads mail on the database server and outputs JSON. Good deal.
To connect his demo to the data store, he inserts this below the BODY tag:
/* 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;}He has decided to use the ItemFileWriteStore to write entries back later (maybe not in the demo, but later on.)
Now drawing the tree is a snap using dijit.Tree:
/* 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;}The store attribute connects to the jsId attribute on the ItemFileWriteStore. LabelAttr tells which attribute is used for the labels. Finally, the query tells dojo.data which entries to retrieve. You can equate a dojo.data query with a SQL query - it works with any backing store that dojo.data understands. As you can see here, a dojo.data query is much simpler than a SQL query. Sorting and compound selection are available for later, but for now this simple query will do fine.
But wow ... what's up with that SCRIPT tag? What is type "dojo/method"?
That came about when Andy learned about tree icons ... the hard way. His first stab at dijit.Tree drew a nice address book but no icons. That wouldn't do. Clickdammit.com really, really likes icons. The trouble is there might be different icons based on the address book entry type.
At first Andy thought he'd have to modify dijit.Tree ... good thing Dojo is open source, right? But he'd been down that road before. You start monkeying around with changing the source and it becomes a never-ending struggle with revision control. So he read over the Dijit catalog entry for Tree and found:
[inline:mail_step2a.png]An extension point is a point at which you can add, remove or replace functionality. The Dijit designers have built lots of extension points so you can change Dijit instances to your heart's content. And that's what Andy did.
First he looked up the signature for the extension point in the API guide: dijit.Tree.getIconClass:
getIconClass: function(/*dojo.data.Item*/ item){
// summary: user overridable function to return CSS class name to display icon
},
Since each of the address book entries was a dojo.data.Item, he had no problem passing something in. Now he can replace the guts of the SCRIPT tag with JavaScript code to read the icon attribute and turn it into a CSS class name. That class name points to the right icon specified in mail/mail.css. Problem solved!
One note about that "item &&" phrase. In Dojo 1.0, Trees always have one and only one root node. There is nothing in dojo.data that specifies items as root nodes, and thus the first item passed to getIconClass is always null. By saying "item &&" we take advantage of JavaScript's short-circuit && (and) operator. It says "if item is a false-y value (= false, 0, null), stop here and don't evaluate what's after the &&."
"Cool!" thought Andy. Extension points made it easy to change widget behavior, and dojo/method calls did the nasty job of wiring JavaScript code.
And with a few more header entries:
dojo.require("dojo.data.ItemFileWriteStore");
dojo.require("dijit.Tree");
step2.html is now a working address book. If you don't believe it, download it and try it yourself! Then we'll move right along...
Pretty nice so far, but this app is long on displaying, short on interaction. So next Andy decides to add some toolbars, tooltips and dialog boxes. "Surely this will involve lots of JavaScript," he thinks.
Here's the cocktail napkin view of the toolbar:
[inline:mail_step3a.png]We already have the dijit.Toolbar widget in the app as a placeholder. Dijit.toolbar expects its immediate children to be buttons:
The New Mail button is a ComboButton:
/* 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;}The dijit.Tooltip does what you think it should - it displays a message when the user hovers over the button. It knows this by connecting the connectId to the id attribute of the widget.
At the heart of a ComboButton or DropDownButton is a dijit.Menu, itself composed of dijit.MenuItem objects. Andy chooses not to wire the items - Yahoo and GMail - to an event handler yet. But he does tie the onClick handler of the ComboButton itself.
Like for the Tree, Andy uses a dojo/method script to fire off a snippet of JavaScript. In this case, it's a stubbed out function called fakeDownload(). But wait! Can't you just do this? /* 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;}
Yes, but you wouldn't want to. Using dojo/method and connecting to the extension point gets you the DOM Level 2 event model for free. DOM Level 2 events pass much more information to the event handler. And before you ask ... yes, this even works in Internet Explorer, which doesn't natively support the DOM Level 2 event model. Dojo adds a nice layer which emulates the model on IE.
Sweet! That's cross-browser functionality for free. Dojo is really, really good at that.
So let's look at the other buttons:
/* 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;}There's nothing here we haven't seen before ... except the call to optionsDialog. This displays a dialog box, which should look like this:
[inline:mail_step3b.png]Now this looks like a regular HTML form, and in fact we'll code it as if it were. But ... we'll use a dijit.Dialog to do it. The good news is that unlike HTML forms, a dijit.Dialog acts modally, leaving all the information and page state alone while the user fills in the dialog details. So the server interaction happens in the background.
The code for the dialog box goes at the bottom of our app, just above the /BODY tag, separated away from all the other HTML:
/* 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;}You can look at dijit.Dialog as a mini-form, with Dijit form elements sprinkled into it. Note, we don't do anything with the data here, but a finished app would save the settings and communicate them back to the server.
Andy places the dojo.require's in the header
dojo.require("dijit.form.Button");
dojo.require("dijit.Menu");
dojo.require("dijit.Tooltip");
dojo.require("dijit.Dialog");
dojo.require("dijit.form.ComboBox");
dojo.require("dijit.form.CheckBox");
dojo.require("dijit.form.FilteringSelect");
dojo.require("dijit.form.Textarea");
And step 3 is done. "Wow," Andy thought, "that's a lot less JavaScript than I thought." To tell the truth, there is a lot of JavaScript behind it, but you didn't have to write it. Dojo has it all!
Next, Andy needs a Compose Mail window that looks like this:
[inline:mail_step4.png]Hmmmm, composing HTML email looks like it might be a problem. There's that huge toolbar and all those display stuff. Well, to be honest, with Dojo that's the easiest part of this step. Simply write a template like this:
/* 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;}And save it in the file mail/newMail.html on your server. The dijit.Editor widget sets up the word-processor like environment for you, complete with toolbar, accelerator keys, and a fairly full array of HTML tools. The LinkDialog plugin adds an "Add Link..." button to the editor. Wow! It's a 5 line word processor. Andy mentally files this away - it could come in handy for composing his heartfelt speech later!
Using Dijit components is fun in a geeky sorta way. Still, Andy was one of those kids who got bored with Lego's ... until he made new ones with a band saw and a soldering iron.
If there were only one Compose Mail screen at a time, he could model it as a single tab and show it when necessary. But we need more than one. Andy thinks ... if one could just make a Compose Mail widget, then you can create as many instances as you want.
You can do that! And just like you can create widget instances declaratively or programmatically, so you can create new widget classes declratively or programmatically. As you might guess, the declarative way is easier. You simply pull out the dijit.Declaration widget.
Dijit.Declaration defines a widget class using a familiar macro-like template. Let's take a look:
/* 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;}The HTML defined inside the dijit.Declaration tagis dropped in wherever the widget class "mail.newMessage" is requested. You could, in fact, drop one in like this:
/* 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;}You pass parameters to your new widget by using:
This declarative usage is neat, but not helpful in our case since we don't know how many newMessage components to create. Instead, we create instances programmatically using the Compose Mail button:
/* 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;}This new code creates a mail.NewMessage programmatically, then creates a Tab out of it and adds it to the TabContainer (specified by the JavaScript variable newTabs)
Add some declarations and a few supplemental variables to the header:
dojo.require("dijit.Declaration");
dojo.require("dijit.Editor");
dojo.require("dijit._editor.plugins.LinkDialog");
// Global var for new mail pane
var paneId=1;
// for "new message" tab closing
function testClose(pane,tab){
return confirm("Are you sure you want to leave your changes?");
}
And now we can compose mail to people.
The people who run clickdammit.com are cheapskates. Their servers are Pentium Pros bought off Ebay in exchange for some Beanie babies. So Andy knows that users will click "Get Mail" and wonder what's taking so long.
This is a common problem in Web 2.0 applications. Hopefully your servers are faster than Pentium Pros, but even then you can't always be sure when response times will be slow. Since you're not submitting the form, you can't rely on the browser's spinning logo to tell the user something's happening.
Fortunately, Dijit has a progress bar for those situations. Andy doesn't have access to clickdammit's mail server yet, but he decides to sketch out the progress bar and write some Wait loops behind them to simulate delays.
First he places the progress bar where the placeholder text was:
/* 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;}We make this progress bar hidden so it can be called up when needed. At first the progress will be incalculable since we don't know the number of messages to download. That's what "indeterminate=true" shows.
We have already stubbed out fakeDownload() and connected it to the Get Mail button. Now we can actually add some code to it.
var fakeDownload = function(){
dojo.byId('fetchMail').style.visibility='visible';
numMails = Math.floor(Math.random()*10)+1;
dijit.byId('fakeFetch').update({ maximum: numMails, progress:0 });
dojo.fadeIn({ node: 'fetchMail', duration:300 }).play();
for (var i=0; i<=numMails; i++){
setTimeout(
"updateFetchStatus("+i+")",
((i+1)*(Math.floor(Math.random()*100)+400))
);
}
}
First we turn on the progress bar, then pick a random number (1 to 10) of emails to download. Now to update the progress bar ... Andy looks up the dijit.ProgressBar widget in the Dijit catalog and finds under Methods:
|
Methods
|
||
| update | update progress information | |
Update looks good, and he consults the example before writing the update statement.
dojo.fadeIn does what you think. It fades-in an object but turning up the opacity from 0% to 100%. The "300" here is the number of milliseconds fadeIn should take to complete its job. 0.3 seconds sounds small, but it's enough to make the animation noticeable without stopping the proceedings.
Any adds the updateFetchStatus function, which will fire once for every email.
var numMails;
var updateFetchStatus = function(x){
if (x == 0) {
dijit.byId('fakeFetch').update({ indeterminate: false });
return;
}
dijit.byId('fakeFetch').update({ progress: x });
if (x == numMails){
dojo.fadeOut({ node: 'fetchMail', duration:800,
// set progress back to indeterminate. we're cheating, because this
// doesn't actually have any data to "progress"
onEnd: function(){
dijit.byId('fakeFetch').update({ indeterminate: true });
// remove progress bar from tab order
dojo.byId('fetchMail').style.visibility='hidden';
}
}).play();
}
}
dojo.fadeOut looks a lot like fadeIn, but the onEnd property is new. It expects a function as its property. Here we add a function literal that removes the progress bar from the screen. But why put this in onEnd? Why not just put it after play()? Andy tried that first, and found the progress bar would blink out before it had completely faded. That's because fadeOut, like all Dojo animations, runs asynchronously. In other words, it kicks off the fadeOut and starts executing the next statement. FadeOut continues executing. So setting the duration to 800000000 would make the animation slow, but the browser would still be immediately usable.
Finally, we connect a function to ProgressBar's "report" extension point. Remember how we specified report="fakeReport" in the Progress Bar tag? Now we can fill it in. Report gets passed in a percent and expects a string to place on the progress bar.
var fakeReport = function(percent){
return "Fetching: "+(percent*this.maximum) + " of " + this.maximum + " messages.";
}
Andy clicks on the Get Mail and sits back. It looks darn good! He clicks it a few more times just to make sure. Very nice. We're getting pretty close to a working demo to show off. Just a few more things need to go in place.
JSON and dojo.data stores worked pretty well for address book data. Andy wonders whether they would work to send mail and folders too.
[inline:mail_step6.png]Not only is it possible ... you can pass all the data back in one fell swoop. This is possible because ItemFileReadStore and JSON handle heirarchical data with ease. In mail land, you have folders which contain other folders or mail items. In JSON notation, you can model folders like this:
{
identifier: 'id',
label: 'label',
items: [
// Hierarchy of folders
{ type: 'folder', id: 'mailbox', label:'Folders', folders: [
{ type: 'folder', id: 'inbox', label:'Inbox', icon:'mailIconFolderInbox' },
{ type: 'folder', id: 'deleted', label:'Trash', icon:'mailIconTrashcanFull' },
{ type: 'folder', id: 'save', label:'Save', folders:[
{ id: 'work', label:'stuff for work'},
{ id: 'fun', label:'stuff for fun'}
]}
]},
You can see how we nest objects within objects, like the "work" and "fun" folders underneath the "Save" folder. We can model mail like this:
// Flat list of messages (each message lists its folder)
{ type: 'message', id: 'node1.1',
folder: 'inbox', label: "today's meeting",
sender: "Adam Arlen", sent: "2005-12-19",
text: "Today's meeting is cancelled.
Let's do it tomorrow instead.
Adam"
},
{ type: 'message', id: 'node1.2',
folder: 'inbox', label: "remaining work",
sender: "Bob Baxter", sent: "2005-12-18",
text: "Hey, we need to talk about who's gonna do all the left over work. Pick a day you want to meet: "
},
Now you have three types of objects, delineated by type: address, folder and message. Andy decides to stick them all in one JSON data message. That means only one trip ito the server grabs everything about the user's mail store. Gotta love that!
The actual folder listing looks a lot like the address book code. The getIconClass extension point draws the folder icons:
/* 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;}The "childrenAttr" in the Tree tag does all the sub-folder magic.
One more thing and he'd be ready to show it to Ms. Opulence: connect the folders to messages and the messages to a preview pane. Andy looks at his watch. He's got about 15 minutes before the el stops by his office. If he could catch it and get home early, that'd give him some serious writing and reflecting time. So let's make short work of this, shall we?
Andy has been studying up on Dojo 1.0, and found one of the great new features - the Dijit grid. A grid is like a mini spreadsheet, or a maxi-HTML-table. You can sort the columns, edit cells, and enable all kinds of cool stuff. It sounds like the perfect container for a message list.
[inline:mail_step7.png]Once you've worked with Dojo awhile, you start to see Grand Unifying Themes. One of them is dojo.data, feeding server-pulled data to different kinds of widgets. Grid is no different. Fortunately, Andy already has the data store defined. And since that data store has messages in it, hierarchically tied to folders, it's a short step to display them. First he adds the separate Grid style sheet:
/* 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;}A grid has two main elements: the model and the structure. The model is the data behind the grid, while the structure is a set of display instructions.
Andy does the model first. We already havy the data in a dojo.data store called mailModel. To make the data source mailStore work with Grid, he declares a dojox.grid.data.DojoData adapter. You can think of it as a pipe where the data flows in, and the appropriately filtered data comes out for use in a grid. When the app initially starts, there is no folder selected. Hence there are no messages to display. Andy decides to initialize the adapter, but issues a query that will return no items. This has performance benefits, as we'll see in a minute.
/* 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;}The structure is a description of the Grid layout. Structures are composed from views, which are little independently scrollable subgrids called views. Our grid is simple, containing only one view. A cell is Grid's name for a column.
var mailView = {
cells: [[
{name: 'From', field:'sender',width:"25%"},
{name: 'Subject', field:'label',width:"65%"},
{name: 'Date', field:'sent',width:"10%"}
]]
};
var layout = [ mailView ];
The cells property is a two dimensional JavaScript array, hence the double square brackets. In the Grid section, you'll see how to split large amounts of data into subrows, all selectable as one unit. Here we have just one subrow with a small amount of data.
Wiring up the actual Grid is a snap. The Grid widget does all the work for you:
/* 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;}Clicking a row in this grid will display the appropriate message in the bottom area. So Andy stubs out the call to the onRowClick event, which he'll fill in later.
In the folder tree, Andy connects some Dojo code to the onClick event. Once you know the folder name, you simply build a new adapter, set that as the Grid model, and the grid will automatically populate. The nice thing here is the Mail messages are already loaded in the dojo.data store. Building a new Grid model on top of it re-uses the data with a new query without a server round-trip.
/* 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;}So now clicking a folder displays the message list. It's time to go back and fill in onRowClick for the message. This turns out to be fairly trivial:
function displayMailMessage(evt) {
var msg = mailGrid.model.getDatum(evt.rowIndex,6);
dijit.byId("message").setContent(msg);
}
So now he has a message list and a message. Andy looks at his watch. 5 minutes to spare. He can't resist adding one more item - displaying a widget inside a mail message. So he quickly adds a test case to the sample data:
{ type: 'message', id: 'node4.4', folder: 'fun',
label: "paint", sender: "Jack Jackson", sent: "2005-12-16",
text: "what color is good for the new office?Let me know soon"
},
And dojo.require's dijit.ColorPalette. Sure enough, displaying the message automagically displays the palette:
[inline:mail_step8.png]Wow! That's a lot of functionality. In a short amount of code, about 1/2 JavaScript and 1/2 HTML, we have a working email client user interface with:
Sure, the server part needs writing. But that can wait until tomorrow, and it shouldn't be too difficult. Find the right words to get a woman to marry you ... now that's difficult. Andy heads out the door for the train