Introduction
It turns out that the file upload piece from last time (the User Feedback article) is going to be used over and over. And that I need to attach a drop-down to let the uploader specify what kind of license is associated with the file. In the dot net world, I'd be tempted to slap the code into a user control and keep moving. That won't work for me, and, anyway, it's probably the wrong way to do things where dojo's concerned.
Dojo has its own support/library system for creating reusable components. For background on this, check out the official Dojo book's chapters on creating your own widgets (or just skip that and read this if you're in a hurry for the nutshell version...that article's a bit out of date, and it leaves out a ton of important details). There's a lot going on here, which is why it looks as convoluted as it does. Things get more complex when the developers make them more flexible.
Infrastructure
Start by adding something similar to the following in a script block in the head of your html:
dojo.registerModulePath("internal", "../internal");
dojo.require("internal.composites.PictureUploader");
where internal (or whatever you choose to call it) is a subdirectory of your Dojo installation parallel to the dojo, dijit, and dojox directories.
If you try to load the page now, it tries to load the file internal/composites/pictureUploader.js
and errors out. So the next step is to create that.
The skeleton pieces look like this:
dojo.provide("internal.composites.PictureUploader");
dojo.require("dojox.form.FileUploader");
dojo.require("dijit.form._FormWidget");
dojo.require("dijit._Templated");
dojo.declare("internal.composites.PictureUploader",
[dijit.form._FormWidget, dijit._Templated],
{
// summary: A composite picture uploader and license selector
//
// description: A control that lets you choose an image file, associate a
// license, and then upload them. You'll have to deal with the license
// yourself
});
If you've looked at dojo modules at all, this is pretty straightforward. In case you haven't, I'll break that down:
What is this file "providing"?
dojo.provide("internal.composites.PictureUploader");
Which other pieces/controls do we need? This will also allow you to remove the reference from your page.
dojo.require("dojox.form.FileUploader");
More required bits. These are specific to creating your own widget:
dojo.require("dijit.form._FormWidget");
dojo.require("dijit.form._Templated");
Then define your widget's class. The first parameter is the class name (with name spaces). The second is the parent class and whichever mixins it gets. The third is a hash table of class members.
dojo.declare("internal.composites.PictureUploader",
[dijit.form._FormWidget, dijit._Templated],
{
// summary: A composite picture uploader and license selector
//
// description: A control that lets you choose an image file, associate a
// license, and then upload them. You'll have to deal with the license
// yourself
});
You don't actually have to include the summary and description comments, but it's probably a good idea to stick with dojo's coding standards.
Some Member Variables
Inside that hash table, add some variables that let me customize a few things (I'm still hard-coding far too much, but I'm really not going for anything general-purpose here).
// The title of the "Browse for file" button
browse_label: "Browse...",
// ID of the Button to replace
upload_button_id: "chooseFile",
// Where to post to when uploading
upload_url: "you must specify this",
// ID of whichever element to use to indicate the name of the file
file_name: "fileToUpload",
// ID of the element that tracks upload progress
upload_progress: "uploadProgress",
// ID of the element that shows the results
upload_results: "uploadResult",
And specify where the "template" file is located:
templatePath: dojo.moduleUrl("internal.composites",
"PictureUploader/PictureUploader.html"),
Note that those lines are all separated by commas. You're declaring a dictionary, not writing javascript statements.
The Template File
Now we need to create that template file. It's really just a matter of cutting and pasting the relevant portions from our existing form.
<div class="InternalPictureUploader">
<div id="${upload_button_id}" class="browse"
dojoType="dijit.form.Button">${browse_label}</div><span
id="fileName"></span><br />
<div id="${file_name}"></div>
</div>
<div class="InternalPictureUploaderProgress">
<div id="${upload_progress}"></div>
<div id="${upload_results}"></div>
</div>
The ${} things are replaced by member variables of the UploadPicture widget.
Add some more dojo magic stuff, to make the control more flexible (and stylable) when it's declared:
<div class="InternalPictureUploaderFileName" dojoAttachPoint="fileName"
id="${file_name}"></div>
<div class="InternalPictureUploaderProgress" dojoAttachPoint="progress"
id="${upload_progress}">
<div class="InternalPictureUploaderResults" dojoAttachPoint="result"
id="${upload_results}"></div>
(I'm not going into the why's/wherefore's of dojoAttachPoint
here. It's a huge topic, the documentation about it is pretty scanty, and I don't feel qualified to comment on it at this point).
At this point, I had to comment out all the pieces in the head of my HTML that referred to any of these pieces. Basically, all the code that has anything at all to do with uploading.
Declaring Your Widget
In your HTML, replace the pieces we've refactored into the control with a control declaration:
<div dojoType="internal.composites.PictureUploader"></div>
Debugging and Gotchas
At this point, the page errors out trying to load ValidationTextBox. The error message in the console is "too much recursion," but it happens right after it loads the FileUploader, which seems suspicious. Besides, the error goes away when I comment out that control.
Looking at the stack trace, the problem starts when dijit.form._FormWidget
tries to call dijit._Templated.create()
.
A quick google revealed that the problem came from using that old tutorial I recommended at the beginning as my basis. dijit.form._FormWidget
now mixes in dijit._Templated
. When I tried to mix it in as well, it caused Bad Things®.
Fixing that left me with a "node is undefined" error. The error originally seemed coming from my templating file. When I switched to a hard-coded template string in the class members dictionary, the HTML did get rendered, but the error did not go away. Adding some logging to the relevant dojo source code revealed that the error happens when I (or dojo's internal magic, rather) try to set the id
attribute of the widget.
More specifically, it was trying to set an attributes named id
and tabIndex
to the value specified in my template file (or something it magically generates). That attribute is actually trying to get attached to a DOM node associated with the 'command' focusNode
.
(Not that focusNode
is not actually a command. It's the value of the dojoAttachPoint
attribute that needs to be assigned to some focusable DOM node).
Adding that value to the file upload button in my template made the errors go away:
<div id="${upload_button_id}" class="browse" dojoAttachPoint="focusNode"
dojoType="dijit.form.Button">${browse_label}</div><span
id="fileName"></span><br />
Making it Do Something
That seems like a ridiculous amount of trouble to get a single visible div that does absolutely nothing. It's time to restore the code that does the uploading.
Again, that's mostly a matter of cut and paste. Cut the pieces that were commented out of the HTML and paste them into a function associated with a key named startup
in the control's "class body."
startup:function(){
// ...all that code
},
Then replace all those ID strings that we'd been hard-coding with the names of the new member variables. (e.g. dojo.byId(this.fileName).innerHTML += "File to upload: " +
d.name+" " + Math.ceil(d.size*.001)+"kb \n";
becomes dojo.byId(this.file_name).innerHTML += "File to upload: " +
d.name+" " + Math.ceil(d.size*.001)+"kb \n";
Add a wrapper around the file upload control:
upload = function(){
this.uploader.upload();
}
And update your HTML's doUpload() method to call that:
doUpload = function(){
// Actually upload the file
var uploader = dijit.byId("pictureUploader");
uploader.upload();
// And submit the metadata
metaDataSubmit();
};
And run headfirst into a brick wall. No matter what I tried, the button widget was returning as null when I tried to access it in my startup method.
So I whittled away everything extraneous and took it to #dojo on IRC. neonstalwart and slightlyoff (thanks again!) were kind enough to look at my mess and straighten me out.
In the example that I gave them, I had my widget declared with these "parents":
[dijit._Templated, dijit._Widget],
which was completely backwards. dijit._Widget
is designed as a base class. dijit._Templated
is a mixin that adds functionality. Trying to use it as the "actual" base class causes all sorts of nastiness. (Yes, I switched from deriving from FormWidget. This just seemed cleaner).
Since I want widgets in my template to be expanded, I also needed to set dijit._Templated.widgetsInTemplate
to true
. This isn't done by default, for performance reasons.
Finally, using a widget's ID the way I was is considered a horrible practice. The correct way to do this is to set a string as a dojoAttachPoint
(I mentioned that thing's important, didn't I?), declare that as a member variable in my widget (defaulting to null
), and just reference by name as needed:
[dijit._Widget, dijit._Templated],
{
uploadButton: null,
uploader: null,
widgetsInTemplate: true,
templateString: "<div id=${id} ><div " +
"dojoAttachPoint='focusNode, uploadButton'" +
"class='browse' " +
"dojoType='dijit.form.Button'>" +
"Browse...</div></div>",
//startup: function(){
postCreate: function(){
var btn = this.uploadButton;
console.debug('Upload Button: ' + btn);
...
Getting Back to Making it Do Something
Now, all of my events are wired up incorrectly. The methods are all getting bound to dojo's global context rather than my widget.Changing dojo.connect
to this.connect
fixes that problem.
Also, it might not be quite as efficient (in terms of bandwidth), but it feels cleaner to me to make the event handlers into member variables rather than anonymous inline functions
For example:
_onChange: function(dataArray){
// ...all the event-handling code
},
And call
this.connect(this.uploader, "onchange", "_onChange");
in
postCreate()
That is actually a shortcut for dojo.connect(this.uploader, "onChange", dojo.hitch(this, "_onChange"));
. dojo.hitch() is an incredibly important function that connects methods to a specific context (or object). I've run across several occasions where things you'd expect to "just work" need dojo.hitch()
because something else changed the meaning of this
. (The first two that bit me were forEach()
and these event wireups). I don't know yet whether this is a dojo wart or just a javascript limitation.
The different pictures that my end-users might upload can be associated with various Creative Commons licenses. I added a combo box to let the user decide which license is appropriate for a given picture. It feels odd to have something like that list hard-coded (an excuse for anyone who looks at my source), but it's not as if the different possible choices will be changing very often.
I ran across one final gotcha when I was working on some final cleanup. I tried to seperate the "requires" in the template file by including a script block and specifying which widgets I was using there, as opposed to the ones that I referenced in the .js file. This led to my widget silently failing to load.
For anyone who's wants to scan over the final version, I'm attaching the source to my widget (and its template) and a test file that uses it.