Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 4 Angularjs Paperclip how to upload file

I'm novice to manipulate angularjs with Rails 4 which provide only api's. I try to create a simple angular service to upload a file. But I use Paperclip to manage file and I have some issues.

First, I don't understand how to collect properly the file of the input. I have see a lot of plugin or fat directive to do that. But i want juste a simple directive that collect my file and put in my ng-model.

And finally I want to know if it is more efficient to encode my file in Base64 ?

My Rails controller

class Api::EmployeesController < Api::BaseController
  def create
    employee = Employee.create(employee_params)
    if employee.save
      render json: employee
    else
     render :json => { :errors => employee.errors.full_messages }, :status => 406
     end
  end

  def employee_params
    params.require(:employee).permit(:first_name,:mobile_phone,:file)
  end
end

My Angularjs Service

angular.module('test').factory 'Employee', ($resource, $http) ->
 class Employee
  constructor: (errorHandler) ->
  @service = $resource('/api/employees/:id',
  {id: '@id'},
  {update: {method: 'PATCH'}})
  @errorHandler = errorHandler

  create: (attrs, $scope) ->
    new @service(employee: attrs).$save ((employee) ->
      $scope.employees.push(employee)
      $scope.success = true
      $timeout (->
        $scope.success = false
      ), 3000
    ), @errorHandler

My Angularjs Controller

angular.module('test').controller "EmployeesController", ($scope, $timeout,  $routeParams, $location, Employee) ->

$scope.init = ->
 @employeeService = new Employee(serverErrorHandler)
 $scope.employees = @employeeService.all($scope)

$scope.createEmployee = (employee) ->
  if $scope.employeeFirstName
    @employeeService.create (
      first_name: $scope.employeeFirstName
      last_name:     $scope.employeeLastName
      promotion: $scope.employeePromotion
      mobile_phone: $scope.employeeMobilePhone
      nationality: $scope.employeeNationality
      social_number: $scope.employeeSocialNumber
      born_place: $scope.employeeBornPlace
      employee_convention: $scope.employeeConvention
      employee_type: $scope.employeeType
  ), $scope
  else
    $scope.error = "fields missing"
like image 270
JeremyP Avatar asked Dec 11 '13 14:12

JeremyP


2 Answers

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 needed

angular.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
  end
end

def 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
  end
end

private 

  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 functions
has_attached_file :userIcon, styles: {thumb: "100x100#"}
#validates :userIcon, :attachment_presence => true
validates_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.

like image 169
rcheuk Avatar answered Oct 17 '22 14:10

rcheuk


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);
      });
   }
});
like image 4
danial Avatar answered Oct 17 '22 13:10

danial