file input type is missing from the ng-model directive, so you need to "roll your own" solution for file uploads with AngularJS.
Joe: Hey, guys. In this video, we are going to talk about uploading files in Angular. Now, to show the result we're hoping to see, I have a simple HTML form here. No Angular, just uploading files to a simple .Net service that I wrote that returns the filenames and sizes of whatever we sent it.
If we look at that -- let's grab a couple files here, hit Submit -- we can see that we got our response back. If we look at what went over the wire, we can see that we have our content type, multi-part form data. We've got this boundary parameter separating each of those files.
In Angular, here's how we might attempt this. We've got a controller on our form called Uploader. We've got an input type with ng-model = files on the input. On our button here, I've got ngClick for upload, and then here I'm just trying to show the names of each of the files that we're trying to upload.
Down in our uploader controller, I've got our upload method. I'm posting to the same endpoint we used in the other example, trying to send scope.files. I've attached a header here of multipart/form-data. Let's try that out.
Again, we'll select some files. The first thing we see, we don't have our model being populated. We'll go ahead and upload. If we take a look at that, we can see a bunch of problems here. One, we're sending over application/json, and our data's not being sent over.
Let's first look at getting our files into our ngModel. The thing about the input type of file is that it doesn't receive all the inherit all the goodness that other inputs do. This first thing is a kludgy way to achieve this. I'm going to say unchange, just a regular unchange, equals angular.element, passing that this. Reach up to the scope. Let's say we're going to have files changed, and it will pass that this element.
Down here in our controller, we'll say scope.files-changed equals function. It will take in that element. We can say scope.files = element.files, and then we need to do an apply, scope.$apply. Let's try that out really quick. Choose our files. Cool! We've got our files in our model.
When we upload this, we're still going to have issues here. Again, this is kind of kludgy. Let's say we're going to have a directive called file-input. We're going to assign that to a model of files.
Let's get our directive on the page. file-input. Into that, we're going to end up using the parse service. Return our directive object. Restrict that to an attribute. In our link function, we can say element.bind. On the change event, we'll use our parse service. Pass it our attribute of file-input.
The assign method from the parse service, to assign the model in our scope to element zero. That's because when we return the element -- in this case it's going to be in an array -- it's going to be the first item in an array. Dot files. Again, we'll do scope.$apply. Now we don't have that weird kludgyness there.
Choose our files. Oops. This should be capital I, and this doesn't need a dollar sign. OK. There's that.
This also gets us the added benefit. If we wanted to create multiple inputs, our model becomes whatever we put in this file-input directive.
Here in our uploader, we didn't provide a transform request. Angular is defaulting to one that turns our post intro JSON data. To make sure that we have form data, let's get our files into a new FormData object. We'll say "for fd = equals new Form Data." Then angular.forEach scope.files. Take in our file and fd.depend to the file key, our file.
Finally, we need to go ahead and overwrite the default transform requests. What we can do here, since we've already got our FormData in a FormData object, is pass it angular.identity, which returns the first argument that it is passed. Here we need to be sending our FormData.
Again, try this one more time. Send our files. We get back the empty string. If we look at what's happening, we're missing our boundary parameter there. That's no good. We're trying to send the boundary-delimited data. It turns out that this is a rare instance where the browser knows best. What we should do is leave that content type undefined.
Let's try that out one more time. Upload. You can see in our response, we got our JSON response with each of our files. Everything's looking good.
Now this is working. It'll work in Firefox, Safari, Chrome, and Opera -- browsers where users are always updating. However, it won't work in Internet Explorer less than 10.
To clean this up, we could turn this form database into it's own little service to make it a bit more reusable, and then use a polyfill like FileAPI and some iframe hacks, but before I go ahead and reinvent the wheel here, I wanted to point out that there are some existing libraries out there that already do this for us.
If you do a Google search for Angular file upload, you're going to find a bunch of great libraries on GitHub. My personal favorite is from Danial Farid, Angular File Upload. He is in fact using file API and then ends up doing a little iframe hackery there to get this done.
There you go. That is how to do Angular file uploads.