loading...
Cover image for Introducing Stimulus-Flatpickr wrapper 📅

Introducing Stimulus-Flatpickr wrapper 📅

adrienpoly profile image Adrien Poly ・6 min read

I really enjoyed using Stimulus JS since it has been released early this year. For once I can now easily organize my JS code without pulling a full-blown frontend framework.

Stimulus-flatpickr wrapper started as an experiment, I needed to use Flatpickr in a project and built a Stimulus Controller for it. I quickly realized that it could make sense to have a generic Stimulus controller for this library.

Today we will demonstrate this package through a simple, yet interesting example. I have worked quite a bit recently with date pickers and realized that it is always very simple to convert a field to a date pickers but they are many small details required to make it a real easy to use solution.

The goal is to create a basic booking system where the user:

  • can only select available dates (the ones already booked must be disabled)
  • gets a datepicker in his language
  • see a date in the input field in the locale format
  • see a list of all bookings
  • all of this playing nicely in a Single Page Application

the stack is rather simple: Rails, Stimulus, Turbolinks, and Flatpickr for the date picking solution.

At the end we won't have any Ajax at all and less than 10 lines of Javascript to do this 🎉🎉🎉

Lets get started: New app and stimulus setup

They are several tutorials out there, I won't get into all the details but settings up Stimulus in your Rails app if you already have webpack installed is as simple as that:

rails webpacker:install:stimulus

This will add Stimulus package to your package.json, adds the initialization code in your main application.js file and a new controllers directory under javascript for all of your new Stimulus controllers

You can read this article for more details about setting up Stimulus in your rails

For today's demo, we are going to create a brand new app

  rails new --webpack=stimulus stimulus-flatpickr

...and don't forget to add the packs

  #app/views/layouts/application.html.erb
  ...
  <%= stylesheet_pack_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  ...

Appointment Model and Controller

We are doing a booking engine. We need an Appointment model with a single column a start_at date and a scope method to get the up_comings appointments for the next n days (we can only book for the next 60 days).

#models/appointment.rb
class Appointment < ApplicationRecord
  validates :start_at, uniqueness: true

  scope :up_comings, ->(nb_days) {
                       where('start_at >= ? AND start_at < ?',
                             Time.zone.now,
                             Time.zone.now + nb_days.days).order(start_at: :asc)
                     }
end

Our controller is pretty standard (for simplicity we only handle the success path here 👶, no error managment)

class AppointmentsController < ApplicationController
  before_action :set_appointment, only: %i[update destroy]

  def index
    @appointments = Appointment.up_comings(60)
    @appointments_dates = @appointments.pluck(:start_at)
    @appointment = Appointment.new
  end

  def create
    redirect_to appointments_path if Appointment.create(appointment_params)
  end

  def update
    redirect_to appointments_path if @appointment.update(appointment_params)
  end

  def destroy
    @appointment.destroy
    redirect_to appointments_path
  end

  private

  def set_appointment
    @appointment = Appointment.find(params[:id])
  end

  def appointment_params
    params.require(:appointment).permit(:start_at)
  end
end

stimulus-flatpickr controller

As per the documentation we are going to create a Stimulus controller that will extend the generic stimulus-flatpickr controller.

First, let's add the packages that we will need

  yarn add stimulus-flatpickr

and add the new controller

// ./controllers/flatpickr_controller.js
import Flatpickr from "stimulus-flatpickr";
import "flatpickr/dist/themes/dark.css";

// creates a new Stimulus controller by extending stimulus-flatpickr wrapper controller
export default class extends Flatpickr {}

The views

Lets put a small structure to have the booking form on the left and the up comings appointments list on the right.

#app/views/appointments/index.html.erb
<div class="main">
  <h2><%= t ".title" %></h2>
  <div class="row">
    <div class="col">
      <h3><%= t ".new" %></h3>
      <%= render "form", appointment: @appointment %>
    </div>
    <div class="col">
      <h3><%= t ".appointments" %></h3>
      <%= render @appointments %>
    </div>
  </div>
</div>

#app/views/appointments/_appointment.html.erb
<div class="appointments">
  <%= render "form", appointment: appointment %>
  <%= link_to "x", appointment_path(appointment), method: :delete%>
</div>

and our form where all the magic will happen 🎉

#app/views/appointments/_form.html.erb
<%= form_with model: appointment do |f| %>
  <%= f.text_field :start_at,
      data: {
        controller: "flatpickr",
        flatpickr_min_date: Time.zone.now,
        flatpickr_max_date: Time.zone.now + 60.days,
        flatpickr_disable: Appointment.up_comings(60).pluck(:start_at),
      } %>
