商品出品機能のコード全文


# フリマアプリの商品出品機能
最終課題で実装した商品出品ページの全コードを記録用に書きます！

## 機能
・ancestry：多階層カテゴリー
・ActiveStorage：画像投稿
・jp_prefecture:都道府県を扱うGem
・active_hash：静的データ作成

## Active Storageのセットアップ
下記をみて導入方法を確認する
[Rails ガイド](https://railsguides.jp/active_storage_overview.html)


## ルーティング

```ruby: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
```

## コントローラー

```ruby: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

```ruby: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&#47;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が０で登録される為、後々の商品詳細ページでデータを表示させる時にエラーが出ます。*


## JS

```ruby: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では大きく分けて２つ。*
*①画像のプレビュー*
*②Inputタグの生成*


```ruby:get_category_child.json.jbuilder
json.array! @category_child do |child|
  json.id child.id
  json.name child.name
end
```


```ruby:get_category_grandchild.json.jbuilder
json.array! @category_grandchild do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end
```
*モデルは割愛します。*

### 今回は以上です。
間違いなどあればご指摘いただけますと幸いです＾＾