Real Time total price with Stimulus

dedemenezes_

Dedé Menezes

Posted on June 3, 2022

Real Time total price with Stimulus

Feature: When the user change the booking date it should update the total price in real time

Steps:

  1. Create Stimlus Controller
  2. Listen to change event on all date inputs
  3. Build date from the inputs field values
  4. Call getTime() on the generated dates
  5. Subtracts start from end
  6. Transform milliseconds into days
  7. Multiply days by price
  8. Display the total price in the browser

Here is the simple_form_for basic structure. There is one new field to display the total price. It was added using the same style as simple_form_for inputs.

<%= simple_form_for([@starship, @booking]) do |f| %>
  <%= f.input :start_date, as: :date, order: [ :day, :month, :year ] %>
  <%= f.input :end_date, as: :date, order: [ :day, :month, :year ] %>
  <div class="mb-3 string">
    <label for="total-price" class='form-label'>Total price</label>
    <p class='form-control'name="total-price" id="totalPrice" data-total-price-target='input'></p>
  </div>
  <%= f.submit class: 'btn btn-primary' %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

First thing we need to do is create the stimulus controller responsible for this behaviour.

touch app/javascript/controllers/total_price_controller.js
Enter fullscreen mode Exit fullscreen mode

Everything will happen in between the form tags. The inputs we need and the tag which we will display the total price are inside the form tags. We should bind our controller to the form opening tag.

<%= simple_form_for([@starship, @booking], data: { controller:"total-price" }) do |f| %>
Enter fullscreen mode Exit fullscreen mode

Make sure your controller is connected

import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    console.log('Hi from total price stimulus controller!\nzo/')
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's listen to the changes on all the dates input fields. In stimulus we use ACTION to bind "microphones" into elements. Here we go:

<%= f.input :start_date, as: :date, order: [ :day, :month, :year ], input_html: { data: { action: 'change->total-price#calculate'} } %>
<%= f.input :end_date, as: :date, order: [ :day, :month, :year ], input_html: { data: { action: 'change->total-price#calculate'} } %>
Enter fullscreen mode Exit fullscreen mode

Make sure your controller is listening to all events

import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
  }

  calculate(event) {
    console.log(event)
  }
}
Enter fullscreen mode Exit fullscreen mode

To calculate the difference in days and get the total price we must build dates using all three input fields values(day, month, year).

Using Date objects we can make the math.

Stimulus makes really easy to retrive HTML elements from the DOM by just creating TARGETS. We can just add data-controller-name-target='TARGETNAME' into our html tag and then we can reference to them all inside our Stimulus Controller.

<%= f.input :start_date, as: :date, order: [ :day, :month, :year ], input_html: { data: { total_price_target: 'startDate', action: 'change->total-price#calculate'} } %>
<%= f.input :end_date, as: :date, order: [ :day, :month, :year ], input_html: { data: { total_price_target: 'endDate', action: 'change->total-price#calculate'} } %>
Enter fullscreen mode Exit fullscreen mode

Make sure all inputs are targets

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ 'startDate', 'endDate', 'input']

  connect() {
    console.log(this.startDateTargets)
    console.log(this.endDateTargets)
  }

  calculate() {
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's build dates from those inputs

  // [...]
  calculate() {
    const begin = this.#retriveDateInputAsDate(this.startDateTargets)
    const end = this.#retriveDateInputAsDate(this.endDateTargets)
  }

  #retriveDateInputAsDate(targets) {
    const dateInputs = targets.map((target) => {
      parseInt(target.value, 10)
    })

    return new Date(dateInputs[2], dateInputs[1] - 1, dateInputs[0])
  }
  // [...]
Enter fullscreen mode Exit fullscreen mode

We can now do our math. Let's get the difference in days.

  // [...]

  calculate() {
    // [...]

    const days = this.#dateDifferenceInDays(begin, end)
  }

  #dateDifferenceInDays() {
    const difference = this.end.getTime() - this.begin.getTime();
    return (Math.floor(difference) / (1000 * 60 * 60 * 24))
  }
  // [...]
Enter fullscreen mode Exit fullscreen mode

Now that we have the difference between the start date and the end date in days we can simply multiply by the price per day of our ship. We don't have access to our lovely starship.price method inside our .js files. We need to send this information from the server to the JS part of our application.

How??

Using our own HTML.

Again, Stimulus make easy to retrive values inside your Javascript controller. You can just add the data-controller-name-value-name-value="ACTUAL VALUE YOU WANT TO USE IN THE JS CONTROLLER". We can then reference to them inside our Stimulus Controller using the same syntax as TARGETS. this.valueNameValue

<%= simple_form_for([@starship, @booking], data: { controller:"total-price", total_price_price_value: @starship.price }) do |f| %>
Enter fullscreen mode Exit fullscreen mode
  static values = {
    price: Number
  }
  calculate() {
    // [...]
    const totalPrice = this.priceValue * days
  }
  // [...]
Enter fullscreen mode Exit fullscreen mode

Our last step is to display the total price into the desired place.

Let's add our data target attribute to the HTML element and add the brand new total price we just generated

<p class='form-control'name="total-price" id="totalPrice" data-total-price-target='input'></p>
Enter fullscreen mode Exit fullscreen mode
this.inputTarget.innerText = totalPrice / 100
Enter fullscreen mode Exit fullscreen mode

You should end with something like this, if you didn't adapt anything.

FORM INSIDE YOUR VIEW

<%= simple_form_for([@starship, @booking], data: { controller:"total-price", total_price_price_value: @starship.price }) do |f| %>
  <%= f.input :start_date, as: :date, order: [ :day, :month, :year ], input_html: { data: { total_price_target: 'startDate', action: 'change->total-price#calculate'} } %>
  <%= f.input :end_date, as: :date, order: [ :day, :month, :year ], input_html: { data: { total_price_target: 'endDate', action: 'change->total-price#calculate'} } %>
  <div class="mb-3 string">
    <label for="total-price" class='form-label'>Total price</label>
    <p class='form-control'name="total-price" id="totalPrice" data-total-price-target='input'></p>
  </div>
  <%= f.submit class: 'btn btn-primary' %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

TOTAL PRICE STIMULUS CONTROLLER

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ 'startDate', 'endDate', 'input']
  static values = {
    price: Number
  }
  connect() {
    this.calculate()
  }

  calculate() {
    this.begin = this.#retriveDateInputAsDate(this.startDateTargets)
    this.end = this.#retriveDateInputAsDate(this.endDateTargets)

    const days = this.#dateDifferenceInDays(begin, end)
    const totalPrice = this.priceValue * days
    this.inputTarget.innerText = totalPrice / 100
  }

  #retriveDateInputAsDate(targets) {
    const dateInputs = targets.map(target => parseInt(target.value, 10))
    return new Date(dateInputs[2], dateInputs[1] - 1, dateInputs[0])
  }

  #dateDifferenceInDays() {
    const difference = this.end.getTime() - this.begin.getTime();
    return (Math.floor(difference) / (1000 * 60 * 60 * 24))
  }
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
dedemenezes_
Dedé Menezes

Posted on June 3, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related