Rails 4 Angularjs Paperclip how to upload file
After a few days of troubleshooting and figuring out how both technologies work (I'm new to both -.-), I managed to get something working. I don't know if it's the best way, but it works. If anyone has any improvements, I'd be happy to hear them.
In general, I did the following:
- Create a Directive in AngularJS to handle the File Upload
- Encoded the file as a base64 String and attached it to a JSON object.
- Rails controller decoded the base64 String using StringIO and re-attached the file to the parameters
- Then I updated or created the model with the new updated parameters.
It felt really roundabout, so if there is another way to do this, I'd like to know!
I'm using Rails 4 and the most recent stable version of AngularJS, Paperclip, and Restangular.
Here's the related code:
Angularjs Directive
var baseUrl = 'http localhost:port'; // fill in as neededangular.module('uploadFile', ['Restangular']) // using restangular is optional.directive('uploadImage', function () {return { restrict: 'A', link: function (scope, elem, attrs) { var reader = new FileReader(); reader.onload = function (e) { // retrieves the image data from the reader.readAsBinaryString method and stores as data // calls the uploadImage method, which does a post or put request to server scope.user.imageData = btoa(e.target.result); scope.uploadImage(scope.user.imagePath); // updates scope scope.$apply(); }; // listens on change event elem.on('change', function() { console.log('entered change function'); var file = elem[0].files[0]; // gathers file data (filename and type) to send in json scope.user.imageContent = file.type; scope.user.imagePath = file.name; // updates scope; not sure if this is needed here, I can not remember with the testing I did...and I do not quite understand the apply method that well, as I have read limited documentation on it. scope.$apply(); // converts file to binary string reader.readAsBinaryString(file); }); }, // not sure where the restangular dependency is needed. This is in my code from troubleshooting scope issues before, it may not be needed in all locations. will have to reevaluate when I have time to clean up code. // Restangular is a nice module for handling REST transactions in angular. It is certainly optional, but it was used in my project. controller: ['$scope', 'Restangular', function($scope, Restangular){ $scope.uploadImage = function (path) { // if updating user if ($scope.user.id) { // do put request $scope.user.put().then( function (result) { // create image link (rails returns the url location of the file; depending on your application config, you may not need baseurl) $scope.userImageLink = baseUrl + result.image_url; }, function (error) { console.log('errors', JSON.stringify(errors)); }); } else { // if user does not exist, create user with image Restangular.all('users') .post({user: $scope.user}) .then(function (response) { console.log('Success!!!'); }, function(error) { console.log('errors', JSON.stringify(errors)); }); } }; }]};});
Angular File with Directive
<input type="file" id="fileUpload" ng-show="false" upload-image /><img ng-src="{{userImageLink}}" ng-click="openFileWindow()" ng-class="{ hidden: !userImageLink}" ><div class="drop-box" ng-click="openFileWindow()" ng-class=" {hidden: userImageLink}"> Click to add an image.</div>
This creates a hidden file input. The userImageLink
is set in the controller, as is the openFileWindow()
method. If a user image exists, it displays, otherwise it displays a blank div telling the user to click to upload an image.
In the controller that is responsible for the html code above, I have the following method:
// triggers click event for input file, causing the file selection window to open$scope.openFileWindow = function () { angular.element( document.querySelector( '#fileUpload' ) ).trigger('click'); console.log('triggering click');};
Rails Side
In the user model's controller, I have the following methods:
# set user params before_action :user_params, only: [:show, :create, :update, :destroy]def create # if there is an image, process image before save if params[:imageData] decode_image end @user = User.new(@up) if @user.save render json: @user else render json: @user.errors, status: :unprocessable_entity Rails.logger.info @user.errors endenddef update # if there is an image, process image before save if params[:imageData] decode_image end if @user.update(@up) render json: @user else render json: @user.errors, status: :unprocessable_entity endendprivate def user_params @up = params.permit(:userIcon, :whateverElseIsPermittedForYourModel) end def decode_image # decode base64 string Rails.logger.info 'decoding now' decoded_data = Base64.decode64(params[:imageData]) # json parameter set in directive scope # create 'file' understandable by Paperclip data = StringIO.new(decoded_data) data.class_eval do attr_accessor :content_type, :original_filename end # set file properties data.content_type = params[:imageContent] # json parameter set in directive scope data.original_filename = params[:imagePath] # json parameter set in directive scope # update hash, I had to set @up to persist the hash so I can pass it for saving # since set_params returns a new hash everytime it is called (and must be used to explicitly list which params are allowed otherwise it throws an exception) @up[:userIcon] = data # user Icon is the model attribute that i defined as an attachment using paperclip generator end
The user.rb file would have this:
### image validation functionshas_attached_file :userIcon, styles: {thumb: "100x100#"}#validates :userIcon, :attachment_presence => truevalidates_attachment :userIcon, :content_type => { :content_type => ["image/jpg", "image/gif", "image/png"] }validates_attachment_file_name :userIcon, :matches => [/png\Z/, /jpe?g\Z/]
I think this is everything that is relevant. Hope this helps. I'll probably post this somewhere else a bit more clearly when I have time.
But i want juste a simple directive that collect my file and put in my ng-model
ng-file-upload just does that and it is light-weight, easy to use, cross-browser solution which supports progress/abort, drag&drop and preview.
<div ng-controller="MyCtrl"> <input type="file" ngf-select ng-model="files" multiple></div>$scope.$watch('files', function(files) { for (var i = 0; i < $files.length; i++) { var file = $files[i]; $scope.upload = $upload.upload({ url: 'server/upload/url', file: file, }).progress(function(evt) { console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total)); }).success(function(data, status, headers, config) { console.log(data); }); }});