Dojo and Air, a fancy file uploader
by Nikolai Onken

How many times have you had trouble uploading files to your favorite CMS? How many times did a client say “I am not happy with uploading one file at a time”? And last but not least, how many times did you implement a third party plugin/software/piece of magic to implement efficient file uploading?

Well, I think I personally can answer those questions with “lots of times”, and finally after the release of AIR 1.0 and Dojo 1.1 we can create a powerful application to upload as many files as you want to your server.

So what this AIR business all about?

Adobe AIR (Adobe Integrated Runtime) allows us web developers to build powerful applications for the desktop, just as we do for the web.
This can include a simple file-uploader as we will create it in this tutorial up to a complex application to deal with both online and off-line data.

All you need to start developing for AIR is the AIR SDK, which comes with all the tools you need to successfully deploy an AIR application. Good places to check out are the AIR pages for AJAX developers and the AIR Ajax documentation

The goal

Fileuploader

What you see is a standalone application - as standalone as any of your other apps - on top of the campus website, 100% HTML, Dojo/Dijit and AIR.

Make a plan

What do we want to accomplish?

  1. Multiple file uploads
  2. Visualization of upload process
  3. Deletion of wrongly added files

In the next part of this tutorial we will add even more features such as dragging files from the desktop, exciting stuff.
Let’s walk through each of the points and see how Dojo or Air can help us.

Multiple file uploads

AIR comes with a very powerful file handling API, it is perfect for our purpose.

Visualization of upload process

While uploading we want to see how far we are with the upload. Dijit.ProgressBar is a perfect helper in combination with the AIR file handling API.

Deletion of wrongly added files

Imagine you have added 50 files and the 36th is not right. With some Dojo magic we’ll be able to delete wrongly added files.

Enough planning, lets get started

First of all I recommend that, while you read this tutorial, you also check the Adobe AIR getting started tutorials. They are of great help.

Set up a new folder on your local server with following folder/file structure:

