Search code examples
ruby-on-railsrubydatepickersimple-formflatpickr

Rails, flatpickr in simple-form not working


First let me explain the context real quick. I created this app where users can organize music jam sessions. So basically they have the possibility to create such events. They are prompted to enter a date and a range of hours during that day. In my database, a jam session has a start_date and an end_date that are both datetime format. This might contribute to making my life harder, since it would be more logical to have a date(y-m-d), and a start_time and end_time. Maybe a new migration to change that is the best solution, but let me ask if I can go around it the way it is now.

So far, I managed to make it work (kind of) but not with flatpickr.

Here is the code related to this issue:

1/ In my app/javascript/packs/application.js I have this (the TimePicker is used on another page for a search, and it works fine by the way. Here I am trying to use datepicker):

import "bootstrap";
import "../plugins/flatpickr";

import flatpickr from "flatpickr";
import rangePlugin from "flatpickr/dist/plugins/rangePlugin";
import 'flatpickr/dist/flatpickr.min.css'

import 'mapbox-gl/dist/mapbox-gl.css'; // <-- you need to uncomment the stylesheet_pack_tag in the layout!
import { initMapbox } from '../plugins/init_mapbox';

flatpickr(".datepicker", {
  minDate: new Date,
  altInput: true,
});

flatpickr(".TimePicker", {
    // enableTime: true,
    minDate: new Date,
    altInput: true,
    plugins: [new rangePlugin({ input: ".timePicker-end"})]
  });

initMapbox();

2/ The schema.rb file (only the part concerning the jam sessions table):

  create_table "jam_sessions", force: :cascade do |t|
    t.string "title"
    t.string "description"
    t.string "genre"
    t.datetime "starts_at"
    t.datetime "ends_at"
    t.string "location"
    t.bigint "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.float "latitude"
    t.float "longitude"
    t.index ["user_id"], name: "index_jam_sessions_on_user_id"
  end

3/ The new file where the form is located:

<% music_genre = ["Acapella", "Accoustic", "Blues", "Classical", "Country", "Electronic", "Folk", "Funk", "Heavy Metal", "Hip Hop", "Jazz", "Latin", "Other", "Pop", "Reggae", "Rock", "World"] %>

<div class="container h-100">
  <div class="row justify-content-center align-items-center mt-3">
    <div class="col col-6">

      <h2>Create your own Jam Session</h2>

      <%= simple_form_for [@jam_session] do |f| %>
      <% f.error_notification %>
      <%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>

        <div class="form-inputs">
          <%= f.input :title %>
          <%= f.input :description %>
          <%= f.input :genre, collection: music_genre %>
          <div class="row justify-content-center align-items-center mt-3">
            <%= f.input :starts_at, as: :string, required: true, input_html: {class: "datepicker"} %>
            <%= f.input :starts_at, as: :time, :minute_step => 15, required: true %>
            <%= f.input :ends_at, as: :time, :minute_step => 15, required: true %>
          </div>
          <%= f.input :location %>
        </div>

        <div class="d-flex justify-content-end">
          <%= f.button :submit, "GO !", class: 'big-button block' %>
        </div>
      <% end %>
    </div>
  </div>
</div>

4/ The jam_sessions Controller

