4
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

商品出品画面を作る①

Last updated at Posted at 2020-03-24

メルカリの商品出品画面を参考にコピーを作る

参考画面メルカリ

使用する機能

  • ActiveStorage(画像投稿)リンク
  • ancestry(多階層カテゴリー)リンク
  • active_hash(静的データ作成)
  • RSpec(テスト)リンク

##Haml

new.html.haml
.sell
  %header.sell-header
    = link_to root_path do
      = image_tag 'mercari_top_logo.svg', alt: 'mercari', height: '49', width: '185'
  -#メイン部分
  %main
    %section.sell-container
      = form_with model: @item do |f|

        -# 画像部分
        .sell-container__content
          .sell-title
            %h3.sell-title__text
              出品画像
              %span.sell-title__require
                必須
          .sell-container__content__max-sheet 最大10枚までアップロードできます
          .sell-container__content__upload
            .sell-container__content__upload__items
              .sell-container__content__upload__items__box
                %ul#output-box
                  %div#image-input{tabindex:"0"}
                    = f.label :images, for: "item_images0", class: 'sell-container__content__upload__items__box__label', data: {label_id: 0 } do 
                      = f.file_field :images, multiple: true, class: "sell-container__content__upload__items__box__input", id: "item_images0", style: 'display: none;'
                      %pre
                        %i.fas.fa-camera.fa-lg
                        ドラッグアンドドロップ
                        またはクリックしてファイルをアップロード
          .error-messages#error-image

        -#商品名部分
        .sell-container__content
          .sell-title
            %h3.sell-title__text
              商品名
              %span.sell-title__require
                必須
          = f.text_field :name, {class:'sell-container__content__name', required: "required", placeholder: '商品名(必須 40文字まで)'}
          .error-messages#error-name

          .sell-title
            %h3.sell-title__text
              商品の説明
              %span.sell-title__require
                必須
          = f.text_area :text,{class: 'sell-container__content__description', required: "required", rows: '7', maxlength: '1000', placeholder: text_placeholder}
          -# placeholderでtems_helperを呼び出す
          .sell-container__content__word-count
            %span#word-count
              0
            /1000
          .error-messages#error-text

        -# 詳細部分
        .sell-container__content
          %h3.sell-sub-head 商品の詳細
          .sell-container__content__details
            .sell-title
              %h3.sell-title__text
                カテゴリー
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :category_id, {class: 'sell-collection_select__label'} do
                = f.collection_select :category_id, @category_parent, :id, :name, {prompt: "選択して下さい"},{ class: 'sell-collection_select__input', id: 'category-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-category

            .sell-title
              %h3.sell-title__text
                商品の状態
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :condition_id, {class: 'sell-collection_select__label'} do
                = f.collection_select :condition_id, Condition.all, :id, :condition, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'condition-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-condition

        -# 配送部分
        .sell-container__content
          %h3.sell-sub-head
            %p 配送について
            = link_to '/delivery',target: '_blank',class: 'sell-sub-head__guides-link' do
              %i.far.fa-question-circle
          .sell-container__content__delivery
            .sell-title
              %h3.sell-title__text
                配送料の負担
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :deliverycost_id, {class: 'sell-collection_select__label'} do
                = f.collection_select :deliverycost_id, Deliverycost.all, :id, :payer, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'deliverycost-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-deliverycost

            .sell-title
              %h3.sell-title__text
                発送元の地域
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :pref_id, class: 'sell-collection_select__label' do
                = f.collection_select :pref_id, Pref.all, :id, :name, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'pref-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-pref

            .sell-title
              %h3.sell-title__text
                発送までの日数
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :delivery_days_id, class: 'sell-collection_select__label' do
                = f.collection_select :delivery_days_id, DeliveryDays.all, :id, :days, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'delivery_days-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-delivery_days

        -# 価格部分
        .sell-container__content
          %h3.sell-sub-head
            %p 販売価格(300〜9,999,999)
            = link_to '/price',target: '_blank', class: 'sell-sub-head__guides-link' do
              %i.far.fa-question-circle
          .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', required: "required"}
          .error-messages#error-price

          .sell-container__content__commission
            .sell-container__content__commission__left
              販売手数料 (10%)
            .sell-container__content__commission__right.sell-container__content__profit
            .sell-container__content__profit__left
              販売利益
            .sell-container__content__profit__right.submit-btn
            = f.submit '出品する', class: 'submit-btn__sell-btn'
            = link_to 'もどる', root_path, class: 'submit-btn__return-btn'
          .attention-box
            %p
              禁止されている
              = link_to '行為', '/prohibited_conduct', target: '_blank'
              および
              = link_to '出品物', '/prohibited_item', target: '_blank'
              を必ずご確認ください。
              = link_to '偽ブランド品', '/counterfeit_goods', target: '_blank'= link_to '盗品物', '/stolen_goods', target: '_blank'
              などの販売は犯罪であり、法律により処罰される可能性があります。また、出品をもちまして
              = link_to '加盟店規約', '/seller_terms', target: '_blank'
              に同意したことになります。
            
  %footer.sell-footer
    %nav
      %ul.clearfix
        %li
          = link_to '#' do
            プライバシーポリシー
        %li
          = link_to '#' do
            メルカリ利用規約
        %li
          = link_to '#' do
            特定商取引に関する表記
    = link_to root_path, class: 'footer__logo' do
      = image_tag 'logo-gray.svg', alt: 'mercari', height: '65', width: '80'
    %p
      %small
        © Mercari, Inc.

CSS

items_new.scss
a {
  color: inherit;j[i
  text-decoration: none;
  box-sizing: border-box;
}
img {
  vertical-align: middle;
  box-sizing: border-box;
}
.error-messages{
  color: #ff0211;
  font-size: 14px;
  line-height: 1.4em;
  margin: 16px 0;
  box-sizing: border-box;
}

.sell-title{
  align-items: center;
  margin: 0!important;
  box-sizing: border-box;
  &__text{
    font-size: 14px;
    font-weight: 600;
    line-height: 1.4em;
  }
  &__require{
    margin-left: 8px;
    font-size: 12px;
    padding: 0 4px;
    background-color: #ff0211;
    color: #fff;
    border-radius: 2px;
    display: inline-block;
    font-style: normal;
    font-weight: 600;
    line-height: 1.4em;
    margin: 0;
  }
}

.sell-sub-head{
  box-sizing: border-box;
  color: rgb(136, 136, 136);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.4em;
  margin-bottom: 24px;
  display: flex;
  &__guides-link{
    color: rgb(0, 149, 238);
    margin-left: 4px;
  }
}

.sell-collection_select{
  box-sizing: border-box;
  margin-top: 16px;
  &__label{
    display: inline-block;
    position: relative;
    width: 100%;
    .fas.fa-chevron-down{
      box-sizing: border-box;
      pointer-events: none;
      position: absolute;
      right: 16px;
      top: 40%;
      color: rgb(136, 136, 136);
      height: 48px;
    }
  }
  &__input{
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background-color: #fff;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  color: #222;
  font-size: 16px;
  height: 48px;
  line-height: 1;
  margin: 0;
  outline: none;
  padding: 0 56px 0 16px;
  width: 100%;
  }
}
// ここより上は繰り返し使用するパーツ

.sell {
  box-sizing: border-box;
  position: relative;
  color: rgb(51, 51, 51);
  background-color: rgb(245, 245, 245);
  font-family: Arial, 游ゴシック体, YuGothic, メイリオ, Meiryo, sans-serif;
  font-size: 14px;
  line-height: 1;
  box-sizing: border-box;
  .sell-header{
    box-sizing: border-box;
    height: 128px;
    align-items: center;
    display: flex;
    justify-content: center;
  }
  .sell-container{
    box-sizing: border-box;
    max-width: 700px;
    width: 100%;
    margin: 0px auto;
    background-color: rgb(255, 255, 255);

    // 画像部分
    &__content{
      height: auto;
      padding: 40px;
      border-bottom: 1px;
      border-bottom-color: #efefef;
      border-bottom-style: solid;
      &__max-sheet{
        margin-top: 16px;
      }
      &__upload{
        margin-top: 16px;
        display: flex;
        flex-wrap: wrap;
        &__items{
          height: auto;
          width: 100%;

          &__box{
            height: auto;
            align-content: center;
            align-items: center;
            cursor: pointer;
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            position: relative;
            border-width: 1px;

            #output-box{
              box-sizing: border-box;
              display: flex;
              flex-wrap: wrap;
              width: 100%;
              height: auto;

              .preview-image{
                box-sizing: border-box;
                height: 150px;
                width: 20%;
                padding: 0px 4px;
                margin-top: 8px;

                &__figure{
                  margin:0 auto;
                  height: 118px;
                  background-color: rgb(245, 245, 245);
                  img{
                    box-sizing: border-box;
                    width: 100%;
                    height: 100%;
                    object-fit: contain;
                  }
                }
                &__button{
                  border-top-width: 1px;
                  border-top-color: rgb(204, 204, 204);
                  border-top-style: solid;
                  background-color: rgb(245, 245, 245);
                  justify-content: space-around;
                  display: flex;
                  align-items: center;
                  height: 32px;
                  color: rgb(0, 149, 238);
                }
              }

              #image-input{
                box-sizing: border-box;
                height: 100%;
                -webkit-flex: 1;
                flex: 1;
                margin-top: 8px;

                .sell-container__content__upload__items__box__label{
                  box-sizing: border-box;
                  background-color: rgb(245, 245, 245);
                  height: 150px;
                  border-width: 1px;
                  border-style: dashed;
                  border-color: rgb(204, 204, 204);
                  text-align: center;
                  display: flex;
                  align-items: center;
                  justify-content: center;
                  i{
                    box-sizing: border-box;
                    margin-bottom: 8px;
                  }
                }
              }
            }
          }
        }
      }
    }

    // 商品名部分
    &__content{
      &__name{
        margin-top: 16px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        height: 48px;
        padding: 0 16px;
        width: 100%;
      }
      &__description{
        margin-top: 16px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        padding: 16px;
        width: 100%;
        font-size: 16px;
        display: block;
      }
      &__word-count{
        text-align: right;
        color: #888;
        font-size: 12px;
        line-height: 1.4em;
      }
    }
    // 詳細は共通パーツのみ
    // 配送は共通パーツのみ
    // 価格部分
    &__content{
      &__price{
        -webkit-box-align: center;
        align-items: center;
        box-sizing: content-box;
        display: flex;
        height: 46px;
        justify-content:space-between;
        &form__label{
          font-size: 14px;
        }
        &__form__box{
          border: 1px solid #ccc;
          border-radius: 4px;
          height: 48px;
          margin-left: 8px;
          padding: 0 16px;
          width: 300px;
          align-items: center;
          display: inline-flex;
          text-align: right;
        }
      }
      #error-price{
        box-sizing: border-box;
        text-align: right;
      }
      &__commission{
        display: flex;
        justify-content:space-between;
        height: 70px;
        padding: 12px 0px;
        align-items: center;
        border-bottom: 1px;
        border-bottom-color: #efefef;
        border-bottom-style: solid;
      }
      &__profit{
        display: flex;
        justify-content:space-between;
        height: 70px;
        padding: 12px 0px;
        align-items: center;
      }
    }
    .submit-btn{
      box-sizing: border-box;
      margin: 0 auto;
      width: 360px;
      margin-bottom: 32px;
      &__sell-btn{
        background-color: #ea352d;
        color: #fff;
        margin-bottom: 24px;
        width: 100%;
        font-size: 17px;
        height: 48px;
        font-weight: 600;
        border-radius: 4px;
      }
      &__return-btn{
        background-color: #ccc;
        color: #222;
        width: 100%;
        font-size: 17px;
        height: 48px;
        font-weight: 600;
        padding: 14px 0;
        border-radius: 4px;
        display: inline-block;
        text-align: center;
      }
    }
    .attention-box{
      box-sizing: border-box;
      font-size: 12px;
      line-height: 1.4em;
      word-break: keep-all;
      a{
        box-sizing: border-box;
        color: #0095ee;
      }
    }
  }
  .sell-footer{
    box-sizing: border-box;
    padding: 40px 0px;
    text-align: center;
    nav{
      box-sizing: border-box;
      .clearfix{
        box-sizing: border-box;
        display: inline-block;
        font-size: 12px;
        display: flex;
        justify-content: center;
        margin-bottom: 40px;
        li{
          box-sizing: border-box;
          margin: 0px 8px;
        }
      }
    }
  }
}

