DEV Community

Douglas Berkley
Douglas Berkley

Posted on

Update total booking price on a form (like Airbnb) without refreshing using Stimulus

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 booking form for 3 days
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.
Airbnb booking form for 5 days

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).
Schema for homes
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.
Our booking form
Then when our user chooses the dates, we want to calculate and display the total amount
Our booking form

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
Enter fullscreen mode Exit fullscreen mode

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>";
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Voilà! And now you should be be able to give your user a better date picking experience.

Top comments (1)

Collapse
 
yannklein profile image
Yann Klein

wow amazing!