フリマアプリの商品出品機能
最終課題で実装した商品出品ページの全コードを記録用に書きます!
機能
・ancestry:多階層カテゴリー
・ActiveStorage:画像投稿
・jp_prefecture:都道府県を扱うGem
・active_hash:静的データ作成
Active Storageのセットアップ
下記をみて導入方法を確認する
Rails ガイド
ルーティング
routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: {
registrations: 'users/registrations',
}
devise_scope :user do
get 'sending_destinations', to: 'users/registrations#newSendingDestination'
post 'sending_destinations', to: 'users/registrations#createSendingDestination'
end
root "items#index"
resources :items, only: [:new, :create, :update] do
collection do
get 'get_category_child', to: 'items#get_category_child', defaults: { format: 'json' }
get 'get_category_grandchild', to: 'items#get_category_grandchild', defaults: { format: 'json' }
end
end
end
コントローラー
items_controller
class ItemsController < ApplicationController
def index
@items_category = Item.where("buyer_id IS NULL AND trading_status = 0 AND category_id < 200").order(created_at: "DESC")
@items_brand = Item.where("buyer_id IS NULL AND trading_status = 0 AND brand_id = 1").order(created_at: "DESC")
end
def new
@item = Item.new
@item.item_imgs.new
@category_parent = Category.where(ancestry: nil)
# 親カテゴリーが選択された後に動くアクション
def get_category_child
@category_child = Category.find("#{params[:parent_id]}").children
render json: @category_child
#親カテゴリーに紐付く子カテゴリーを取得
end
# 子カテゴリーが選択された後に動くアクション
def get_category_grandchild
@category_grandchild = Category.find("#{params[:child_id]}").children
render json: @category_grandchild
#子カテゴリーに紐付く孫カテゴリーの配列を取得
end
end
def create
@item = Item.new(item_params)
unless @item.valid?
@item.item_imgs.new
render :new and return
end
@item.save
redirect_to root_path
end
private
def item_params
params.require(:item).permit(:name, :introduction, :price, :prefecture_code, :brand_id, :pref_id, :size_id, :item_condition_id, :postage_payer_id, :preparation_day_id, :postage_type_id, :category_id, :trading_status, item_imgs_attributes: [:url, :id]).merge(seller_id: current_user.id)
end
Haml
sell.html.haml
.sell-container
= form_for @item do |f|
-# 画像部分
.sell-container__content
.sell-title
%h3.sell-title__text
出品画像
%span.sell-title__require
必須
.sell-container__content__box
%ul.output-box
%div#image-input
= f.fields_for :item_imgs do |image|
= image.label :url, id: 'image-input__label' do
= image.file_field :url, accept: "image/*", class: "js-file", data: {index: image.index}, style: 'display: none;'
%pre
%i.fas.fa-camera.fa-lg
.error-messages
%p
=@item.errors.messages[:item_imgs][0]
-# 商品概要部分
.sell-container__content
.sell-title
%h3.sell-title__text
商品名
%span.sell-title__require
必須
= f.text_field :name, {class: 'sell-container__content__name', maxlength: '40', placeholder: '商品名(必須 40文字まで)'}
.error-messages
%p
=@item.errors.messages[:name][0]
.sell-title
%h3.sell-title__text
商品の説明
%span.sell-title__require
必須
= f.text_area :introduction,{class: 'sell-container__content__description', rows: '7', maxlength: '1000', placeholder: '商品説明'}
.sell-container__content__word-count
%p.error-messages
=@item.errors.messages[:introduction][0]
%span#word-count
0/1000
-# 詳細部分
.sell-container__content
%h3.sell-sub-head 商品の詳細
.sell-container__content__details
.sell-title
%h3.sell-title__text
カテゴリー
%span.sell-title__require
必須
.sell-collection_select.category
.select_collection_select-category
= f.collection_select :category_id, @category_parent,:id, :name, {prompt: "--"},{class: 'sell-collection_select__label', id:'parent_category'}
.error-messages
%p
=@item.errors.messages[:category_id][0]
.sell-title
%h3.sell-title__text
サイズ
%span.sell-title__require
必須
.sell-collection_select
= f.collection_select :size_id, Size.all, :id, :value, {prompt: "--"} ,{class: 'sell-collection_select__label'}
.error-messages
%p
=@item.errors.messages[:size_id][0]
.sell-title
%h3.sell-title__text
ブランド
%span.sell-title__require.arbitrary
任意
.sell-collection_select
= f.collection_select :brand_id, Brand.all, :id, :name, {prompt: "--"} ,{class: 'sell-collection_select__label'}
.error-messages
.sell-title
%h3.sell-title__text
商品の状態
%span.sell-title__require
必須
.sell-collection_select
= f.collection_select :item_condition_id, ItemCondition.all, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'}
.error-messages
%p
=@item.errors.messages[:item_condition_id][0]
-# 配送部分
.sell-container__content
%h3.sell-sub-head
%p 配送について
.sell-container__content__details
.sell-title
%h3.sell-title__text
配送方法
%span.sell-title__require
必須
.sell-collection_select
= f.collection_select :postage_type_id, PostageType.all, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'}
.error-messages
%p
=@item.errors.messages[:postage_type_id][0]
.sell-title
%h3.sell-title__text
配送料の負担
%span.sell-title__require
必須
.sell-collection_select
= f.collection_select :postage_payer_id, PostagePayer.all.map, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'}
.error-messages
%p
=@item.errors.messages[:postage_payer_id][0]
.sell-title
%h3.sell-title__text
発送元の地域
%span.sell-title__require
必須
.sell-collection_select
= f.collection_select :prefecture_code, JpPrefecture::Prefecture.all, :code, :name, {include_blank: '--'}, class: 'sell-collection_select__label'
.error-messages
%p
=@item.errors.messages[:prefecture_code][0]
.sell-title
%h3.sell-title__text
発送までの日数
%span.sell-title__require
必須
.sell-collection_select
= f.collection_select :preparation_day_id, PreparationDay.all, :id, :value, {prompt: "--"}, {class: 'sell-collection_select__label'}
.error-messages
%p
=@item.errors.messages[:preparation_day_id][0]
-# 価格部分
.sell-container__content
%h3.sell-sub-head
%p 販売価格(300〜9,999,999)
.sell-container__content__price
.sell-title
%h3.sell-title__text
販売価格
%span.sell-title__require
必須
.sell-container__content__price__form
= f.label :price, class: 'sell-container__content__price__form__label' do
¥
= f.number_field :price, {placeholder: '0', value: '', autocomplete:"off", class: 'sell-container__content__price__form__box'}
.error-messages#error-price
=@item.errors.messages[:price][0]
.sell-container__content__commission
.sell-container__content__commission__left
販売手数料 (10%)
.sell-container__content__commission__right ー
.sell-container__content__commission
.sell-container__content__commission__left
販売利益
.sell-container__content__commission__right ー
.submit-btn
= f.submit '出品する', class: 'submit-btn--Btn'
.submit-btn
= link_to 'もどる', root_path, class: 'submit-btn--Btn return'
.attention-box
%p
禁止されている行為および出品物を必ずご確認ください。偽ブランド品や盗品物
などの販売は犯罪であり、法律により処罰される可能性があります。また、出品をもちまして加盟店規約に同意したことになります。
必須項目が見入力の場合、エラーメッセージが出るように設定しています。
また、住所はjp_prefectureという都道府県を扱うGemを使用しました!
*ポイント*
ボックスはf.collectionに統一して記述する。
f.selectを使用するとvalueが文字でDBに送られる為、idが0で登録される為、後々の商品詳細ページでデータを表示させる時にエラーが出ます。
JS
items.js
$(document).on('turbolinks:load', ()=> {
// 画像用のinputを生成する関数
const buildFileField = (index)=> {
const html = `<input accept="image/*" class="js-file" data-index="${index}" style="display: none;", type="file" name="item[item_imgs_attributes][${index}][url]" id="item_item_imgs_attributes_${index}_url">`;
return html;
}
// プレビュー用のimgタグを生成する関数
const buildImg = (index, url)=> {
const html = `<img data-index="${index}" src="${url}" width="100px" height="100px">`;
return html;
}
// file_fieldのnameに動的なindexをつける為の配列
let fileIndex = [1,2,3,4,5,6,7,8,9,10];
$('#image-input').on('change', '.js-file', function(e) {
// labelタグのfor属性を変更
$('#image-input__label').attr('for', 'item_item_imgs_attributes_' + fileIndex[0] + '_url');
// fileIndexの先頭の数字を使ってinputを作る
$('#image-input').append(buildFileField(fileIndex[0]));
fileIndex.shift();
// 末尾の数に1足した数を追加する
fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
const targetIndex = $(this).parent().data('index');
// ファイルのブラウザ上でのURLを取得する
const file = e.target.files[0];
const blobUrl = window.URL.createObjectURL(file);
$('#image-input').before(buildImg(targetIndex, blobUrl));
});
});
JSでは大きく分けて2つ。
①画像のプレビュー
②Inputタグの生成
get_category_child.json.jbuilder
json.array! @category_child do |child|
json.id child.id
json.name child.name
end
get_category_grandchild.json.jbuilder
json.array! @category_grandchild do |grandchild|
json.id grandchild.id
json.name grandchild.name
end
モデルは割愛します。
今回は以上です。
間違いなどあればご指摘いただけますと幸いです^^