When.
Some things to be clear about up front. At the time of writing this, Angular is in beta version 3. The Angular team claims that no breaking changes will be introduced in the betas, but I figured it is best we lay it out on the table that it could be a possibility. I also want to be clear that a lot of this code was co-produced with a friend of mine, Todd Greenstein.
This tutorial will be broken up into two parts:
We won’t be doing anything fancy here such as file manipulations or storing them in a database. Our purpose is just to show how it is done.
There aren’t many requirements here, but you can see what is necessary below:
The core of this tutorial will use NPM for getting our dependencies.
The first thing we want to do is create a very simplistic Angular application. We will make sure to use TypeScript since not only does it come recommended by the Angular team, but it is also very convenient to use.
From the Command Prompt (Windows) or Terminal (Mac and Linux), execute the following commands to create our file and directory structure:
mkdir src touch src/index.html touch src/tsconfig.json mkdir src/app touch src/app/app.html touch src/app/app.ts npm init -y
Of course make sure you do this in a root project directory. I have my project root at ~/Desktop/FrontEnd.
With the project structure in place, let’s grab the Angular dependencies for development. They can be installed by running the following from your Command Prompt or Terminal:
npm install angular2@2.0.0-beta.2 systemjs typescript live-server --save
Yes you’ll need Node and the Node Package Manager (NPM) to install these dependencies.
Before we start configuring TypeScript and our project, let’s define how it will be run. This can be done via the package.json file that was created when running the
npm init -y command. In the scripts section of the file, replace it with the following:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "tsc": "tsc -p src", "start": "live-server --open=src" },
This will allow us to run
npm run tsc to compile our TypeScript files and
npm run start to create our simple server.
Next up is configuring how our TypeScript files get compiled. These settings end up in our src/tsconfig.json file. Open it and add the following code:
{ "compilerOptions": { "target": "ES5", "module": "commonjs", "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "removeComments": false, "noImplicitAny": false } }
Pretty standard configuration file.
Time to start building the Angular application. The file upload stuff will happen towards the end. Open the project’s src/index.html file so we can include the Angular dependencies in our code. Make sure yours looks like the following:
<html> <head> <title>File Uploading</title></script> <script> System.config({ packages: {'app': {defaultExtension: 'js'}} }); System.import('app/app'); </script> </head> <body> <my-app>Loading...</my-app> </body> </html>
Again we’re basically just including the Angular JavaScript dependencies and telling our application to make use of the src/app/app.js file that will be generated when compiling the src/app/app.ts file. The
<my-app> tag will be populated when the
App class loads.
Now it makes sense to get the base of our project’s src/app/app.ts file started. Open it and include the following code. I’ll explain what is happening after:
import { Component, View } from "angular2/core"; import { bootstrap } from "angular2/platform/browser"; @Component({ selector: "my-app", templateUrl: "app/app.html", directives: [] }) class App { constructor() { } } bootstrap(App, []);
This is a basic TypeScript file for Angular. We’re creating an
App class and bootstrapping it at the end. This class maps to the src/app/app.html template as defined in the
@Component tag.
This is where things could start to get weird.
Open the src/app/app.html file and include the following code:
<input type="file" (change)="fileChangeEvent($event)" placeholder="Upload file..." /> <button type="button" (click)="upload()">Upload</button>
Our HTML view will be very basic. Just a file-upload form element and an upload button. However there are two important pieces here. First, take note of the
(change) tag in the file input. If you’re familiar with AngularJS 1 and Angular and how they use the
ng-model or
[(ngModel)] you might be wondering why I’m not just using that.
The short answer on why we can’t use
[(ngModel)] is that it doesn’t work. In the current version of Angular, it doesn’t map correctly which is why I had to cheat with the change event.
When a file is chosen from the picker, the
fileChangeEvent function is called and an object is passed. We’re looking for
object.target.files, but let’s not get ahead of ourself here. The whole purpose of the change event is to store reference to the files in question so that when we click the upload button, we can upload them.
This is where the
(click) event of the upload button comes into play. When the user clicks it, we will use the reference obtained in the change event and finalize the upload via a web request.
Let’s jump into our project’s src/app/app.ts file again. We need to expand upon it. Make it look like the following:
import { Component, View } from "angular2/core"; import { bootstrap } from "angular2/platform/browser"; @Component({ selector: "my-app", templateUrl: "app/app.html", directives: [] }) class App { filesToUpload: Array<File>; constructor() { this.filesToUpload = []; } upload() { this.makeFileRequest("", [], this.filesToUpload).then((result) => { console.log(result); }, (error) => { console.error(error); }); } fileChangeEvent(fileInput: any){ this.filesToUpload = <Array<File>> fileInput.target.files; } makeFileRequest(url: string, params: Array<string>, files: Array<File>) { return new Promise((resolve, reject) => { var formData: any = new FormData(); var xhr = new XMLHttpRequest(); for(var i = 0; i < files.length; i++) { formData.append("uploads[]", files[i], files[i].name); } xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status == 200) { resolve(JSON.parse(xhr.response)); } else { reject(xhr.response); } } } xhr.open("POST", url, true); xhr.send(formData); }); } } bootstrap(App, []);
We just added a lot of code so we should break it down. Let’s start with what we know based on the src/app/app.html file that we created.
The first thing that happens in our UI is the user picks a file and the
fileChangeEvent is triggered. The event includes a lot of information that is useless to us. We only want the
File array which we store in a
filesToUpload scope.
The second thing that happens is we click the upload button and the
upload function is triggered. The goal here is to make an asynchronous request passing in our array to an end-point that we’ve yet to build.
This is where things get even more crazy. As of Angular beta version 3, there is no good way to upload files. JavaScript’s FormData is rumored to work with very sketchy behavior, something we don’t want to waste our time with. Because of this we will use XHR to handle our requests. This is seen in the
makeFileRequest function.
In the
makeFileRequest function we create a promise. We are going to loop through every file in the
File array even though for this particular project we’re only passing a single file. Each file is appended to the XHR request that we’ll later send.
The promise results happen in the
onreadystatechange function. If we get a
200 response let’s assume it succeeded, otherwise lets say it failed. Finally, fire off the request.
To receive files from an upload in Node.js we need to use a middleware called Multer. This middleware isn’t particularly difficult to use, but that doesn’t matter, I’m going to give you a step by step anyways!
In another directory outside your front-end project, maybe in ~/Desktop/BackEnd, execute the following with your Terminal (Mac and Linux) or Command Prompt (Windows):
npm init -y
We’ll need to install the various Node.js, Express and Multer dependencies now. Execute the following:
npm install multer express body-parser --save
We can now start building our project. Multer will save all our files to an uploads directory because are going to tell it to. Create it at the root of our Node.js project by executing:
mkdir uploads
Now create a file at the root of your project called app.js and add the following code:
var express = require("express"); var bodyParser = require("body-parser"); var multer = require("multer"); var app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(function(req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); next(); }); app.post("/upload", multer({dest: "./uploads/"}).array("uploads", 12), function(req, res) { res.send(req.files); }); var server = app.listen(3000, function() { console.log("Listening on port %s...", server.address().port); });
Breaking it down, we are first requiring all the dependencies that we downloaded. We are using Express Framework and allowing the
body-parser to accept POST requests. Currently the front-end and back-end are separated. Running them as of now will require different host names or ports. Because of this we need to allow cross origin resource sharing (CORS) within our application.
Now we get into the small bit of Multer. When a POST request hits the
/upload endpoint Multer will place the files in the uploads directory. The files are discovered via the
uploads property defined in
.array("uploads[]"). This property is also defined in our Angular front-end in the XHR request.
Finally we operate our Node.js server on port 3000.
As of right now the application is split into two parts, the front-end and back-end. For this example it will remain that way, but it doesn’t have to.
Starting with the back-end, execute the following from the Terminal or Command Prompt:
node app.js
Remember, the back-end must be your current working directory. With the back-end serving on, navigate to the front-end project with a new Terminal or Command Prompt.
From the front-end directory, execute the following:
npm run tsc npm run start
The above commands will compile all the TypeScript files and then start the simple HTTP server. You can now navigate to from your browser.
We saw quite a bit here, however most of this was related to configuring either Angular or Node.js. The actual file upload requests were rather small. Now you might be wondering why I didn’t just choose to use the Angular File Upload library. I couldn’t figure out how to get it working. It is a bummer that as of right now these upload features aren’t baked into Angular through some kind of function, but at least we have XHR and I found it to get the job done with minimal effort.
If you want to see a more complex version of this example, check out this project I worked on with Todd Greenstein on GitHub.