/
/dojotoolkit/AIRAliases.js
/dojotoolkit/dojo/*
/dojotoolkit/dijit/*
/dojotoolkit/uploader/FancyUploader.js
/dojotoolkit/uploader/templates/FancyUploader.html
/dojotoolkit/uploader/templates/FancyUploaderItem.html
/dojotoolkit/uploader/resources/delete.gif
/dojotoolkit/uploader/resources/FancyUploader.css
/index.html
/application.xml

Copy the dojo and dijit folders from a dojo release. You can find the AIRAliases.js in the framework folder of tyour AIR SDK installation.
The AIRAliases.js file is a little helper saving you lots of writing time. Instead of writing

“window.runtime.flash.desktop.NativeApplication”

using AIRAliases.js you can simply write

“air.NativeApplication”

All other files will be created in this tutorial.

application.xml

<?xml version="1.0" encoding="utf-8" ?>
<application xmlns="http://ns.adobe.com/air/application/1.0">
  <filename>Dojo Fileuploader</filename>
  <programMenuFolder>Dojo Fileuploader</programMenuFolder>
  <copyright>2008</copyright>
  <description>DojoCampus.org</description>
  <customUpdateUI>false</customUpdateUI>
  <name>Dojo File Uploader</name>
  <id>DojocampusTutorialFile</id>
  <version>1</version>
  <initialWindow>
    <content>index.html</content>
    <height>300</height>
    <width>500</width>
    <systemChrome>standard</systemChrome>
    <transparent>false</transparent>
    <visible>true</visible>
  </initialWindow>
  <icon>
    <image128x128>logo128.png</image128x128>
    <image48x48>logo48.png</image48x48>
    <image32x32>logo32.png</image32x32>
    <image16x16>logo16.png</image16x16>
  </icon>
</application>

<application xmlns=”http://ns.adobe.com/air/application/1.0″>: The root element of your AIR xml, including the AIR namespace attribute. Note the 1.0 at the end?It inidicated the version of AIR which needs to be used to run your application.

<filename>: The filename of your Application.

<programMenuFolder>: The folder your application gets stored on your computer (Not OSX)

<copyright>: Make sure no one steals your app.

<description>: A good description of your program.

<customUpdateUI>: It is possible to provide your own updating mechanism when the user updates your app. This is not used at the moment.

<name>: Give your application a name.

<id>: Quote from the Adobe AIR docs The application id uniquely identifies your application along with the publisher id (which AIR derives from the certificate used to sign the application package). The recommended form is a dot-delimited, reverse-DNS-style string, such as “com.company.AppName”. The application id is used for installation, access to the private application file-system storage directory, access to private encrypted storage, and interapplication communication.

<version>: The version number of this application

<initialWindow>: The initialWindow element containing following children:

<content>: The html file to load.

<height> / <width>: Initial height/width of the application window

<systemChrome>: We use “standard” since we are creating a “normal” desktop app. Set it to none if you don’t want the System default window style show up.

<transparent>: We’ll leave that at false since we don’t want to support any transparent windows.

<visible>: Of course we want to see the main window on program start.

<icon>: The icon element has a couple of icon children where you can define useful icons for your app. Rememebr to place those icons in your folder as well.

The html file

As we wrote in our application.xml we need to have an index.html which is the default file for our AIR application.
The great thing about dojo widgets is that they keep your HTML markup very simple. This is all you need for your index.html:

<html>
<head>
	<title>FancyFileUploader</title>
   	<link href="dojotoolkit/dijit/themes/nihilo/nihilo.css" type="text/css" rel="stylesheet">
   	<style type="text/css">
		@import "dojotoolkit/dojo/resources/dojo.css";
	</style>
	<script type="text/javascript" src="dojotoolkit/dojo/dojo.js" 
			djConfig="isDebug: false, parseOnLoad: true"></script>
	<script type="text/javascript" src="dojotoolkit/uploader/FancyUploader.js"></script>
	<script type="text/javascript" src="dojotoolkit/AIRAliases.js"></script>
 
	<style type="text/css">
		@import "dojotoolkit/uploader/resources/FancyUploader.css";
		html {
			height: 100%;
		}
		body {
			background-color: #fff;
			margin: 0;
		}
	</style>
</head>
<body class="nihilo">
	<div dojoType="uploader.FancyUploader" id="uploader" url="http://yourwebsite.php/upload.php" fileBrowserTitle="Select an image to upload"></div>
</body>
</html>

That is all, and you already see some code for creating the widget.

<div dojoType="uploader.FancyUploader" id="uploader" url="http://yourwebsite.php/upload.php" fileBrowserTitle="Select an image to upload"></div>

dojoType=”uploader.FancyUploader”: This will be our uploader widget

id=”uploader”: The id so we can for instance connect to some uploader events

url=”http://yourwebsite.php/upload.php”: The uploader widget just needs an url where the files get uploaded to

fileBrowserTitle=”Select an image to upload”: And the title of the window for selecting the files

That is all you need. Save the file in your dir as index.html and go to your command line, enter “adl application.xml” and………….
If everything went well and you created all files (even though they are empty), at least the app starts up with a blank white piece of screen. Nice.

The real stuff

After a couple of minutes of thinking about a usable design solution, something useful would be (of course there can be many other approaches):

  1. Develop an upload widget which has “upload” and “select files” buttons, and holds all files to be uploaded
  2. Design independent widgets for each files upload progress, so we can easily destroy finished uploads, can connect to events and stuff like that.

We’ll declare two new classes:

dojo.declare("uploader.FancyUploader", 
    [dijit._Widget, dijit._Templated], 
    {
 
});

and

dojo.declare("uploader.FancyUploaderItem", 
    [dijit._Widget, dijit._Templated], 
    {
 
});

If you are wondering what those [dijit._Widget, dijit._Templated] lines in both classes mean, the syntax above says that both classes (uploader.FancyUploader and uploader.FancyUploaderItem) inherit from the two classes dijit._Widget and dijit._Templated.

dijit._Widget: Is a very useful helper class to provide widget generation and destruction methods. So you don’t have to worry about things like disconnecting events on widget destruction.

dijit._Templated: Is another very useful class which allows you to use templates in your widgets. We will see in just a few minutes why it is such a cool thing.

Lets put both of those classes into the file FancyUploader.js in the /dojotoolkit/uploader/ folder and get started writing the uploader.FancyUploader class:

dojo.declare("uploader.FancyUploader", [dijit._Widget, dijit._Templated], {
	templatePath: dojo.moduleUrl("uploader","templates/FancyUploader.html"),
	widgetsInTemplate: true,
 
	files: [], // array holding all files to upload
	url: "",
 
	_isUploading: false,
	_serverResponse: [],
	_uploadConnects: [],
 
	fileBrowserTitle: "Please select a file",
 
	constructor: function(){
		this.airFile = air.File.documentsDirectory;
	    this.airFile.addEventListener( air.FileListEvent.SELECT_MULTIPLE, dojo.hitch(this, this.selectFiles) );
	},
 
	postCreate: function() {
		dojo.connect(this.selectFiles, 'onClick', this, 'browseFiles');
		dojo.connect(this.uploadFiles, 'onClick', this, "upload");
	}
});

To make things easier to explain we also have to create the FancyUploader.html file in the /dojotoolkit/uploader/templates/ directory:

<div id="fancyUploaderContainer" dojoAttachPoint="domNode">
	<div class="fancyUploaderButtons">
		<button dojoType="dijit.form.Button" dojoAttachPoint="selectFiles">Browse</button>
		<button dojoType="dijit.form.Button" dojoAttachPoint="uploadFiles">Upload Files</button>
	</div>
	<div class="fancyUploaderFiles" dojoAttachPoint="filesNode"></div>
</div>

All what is really important at the moment are the dojoAttachPoints. Setting those makes it easy to refer to them from within the widget. We will see how this works when looking at the postCreate() method. But first things first.

    templatePath: dojo.moduleUrl("uploader","templates/FancyUploader.html"),
    widgetsInTemplate: true,

All we do here is define the path to the template we use in this widget “templatePath” and the fact that our widget should assume that the template includes other widgets “widgetsInTemplate” - as you see we have two dijit.form.Button dijits in the template.
Then we have a few variables which we need in our widget:

    files: [], // array holding all files to upload
    url: "", // uploading url
 
    _isUploading: false, // internal status 
    _serverResponse: [], // array to store server responses for each upload
    _uploadConnects: [],
 
    fileBrowserTitle: "Please select a file", // title of file select

Now two very important methods are coming when looking at the code

    constructor: function(){
        this.airFile = air.File.documentsDirectory;
        this.airFile.addEventListener( air.FileListEvent.SELECT_MULTIPLE, dojo.hitch(this, this.createFiles) );
    },
 
    postCreate: function() {
        dojo.connect(this.selectFiles, 'onClick', this, 'browseFiles');
        dojo.connect(this.uploadFiles, 'onClick', this, "upload");
    }

The first one to come is the “constructor” method. In there, we are creating a reference to the air.File.documentsDirectory object which will deal with lots of the heavy lifting.
Secondly we add an event listener to the this.airFile variable. Once we have selected a few files and click the “Select” or “Ok” button, this event gets fired and we call the “createFiles” method to add the selected files to the upload queue.

The second method is the postCreate method, postCreate is very useful when you need to access nodes from within your template before the widget gets started up completely. All we do here is connect each of the buttons to their respective methods.

Add following code to your FancyUploader.js

	browseFiles: function(){
	    var filters = new window.runtime.Array( );
 
	    filters.push( new air.FileFilter( "Image Files", "*.jpg" ) );
	    this.airFile.browseForOpenMultiple( this.fileBrowserTitle, filters );
	},

This method gets firet when we click the browse button and uses really handy AIR methods to deal with the system file selection. First we define a filter, since we only want to allow JPG files (Of course you should try and set a different filter) and then we call the browseForOpenMultiple method to open the file selection window and allow for multiple file selection. Since we connected to the event which fires when we click the “Select” button of this window and call the “createFiles” method lets take a look at that method right away. Don’t forget to add it to your code as well!

	createFiles: function(event){
		// we add new files to the list
	    for( var f = 0; f < event.files.length; f++ ){
	    	if (dijit.byId(event.files[f].name)) continue; // file already in list
	    	var file = new FancyUploaderItem({file:event.files[f], url: this.url});
	       	this.files.push( file );
	    	this._uploadConnects[file.id] = dojo.connect(file, "destroy", dojo.hitch(this, "deleteFile", file));
 
	       	this.filesNode.appendChild(file.domNode);
	    }
	    if (this.files.length){
			this.uploadFiles.disabled = false;
		}
	},

The createFiles method takes an array of files stored in event.files which you selected in the file picker.

if (dijit.byId(event.files[f].name)) continue; // file already in list

If we have added that file already we just skip the current iteration.

var file = new FancyUploaderItem({file:event.files[f], url: this.url});

We create a new instance of the FancyUploaderItem class passing the upload url and the file selected in the file selector.
The we push that instance to our this.files array so we can call the objects upload function when we upload all files (Note: we have not yet done anything with the FancyUploaderItem class, but it will containe a upload method)

The following line is looking very complicated, lets look at it line by line

			this._uploadConnects[file.id] = dojo.connect(file, "destroy", dojo.hitch(this, "deleteFile", file));

We added this line because we need to make sure that once we delete a FancyUploaderItem we also remove it from the this.files array.
There is one problem when you want to connect to objects created in a loop. Basically all we want to do is call a FancyUploader method called “deleteFile” which takes the file to delete as an argument. You might think we could just do something like:

var file = new FancyUploaderItem({file:event.files[f], url: this.url});
this._connects[file.id] = dojo.connect(file, "destroy", function() {
	    		this.deleteFile(file);
	    		});

The problem is, that when the destroy method of the FancyUploaderItem gets called it will call the this.deleteFile method passing the last file variable from the for loop. This is not wat we want, we need the file variable from the actual current loop when the variable was set.
To deal with that we have to use something called closures. Luckily dojo provides the almighty dojo.hitch method which takes all the heavy load of your shoulders. I recommend reading the cookie by Peter Higgins about dojo.hitch
Note the third parameter passed to the dojo.hitch method. All parameters after the second one just get passed to the function and that is exactly what we need:

			this._uploadConnects[file.id] = dojo.connect(file, "destroy", dojo.hitch(this, "deleteFile", file));

While we are talking about the this.deleteFile function, lets add it to the code:

	deleteFile: function(file){
		dojo.disconnect(this._uploadConnects[file.id]);
		delete this._uploadConnects[file.id];
 
		this.files = dojo.filter(this.files,function(item){  return item !== file });
	}

All this function does is disconnecting the connect and deleting the file from the this.files array.
There are just a few methods left until we finished the FancyUploader class:

upload: function(){
		if (!this._isUploading){
			this.onUploadStart();
			this._isUploading = true;
			this._serverResponse = [];
		}
 
		if (this.files.length){
			this.selectFiles.disabled = true;
			this.uploadFiles.disabled = true;
			itemToGo = this.files.shift();
			var conn = dojo.connect(itemToGo, "onComplete", dojo.hitch(this,function(data){
				dojo.disconnect(conn);
				this._addServerResponse(data);
				this.upload();
			}));
			itemToGo.upload();
		}else{
			this.selectFiles.disabled = false;
			this.uploadFiles.disabled = true;
 
			this._isUploading = false;
 
			this.onUploadComplete();
		}
	},
 
	onUploadStart: function(){},
 
	onUploadComplete: function(){},
 
	_addServerResponse: function(data) {
		this._serverResponse.push(data);
	},

Take a look at the upload method which gets called once you press the upload button.

if (!this._isUploading){
			this.onUploadStart();
			this._isUploading = true;
			this._serverResponse = [];
		}

these few lines are helping us out to provide the user of our widget with a few methods he can connect on. If we are not uploading yet we fire the onUploadStart method, set the _isUploading variable to true and reset the _serverResponse array which will hold the response for each file.

if (this.files.length){
			this.selectFiles.disabled = true;
			this.uploadFiles.disabled = true;
			itemToGo = this.files.shift();
			var conn = dojo.connect(itemToGo, "onComplete", dojo.hitch(this,function(data){
				dojo.disconnect(conn);
				this._addServerResponse(data);
				this.upload();
			}));
			itemToGo.upload();
		}

Then if we actually have files to upload we disable the upload and browse buttons and take the first item of the this.files array to upload.
Each FancyFileItem provides a method called “onComplete” we can connect to. We connect to that method so once one file is complete we first disconnect the connection, then add the server response data from that file and finally call this.upload to upload the next file. Remember this only happens once the itemToGo fires the onComplete. We still have to upload the file itself so after the connect we call

itemToGo.upload();

Next let’s take a look at

else{
			this.selectFiles.disabled = false;
 
			this._isUploading = false;
 
			this.onUploadComplete();
		}

The “else” gets executed once there are no more files. We enable the select files button, set the _isUploading variable to false and fire the onUploadComplete method so again we provide the widget user with a nice method to connect to.

The FancyUploaderItem class

To get started we define a few variables and call the postMixInProperties methos to do some file size calculations:

dojo.declare("FancyUploaderItem", [dijit._Widget, dijit._Templated], {
	templatePath: dojo.moduleUrl("uploader","templates/FancyUploaderItem.html"),
	iconSrc: dojo.moduleUrl("uploader","resources/delete.gif"),
 
	widgetsInTemplate: true,
	destroyOnComplete: true, // set to false if you want widget to stay open
 
	postMixInProperties: function(){
		this.size = Math.ceil( this.file.size / 1000 );
		this.id = this.file.name;
	},

Again, we’ll take a look at te code line by line.

templatePath: dojo.moduleUrl("uploader","templates/FancyUploaderItem.html"),
	iconSrc: dojo.moduleUrl("uploader","resources/delete.gif"),

templatePath defines the path to the template for each file upload item. This template basically contains the progress bar, some information and the delete icon. Put following HTML code into the FancyuploaderItem.html

<div dojoAttachPoint="containerNode" class="fancyUploaderFileContainer">
	<div dojoAttachPoint="infoNode" class="fancyUploaderInfo">
		<img src="${iconSrc}" class="fancyUploaderImgDel" dojoAttachEvent="onclick: destroy" />
		<span dojoAttachPoint="infoContent">${id} (${size})</span>
	</div>
	<div dojoAttachPoint="progressWrapper">
		<div dojoType="dijit.ProgressBar" dojoAttachPoint="progressNode" class="fancyUploaderProgress" width="100" annotate="true" maximum="${size}"></div>
	</div>
</div>

All we really have in this template are two divs holding the uploading progress info (such as file name, file size) and the progress bar.
the iconSrc variable hold the link to our delete icon:

delete.gif

Just put this file into the “resources” directory. As we did in the FancyUploader class also in the FancyUploaderItem class we set widgetsInTemlpate to true so the Progress bar gets parsed correctly.

	destroyOnComplete: true, // set to false if you want widget to stay open

We also add a boolean variable destroyOnComplete which either lets the widget destroy itself on upload completion or not.

	postMixInProperties: function(){
		this.size = Math.ceil( this.file.size / 1000 );
		this.id = this.file.name;
	},

postMixInProperties is a method provided by dijit._Widget which gets fired after the properties have been set, but before the widget has been parsed.
All we do is calculate the size so we can display it in a more user friendly way (Kilobytes rather than Bytes).

We’ll now add the most important method to our widget, the upload method:

upload: function(file){
		var urlRequest = new air.URLRequest(this.url);
		urlRequest.method = air.URLRequestMethod.POST;
		this.file.addEventListener(air.ProgressEvent.PROGRESS, 				dojo.hitch(this, "progress"));
		this.file.addEventListener(air.DataEvent.UPLOAD_COMPLETE_DATA, 		dojo.hitch(this, "complete"));
		//this.file.addEventListener(air.Event.COMPLETE, 					dojo.hitch(this, "complete"));
		this.file.addEventListener(air.SecurityErrorEvent.SECURITY_ERROR, 	dojo.hitch(this, "error"));
		this.file.addEventListener(air.HTTPStatusEvent.HTTP_STATUS, 		dojo.hitch(this, "error"));  
		this.file.addEventListener(air.IOErrorEvent.IO_ERROR, 				dojo.hitch(this, "error"));
		this.file.upload(urlRequest, "uploadfile");
	},

What happens here is, that we add a whole bunch of event listeners to our uploading process. This part is actually the only reason why we would not want to use the HTML native File input. It does not give any options to track progress of an upload and if you are uploading very large files, there is nothing worse than not letting your user know how the upload is progressing.

var urlRequest = new air.URLRequest(this.url); : we instantiate a new URLrequest passing our url.
urlRequest.method = air.URLRequestMethod.POST; : we set the request method (we want to use POST).
this.file.addEventListener(air.ProgressEvent.PROGRESS, dojo.hitch(this, “progress”)); : we add an event listener to the upload progress and call the progress method of our FancyUploadItem to update the progress bar.
this.file.addEventListener(air.DataEvent.UPLOAD_COMPLETE_DATA, dojo.hitch(this, “complete”)); : we connect to the UPLOAD_COMPLETE_DATA event which gets fired once the server returns a response such as “File uploaded!”
//this.file.addEventListener(air.Event.COMPLETE, dojo.hitch(this, “complete”)); : We could also connect to the COMPLETE event but this would fire when the uploading process is finished for the file itself and that is before the server returns a response. In our case the response of the server is essential so we just comment it out

this.file.addEventListener(air.SecurityErrorEvent.SECURITY_ERROR, 	dojo.hitch(this, "error"));
this.file.addEventListener(air.HTTPStatusEvent.HTTP_STATUS, 		dojo.hitch(this, "error"));  
this.file.addEventListener(air.IOErrorEvent.IO_ERROR, 				dojo.hitch(this, "error"));

We connect to a few other events to handle errors, and finally we actually upload the file:

The second parameter of the ulpoad method defines the POST variable of the file so you can easily identify the file on the server side.
All what is left, are the methods which can get called by potentially fired events:

progress: function(event){	
		this.progressNode.update({
			progress:event.bytesLoaded/1000
		});
	},

This method just updates the progress bar.

error: function(event){
		var errorStr = event.toString();
		this.infoContent.innerHTML = "Error uploading: " + this.file.nativePath + "\n  Message: " + errorStr;
		this.destroy();
	}

This method will handle errors. And last but not least the complete method

	complete: function(event){
		var data = dojo.fromJson(event.data);
 
		if (data.status == 'ERROR'){
			this.infoContent.innerHTML = "An error occured: " + data.msg;
		}else{
			this.infoContent.innerHTML = "Offered: " + data.msg;
			this.onComplete(data,event);
		}
 
		dojo.fadeOut({
			node:this.progressWrapper,
			onEnd: dojo.partial(dojo.style,this.progressWrapper,"display","none")
		}).play();
 
		if(this.destroyOnComplete){
			setTimeout(dojo.hitch(this,function(){
				dojo.fadeOut({
					node: this.containerNode,
					duration:420,
					onEnd: dojo.hitch(this,function(){
						dojo.fx.wipeOut({
							node: this.containerNode,
							duration: 420,
							onEnd: dojo.hitch(this, "destroy")
						}).play();
					})
				}).play();
			}), 2000);		
		}
	},

This method gets passed the server response as a JSON string (So you have to make sure the Server passes the result in a valid JSON format).
If the server returns an error such as duplicate files or so it just shows that error.

if (data.status == 'ERROR'){
			this.infoContent.innerHTML = "An error occured: " + data.msg;
		}

If everything went to plan we also display the server response and call the onComplete method. Remember we connected to just that method in the upload method of the FancyUploader object?

else{
			this.infoContent.innerHTML = "Offered: " + data.msg;
			this.onComplete(data,event);
		}

Then we fade out our Progress bar:

dojo.fadeOut({
			node:this.progressWrapper,
			onEnd: dojo.partial(dojo.style,this.progressWrapper,"display","none")
		}).play();

And finally destroy the entire widget using a few nice animations if we set "this.destroyOnComplete" to true:

if(this.destroyOnComplete){
			setTimeout(dojo.hitch(this,function(){
				dojo.fadeOut({
					node: this.containerNode,
					duration:420,
					onEnd: dojo.hitch(this,function(){
						dojo.fx.wipeOut({
							node: this.containerNode,
							duration: 420,
							onEnd: dojo.hitch(this, "destroy")
						}).play();
					})
				}).play();
			}), 2000);		
		}

We still need to add the "onComplete" method to out FancyUploaderItem object

onComplete: function(event) {},

That is it! We are done, the only thing left is to write a Server app which can take the posted file.
I have added two files upload.php and download.php to the zip file so you can take a look yourself.

Going on Air

Test your app running "adl application.xml" in your console. Once it works fine, all we need to do is compile a Air application. this process has been well documented so I will leave it to you to create a working .air file.

Dojo-Mini

As you probably know, dojo is a very powerful toolkit which initially comes along with a quite impressive file size. Lots of files are actually not needed in your application (tests, utils, etc.) so Pete Higgins has written a great tutorial on how to minify dojo to an unbelievable 1.1MB. Packaging your AIR application with such a small dojo release is hightly recommended.

Downloads

You can download all files in a zip file. Please note that I deleted the dojo, dijit and dojox directories from the dojotoolkit folder. You need to put those back in. As usual you can get the latest Dojo release right here. or even better check Petes tutorial about minifying dojo

Tags: , ,

This entry was posted on Wednesday, April 2nd, 2008 at 10:00 am and is filed under Beginners, Tutorials. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

7 Responses to “Dojo and Air, a fancy file uploader”

  1. Pythoneer » Adobe Air Tour Europe stopped in Munich Says:

    [...] are all the AJAX devs. So I hooked him up with Nikolai right away, who I knew had been creating the nice Air demo over at [...]

  2. etreby Says:

    not working air is not defined at FancyUploader.js this.airFILE

  3. nonken Says:

    Hi etreby, can you send a more detailed error message? and describe the environment you are working in, AIR version, etc.
    Regards,

    Nikolai

  4. rambala Says:

    Hello Nikolai,

    I am getting the error: “air is undefined” FancyUploader.js (Line 23)

    I didn’t do any changes but just wanted to follow the tutorial.

    Thanks.

  5. 5 Easy Tutorials for Advanced JavaScript using Dojo | Kyle Hayes Says:

    [...] Dojo and Air, a fancy file uploader How many times have you had trouble uploading files to your favorite CMS? How many times did a client say “I am not happy with uploading one file at a time”? And last but not least, how many times did you implement a third party plugin/software/piece of magic to implement efficient file uploading? View tutorial >> [...]

  6. Dojo Javascript Framework Toolkit, Take your Apps to the Next Level | tripwire magazine Says:

    [...] Dojo and Air, a fancy file uploader [...]

  7. Getting StartED with Dojo » 5 Easy Tutorials for Advanced JavaScript using Dojo Says:

    [...] Dojo and Air, a fancy file uploader How many times have you had trouble uploading files to your favorite CMS? How many times did a client say “I am not happy with uploading one file at a time”? And last but not least, how many times did you implement a third party plugin/software/piece of magic to implement efficient file uploading? View tutorial >> [...]

Leave a Reply

You must be logged in to post a comment.