class JamSessionsController < ApplicationController
  def index
    # @jam_sessions = JamSession.search_by_location(params[:query])

    @jam_sessions = JamSession.includes(:spots, :instruments)
    # @jam_sessions = @jam_sessions.where(instruments: { id: jam_session_params[:instrument_id] }) if jam_session_params[:instrument_id].present?
    @jam_sessions = @jam_sessions.where(instruments: { id: params["jam_session"]["instrument_id"] }) if params["jam_session"]["instrument_id"].present?
    @jam_sessions = @jam_sessions.where(genre: params["jam_session"]["genre"]) if params["jam_session"]["genre"].present?
    @jam_sessions = @jam_sessions.search_by_location(params["jam_session"]["location"]) if params["jam_session"]["location"].present?


    if params[:jam_session] && params[:jam_session][:starts_at]
      x = params[:jam_session][:starts_at]

      @start_date = x&.split("to")[0]&.strip
      @end_date = x&.split("to")[1]&.strip

      @jam_sessions = @jam_sessions.where("starts_at > ?", @start_date) if @start_date.present?
      @jam_sessions = @jam_sessions.where("starts_at < ?", @end_date) if @end_date.present?
    end

    # -------- access input from user ---------
    # @location_input = params["jam_session"]["location"]
    # @starts_at_input = params["jam_session"]["starts_at"]
    # @ends_at_input = params["jam_session"]["ends_at"]
    # @instrument_input = Instrument.find(params["jam_session"]["instrument_id"]).name if params["jam_session"]["instrument_id"].present?
    geocode(@jam_sessions)
    # access id of instruments
    @session_instruments_id = Instrument.all
  end

  def show
    @jam_session = JamSession.find(params[:id])
    @markers = [{
      lat: @jam_session.latitude,
      lng: @jam_session.longitude,
      infoWindow: render_to_string(partial: "info_window", locals: { jam_session: @jam_session }),
      image_url: helpers.asset_url("custom_marker.png"),
    }]
    @messages = JamSession.includes(messages: :user).find(params[:id])
  end

  def new
    @jam_session = JamSession.new
  end

  def create
    @jam_session = JamSession.new(jam_session_params)
    @jam_session.user = current_user
    if @jam_session.save
      redirect_to jam_session_path(@jam_session)
    else
      render "new"
    end
  end

  private

  def geocode(jam_sessions)
    @jam_sessions = @jam_sessions.geocoded #returns jam_sessions with coordinates

    @markers = @jam_sessions.map do |jam_session|
      {
        lat: jam_session.latitude,
        lng: jam_session.longitude,
        infoWindow: render_to_string(partial: "info_window", locals: { jam_session: jam_session }),
        image_url: helpers.asset_url("custom_marker.png"),
      }
    end


  end

  def jam_session_params
    params.require(:jam_session).permit(:title, :description, :genre, :starts_at, :ends_at, :location, :instrument_id)
  end
end

But it doesn't work. Visually it does, meaning that the calendar appears and it seems like the user enters the right date, but in the end, the date is ignored and instead the date of the day is recorded by default in the database.

It works with this code in simple-form, but it's ugly, and only if I put the date field after the hours fields, which is counter intuitive:

<div class="row justify-content-center align-items-center mt-3">
  <%= f.input :starts_at, as: :time, :minute_step => 15, required: true %>
  <%= f.input :ends_at, as: :time, :minute_step => 15, required: true %>
  <%= f.input :starts_at, as: :date, required: true %>
</div>

Any idea how I could make this work with flatipckr.

Here are a couple if picks of the 2 versions of the form.

With the datepicker (NOT WORKING): datepicker not working

Just simple-form (WORKING but ugly): enter image description here

