Reset form that has fields controlled by a javascript library and by liveview

Problem

I’ve run in to a problem working on a set of filters for a liveview in my project. I initially set up one filter which was for “Subject” which is just a drop down. It is important for my application that the filters be added as parameters to the URL so the query parameters are checked to set an initial value of the filter. Next I have to add date range filtering using a timestamp. I decided to use flatpickr to give the user an intuitive way to set the date and time.

I’ve gotten flatpickr set up using hooks and I got the whole URL parameter loop set up but now the issue I’m having is that I need a reset button which will both reset the “Subjects” field and will set the “From Date” and “To Date” fields back to their default values. With only the “Subjects” filters I was doing this by triggering a push_patch to a route that removed the relevant query parameters. Using this technique doesn’t reset the date filters because their value only gets set in the mount function.

Part of what I think might be complicating this is having to use phx-update="ignore" to prevent the flatpickr instances from being destroyed when a phx-change event is handled. I thought that the updated method of a hook would be the answer but it doesn’t seem to get run when the form is updated(maybe due to “ignore” mentioned above).

It feels like the solution is to run some javascript that resets those fields when I click “Reset” but it needs to reset those fields to a default value which is dynamic basses on the list of records passed in to the component, so I need the javascript to be able to access the components assigns, I’m at a bit of a loss as to how to get that all strung together. Thank you for any help or nudges in the right direction.

Code

Code is from a demo that I set up to explore the problem better, full demo app can be found HERE the code relevant to this example is in: lib/date_input_web/live and assets/js/hooks.js
If you start up the project the form is at /date. Some parts are a bit convoluted for the demo but its to replicate roughly the way my actual application needed to be set up due to other implementation details.

Hook

import flatpickr from 'flatpickr';

export default {
  DatePicker: {
    mounted() {
      console.log(this.el.dataset.datetime);
      this.fp = flatpickr(this.el, {
        defaultDate: this.el.dataset.datetime,
        enableTime: true,
        altInput: true,
        altFormat: 'm-d-Y H:i',
        dateFormat: 'Z',
      });
    },
    updated() {
      console.log('Updated...');
    },
  },
};

Markup

<form id="visits-filters" phx-change="filter">
  <!-- Add filters etc here -->
  <div id="ignore-from-date" phx-update="ignore">
    <label for="from-date">From:</label>
    <input
      id="visit-from-date-input"
      type="text"
      name="from-date"
      phx-hook="DatePicker"
      data-datetime={@from_date}
    />
  </div>
  <div id="ignore-to-date" phx-update="ignore">
    <label for="to-date">To:</label>
    <input
      id="visit-to-date-input"
      type="text"
      name="to-date"
      phx-hook="DatePicker"
      data-datetime={@to_date}
    />
  </div>
  <div>
    <label for="subjects">Filter:</label>
    <select name="subject" id="subjects">
      <%= options_for_select([{"-- Select Subject --", ""} | ["a", "b", "c"]], @subject) %>
    </select>
  </div>
  <div>
    <%= link("Reset", to: "#", phx_click: "reset") %>
  </div>
</form>

You can have your hook defined on a parent element of the element that you’re creating the flatpicker instance on. Such that the parent doesn’t have ignore on it, but a child for flatpikr does.

Then you can change an attribute on the parent (or a different hidden element) which will be picked up by your updated method in the hook.

I gave this a try by adding another div around the input. I then moved the phx-ignore to that new div. I then moved the hook to the outer div and modified the hook to still target the correct element. This still didn’t trigger the update method of my hook when changes where made. Here is what I tried:

<div phx-hook="DatePicker" id="visit-to-date-hook" data-datetime={@to_date}>
    <label for="to-date">To:</label>
    <div phx-update="ignore" id="ignore-to-date">
      <input
        id="visit-to-date-input"
        type="text"
        name="to-date"
      />
    </div>
  </div>

and the JS

import flatpickr from 'flatpickr';

export default {
  DatePicker: {
    mounted() {
      console.log(this.el);
      this.inputEl = this.el.querySelector('input');
      this.fp = flatpickr(this.inputEl, {
        defaultDate: this.el.dataset.datetime,
        enableTime: true,
        altInput: true,
        altFormat: 'm-d-Y H:i',
        dateFormat: 'Z',
      });
    },
    update() {
      console.log('Updating...');
    },
  },
};

I’m not in love with the system as a whole that I’ve made to deal with query parameters and filters but the solution I came up with was just to add a ‘click’ event listener to the reset link element when I mount the hook. What I’m delighted by is that the data-datetime attribute on the ignored element does get updated by liveview which allows me to use the assigns to set that default value. Much props to the liveview team for that feature!

The new hook looks something like this:

import flatpickr from 'flatpickr';

export default {
  DatePicker: {
    mounted() {
      this.fp = flatpickr(this.el, {
        defaultDate: this.el.dataset.datetime,
        enableTime: true,
        altInput: true,
        altFormat: 'm-d-Y H:i',
        dateFormat: 'Z',
      });
      // Adds event listener to reset link to reset this field to
      // its default value
      let el = document.getElementById('reset-filters');
      el.addEventListener('click', this.resetInput.bind(this));
    },
    resetInput() {
      this.fp.setDate(this.el.dataset.datetime);
    },
  },
};

Nice one.

What I was saying is that if you want to trigger it from the server, you add another data attribute, which triggers updated(). Or you push_event to it.

<div phx-hook="DatePicker" id="visit-to-date-hook" data-datetime={@to_date} data-something={@something}>
   ...

My reset-all-filters button informs the server and Alpine <button phx-click="clear-all-filters" @click="$dispatch('clear-filters')">. It’s fun keeping everyone in sync :wink:

1 Like