商品詳細表示機能② 販売価格表示も3桁区切りにしてみよう
商品出品機能実装過程で、販売手数料と販売利益はカンマ区切り表示になるようにしました。
ただ、販売価格についてはJavaScriptの絡みがあってうまく実装できずにいました。
商品詳細表示機能を実装していく中で、詳細画面に遷移した際に表示される情報としては、
- 商品名
- 商品画像
- 販売価格
- 出品者
- カテゴリー
- 商品の状態
- 配送料の負担
- 発送元の地域
- 発送日の目安
上記の項目を表示させるようにします。
インスタンス変数.カラム名
でデータの取得が可能です。
販売価格を表示させようと思ったら、
@item.price
で可能ではあるのですが、ここもどうにかしたらカンマ区切り表示させられるんじゃね?
と思い立ち、調べてみることにしました。
すると、やっぱりあったー。
number_to_currency 通貨表示
number_to_currency は Rails フレームワークに組み込まれているヘルパーメソッドで、通貨をフォーマットするための便利なメソッドです。 これは Rails の ActionView ヘルパーの一部であり、HTML テンプレート内でよく使われます。
もうひとつ、数値を3桁区切り表示にさせる技があります。
to_sメソッド 数値を文字列オブジェクトに変換
to_s メソッドは基本的な Ruby メソッドで、オブジェクトを文字列に変換するためのものです。 例えば、数値を文字列に変換したい場合は、
12345678.to_s # => 12345678
このように記述します。
ここに引数で :delimited
を渡すことによってカンマ区切り表示されます。
12345678.to_s(:delimited) # => 12,345,678
to_s メソッドを使って数値を文字列に変換することはできますが、あくまでも文字列。 さくっとカンマ表示できたらいいや、ってときはいいかもです。 ただ、通貨のフォーマットなどは手動で行う必要があります。
number_to_currency を使用すると、通貨のフォーマットや桁区切りなどが自動的に適用され、より便利になります。
わたしが採用した記述はこちら。
<%= number_to_currency(@item.price, unit: "¥", precision: 0, format: "%u %n" ) %>
このように記述すると、
こんな感じで表示されます。
オプション | 説明 |
---|---|
unit: "¥" | 通貨マーク |
precision: 0 | 小数点以下非表示(整数) |
format: "%u %n" | 円マークと数値の間に空白 |
precisionオプション
を使わないと小数点以下が表示されてしまいます。
precision: 0
とすることで整数表示になります。
number_to_currency
は通貨なので数値ですね。currendy
ですからね。
もうこれは好みですね。お好きな方でどうぞって感じ。
ChatGPT先生によると、
# format: "%u %n" は、通貨の表示形式を指定するためのオプションです。具体的には、%u は通貨単位を表し、%n は数値部分を表します。 例えば、unit: "¥" を指定している場合、%u は "¥" を表示し、%n は数値を表示します。 このようなフォーマット指定を使うことで、通貨の単位と数値部分をカスタマイズすることができます。
とのこと。
◆例
number_to_currency(1234.56, unit: "¥", format: "%u %n") # 結果: "¥ 1,234.56"
この場合、"%u %n" により "¥" と数値がスペースで区切られて表示されます。 この部分を変更することで、通貨の表示形式をカスタマイズできます。
いろいろなアプローチができますね。
商品詳細表示機能① ~link_to~
link_to メソッド
link_to メソッドとは、リンクを作成するRailsのヘルパーメソッドです。 ビューファイルに記述できるメソッドになります。
以下は基本的な構文となります。
<%= link_to "テキスト", "リンク先のパス" %>
HTMLでリンクを作成するときはaタグを、Railsでリンクを作成するときは一般的に link_to を使います。 link_toメソッドを使うと、以下のようにaタグにコンパイルされます。
<a href="リンク先のパス">テキスト</a>
たとえばGoogleさんだと、
<%= link_to "Google","https://www.google.com/" %>
aタグにコンパイルされると、
<a href="https://www.google.com/">Google</a>
となります。
フリマアプリの場合は、複数の商品が存在し、それぞれにidが付与されます。 したがって、引数を指定するようになります。
rails routes
でルーティングを確認すると、
少し見づらいですが、商品詳細ページに該当する item#show
は item_path
がリンク先のパスで items/id
となっていることから
idごとにページが存在することがわかります。
上記をふまえると、
<%= link_to item_path(item.id) do %> 中略 <% end %>
このように記述することで目的の商品ページに遷移できるようになります。
do
って何!?
上記コードのように do
を記述することで、リンクの中に複数の要素やコンテンツを含めることができます。
do ~ end
内に含めたいものを記述します。
<%= link_to "Visit our site" do %> <strong>Click here!</strong> <% end %>
上記の例では、"Visit our site" テキストをクリックすると、Click here! のような強調されたテキストが表示されるリンクが生成されます。
do ブロック内には、任意のHTMLやRubyコードを含めることができます。
こうすることで、柔軟なリンクの作成が可能となります。 画像を指定することも可能です。
表示させる順番を並び替えよう
フリマアプリの商品一覧表示機能の実装の条件の中に 『出品された日時が新しい順に表示されること』 とあります。
現状のままでは出品された日時の古い順になっています。 直近の出品商品が一番最後に表示される感じです。
これを表示の時に並び替えるのではなく、情報取得時に並び替える方法があります。
order メソッド
データベースから取り出すレコードを特定の順序で並べ替えたい場合は、orderメソッドが使えます。 今回は日時順に並び替えたいので、created_atカラムを指定します。
コントローラーに記述します。
def index @items = Item.all.order("created_at DESC") end
これで出品された日時の新しい順に表示されるようになります。
補足情報 N+1問題を考える
N+1問題とは、アソシエーションを利用した場合に限り、データベースへのアクセス回数が多くなってしまう問題です。
userテーブルとitemテーブルがそれぞれ以下のようにアソシエーションで関連付けされているとします。
- has_many :items
- belongs_to :user
Item.all
でデータを取得する場合は一度のアクセスで大丈夫なのですが、
トップページに一覧表示させる場合に出品者名も表示させるとすると、
userテーブルへもitemテーブルへアクセスするのと同じだけアクセスするようになります。
それぞれのデータ数が多ければ多いほどアクセス回数も多くなり、パフォーマンスの低下につながります。
この問題を解決するのに一役買うのが includesメソッド
です。
このメソッドは、関連するモデルの情報をまとめて取得することができます。
def index @items = Item.inclues(:user).order("created_at DESC") end
includesメソッド
を使用することですべてのデータを取得できるので、allメソッドは省略することが可能です。
さあ、次は商品詳細表示機能の実装です。
たわいのないつぶやき
プログラミングとは全然関係ないんですけど、今、Tverで『ヤンキー君とメガネちゃん』が視聴可能です。 初めて見るんですけど、おもしろい!
何も考えずに見ることができるのと、よくよく見ていると出演者たちが名だたる顔ぶれで、それだけでも興味津々。 けっこう有名どころがが出ています。 見たことない―、という人はぜひ。
データあるなしでの条件分岐
present?
商品一覧表示機能の実装に取り組みます。 出品された商品データをトップページに一覧表示させる機能を実装していきます。 また、商品が何も出品されていな場合はダミー商品を表示させることとします。
まず、商品のインスタンス変数に何かしらのデータが入っている場合は、データをひとつずつ取り出して一覧表示をさせるようにします。
データがあるかないかを確認するため presentメソッド
を、データが入っている場合はそれらをすべて表示できるように eachメソッド
を使います。
● items_controller.rb
def index @items = Item.all end
● index.html.erb
<% if @items.present? %> <% if @items.each do |item| %> 中略 <% end %>
これでモデルにデータが保存されている場合は、商品情報がトップページに表示されるようになりました。
商品が何も出品されていない場合は else
で分岐させ、何も商品が出品されていないときのみダミー商品が表示されることとします。
実装完了!
Issue内には商品が売れてしまっている場合は『soldout』と表示させる条件も入っていますが、
現時点では商品購入機能の実装がまだなのでステイです。
presentメソッドとあわせて紹介されていたメソッドがあるので備忘録として記載しておきます。
nil?メソッド
変数.nil?
nilとは一言で言うと「何も存在しない」という意味です。 変数自体が存在しないかどうかを判定します。
empty?メソッド
変数.empty?
変数の値が空白かどうかを判定します。
blank?メソッド
変数.blank?
変数そのものが存在しないか、変数の値が空白かどうかを判定します。 nil?メソッドとempty?メソッドの併せ技みたいなもののようです。
なので、else
とせずとも blank?
でも同様の結果が得られます。
<% if @items.blank? %>
これもまた然り。
ブログを綴っていくにあたって、メモに題材を残しているのをふと見ると
- valueとparseFloat()とparseInt() 整数どっちがいいんだ?
- textContentと.innerHTMLどっちがいいんだ
と書き記してある。 一体何を思って残したのだろう? このとき、私は何をしようとしていたのだろう?
メモを残すにももう少し工夫が必要かもしれない...
次回は表示順の並び替えについて綴ります。
数字のカンマ表示について
前回の商品出品機能実装完了から、気がつけば2週間も経ってしまっていた... 気を取り直して続きに取り掛かろう!と再び実装を進めるつもりだったのですが、 Issueに記載している 『販売手数料と販売利益におけるカンマでの区切り表示は必須ではない』
に目がとまり、販売手数料と販売利益はカンマ表示できてる。 でも入力する販売価格のカンマ表示ができてない。 ここもできるのでは!? と気になりだしたら試したくなるわたし。
きっと方法があるはず、と調べてみると
number_to_currency(数値, オプション={})
数値を通過のフォーマットに変換させるものとしてこのようなものがあるらしい。
オプションとして :delimiter
を使うとカンマ表示ができるようだ。
でも、一体どこにコードを書くのか... ChatGPTに聞いてみる。
すると、
number_to_currency メソッドは、Railsのヘルパーメソッドであり、通常は ActionView::Helpers::NumberHelper モジュールに含まれています。このメソッドを使用するためには、ActionView::Helpers::NumberHelper モジュールを ItemsHelper モジュールに取り込む必要があります。
とのこと。
# app/helpers/items_helper.rb module ItemsHelper include ActionView::Helpers::NumberHelper def number_to_currency(price) super(price, delimiter: ',') end end
このようなコードを紹介される。
早速やってみるがこれだけでは表示されない。
もう少しChatGPTとやりとりを続けてみると、このヘルパーメソッドを使用するためには、関連するビューでヘルパーモジュールを読み込む必要があるらしい。 例えばの提案コードがこちら。
# app/controllers/items_controller.rb class ItemsController < ApplicationController helper ItemsHelper # コントローラのコード end
同じように "helper ItemHelper" をわたしのコードにも追記してみる。 でもまだ表示されない。
さらに続くやりとり。
初心者脳のわたしの質問にイラッとした様子のChatGPT。 新たに二択の提案をしてくれました。
ビューファイルの適切な場所で "number_to_currency" を使用する
JavaScriptを使用して動的にフォーマットを変更する
ビューファイルに "number_to_currency" を組み込んでみるとエラーで真っ赤になってしまったので後者で攻めてみることにする。きっと適切な場所ではなかったんだろう。
window.addEventListener('turbo:load', () => { const itemPrice = document.getElementById("item-price"); itemPrice.addEventListener('input', () => { const addTaxPrice = Math.floor(itemPrice.value * 0.1); const profit = itemPrice.value - addTaxPrice; const addTaxPriceElement = document.getElementById("add-tax-price"); const profitElement = document.getElementById("profit"); // itemPriceの値をtoLocaleStringを使ってフォーマット itemPrice.value = Number(itemPrice.value).toLocaleString('ja-JP'); addTaxPriceElement.textContent = addTaxPrice.toLocaleString('ja-JP'); profitElement.textContent = profit.toLocaleString('ja-JP'); }); });
このようにファイルを修正。 すると、数字の1,000まではうまくいく。でも、10,000にすると "NaN"と表示されてしまう。 うーーーーん、わからんーーーーー。
と、ちょっとした好奇心から数時間消費するという悪夢。 いまだ解決せず。
どなたか、詳しい方がいらっしゃったら教えてください。
気分転換に次のIssueに進みたいと思います。
Ajaxで手数料と利益の計算をするの巻③ inオプション
numericality
このヘルパーは、属性に数値のみが使われていることをバリデーションします。 デフォルトでは、整数値または浮動小数点数値にマッチします。 これらの冒頭に符号がある場合もマッチします。
1つ前の記事でもふれましたが、範囲指定のところ。 300から9,999,999の間のみ許す。 私が採用したのは
こちらなんですが、どうやら in: 300. .9_999_999
でも可能らしい。
いろいろ検索してたら見つけました。 沼ってた時に採用していたのですが、エラーメッセージがうまくいってなくてエラー出まくりだったので戻しました。
でも、エラーが解決した今。 いけるんじゃね!?と思って試してみたらいけましたー。 めっちゃDRY。でも戻しておく。
Railsガイドでは numericality
のオプションとしては greater_than
と less_than
になってる。
inオプション
は inclusion
と exclusion
のヘルパーみたい。
きっとどっちでもいいんだろうけど。
商品出品機能実装完了!
次は商品一覧表示機能です。
Ajaxで手数料と利益の計算をするの巻② モデル単体テスト
JavaScript 実装が完了し、最後の詰めでモデルのバリデーションをかけ直しました。
販売価格は、
- ¥300~¥9,999,999の間のみ保存可能であること。
- 価格は半角数値のみ保存可能であること。
こちらは numericality
でいけそう。
さっそく実装します。
こんな感じ。
値として数値のみを許す
only_integer: true
¥300~¥9,999,999の間のみ許可
greater_than_or_equal_to: 300,less_than_or_equal_to: 9_999_999
これで販売価格のバリデーション設定は完了! ちなみに、数字の間にあるアンダーバーは表示上数字を見やすくするためのもので、 データを取得しても『1_000』であれば『1000』となります。 Web上でカンマ区切りで表示させることもできます。
そして、画像で示してある通り、わたしはmessageオプションを使いました。 それが沼の始まりでした。 というか、バリデーションの仕組みを理解していなかったのがいけなかったんです。
たとえば、
only_integer: true
でバリデーションをかけると、
デフォルトのエラーメッセージは「must be an integer」なんです。
同様に、
greater_than
の場合は「must be greater than %{count}」、
less_than
の場合は「must be less than %{count}」なんです。
Active Record バリデーション - Railsガイド
そこをもうみんな一緒でいいよねー、ってことで「is invalid」 無効です、にしてたのにテストファイルとモデルファイルのエラーメッセージの統合性が取れていなかったものだから エラー、エラーのオンパレードで、エラーメッセージの始まりは大文字でなければいけない、とか 「can't be blank」なのに「is valid」になっているとかでひとつずつエラーを解消していったのですが、 最後に残ったのが販売価格のところ。
販売価格は値が空ではいけない、半角数字でなければいけない、指定の範囲でなければいけない、と複数のバリデーションがかかっており ChatGPTに聞いてみたり検索したりする中でいろいろな意見があるわけです。 それらすべてを試してみて、最終的にこちら。
require 'rails_helper' RSpec.describe Item, type: :model do before do @item = FactoryBot.build(:item) end describe '商品情報登録' do context '商品情報が登録できるとき' do it '正常に登録できるとき' do expect(@item).to be_valid end end context '商品情報が登録できないとき' do it 'itemが空では登録できない' do @item.name = '' @item.valid? expect(@item.errors.full_messages).to include "Name can't be blank" end it '商品画像が空では登録できない' do @item.image = nil @item.valid? expect(@item.errors.full_messages).to include "Image can't be blank" end it 'カテゴリーが空では登録できない' do @item.category_id = '' @item.valid? expect(@item.errors.full_messages).to include "Category can't be blank" end it '商品の状態が空では登録できない' do @item.condition_id = '' @item.valid? expect(@item.errors.full_messages).to include "Condition can't be blank" end it '配送料の負担情報が空では登録できない' do @item.postage_type_id = '' @item.valid? expect(@item.errors.full_messages).to include "Postage type can't be blank" end it '発送元の地域情報が空では登録できない' do @item.prefecture_id = '' @item.valid? expect(@item.errors.full_messages).to include "Prefecture can't be blank" end it '発送までの日数情報が空では登録できない' do @item.shipping_time_id = '' @item.valid? expect(@item.errors.full_messages).to include "Shipping time can't be blank" end it '価格情報が空では登録できない' do @item.price = nil @item.valid? expect(@item.errors.full_messages).to include "Price can't be blank" end it '価格は¥299以下の場合は登録できない' do @item.price = '299' @item.valid? expect(@item.errors.full_messages).to include "Price is invalid" end it '価格は¥1,000,000以上の場合は登録できない' do @item.price = '10000000' @item.valid? expect(@item.errors.full_messages).to include "Price is invalid" end it '価格は半角数値でなければ登録できない' do @item.price = 'ああああ' @item.valid? expect(@item.errors.full_messages).to include "Price is invalid" end end end end
やっと通りましたー!
エラーメッセージも適当に入れたらだめなのね、とかデフォルト値があるのね、とか めっちゃ勉強になりました。 さらに続く。