AngularJS has an interesting gap in functionality that can make working with file uploads difficult. You might expect attaching a file to an <input type=”file”> to trigger the ng-change event, however this does not happen. There are a number of Stackoverflow questions on the subject, with a popular answer being to use a native onclick attribute and call into Angular’s internals (e.g. onchange=”angular.element(this).scope().fileNameChaged()”)
This solution feels brittle, and relies on some unsupported Angular interactions from the template. To work around this issue, Github user danialfarid has provided the awesome angular-file-upload library to simplify this process by extending Angular’s attributes to include ng-file-select. This is a cleaner implementation. This library also includes an injectable $upload object and its documentation shows how this abstracts the file upload process in the controller. This abstraction (if used) sends the uploaded file to the server immediately, and without the contents of the rest of the form. I wanted to submit this file change with the traditional all-at-once approach that HTML forms take. This way, the user can abandon form changes by neglecting to press the submit button, and keep the original file attachment unmodified.
In order to achieve this, I’ve created a solution that uses the HTML5 FileAPI to base64 encode the contents of the file, and attach it to the form. Instead of reinventing the ng-file-select event, I opted to use the angular-file-upload library described above. However instead of using the injected $upload functionality referenced in its README, we will serialize the attachment with a base64 encoded string.
To begin, create an AngularJS module for your application, and include the angularFileUpload dependency:
window.MyApp = angular.module('MyApp', [ 'angularFileUpload' ] )
Next, we will create our AngularJS template and include our HTML input tags:
<div ng-controller="MyCtrl"> <form ng-submit="save()"> <input type="file" ng-file-select="onFileSelect($files)" /> <input type="submit" /> </form> </div>
Now we can create our AngularJS controller, and define the onFileSelect function referenced in the the ng-file-select attribute:
class exports.MyCtrl @$inject: ['$scope', '$http'] constructor: (@scope, @$http) -> @scope.onFileSelect = @onFileSelect onFileSelect: ($files) => angular.forEach $files, (file) => reader = new FileReader() reader.onload = (e) => @scope.attachment = e.target.result reader.readAsDataURL file save: => @$http( method: 'POST', url: "/path/to/handler", data: $.param( attachment: @scope.attachment ) headers: 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 'Accept': 'text/javascript' )
Our controller is now in place. When the input’s attachment changes, onFileSelect is called which iterates through the collection of files (if multiple) and creates a FileReader instance for each one. The reader then has functionality attached to its onload event in the way of assigning the result to an attribute in our @scope object. The call to readAsDataURL starts reading the file and creates a data: URL representing the file’s data as a base64 encoded string.
Once the form is submitted, the save function is called from the value of ng-submit on our form tag. This performs a standard AngularJS XHR action, and includes the attachment assignment in the params. I have adjusted the Content-Type in the headers to communicate to the server that the content contains URL encoded data. If we had other form fields, we could serialize and append them to the params collection to send to the server alongside the attachment in the same place in the code.
Image Attachments
For added feedback to the user on image attachments, the img tag’s src attribute can accept a base64 encoded string as a value. Since we have this value from our FileReader object, we can update the view instantly with the file without doing any server side processing. To achieve this, we can add an image tag to our HTML file:
<div ng-controller="MyCtrl"> <form ng-submit="save()"> <img ng-src="{{attachment}}" /> <input type="file" ng-file-select="onFileSelect($files)" /> <input type="submit" /> </form> </div>
Next, we can make a few modifications to our onFileSelect function:
onFileSelect: ($files) => angular.forEach $files, (file) => if file.type in ["image/jpeg", "image/png", "image/gif"] reader = new FileReader() reader.onload = (e) => @scope.$apply () => @scope.attachment = e.target.result @scope.reader.readAsDataURL file
AngularJS two way data binding takes care of the messy details for us. The template is bound to @scope.attachment_url. We do some safety checks that the filetype is an image, and then we assign the attachment_url key to the base64 encoded image. A call to scope.apply() will repaint the screen, and the user will see the image they have attached displayed.
Thanks to Nick Karpenske, and Robert Lasch for help with the implementation!
Hi Ben, I am somewhat green when it comes to angular. I don’t understand the syntax of your controller, it doesn’t look like anything I have encountered in angular documentation/posts. Can you explain? Thanks!
LikeLike
I think I found the answer: coffeescript! Great post btw!
LikeLike