HERE ARE THE LOGS WHEN I CREATE A JAM SESSION. The first part (before the ^[[B^[[B is when the new page is loaded, where the form is located. After that are the logs once the form is submitted. To be clear, the start date I entered was 28th but it recorded today's date buy defautl)

Started GET "/jam_sessions/new" for ::1 at 2020-04-08 09:54:14 +0200
Processing by JamSessionsController#new as HTML
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ /home/olivier/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4.1/lib/active_record/log_subscriber.rb:98
  Rendering jam_sessions/new.html.erb within layouts/application
  Rendered jam_sessions/new.html.erb within layouts/application (41.1ms)
  ActiveStorage::Attachment Load (0.3ms)  SELECT  "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 LIMIT $4  [["record_id", 1], ["record_type", "User"], ["name", "photo"], ["LIMIT", 1]]
  ↳ app/views/shared/_navbar.html.erb:19
  ActiveStorage::Blob Load (0.4ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/shared/_navbar.html.erb:19
  Rendered shared/_navbar.html.erb (5.0ms)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered shared/_footer.html.erb (0.3ms)
Completed 200 OK in 67ms (Views: 63.4ms | ActiveRecord: 1.0ms)


^[[B^[[B
Started POST "/jam_sessions" for ::1 at 2020-04-08 09:54:59 +0200
Processing by JamSessionsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"rA4WsBD2H9qDMp9/Jt/kxvJ6sJ+cHnRLByH0yFJlO/dm7EbNeuCtV3DK25pKVvb0RyaTWz2cUDhxWukL3HGMWQ==", "jam_session"=>{"title"=>"Let's play", "description"=>"sdfkjqsdhflkjshdflkjqs", "genre"=>"Country", "starts_at"=>"2020-04-28", "starts_at(1i)"=>"2020", "starts_at(2i)"=>"4", "starts_at(3i)"=>"8", "starts_at(4i)"=>"07", "starts_at(5i)"=>"00", "ends_at(1i)"=>"2020", "ends_at(2i)"=>"4", "ends_at(3i)"=>"8", "ends_at(4i)"=>"09", "ends_at(5i)"=>"00", "location"=>"Berlin"}, "commit"=>"GO !"}
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ /home/olivier/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4.1/lib/active_record/log_subscriber.rb:98
   (0.3ms)  BEGIN
  ↳ app/controllers/jam_sessions_controller.rb:50
  JamSession Create (0.8ms)  INSERT INTO "jam_sessions" ("title", "description", "genre", "starts_at", "ends_at", "location", "user_id", "created_at", "updated_at", "latitude", "longitude") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING "id"  [["title", "Let's play"], ["description", "sdfkjqsdhflkjshdflkjqs"], ["genre", "Country"], ["starts_at", "2020-04-08 07:00:00"], ["ends_at", "2020-04-08 09:00:00"], ["location", "Berlin"], ["user_id", 1], ["created_at", "2020-04-08 07:54:59.230840"], ["updated_at", "2020-04-08 07:54:59.230840"], ["latitude", 52.5170365], ["longitude", 13.3888599]]
  ↳ app/controllers/jam_sessions_controller.rb:50
   (0.8ms)  COMMIT
  ↳ app/controllers/jam_sessions_controller.rb:50
Redirected to http://localhost:3000/jam_sessions/22
Completed 302 Found in 191ms (ActiveRecord: 2.3ms)


Started GET "/jam_sessions/22" for ::1 at 2020-04-08 09:54:59 +0200
Processing by JamSessionsController#show as HTML
  Parameters: {"id"=>"22"}
  JamSession Load (0.5ms)  SELECT  "jam_sessions".* FROM "jam_sessions" WHERE "jam_sessions"."id" = $1 LIMIT $2  [["id", 22], ["LIMIT", 1]]
  ↳ app/controllers/jam_sessions_controller.rb:33
  Rendered jam_sessions/_info_window.html.erb (0.7ms)
  CACHE JamSession Load (0.0ms)  SELECT  "jam_sessions".* FROM "jam_sessions" WHERE "jam_sessions"."id" = $1 LIMIT $2  [["id", 22], ["LIMIT", 1]]
  ↳ app/controllers/jam_sessions_controller.rb:40
  Message Load (0.3ms)  SELECT "messages".* FROM "messages" WHERE "messages"."jam_session_id" = $1  [["jam_session_id", 22]]
  ↳ app/controllers/jam_sessions_controller.rb:40
  Rendering jam_sessions/show.html.erb within layouts/application
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/jam_sessions/show.html.erb:12
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/jam_sessions/show.html.erb:12
  Spot Load (0.3ms)  SELECT "spots".* FROM "spots" WHERE "spots"."jam_session_id" = $1  [["jam_session_id", 22]]
  ↳ app/views/jam_sessions/show.html.erb:13
  User Exists (0.6ms)  SELECT  1 AS one FROM "users" INNER JOIN "participations" ON "users"."id" = "participations"."user_id" INNER JOIN "spots" ON "participations"."spot_id" = "spots"."id" WHERE "spots"."jam_session_id" = $1 AND "users"."id" = $2 LIMIT $3  [["jam_session_id", 22], ["id", 1], ["LIMIT", 1]]
  ↳ app/models/jam_session.rb:35
  Rendered shared/_chat-messages.html.erb (4.2ms)
  Spot Exists (0.4ms)  SELECT  1 AS one FROM "spots" LEFT OUTER JOIN "participations" ON "participations"."spot_id" = "spots"."id" WHERE "spots"."jam_session_id" = $1 AND "participations"."spot_id" IS NULL LIMIT $2  [["jam_session_id", 22], ["LIMIT", 1]]
  ↳ app/views/jam_sessions/show.html.erb:86
  CACHE User Exists (0.0ms)  SELECT  1 AS one FROM "users" INNER JOIN "participations" ON "users"."id" = "participations"."user_id" INNER JOIN "spots" ON "participations"."spot_id" = "spots"."id" WHERE "spots"."jam_session_id" = $1 AND "users"."id" = $2 LIMIT $3  [["jam_session_id", 22], ["id", 1], ["LIMIT", 1]]
  ↳ app/models/jam_session.rb:35
  SQL (0.5ms)  SELECT "spots"."id" AS t0_r0, "spots"."jam_session_id" AS t0_r1, "spots"."instrument_id" AS t0_r2, "spots"."created_at" AS t0_r3, "spots"."updated_at" AS t0_r4, "participations"."id" AS t1_r0, "participations"."spot_id" AS t1_r1, "participations"."user_id" AS t1_r2, "participations"."created_at" AS t1_r3, "participations"."updated_at" AS t1_r4 FROM "spots" LEFT OUTER JOIN "participations" ON "participations"."spot_id" = "spots"."id" WHERE "spots"."jam_session_id" = $1 AND "participations"."spot_id" IS NULL  [["jam_session_id", 22]]
  ↳ app/views/jam_sessions/show.html.erb:95
  Rendered jam_sessions/show.html.erb within layouts/application (14.5ms)
  ActiveStorage::Attachment Load (0.4ms)  SELECT  "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 LIMIT $4  [["record_id", 1], ["record_type", "User"], ["name", "photo"], ["LIMIT", 1]]
  ↳ app/views/shared/_navbar.html.erb:19
  ActiveStorage::Blob Load (0.3ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/shared/_navbar.html.erb:19
  Rendered shared/_navbar.html.erb (4.2ms)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered shared/_footer.html.erb (0.3ms)
Completed 200 OK in 41ms (Views: 32.3ms | ActiveRecord: 3.9ms)

Thank you! Olivier


Solution

  • Ok, you have two fields that are both “starts_at”, so the second one overwrites the first one.

    I am not sure, if flatpickr can do an end time too in the little popup. I know it can have start and end day in one popup as well as a time, but ending time I am not sure. So what you want to do probably, is one field where you just select the date and then the two for the time as you prepared it already. And then in the controller in the creates action you put everything together before you create a new jam session

    So one of the “starts_at” fields, you need to rename. Simple_form won’t like that, because it looks at the jam session object and that doesn’t just have another date column. So here we need to trick it a bit and just write the input field either in plan html or with a helper from rails.

    You can just include an input field into a form and it will be submitted together with the form, but we want to make sure that the information gets the right place in the params. You know all the info about jam session is nested under the key "jam_session" in the params hash.

    params = {..., "jam_session"= {"title"=>"Let's play", ... }... }
    

    How does it end up there nested? Well that’s because each line in your simple form, once it is rendered into html, has a name attribute which determines the structure of the params hash. (Check each input field of your form in the browser). It looks something like that:

    <input class="form-control" type="text" name="jam_session[title]" id="jam_session_title">
    

    And we have to copy that for the date - let’s call it day, so we can distinguish it.

    <input class="form-control" type="text" name="jam_session[day]" id="jam_session_day" class="datepicker">
    

    Don’t forget to add the datepicker class.

    Now this should result in a params hash, that has a day, but also start_date and end_date. From start date and end date we will only need the times, not the date. So in the jam_sessions controller in the create action, you need to create the right datetime formats before you create a new instance.

    Inspect the params to see what you get and create new date instances that you then use to create a new jam session instance. Let me know if you need help with that.

    Key things to learn:

    • Simple form is a great tool, but it also hides a lot of the important element in forms, that make them pass information like they do. Always inspect the input elements in the browser.
    • Simple form creates a connection between the model and the form. However, you can add more fields to a form - even a simple form.
    • The structure of the hash is determined by the name attributes in the input fields.