【Rails】フォームで過去の日付を選択できないようにする
はじめに
現在作成中のアプリで、スケジュールを作成する際に、過去の日付を選択できないようにする実装を行いました。
保存するモデルのカラムはDate型を使用しており、form_withでフォームを作成しています。
環境
Ruby 3.2.3
Rails 7.1.3.4
実装内容(必要箇所のみ記載)
私のアプリでは旅程を立てるためにtripモデルのdeparture_date
とreturn_date
(Date型)に保存します。
元のコード🔻
<%= form_with(model: @trip, local: true) do |form| %>
<%= form.date_field :departure_date, class: "form-input block w-full mt-2 p-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-base" %>
<% end %>
現在は過去の日付も選択できるようになっています。
アプリでは旅の一週間前から通知を送りたいので、過去の日付けをユーザーが誤って登録しないように変更します。
<%= form_with(model: @trip, local: true) do |form| %>
<%= form.date_field :departure_date, class: "form-input block w-full mt-2 p-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-base", min: Date.current.strftime("%Y-%m-%d"), id: "departure_date" %>
<% end %>
- Date.current.strftime("%Y-%m-%d"): Date.current を YYYY-MM-DD 形式に変換して、 min 属性に渡します。strftime("%Y-%m-%d") は、Ruby の Date や Time オブジェクトを特定の形式の文字列に変換するためのメソッド。
- min 属性: HTML5の<input>要素の一部として使用され、特に日付や時間などの値を入力するフィールドで利用されます。この属性を指定することで、ユーザーが入力できる最小値を制限することができます。
ブラウザはこのmin属性を検出すると、指定された最小値よりも前の日付を選択することを防ぐために、カレンダーで選択できる日付を自動的に制限します。 - idは後ほどJSで使用します。
Date.currentとは?
Railsガイドによると、Active SupportではDate.current
を定義して現在のタイムゾーンにおける「今日」を定めています。
つまり、Date.todayはサーバーのシステムのタイムゾーンを使うので、ユーザーのタイムゾーンとズレが生じる(Date.todayがDate.yesterdayと等しくなる)可能性があります。
ユーザーの環境に合わせた日付の処理が必要な場合は、Date.currentを使うことが推奨されています。
詰まった点: 出発日より未来の日付を帰国日に選択する
出発日を選択した後に、その日付より未来の日付のみを帰国日として選べるようにするには、JavaScriptを使って出発日が選択された時に、その日付を基準に帰国日のmin属性を動的に設定する必要があると分かりました。
私はこの方法がすぐに思い浮かばず、難しく感じました。当初予想していたDate.futureはJavaScriptのライブラリには存在しないようです。
最終的なコード🔻
<%= form_with(model: @trip, local: true) do |form| %>
~~~~~~省略~~~~~~~~~~~~
<div class="flex flex-col md:flex-row justify-between mb-6 space-y-4 md:space-y-0 md:space-x-4">
<div class="w-full">
<%= form.label :departure_date, "出発日", class: "block text-gray-700 text-lg font-semibold mb-2" %>
<%= form.date_field :departure_date, class: "form-input block w-full mt-2 p-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-base", min: Date.current.strftime("%Y-%m-%d"), id: "departure_date" %>
</div>
<div class="w-full">
<%= form.label :return_date, "帰国日", class: "block text-gray-700 text-lg font-semibold mb-2" %>
<%= form.date_field :return_date, class: "form-input block w-full mt-2 p-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-base", min: Date.current.strftime("%Y-%m-%d"), id: "return_date" %>
</div>
</div>
<script>
document.getElementById('departure_date').addEventListener('change', function() {
const departureDate = new Date(this.value); #選択された出発日の日付をJavaScriptのDateオブジェクトに変換。この値を使って帰国日の最小選択日を設定します。this.valueは、変更された要素の値(選択された日付)を指す。
const returnDateInput = document.getElementById('return_date'); #帰国日の日付入力フィールドを取得
returnDateInput.min = this.value; // 出発日を基に最小日付を設定
});
</script>
<% end %>
出発日を選択すると、その日付を基準にして帰国日として選べる範囲が自動的に制限されるようになりました。
終わりに
今回はビュー側で過去の日付を選択できないようにしましたが、コントローラーやモデルのバリデーションで保存できないようにする方法も考えられます。動的に日付の制限を行う際には、JavaScriptを使用することが重要であると改めて理解できました。
今回もご覧いただきありがとうございました。
Discussion
mockeyさん、こんにちは。
常に「出発日 <= 帰国日」になるようにするUI制御はプログラミングのお題としてなかなか興味深いですね。
ただ、erbの中にJSを埋め込むのはちょっとスマートさに欠けるので、Stimulusを使ってみるといいかも、と思いました。(Rails 7.0以降なら最初からStimulusが使えるようになってるはず)
作り方はこんな感じです。
まず、Stimulusのコントローラを作ります。
次にerbを修正してStimulusと連動できるようにします。
最後に、
app/javascript/controllers/date_range_controller.js
を以下のように編集します。上のコードに出てきた
date-range
,from
,to
,updateTo
はただの名前なので好きな名前を付けてもらって構いません。これでこの記事と同じことが実現できるはずです。
さらに
updateTo
メソッドはこんなふうに実装するとさらにユーザーフレンドリーになると思います。上のコードを実行するとこんな動きになります。
よかったら参考にしてみてください。
おまけ
前のコメントの実装だとminの設定がerbとJSに分散するので、JSに一本化するのも良いかもしれません。
伊藤さん、こんばんは。
昨日はご丁寧にコメントを頂きましてありがとうございました。
ご提案してくださったコードを実行したところ、同じ挙動になったことを確認できました。
自分ではstimulusで実装できると考えられていなかったので、大変参考になりました。
また、ユーザビリティを考慮して削除機能を連動させる点も今後の開発で意識していきます。
Ruby入門の本🍒で現在学習中なので、引き続き学習を頑張ります。
ありがとうございました。