- The Book of Dojo
- Quick Installation
- Hello World
- Debugging Tutorial
- Introduction
- Part 1: Life With Dojo
- Part 2: Dijit
- Part 3: JavaScript With Dojo and Dijit
- Part 4: Testing, Tuning and Debugging
- Part 5: DojoX
- The Dojo Book, 0.4
Example: File Upload Dialog Box
Submitted by dante on Thu, 09/13/2007 - 20:44.
So now, we'll build a widget to solve a practical problem. Your site is all neatly designed and feng-shui'ed ... except this one <input type="file">
tag. The user
clicks on the button, and all of a sudden gets an operating system-styled box that doesn't look right, and is completely out of your control. Ugggh.
This is a perennial thorn-in-the-side for web developers. A Google search finds the ever-useful web site quirksmode, and they have a pretty simple solution: hide the file input, put a real input directly beneath where it would be, and make your own button off to the side. Logical enough. How hard would that be to make a widget?
Not hard at all, it turns out. Here's the plan:
- Hide the input boxes and draw our own
- Use Dojo to handle all my connections and fancy stuff
- Wrap it all up using
dijit.form._FormWidget
anddiijt._Templated
to take care of everything else
Hiding the Built-In File Upload Box
We'll use the programmatic method here. First we'll need a location on-disk for our widget. Since others have expressed interest in this widget, and we hope to contribute it to dojox, we'll place the widget in dojox/widget/FileInput.js. This means we'll be able to require() and provide() the module "dojox.widget.FileInput" as per the package system conventions. To get additional behavior we'll need to require() mixin classes, in case we don't already have them. So here's a stub for the class:
dojo.provide("dojox.widget.FileInput");
dojo.require("dijit.form._FormWidget");
dojo.require("dijit._Templated");
dojo.declare("dojox.widget.FileInput",
[dijit.form._FormWidget,dijit._Templated],
{
// summary: A styled input type="file"
//
// description: A input type="file" form widget, with a button for uploading to be styled via css,
// a cancel button to clear selection, and FormWidget mixin to provide standard
// dijit.form.Form
//
});
dojo.require("dijit.form._FormWidget");
dojo.require("dijit._Templated");
dojo.declare("dojox.widget.FileInput",
[dijit.form._FormWidget,dijit._Templated],
{
// summary: A styled input type="file"
//
// description: A input type="file" form widget, with a button for uploading to be styled via css,
// a cancel button to clear selection, and FormWidget mixin to provide standard
// dijit.form.Form
//
});
Next we'll need a template, a label for our submit button, a label for our cancel button, and the name of the input:
// label: String
// the title text of the "Browse" button
label: "Browse ...",
// cancelText: String
// the title of the "Cancel" button
cancelText: "Cancel",
// name: String
// ugh, this should be pulled from this.domNode
name: "uploadFile",
templatePath: dojo.moduleUrl("dojox.widget","FileInput/FileInput.html"),
// the title text of the "Browse" button
label: "Browse ...",
// cancelText: String
// the title of the "Cancel" button
cancelText: "Cancel",
// name: String
// ugh, this should be pulled from this.domNode
name: "uploadFile",
templatePath: dojo.moduleUrl("dojox.widget","FileInput/FileInput.html"),
Also note that whenever we use template variables , it's good practice to supply a default, e.g. "Browse ..." for the label. Otherwise, if your widget user omits the label attribute, the parser will complain.
As we said earlier in Direct Extension, it's preferrable to separate the template out into a different file. Ours will look like the following, placed in dojox/widget/FileInput/FileInput.html:
<div class="dijitFileInput">
<input id="${id}" class="dijitFileInputReal" type="file"
dojoAttachPoint="fileInput" name="${name}" />
<div class="dijitFakeInput">
<input class="dijitFileInputVisible" type="text"
dojoAttachPoint="focusNode, inputNode" />
<span class="dijitFileInputText"
dojoAttachPoint="titleNode">${label}</span>
<span class="dijitFileInputButton" dojoAttachPoint="cancelNode"
dojoAttachEvent="onclick:_onClick">
${cancelText}
</span>
</div>
</div>
<input id="${id}" class="dijitFileInputReal" type="file"
dojoAttachPoint="fileInput" name="${name}" />
<div class="dijitFakeInput">
<input class="dijitFileInputVisible" type="text"
dojoAttachPoint="focusNode, inputNode" />
<span class="dijitFileInputText"
dojoAttachPoint="titleNode">${label}</span>
<span class="dijitFileInputButton" dojoAttachPoint="cancelNode"
dojoAttachEvent="onclick:_onClick">
${cancelText}
</span>
</div>
</div>
Note how we assign classes to each major part, so we can apply design as CSS styles.
We're also using dojoAttachPoint="aString"
so the parser makes these nodes available to us in code at the location this.fileInput
.
dojoAttachEvent="onclick: _onClick"
connects the onclick event of this.cancelNode to this._onClick, the method we are about to define (otherwise dojo.hitch will throw an error mentioning something about _onClick not having properties). We'll get to the onClick handler for the cancel button, and the reasons for having to do it later.
What is the following for?
// nonRequired: String
// this way, the build system knows about me
nonRequired: "",
// this way, the build system knows about me
nonRequired: "",
for now, lets make sure our widget starts up, and looks right. lets make some simple css rules using the class names we set in our template, based on the relative / absolute, opacity:0 stuff we learned earlier:
.dijitFileInput {
position:relative;
height:1.3em;
padding:2px;
}
.dijitFileInputReal {
position:absolute;
z-index:2;
opacity:0;
filter:alpha(opacity:0);
}
.dijitFileInputButton,
.dijitFileInputText {
border:1px solid #333;
padding:2px 12px 2px 12px;
cursor:pointer;
}
.dijitFileInputButton {
opacity:0;
filter:alpha(opacity:0);
z-index:3;
visibility:hidden;
}
.dijitFakeInput { position:absolute; top:0; left:0; z-index:1; }
position:relative;
height:1.3em;
padding:2px;
}
.dijitFileInputReal {
position:absolute;
z-index:2;
opacity:0;
filter:alpha(opacity:0);
}
.dijitFileInputButton,
.dijitFileInputText {
border:1px solid #333;
padding:2px 12px 2px 12px;
cursor:pointer;
}
.dijitFileInputButton {
opacity:0;
filter:alpha(opacity:0);
z-index:3;
visibility:hidden;
}
.dijitFakeInput { position:absolute; top:0; left:0; z-index:1; }
looks good!
Connecting the Elements with Events
dojoAttachEvent="onclick: _onClick"
connects the onclick event of this.cancelNode to this._onClick, the method we are about to define (otherwise dojo.hitch will throw an error mentioning something about _onClick not having properties). We'll get to the onClick handler for the cancel button, and the reasons for having to do it later.
We also need to implement a simple onchange listener, like the article hints, so that when our onchange is detected in on our real file input (this.fileInput), we will call this._matchValue() to steal the value from it, and populate our visible input:
startup: function(){
// summary: listen for changes on our real file input
this.inherited("startup",arguments);
this._listener = dojo.connect(this.fileInput,"onchange",this,"_matchValue");
this._keyListener = dojo.connect(this.fileInput,"onkeyup",this,"_matchValue");
},
_matchValue: function(){
// summary: set the content of the upper input based on the semi-hidden file input
this.inputNode.value = this.fileInput.value;
if(this.inputNode.value){
this.cancelNode.style.visibility = "visible";
dojo.fadeIn({ node: this.cancelNode, duration:275 }).play();
}
}
// summary: listen for changes on our real file input
this.inherited("startup",arguments);
this._listener = dojo.connect(this.fileInput,"onchange",this,"_matchValue");
this._keyListener = dojo.connect(this.fileInput,"onkeyup",this,"_matchValue");
},
_matchValue: function(){
// summary: set the content of the upper input based on the semi-hidden file input
this.inputNode.value = this.fileInput.value;
if(this.inputNode.value){
this.cancelNode.style.visibility = "visible";
dojo.fadeIn({ node: this.cancelNode, duration:275 }).play();
}
}
You've probably also noticed we added an onkeyup connection, running the same code. This way, if we type in the input, our changes will be reflected after each key press. Fortunately for this example, we'll ignore little nitpicks like "holding backspace doesn't fire onkeyup".
The _matchValue() function simply steals the file input value, sets it to the visible input value and fades in the cancel button (which we set earlier to visbility:hidden in FileInput.css).
So next, we need a reset button.
Unfortunately because we're faking HTML out a bit, a plain old Reset button won't work.
Since we aren't allowed write access to the file input, we can't just null the data.
So our _onClick method actually destroys the <input type="file">
and reads it.
_onClick: function(/* Event */e){
// summary: on click of cancel button, since we can't clear the input because of
// security reasons, we destroy it, and add a new one in it's place.
// Disconnect the listeners so they're not orphaned, and cleanly remove the tag
dojo.disconnect(this._listener);
dojo.disconnect(this._keyListener);
this.domNode.removeChild(this.fileInput);
// Fade our the cancel button so we no longer can press it
dojo.fadeOut({ node: this.cancelNode, duration:275 }).play();
// Create an identical input tag
this.fileInput = document.createElement('input');
this.fileInput.setAttribute("type","file");
this.fileInput.setAttribute("id",this.id);
this.fileInput.setAttribute("name",this.name);
dojo.addClass(this.fileInput,"dijitFileInputReal");
// this.domNode is the root DOM node of the widget
this.domNode.appendChild(this.fileInput);
// Finally, connect the listeners to this new node.
this._keyListener = dojo.connect(this.fileInput,"onkeyup",this,"_matchValue");
this._listener = dojo.connect(this.fileInput,"onchange",this,"_matchValue");
this.inputNode.value = "";
}
// summary: on click of cancel button, since we can't clear the input because of
// security reasons, we destroy it, and add a new one in it's place.
// Disconnect the listeners so they're not orphaned, and cleanly remove the tag
dojo.disconnect(this._listener);
dojo.disconnect(this._keyListener);
this.domNode.removeChild(this.fileInput);
// Fade our the cancel button so we no longer can press it
dojo.fadeOut({ node: this.cancelNode, duration:275 }).play();
// Create an identical input tag
this.fileInput = document.createElement('input');
this.fileInput.setAttribute("type","file");
this.fileInput.setAttribute("id",this.id);
this.fileInput.setAttribute("name",this.name);
dojo.addClass(this.fileInput,"dijitFileInputReal");
// this.domNode is the root DOM node of the widget
this.domNode.appendChild(this.fileInput);
// Finally, connect the listeners to this new node.
this._keyListener = dojo.connect(this.fileInput,"onkeyup",this,"_matchValue");
this._listener = dojo.connect(this.fileInput,"onchange",this,"_matchValue");
this.inputNode.value = "";
}
Wiring It All Together
So now we have our widget and our basic styles. We include this widget in our page declaratively by:
<input type="file" name="uploadFile" dojoType="dojox.widget.FileInput">
The real file input tag in the DOM goes away our templated input gets put in it's place. But if JavaScript is not present, it stays a regular HTML input tag, so it degrades nicely. Here is a sample test page to work with:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>dojox.widget.FileInput | The Dojo Toolkit</title>
<style type="text/css">
@import "../../../dojo/resources/dojo.css";
@import "../../../dijit/themes/dijit.css";
@import "../FileInput/FileInput.css";
</style>
<script type="text/javascript" src="../../../dojo/dojo.js"
djConfig="isDebug:true, parseOnLoad: true"></script>
<script type="text/javascript">
dojo.require("dojox.widget.FileInput");
dojo.require("dojo.parser"); // scan page for widgets and instantiate them
</script>
</head>
<body>
<h3>A standard file input:</h3>
<input type="file" id="normal" name="inputFile" />
<h3>The default dojox.widget.FileInput:</h3>
<input dojoType="dojox.widget.FileInput" id="default" name="uploadFile" />
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>dojox.widget.FileInput | The Dojo Toolkit</title>
<style type="text/css">
@import "../../../dojo/resources/dojo.css";
@import "../../../dijit/themes/dijit.css";
@import "../FileInput/FileInput.css";
</style>
<script type="text/javascript" src="../../../dojo/dojo.js"
djConfig="isDebug:true, parseOnLoad: true"></script>
<script type="text/javascript">
dojo.require("dojox.widget.FileInput");
dojo.require("dojo.parser"); // scan page for widgets and instantiate them
</script>
</head>
<body>
<h3>A standard file input:</h3>
<input type="file" id="normal" name="inputFile" />
<h3>The default dojox.widget.FileInput:</h3>
<input dojoType="dojox.widget.FileInput" id="default" name="uploadFile" />
In theory, it will work inside of a tag just as the original element did.
Because this is dojox, and we're bigs fan of re-using code, we can steal some CSS stuff from tundra.css and soria.css to provide theme-specific styles, so our input nodes look like they would in with all the other dijit.form Widgets (like ComboBox, FilteringSelect, ValidationTextBox, etc) ...
/* tundra */
.tundra .dijitProgressOverlay {
border:1px solid #84a3d1;
background-color:#cad2de;
}
.tundra .dijitFakeInput input {
font-size: inherit;
background:#fff
url("../../../dijit/themes/tundra/images/validationInputBg.png")
repeat-x top left;
border:1px solid #9b9b9b;
line-height: normal;
padding: 0.2em 0.3em;
}
.tundra .dijitFileInputButton,
.tundra .dijitFileInputText {
border:1px solid #9b9b9b;
padding:2px 12px 2px 12px; /* .3em .4em .2em .4em; */
background:#e9e9e9
url("../../../dijit/themes/tundra/images/buttonEnabled.png")
repeat-x top;
}
/* Soria */
.soria .dijitProgressOverlay {
border:1px solid #333;
background-color:#cad2de;
}
.soria .dijitFakeInput input {
border:1px solid #333;
background:#fff
url("../../../dijit/themes/soria/images/gradientInverseTopBg.png")
repeat-x top left;
line-height:normal;
background-position:0 -30px;
padding:0.2em 0.3em;
}
.soria .dijitFileInputButton,
.soria .dijitFileInputText {
border:1px solid #333;
padding:2px 12px 2px 12px;
background:#b7cdee
url('../../../dijit/themes/soria/images/gradientTopBg.png') repeat-x;
}
.tundra .dijitProgressOverlay {
border:1px solid #84a3d1;
background-color:#cad2de;
}
.tundra .dijitFakeInput input {
font-size: inherit;
background:#fff
url("../../../dijit/themes/tundra/images/validationInputBg.png")
repeat-x top left;
border:1px solid #9b9b9b;
line-height: normal;
padding: 0.2em 0.3em;
}
.tundra .dijitFileInputButton,
.tundra .dijitFileInputText {
border:1px solid #9b9b9b;
padding:2px 12px 2px 12px; /* .3em .4em .2em .4em; */
background:#e9e9e9
url("../../../dijit/themes/tundra/images/buttonEnabled.png")
repeat-x top;
}
/* Soria */
.soria .dijitProgressOverlay {
border:1px solid #333;
background-color:#cad2de;
}
.soria .dijitFakeInput input {
border:1px solid #333;
background:#fff
url("../../../dijit/themes/soria/images/gradientInverseTopBg.png")
repeat-x top left;
line-height:normal;
background-position:0 -30px;
padding:0.2em 0.3em;
}
.soria .dijitFileInputButton,
.soria .dijitFileInputText {
border:1px solid #333;
padding:2px 12px 2px 12px;
background:#b7cdee
url('../../../dijit/themes/soria/images/gradientTopBg.png') repeat-x;
}
Future Plans
If you've been paying attention all this way, you probably noticed a class up there that we didn't define. .dijitProgressOverlay ... it doesn't exist anywhere in the dojox.widget.FileInput template or code.
The Dojo folks are building an extension to this widget called dojox.widget.FileInputAuto. It works like FileInput except it submits itself after a delay following a blur() on the element. For details (as yet undocumented), get the latest nightly build of Dojo and look for it in dojox.widget.
- Printer-friendly version
- Login or register to post comments
- Unsubscribe post
Where is the Dialog?
This example says that it is a File Upload Dialog box, but yet dijit.Dialog is not used at all. After a bit of playing, I was able to create my own custom Dialog widget, but I don't think I did it exactly the way it should be done. I wanted a dijit.Editor in my dialog, so I used someone else's suggestion to use the ContentPane's href to load the html dynamically to create the editor. That worked. However, it does not look like you can substitute parameters (such as ${title} or ${id}) in the dynamically loaded html.
Anyways, it was hard to try to figure out if I should derive from Dialog (the approach I took), or just make a new widget that has a Dialog as its root element in the widget template, or what. It would be nice to see how you would really design a File Upload Dialog.
This was my first attempt at creating (and really using) dijit, so I'm sure that was one reason I struggled. But a full example would be nice.
Thanks!
Opacity question
The article says "lets make some simple css rules using the class names we set in our template, based on the relative / absolute, opacity:0 stuff we learned earlier", but I don't see where that earlier discussion is. Was it on a page that is no longer included on the site? A search did not reveal anything. I am trying to understand how to best use css with custom widgets and would like to learn more about this. What is the idea behind the "relative / absolute, opacity:0 stuff" ?
Rhyme & Reason
Online tools for poets and lyricists