Multiple Images upload in Rails with Gem Carrierwave

1.1 Giới thiệu

splash.png

Upload ảnh là một trong những chức năng hầu như không thể thiếu trong bất kì Rails nào. Hôm nay tôi sẽ giới thiệu tới các bạn một Gem trong rails giúp bạn có thể làm chức năng upload ảnh một cách dễ dàng và nhanh chóng đó là Gem Carrierwave.

2.2 Các bước thực hiện

  • Tạo một project mới.

rails new upload_multiple_image -d mysql
  • Add thêm gem vào Gemfile
#Gemfile

gem 'carrierwave'
  • Chạy bundle install
bundle install
  • Chạy tiếp lệnh sau 'rails generate uploader Photo' để tạo ra file "app/uploaders/photo_uploader.rb"

photo_uploader.png

  • Tạo ra Item đầy đủ chức năng thêm, sửa, xóa với lệnh scaffold

rails generate scaffold item name:string
#config/routes.rb

Rails.application.routes.draw do
  root "items#index"
  resources :items
end
rake db:migrate
rails s

Trên trang 'root' http://localhost:3000/ ta thu được như sau

items_ index.png

  • Tạo ra Model chứa ảnh
rails generate model ItemPhoto item_id:integer photo:string
  • Ở trong file models/item.rb chúng ta phải khai báo quan hệ 1 item có nhiều item_photos bằng cách thêm vào "has_many :item_photos"

  • Nested Attributes cho phép bạn lưu các thuộc tính liên quan đến Item. Ở đây chúng ta dùng "accepts_nested_attributes_for" để định nghĩa cho các specified association

#models/item.rb

class Item < ApplicationRecord
  has_many :item_photos
  accepts_nested_attributes_for :item_photos, allow_destroy: true, reject_if: proc { |attributes| attributes['photo'].blank? }
end
  • Item_photos mount_uploader đến trường "photo" trong bảng Item_photo. Điều đó có nghĩa là link của file ảnh sau khi upload sẽ được lưu tại trường photo trong bảng Item_photo.
#models/item_photo.rb

class ItemPhoto < ApplicationRecord
  mount_uploader :photo, PhotoUploader
  belongs_to :item
end

  • Trong controllers/items_controller.rb cần thêm attributes cho item_params " params.require(:item).permit(:name, item_photos_attributes: [:id, :item_id, :photo])"
#controllers/items_controller.rb

class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :edit, :update, :destroy]

  def index
    @items = Item.all
  end

  def show
    @item_photos = @item.item_photos.all
  end

  def new
    @item = Item.new
    @item_photo = @item.item_photos.build
  end

  def edit
  end

  def create
    @item = Item.new(item_params)

    respond_to do |format|
      if @item.save
        params[:item_photos]['photo'].each do |a|
          @item_photo = @item.item_photos.create!(:photo => a)
        end
        format.html { redirect_to @item, notice: 'Item was successfully created.' }
      else
        format.html { render :new }
      end
    end
  end

  def update
    respond_to do |format|
      if @item.update(item_params)
        params[:item_photos]['photo'].each do |a|
          @item_photo = @item.item_photos.create!(:photo => a)
        end
        format.html { redirect_to @item, notice: 'Item was successfully updated.' }
      else
        format.html { render :edit }
      end
    end
  end

  def destroy
    @item.destroy
    respond_to do |format|
      format.html { redirect_to items_url, notice: 'Item was successfully destroyed.' }
    end
  end

  private
  def set_item
    @item = Item.find(params[:id])
  end

  def item_params
    params.require(:item).permit(:name, item_photos_attributes: [:id, :item_id, :photo])
  end
end

  • Sau khi có được các setting trong model và controller thì trong form tạo mới item chúng ta cần thêm thuộc tính sau để có thể upload nhiều ảnh: "html: {multipart: true}".
#views/items/_form.html.erb

<%= form_for(@item, html: {multipart: true}) do |f| %>
  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <%= f.fields_for :item_photos do |p| %>
    <div class="field">
      <%= p.label :photo %><br>
      <%= p.file_field :photo, multiple: true, name: "item_photos[photo][]", class: "upload-image" %>
    </div>
  <% end %>
  <div id="preview"></div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

#views/items/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @item.name %>
</p>

<% @item_photos.each do |p| %>
  <%= image_tag p.photo_url, style: "max-width: 200px; max-height: 200px" %>
  <%= link_to "Destroy", item_photo_path(p), method: :delete %>
<% end %>

<%= link_to 'Edit', edit_item_path(@item) %> |
<%= link_to 'Back', items_path %>

Chúng ta cần tạo thêm chức năng xóa ảnh trong màn hình show Item

rails generate controller ItemPhotos
#controllers/item_photos_controller.rb

class ItemPhotosController < ApplicationController
  def destroy
    @item_photo = ItemPhoto.find(params[:id])
    item = @item_photo.item
    @item_photo.destroy
    respond_to do |format|
      format.html {redirect_to item_url(item), notice: 'Item photo was successfully destroyed.'}
    end
  end
end

=> Đến đây chúng ta đã hoàn thành các công việc để upload nhiều ảnh lên. Trong màn hình tạo Item, sau khi nhấn nút "Chọn tệp" chúng ta có thể dùng phím shift để lựa chọn nhiều file ảnh để upload cùng lúc như hình bên dưới đây.

multiple_select.png

Và sau khi tạo xong Item thì sẽ chuyển sang màn hình show như sau:

show2.png

  • Xem ảnh trước khi upload

Để có thể làm được chức năng preview image chúng ta tạo file preview_images.js sau

#app/assets/javascripts/preview_images.js

$(function(){
  $(".upload-image").on("change", function(){
    var preview = document.querySelector('#preview');
    var files   = document.querySelector('input[type=file]').files;

    function readAndPreview(file) {

      if ( /\.(jpe?g|png|gif)$/i.test(file.name) ) {
        var reader = new FileReader();

        reader.addEventListener("load", function () {
          var image = new Image();
          image.height = 100;
          image.width = 100;
          image.title = file.name;
          image.src = this.result;
          preview.appendChild( image );
        }, false);

        reader.readAsDataURL(file);
      }

    }

    if (files) {
      [].forEach.call(files, readAndPreview);
    }
  })
})

Sau khi hoàn thành chúng ta thu được màn hình như sau:

image_preview.png

3. Lời kết và tài liệu tham khảo

Tất cả source code của toàn bộ bài viết được upload tại đây:

>>Source Code

Như vậy là tôi đã giới thiệu xong tới các bạn chức năng upload nhiều ảnh sử dụng gem Carrierwave

Mọi thắc mắc các bạn xin vui lòng comment bên dưới. Cảm ơn đã đọc bài viết của tôi.

-- Hoàng Văn Trình AS Việt Nhật K55 Đại học Bách Khoa Hà Nội