Khi làm chức năng upload file ở với gem carrierwave của rails, mình gặp phải 3 vấn đề nhỏ:
- Làm sao để validate extension của file ở client side và server side?
- Làm sao để preview được ảnh ngay sau khi chọn?
Mình sẽ chia sẻ cách giải quyết của mình trong 2 vấn đề đó.
Tạo form
Đầu tiên nhớ là bạn đã install carrierwave:
1 2 3 | gem 'carrierwave', '1.1.0' gem 'mini_magick', '4.7.0' |
Giả sử mình có một model Article với 2 trường title, thumbnail như sau:
1 2 3 4 5 | <span class="token keyword">class</span> <span class="token class-name">Article</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> mount_uploader <span class="token symbol">:thumbnail</span><span class="token punctuation">,</span> <span class="token constant">ImageUploader</span> validates <span class="token symbol">:title</span><span class="token punctuation">,</span> presence<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">class</span> <span class="token class-name">CreateArticles</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Migration</span><span class="token punctuation">[</span><span class="token number">6.0</span><span class="token punctuation">]</span> <span class="token keyword">def</span> change create_table <span class="token symbol">:articles</span> <span class="token keyword">do</span> <span class="token operator">|</span>t<span class="token operator">|</span> t<span class="token punctuation">.</span>string <span class="token symbol">:title</span> t<span class="token punctuation">.</span>string <span class="token symbol">:thumbnail</span> t<span class="token punctuation">.</span>timestamps <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Tạo routes, controllers và views cho form
1 2 3 4 | <span class="token constant">Rails</span><span class="token punctuation">.</span>application<span class="token punctuation">.</span>routes<span class="token punctuation">.</span>draw <span class="token keyword">do</span> resource <span class="token symbol">:articles</span> <span class="token keyword">end</span> |
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 | <span class="token keyword">class</span> <span class="token class-name">ArticlesController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token keyword">new</span> <span class="token variable">@article</span> <span class="token operator">=</span> <span class="token constant">Article</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token class-name">end</span> <span class="token keyword">def</span> create <span class="token variable">@article</span> <span class="token operator">=</span> <span class="token constant">Article</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token class-name">article_params</span> <span class="token keyword">if</span> <span class="token variable">@article</span><span class="token punctuation">.</span>save redirect_to <span class="token function">article_path</span><span class="token punctuation">(</span><span class="token variable">@article</span><span class="token punctuation">)</span> <span class="token keyword">else</span> render <span class="token symbol">:new</span> <span class="token class-name">end</span> <span class="token keyword">end</span> <span class="token keyword">def</span> show <span class="token variable">@article</span> <span class="token operator">=</span> <span class="token constant">Article</span><span class="token punctuation">.</span>find parmas<span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">]</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> article_params params<span class="token punctuation">.</span><span class="token keyword">require</span><span class="token punctuation">(</span><span class="token symbol">:article</span><span class="token punctuation">)</span><span class="token punctuation">.</span>permit <span class="token symbol">:thumbnail</span><span class="token punctuation">,</span> <span class="token symbol">:title</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #app/views/articles/new.html.erb <div class="col-md-4" style="padding: 88px 0 0 85px;"> <%= form_for @article do |f|%> <div class="form-group"> <%= f.label :title, "Title: " %> <%= f.text_field :title, class: "form-control" %> </div> <div class="form-group"> <%= f.label :thumbnail, "Select a picture:" %> <%= f.file_field :thumbnail, class: "form-control" %> </div> <%= f.submit "Submit" %> <% end %> </div> |
Mình sẽ thêm 1 đoạn initializers để add errors_message vào dưới mỗi field trong form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # Tạo file config/initializers/form_error.rb ActionView::Base.field_error_proc = Proc.new do |html_tag, instance_tag| fragment = Nokogiri::HTML.fragment(html_tag) field = fragment.at('input,select,textarea') model = instance_tag.object error_message = model.errors.full_messages.join(', ') html = if field field['class'] = "#{field['class']} invalid" html = <<-HTML #{fragment.to_s} <p class="error">#{error_message}</p> HTML html else html_tag end html.html_safe end |
Và ta có được một form như sau:
Validate extension của file
Bây giờ, để thực hiện validate extension của file , mình sẽ chia sẻ 2 cách:
- Validate client side: Sử dụng javascript.
- Validate model level : Sử dụng validate ở class uploader của carrierwave
Để validate ở client side ta bắt sự kiện onchange ở file_field như sau:
1 2 | <%= f.file_field :thumbnail, class: "form-control", onchange: "validateFiles(this);" %> |
Và thêm 1 đoạn javascript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token keyword">function</span> <span class="token function">validateFiles</span><span class="token punctuation">(</span>inputFile<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> extErrorMessage <span class="token operator">=</span> <span class="token string">"File bạn muốn tải lên không đúng định dạng!"</span><span class="token punctuation">;</span> <span class="token keyword">var</span> allowedExtension <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"jpg"</span><span class="token punctuation">,</span> <span class="token string">"jpeg"</span><span class="token punctuation">,</span> <span class="token string">"png"</span><span class="token punctuation">,</span> <span class="token string">"gif"</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">var</span> extName<span class="token punctuation">;</span> <span class="token keyword">var</span> extError <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> $<span class="token punctuation">.</span><span class="token function">each</span><span class="token punctuation">(</span>inputFile<span class="token punctuation">.</span>files<span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> extName <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>name<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>$<span class="token punctuation">.</span><span class="token function">inArray</span><span class="token punctuation">(</span>extName<span class="token punctuation">,</span> allowedExtension<span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>extError<span class="token operator">=</span><span class="token boolean">true</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>extError<span class="token punctuation">)</span> <span class="token punctuation">{</span> window<span class="token punctuation">.</span><span class="token function">alert</span><span class="token punctuation">(</span>extErrorMessage<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">$</span><span class="token punctuation">(</span>inputFile<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">val</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Và nó chạy như sau:
Nhưng với cách nói trên, khi browser tắt js, thì nó trở lên vô dụng. Vì vậy, mình thường thực hiện validate ở model level.
Carrierwave cung cấp cho bạn method extension_whitelist
để validate extension trong class CarrierWave::Uploader::Base
.
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">class</span> <span class="token class-name">ImageUploader</span> <span class="token operator"><</span> <span class="token constant">CarrierWave</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Uploader</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> storage <span class="token symbol">:file</span> <span class="token keyword">def</span> store_dir <span class="token string">"uploads/<span class="token interpolation"><span class="token delimiter tag">#{</span>model<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span>to_s<span class="token punctuation">.</span>underscore<span class="token delimiter tag">}</span></span>/<span class="token interpolation"><span class="token delimiter tag">#{</span>mounted_as<span class="token delimiter tag">}</span></span>/<span class="token interpolation"><span class="token delimiter tag">#{</span>model<span class="token punctuation">.</span>id<span class="token delimiter tag">}</span></span>"</span> <span class="token keyword">end</span> <span class="token keyword">def</span> extension_whitelist <span class="token string">%w(jpg jpeg gif png)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Thêm I18n cho message :
1 2 3 4 5 | en: errors: messages: extension_whitelist_error: "Your file was wrong extensions." |
Và phần validation sẽ chạy như thế này:
Preview file ảnh ngay sau khi chọn
Để preview được ảnh ngay sau khi chọn file (mà chưa upload lên server) , đầu tiên mình sẽ thay đổi nút select file 1 chút:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token comment">#new.html.erb</span> <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"col-md-4"</span> style<span class="token operator">=</span><span class="token string">"padding: 88px 0 0 85px;"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= form_for @article do |f|%> <div class=</span><span class="token string">"form-group"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= f.label :title, "Title: " %> <%=</span> f<span class="token punctuation">.</span>text_field <span class="token symbol">:title</span><span class="token punctuation">,</span> <span class="token keyword">class</span><span class="token punctuation">:</span> <span class="token string">"form-control"</span> <span class="token string">%> </div></span> <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"form-group"</span><span class="token operator">></span> <span class="token operator"><</span>p<span class="token operator">></span><span class="token constant">Select</span> a picture<span class="token punctuation">:</span><span class="token operator"><</span><span class="token operator">/</span>p<span class="token operator">></span> <span class="token operator"><</span>label id<span class="token operator">=</span><span class="token string">"image-label"</span> <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"image-hover"</span> <span class="token keyword">for</span><span class="token operator">=</span><span class="token string">"article_thumbnail"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= image_tag "default.png", id: "thumbnail-img", size: "200x200" %> </label> <%=</span> f<span class="token punctuation">.</span>file_field <span class="token symbol">:thumbnail</span><span class="token punctuation">,</span> <span class="token keyword">class</span><span class="token punctuation">:</span> <span class="token string">"form-control none"</span> <span class="token string">%> </div></span> <span class="token operator"><</span><span class="token operator">%</span><span class="token operator">=</span> f<span class="token punctuation">.</span>submit <span class="token string">"Submit"</span> <span class="token string">%> <% end %></span> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> |
Ảnh “default.png” các bạn tải tại đây và cho vào assets/images. Thêm 1 chút css:
1 2 3 4 5 6 7 8 9 10 11 12 | .none { display: none; } .image-hover{ &:hover { opacity: 0.5; transition: all 0.3s ease; } cursor: pointer; } |
Và ta được 1 cái field chọn ảnh như sau:
Ta sử dụng class FileReader của js để thực hiện chức năng preview ảnh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | $(document).on('ready', function() { handle_preview($("#article_thumbnail"), $("#thumbnail-img")); }); function readURL(input, image) { if (input.files && input.files[0]) { var reader = new FileReader(); reader.onload = function(e) { image.attr('src', e.target.result); } reader.readAsDataURL(input.files[0]); } } function handle_preview(input_tag, image){ input_tag.change(function(e){ var file = e.target.files[0]; readURL(e.target, image); }); } |
Và việc chức năng preview ảnh đã hoàn thành:
Mình đã viết hàm handleprevew(input_tag, image)
theo cách dễ sử dụng lại nhất. Bạn chỉ cần lấy được id của thẻ input[type="file"]
và id của ảnh label của nó là được.
Bài viết của mình đến đây là kết thúc.
Refernces:
https://github.com/carrierwaveuploader/carrierwave/wiki/CarrierWave-and-multiple-databases
https://github.com/carrierwaveuploader/carrierwave/wiki/CarrierWave-and-multiple-databases