最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Rails 4 Angularjs Paperclip how to upload file - Stack Overflow

programmeradmin4浏览0评论

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"

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"
Share Improve this question edited Apr 4, 2014 at 13:54 Alter Lagos 12.6k1 gold badge73 silver badges97 bronze badges asked Dec 11, 2013 at 14:11 JeremyPJeremyP 5851 gold badge9 silver badges20 bronze badges 5
  • I try with simple directive but when I post my file paperclip obtain an error Paperclip::AdapterRegistry::NoHandlerError: No handler found ... – JeremyP Commented Dec 16, 2013 at 0:45
  • did you ever find a solution? I'm working on a similar situation right now. – rcheuk Commented Mar 21, 2014 at 15:14
  • No, I gave up for now – JeremyP Commented Mar 24, 2014 at 21:46
  • Haha ok. Thanks for reply. I've ruled out most available modules for this combination, and am now looking at encoding the image as a string to be decoded in the back end. Sigh. Will post if I figure out something... – rcheuk Commented Mar 25, 2014 at 13:25
  • Guys, check this out. gist.github.com/vajapravin/48059fd9d64bb42f012f513cebd391ea – vajapravin Commented Apr 12, 2016 at 12:41
Add a comment  | 

2 Answers 2

Reset to default 14

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.

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);
      });
   }
});
发布评论

评论列表(0)

  1. 暂无评论