Rails 4 Angularjs Paperclip how to upload file Rails 4 Angularjs Paperclip how to upload file angularjs angularjs

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);      });   }});