Update total booking price on a form (like Airbnb) without refreshing using Stimulus
Tags: tutorial, rails, stimulus, javascript
Published: 2023-10-05 07:45:13



At [Le Wagon](https://www.lewagon.com/tokyo), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbnawtc4p9df870sl18v.png)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e3ae6jhlcodx541bweu1.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fthdtrarl0rdkg962iyc.png)
When we're building our form (using [Simple Form](https://github.com/heartcombo/simple_form) and [Bootstrap 5.2](https://getbootstrap.com/docs/5.2/getting-started/introduction/)), we'll try to replicate the same idea as the Airbnb form.
![Our booking form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5542a0dhpevoba32kab6.png)
Then when our user chooses the dates, we want to calculate and display the total amount
![Our booking form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5gjwc1w95z5q4e9upyj.png)

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.