Update total booking price on a form (like Airbnb) without refreshing using Stimulus
Douglas Berkley
Posted on October 5, 2023
At Le Wagon, our students spend 1 week building a Ruby on Rails(v7) Airbnb clone. There are times where we want to update booking prices in real-time for a user based on the dates they choose.
Airbnb Example
Let's base our idea on the booking form from Airbnb.
Airbnb is telling us how much it costs to book a specific place per night, but it's also doing the math for the entire 3-day stay.
So if I want to add two extra days to my trip, I'd like to have the form update the final price as soon as I change the dates.
Setup
Let's walk through how we can do this in Ruby on Rails(v7) and Stimulus.
In our example here, we're booking a home. So our booking
has a start_date
, end_date
, and the home
has a price
(per night).
When we're building our form (using Simple Form and Bootstrap 5.2), we'll try to replicate the same idea as the Airbnb form.
Then when our user chooses the dates, we want to calculate and display the total amount
Okay so let's get this feature installed and plugged into our app. We'll do this in two steps: the Stimulus controller and HTML(erb) form.
Stimulus controller
Let's generate our Stimulus controller first in the Terminal
rails g stimulus booking_price
Then we can add in the logic to calculate the days and prices
// booking_price_controller.js
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="booking-price"
export default class extends Controller {
static targets = ["start_date", "end_date", "price", "info"];
static values = { price: Number };
update() {
const pricePerDay = parseInt(this.priceValue, 10);
// make sure the user has chosen a start date and end date
if (
this.start_dateTarget.value !== "" &&
this.end_dateTarget.value !== ""
) {
// calculating the time from the values in the HTML
const diffInMs =
new Date(this.end_dateTarget.value) -
new Date(this.start_dateTarget.value);
const diffInDays = diffInMs / (1000 * 60 * 60 * 24);
if (diffInDays > 0) {
// displays the total price per nigh
this.infoTarget.innerText = `¥${pricePerDay} x ${diffInDays} nights`;
this.priceTarget.innerHTML = `<span>¥${(
diffInDays * pricePerDay
).toLocaleString()}</span> <small class='fw-light'>total</small> `;
} else if (diffInDays === 0) {
// displays 1-night minimum if end date is same as start date
this.infoTarget.innerText = "";
this.priceTarget.innerHTML =
"<span class='text-danger fw-light'>1-night mininum</span>";
} else {
// displays invalid if the end date is before the start or not overnight
this.infoTarget.innerText = "";
this.priceTarget.innerHTML =
"<span class='text-danger fw-light'>Invalid dates</span>";
}
}
}
}
View
Then let's head to our view, where we have a number of things to add:
- We are wrapping the entire form in our
data-controller="booking-price"
. - We're adding our price as a
data-booking-price-price-value="<%= @home.price %>"
so that we have access to it in the JS Controller. - We're adding data-actions onto our form inputs so that when the user changes the dates, it'll update our price.
- We're adding a target at the end where we'd like to display the total price once it's been calculated.
- Lots of style with Bootstrap.
<div data-controller="booking-price" data-booking-price-price-value="<%= @home.price %>" class="mt-5 border p-3 rounded">
<!-- Displays price/night on top of form -->
<p class='fw-bold fs-3'>¥<%= @home.price %> <small class='fw-light'>night</small></p>
<%= simple_form_for [@home, @booking] do |f| %>
<div class="d-flex">
<!-- On change of dates, it triggers our update inside of our JS controller -->
<%= f.input :start_date, html5: true, input_html: { data: { booking_price_target: 'start_date', action: 'change->booking-price#update' }, min: Date.today } %>
<%= f.input :end_date, html5: true, input_html: { data: { booking_price_target: 'end_date', action: 'change->booking-price#update' }, min: Date.today } %>
</div>
<%= f.submit 'Book', class: 'btn btn-primary w-100' %>
<!-- Hidden when no dates chosen, total amount displayed here when changed -->
<p class='mt-3 d-flex align-items-center justify-content-between'>
<span data-booking-price-target='info'></span>
<span class='fw-bold' data-booking-price-target='price'></span>
</p>
<% end %>
</div>
Voilà! And now you should be be able to give your user a better date picking experience.
Posted on October 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 5, 2023