Recently I had to refactor an existing multipart form which allowed users to submit an image and a small description. The newer version must be submitted in AJAX and the image must be resized on the client side to save user bandwidth, instead of doing it on the server side like the older version.
This post talks about this refactoring.
Before
I have this “simple” form in app/views/review/_form.html.slim
:
1
2
3
4
= simple_form_for review, html: { multipart: true } do |f|
= f.input :image
= f.input :content
= f.button :submit
And this is the Review
model (I’m using paperclip gem to manage uploads):
1
2
3
4
class Review < ActiveRecord::Base
has_attached_file :image
validates :content, presence: true
end
After
First things first. Let’s add remote: true
to our form, because I want to submit it asynchronously with ajax.
1
2
3
4
= simple_form_for review, remote: true, html: { multipart: true } do |f|
= f.input :image
= f.input :content
= f.button :submit
Add a .js
view (in app/views/review/_create.js.erb
or _update.js.erb
) called from controller responding to js:
1
2
var $container = $("#<%= dom_id(@review); %>")
$container.html("<%= escape_javascript(render 'reviews/form', review: @review) %>");
This solution is fast and simple but won’t work with multipart forms.
Now we have to rewrite our form using a little bit of javascript to fix this problem. The goal is to send a resized image via form as Base64 string
using HTML canvas.
Luckily, HTML5’ Canvas is present in the major browsers today!
This is an useful ImageResize
script that accepts some settings, returns a Base64 string and have a callback function. Source.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
$.fn.ImageResize = function (options) {
var defaults = {
maxWidth: Number.MAX_VALUE,
maxHeigth: Number.MAX_VALUE,
onImageResized: null
}
var settings = $.extend({}, defaults, options);
var selector = $(this);
selector.each(function (index) {
var control = selector.get(index);
if ($(control).prop("tagName").toLowerCase() == "input" && $(control).attr("type").toLowerCase() == "file") {
$(control).attr("accept", "image/*");
control.addEventListener('change', handleFileSelect, false);
}
else {
cosole.log("Invalid file input field");
}
});
function handleFileSelect(event) {
//Check File API support
if (window.File && window.FileList && window.FileReader) {
var count = 0;
var files = event.target.files;
for (var i = 0; i < files.length; i++) {
var file = files[i];
//Only pics
if (!file.type.match('image')) continue;
var picReader = new FileReader();
picReader.addEventListener("load", function (event) {
var picFile = event.target;
var imageData = picFile.result;
var img = new Image();
img.src = imageData;
img.onload = function () {
if (img.width > settings.maxWidth || img.height > settings.maxHeigth) {
var width = settings.maxWidth;
var height = settings.maxHeigth;
if (img.width > settings.maxWidth) {
width = settings.maxWidth;
var ration = settings.maxWidth / img.width;
height = Math.round(img.height * ration);
}
if (height > settings.maxHeigth) {
height = settings.maxHeigth;
var ration = settings.maxHeigth / img.height;
width = Math.round(img.width * ration);
}
var canvas = $("<canvas/>").get(0);
canvas.width = width;
canvas.height = height;
var context = canvas.getContext('2d');
context.drawImage(img, 0, 0, width, height);
imageData = canvas.toDataURL();
if (settings.onImageResized != null && typeof (settings.onImageResized) == "function") {
settings.onImageResized(imageData);
}
}
}
img.onerror = function () {
}
});
//Read the image
picReader.readAsDataURL(file);
}
} else {
console.log("Your browser does not support File API");
}
}
}
Now the _form
refactoring:
1
2
3
4
5
6
input type="file" data-uploader=true
= simple_form_for review, remote: true, html: { multipart: true } do |f|
= f.input :image, as: :hidden # simple_form automatically sets the id to 'review_image'
= f.input :content
= f.button :submit
In this example I’ve hidden the image input and put an input file out of form with a data attribute to indentify it.
And now we can connect ImageResize.js
and the Rails
with:
1
2
3
4
5
6
$("[data-uploader]").ImageResize({
maxWidth: 800,
onImageResized: function (imageData) {
$("#review_image").val(imageData);
}
});
In this JavaScript snippet we select [data-uploader]
input file and we call the ImageResize
function that takes as input a max width value and a callback. The callback is called after the original image has been resized, so we fill in our hidden input with the Base64 string of the image.
Paperclip works out of the box with Base64 strings.
That’s all!