JS

items_new.js
$(document).on('turbolinks:load', function(){
  // 画像が選択された時プレビュー表示、inputの親要素のdivをイベント元に指定
  $('#image-input').on('change', function(e){
    
    //ファイルオブジェクトを取得する
    let files = e.target.files;
    $.each(files, function(index, file) {
      let reader = new FileReader();

      //画像でない場合は処理終了
      if(file.type.indexOf("image") < 0){
        alert("画像ファイルを指定してください。");
        return false;
      }
      //アップロードした画像を設定する
      reader.onload = (function(file){
        return function(e){
          let imageLength = $('#output-box').children('li').length;
          // 表示されているプレビューの数を数える
          
          let labelLength = $("#image-input>label").eq(-1).data('label-id');
          // #image-inputの子要素labelの中から最後の要素のカスタムデータidを取得

          // プレビュー表示
          $('#image-input').before(`<li class="preview-image" id="upload-image${labelLength}" data-image-id="${labelLength}">
                                      <figure class="preview-image__figure">
                                        <img src='${e.target.result}' title='${file.name}' >
                                      </figure>
                                      <div class="preview-image__button">
                                        <a class="preview-image__button__edit">編集</a>
                                        <a class="preview-image__button__delete" data-image-id="${labelLength}">削除</a>
                                      </div>
                                    </li>`);
          $("#image-input>label").eq(-1).css('display','none');
          // 入力されたlabelを見えなくする

          if (imageLength < 9) {
            // 表示されているプレビューが9以下なら、新たにinputを生成する
            $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                        <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                        <i class="fas fa-camera fa-lg"></i>
                                      </label>`);
          };
        };
      })(file);
      reader.readAsDataURL(file);
    });
  });

  //削除ボタンが押された時
  $(document).on('click', '.preview-image__button__delete', function(){
    let targetImageId = $(this).data('image-id');
    // イベント元のカスタムデータ属性の値を取得
    $(`#upload-image${targetImageId}`).remove();
    //プレビューを削除
    $(`[for=item_images${targetImageId}]`).remove();
    //削除したプレビューに関連したinputを削除

    let imageLength = $('#output-box').children('li').length;
    // 表示されているプレビューの数を数える
    if (imageLength ==9) {
      let labelLength = $("#image-input>label").eq(-1).data('label-id');
      // 表示されているプレビューが9なら,#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得
      $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                  <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                  <i class="fas fa-camera fa-lg"></i>
                                </label>`);
    };
  });

  // f.text_areaの文字数カウント
  $("textarea").keyup(function(){
    let txtcount = $(this).val().length;
    $("#word-count").text(txtcount);
  });

  //販売価格入力時の手数料計算
  $('#item_price').keyup(function(){
    let price= $(this).val();
    if (price >= 300 && price <= 9999999){
      let fee = Math.floor(price * 0.1);
      // 小数点以下切り捨て
      let profit = (price - fee);
      $('.sell-container__content__commission__right').text('¥'+fee.toLocaleString());
      // 対象要素の文字列書き換える
      $('.sell-container__content__profit__right').text('¥'+profit.toLocaleString());
    } else{
      $('.sell-container__content__commission__right').html('');
      $('.sell-container__content__profit__right').html('');
    }
  });

  $(function(){
    // カテゴリーセレクトボックスのオプションを作成
    function categoryOption(category){
      var optionHtml = `<option value="${category.id}">${category.name}</option>`;
      return optionHtml;
    }
    // 親カテゴリー選択後のイベント
    $('#category-select-parent').on('change', function(){
      let parentCategoryId = $(this).val();
      //選択された親カテゴリーのIDを取得
      if (parentCategoryId == ''){
        //親カテゴリーが空(初期値)の時
        $('#select-children-box').remove();
        $('#select-grandchildren-box').remove();
        //子と孫を削除するする
      }else{
        $.ajax({
          url: '/items/category_children',
          type: 'GET',
          data: { parent_id: parentCategoryId },
          dataType: 'json'
        })
        .done(function(category_children){
          $('#select-children-box').remove();
          $('#select-grandchildren-box').remove();
          //親が変更された時、子と孫を削除するする
          let optionHtml = '';
          category_children.forEach(function(child){
            optionHtml += categoryOption(child);
            //option要素を作成する
          });
          $('#error-category').before(`<div class="sell-collection_select " id="select-children-box">
                                          <label class="sell-collection_select__label" for="item_category_id">
                                            <select class="sell-collection_select__input" id="category-select-children" required="required" name="item[category_id]">
                                              <option value="">選択して下さい</option>
                                              ${optionHtml}
                                            </select>
                                            <i class="fas fa-chevron-down"></i>
                                          </label>
                                        </div>`
          );
        })
        .fail(function(){
          alert('カテゴリー取得に失敗しました');
        });
      }
    });
    // 子カテゴリー選択後のイベント
    $('.sell-container__content__details').on('change', '#category-select-children', function(){
      let childrenCategoryId = $(this).val();
      //選択された子カテゴリーのIDを取得
      if (childrenCategoryId == ''){
        //子カテゴリーが空(初期値)の時
        $('#select-grandchildren-box').remove(); 
        //孫以下を削除する
      }else{
        $.ajax({
          url: '/items/category_grandchildren',
          type: 'GET',
          data: { child_id: childrenCategoryId },
          dataType: 'json'
        })
        .done(function(category_grandchildren){
          $('#select-grandchildren-box').remove();
          //子が変更された時、孫を削除するする
          let optionHtml = '';
          category_grandchildren.forEach(function(grandchildren){
            optionHtml += categoryOption(grandchildren);
            //option要素を作成する
          });
          $('#error-category').before(`<div class="sell-collection_select " id="select-grandchildren-box">
                                          <label class="sell-collection_select__label" for="item_category_id">
                                            <select class="sell-collection_select__input" id="category-select-grandchildren" required="required" name="item[category_id]">
                                              <option value="">選択して下さい</option>
                                              ${optionHtml}
                                            </select>
                                            <i class="fas fa-chevron-down"></i>
                                          </label>
                                        </div>`
          );
        })
        .fail(function(){
          alert('カテゴリー取得に失敗しました');
        });
      }
    });
  });


  // 各フォームの入力チェック
  $(function(){
    //画像
    $('#image-input').on('focus',function(){
      $('#error-image').text('');
      $('#image-input').on('blur',function(){
        $('#error-image').text('');
        let imageLength = $('#output-box').children('li').length;
        if(imageLength ==''){
          $('#error-image').text('画像がありません');
        }else if(imageLength >10){
          $('#error-image').text('画像を10枚以下にして下さい');
        }else{
          $('#error-image').text('');
        }
      });
    });

    //送信しようとした時
    $('form').on('submit',function(){
      let imageLength = $('#output-box').children('li').length;
      if(imageLength ==''){
        $('body, html').animate({ scrollTop: 0 }, 500);
        $('#error-image').text('画像がありません');
      }else if(imageLength >10){
        $('body, html').animate({ scrollTop: 0 }, 500);
        $('#error-image').text('画像を10枚以下にして下さい');
      }else{
        return true;
      }
    });

     //画像を削除した時
    $(document).on('click','.preview-image__button__delete',function(){
      let imageLength = $('#output-box').children('li').length;
      if(imageLength ==''){
        $('#error-image').text('画像がありません');
      }else if(imageLength >10){
        $('#error-image').text('画像を10枚以下にして下さい');
      }else{
        $('#error-image').text('');
      }
    });

    //商品名
    $('.sell-container__content__name').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-name').text('入力してください');
        $(this).css('border-color','red');
      }else{
        $('#error-name').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //商品説明
    $('.sell-container__content__description').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-text').text('入力してください');
        $(this).css('border-color','red');
      }else{
        $('#error-text').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //カテゴリーのエラーハンドリング
    function categoryError(categorySelect){
      let value = $(categorySelect).val();
      if(value == ""){
        $('#error-category').text('選択して下さい');
        $(categorySelect).css('border-color','red');
      }else{
        $('#error-category').text('');
        $(categorySelect).css('border-color','rgb(204, 204, 204)');
      }
    };
    //親カテゴリー
    $('#category-select-parent').on('blur',function(){
      categoryError('#category-select-parent')
    });
    //子カテゴリー
    $('.sell-container__content__details').on('blur', '#category-select-children', function(){
      categoryError('#category-select-children')
    });
    //孫カテゴリー
    $('.sell-container__content__details').on('blur', '#category-select-grandchildren', function(){
      categoryError('#category-select-grandchildren')
    });

    //状態
    $('#condition-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-condition').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-condition').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //送料負担
    $('#deliverycost-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-deliverycost').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-deliverycost').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //発送元
    $('#pref-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-pref').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-pref').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //発送までの日数
    $('#delivery_days-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-delivery_days').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-delivery_days').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //価格
    $('.sell-container__content__price__form__box').on('blur',function(){
      let value = $(this).val();
      if(value < 300 || value > 9999999){
        $('#error-price').text('300以上9999999以下で入力してください');
        $(this).css('border-color','red');
      }else{
        $('#error-price').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });
  });  
});

コントローラー

items.controller.rb
  def new
    @item = Item.new
    @category_parent =  Category.where("ancestry is null")
  end

  # 親カテゴリーが選択された後に動くアクション
  def category_children
    @category_children = Category.find("#{params[:parent_id]}").children
    #親カテゴリーに紐付く子カテゴリーを取得
  end

  # 子カテゴリーが選択された後に動くアクション
  def category_grandchildren
    @category_grandchildren = Category.find("#{params[:child_id]}").children
    #子カテゴリーに紐付く孫カテゴリーの配列を取得
  end

  def create
    @item = Item.new(item_params)
    if @item.save
      redirect_to root_path
     
    else
      render :new
    end
  end


  private

  def item_params
    params.require(:item).permit(:name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :price, images: []).merge(user_id: current_user.id, boughtflg_id:"1")
  end
end

モデル

item.rb
class Item < ApplicationRecord

  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to_active_hash :condition
  belongs_to_active_hash :pref
  belongs_to_active_hash :deliverycost
  belongs_to_active_hash :delivery_days
  belongs_to_active_hash :boughtflg
  # 上記active_hashのアソシエーション
  validate :images_presence
  validates :name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :boughtflg_id,:user_id, presence: true
  validates :price, presence: true, numericality: { greater_than_or_equal_to: 300, less_than_or_equal_to: 9999999 }

  has_many_attached :images
  belongs_to :user, foreign_key: 'user_id'
  # optional: true後で消す belongs_toのnotnull制約解放のため使用している
  belongs_to :category

  #imageのバリデーション
  def images_presence
    if images.attached?
      # inputに保持されているimagesがあるかを確認
      if images.length > 10
        errors.add(:image, '10枚まで投稿できます')
      end
    else
      errors.add(:image, '画像がありません')
    end
  end
end

jbuilder

category_children.json.jbuilder
json.array! @category_children do |child|
  json.id   child.id
  json.name child.name
end
category_grandchildren.json.jbuilder
json.array! @category_grandchildren do |grandchild|
  json.id   grandchild.id
  json.name grandchild.name
end

機能の実装

長いですね、すみません
冒頭の3つ機能についてはリンクで飛んで下さい。

4
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?