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"
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:
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);
});
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With