Dedé Menezes
Posted on June 3, 2022
Feature: When the user change the booking date it should update the total price in real time
Steps:
- Create Stimlus Controller
- Listen to change event on all date inputs
- Build date from the inputs field values
- Call getTime() on the generated dates
- Subtracts start from end
- Transform milliseconds into days
- Multiply days by price
- 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 %>
First thing we need to do is create the stimulus controller responsible for this behaviour.
touch app/javascript/controllers/total_price_controller.js
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| %>
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/')
}
}
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'} } %>
Make sure your controller is listening to all events
import { Controller } from "stimulus"
export default class extends Controller {
connect() {
}
calculate(event) {
console.log(event)
}
}
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'} } %>
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() {
}
}
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])
}
// [...]
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))
}
// [...]
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| %>
static values = {
price: Number
}
calculate() {
// [...]
const totalPrice = this.priceValue * days
}
// [...]
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>
this.inputTarget.innerText = totalPrice / 100
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 %>
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))
}
}
Posted on June 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.