<% end %>

This is the important part to understand :

We defined a flatpickr_controller.js so whenever an HTML element has a data-controller="flatpickr" attribute, then the stimulus controller will enter into action.

So here in the view, data: { controller: "flatpickr" } will be converted to data-controller="flatpickr" and therefore convert the field into a datepicker.

We can also pass options to the datepicker using the same data attributes(data-flatpickr-the-kebab-case-option-name). Here we set a min and max date to disable everything before today and everything after today + 60 days.

Also, we prepare for the next step where we will disable the dates that are already booked by passing an array of the current bookings.

Submit on select

Lets automatically submit whenever a user selects a date (not perfect UX! I know, it is mostly for simple demo purpose).
The flatpickr controller has all the official flatpickr hooks available (open, close, change etc). Here we need to submit the form when the value has changed. So let's override the change() function.

export default class extends Flatpickr {
  // automatically submit form when a date is selected
  change(selectedDates, dateStr, instance) {
    const form = this.element.closest("form");
    Rails.fire(form, "submit");
  }
}

Going Live

At this point we have the following result:

stimulus-flatpickr demo

We start to have something interactive.... The cool thing is that Turbolinks being installed by default kicks in automatically and Ajaxify all links. All Stimulus controllers are by designed working with Turbolinks. So there is nothing else to do here, it just works!. No custom Ajax call or SRJ to have this SPA look and feel. 💪 🚀 ❤️

Localizing the date picker and the date format

We have a SPA look & feel and the availabilities for our booking engine. The next step is to localize it correctly. Currently, it is only in English and the date format if rather ugly 2018-09-12 👎.

date formats

The stimulus-flatpickr wrapper offers a nice bonus feature over the standard library, it will convert strftime date formats to flatpickr custom date formats.

So to customize the date format we can pass the local format directly to the date picker like this:

data: {
  controller: "flatpickr",
  flatpickr_alt_input: true,
  flatpickr_alt_format: t("date.formats.long"),
  flatpickr_default_date: appointment.start_at,
  flatpickr_disable: @appointments_dates - [appointment.start_at],
  flatpickr_min_date: Time.zone.now,
  flatpickr_max_date: Time.zone.now + 60.days,
}

flatpickr_alt_format: t("date.formats.long") -> will output "%B %d, %Y" when the locale is :en and "%e %B %Y" when the locale is :fr and this gets converted automatically to the nearest flatpickr format. As DRY as it can be 🎉!

Translations

We now have almost everything. The last point is to correctly translate the datepicker for every locale.

We are going to import the different locales in our stimulus controller. Every time time Turbolinks silently replace the content of the page, the initialize() function is of the stimulus controller is called. This is where we are going to set our local and pass it to flatpickr.

Our final controller looks like this

import Flatpickr from "stimulus-flatpickr";

// import a theme (could be in your main CSS entry too...)
import "flatpickr/dist/themes/dark.css";

// import the translation files and create a translation mapping
import { French } from "flatpickr/dist/l10n/fr.js";
import { english } from "flatpickr/dist/l10n/default.js";

// create a new Stimulus controller by extending stimulus-flatpickr wrapper controller
export default class extends Flatpickr {
  locales = {
    fr: French,
    en: english
  };

  initialize() {
    //set the locale and also sets the global flatpickr settings  
    this.config = {
      locale: this.locale,
      altInput: true,
      showMonths: 2,
      animate: false
    };
  }

  // automatically submit form when a date is selected
  change(selectedDates, dateStr, instance) {
    const form = this.element.closest("form");
    Rails.fire(form, "submit");
  }

  get locale() {
    if (this.data.has("locale")) {
      return this.locales[this.data.get("locale")];
    }
  }
}

That's all folks!

stimulus-flatpickr demo

You can find the entire demo project here 👉 https://github.com/adrienpoly/rails_stimulus_flatpickr

and more important the stimulus-flatpickr wrapper 👉 https://github.com/adrienpoly/stimulus-flatpickr

I hope you enjoyed this introduction. I personally think we will see more and more standard Stimulus controllers and the Rails community will more and more drop Gems used for front end packages only.

I am not senior dev with tons of experience so feel free to Comments, issues, PR. They are all welcome and if you feel this package is useful for you, leave it a star ⭐

Happy Coding 🎉

Discussion

pic
Editor guide
Collapse
charliechin profile image
Charliechin

Thanks a lot for this post. It really helped!