Wrapping cleave-zen (former cleave.js) with Hotwire Stimulus to format date and time input fields

Today I want to share a nifty stimulus controller I put together to assist users when having to enter dates, times or credit card information.

Googling around first lead me to cleave.js which recently has been deprecated for its successor cleave-zen. While cleave-zen is not yet in parity with cleave.js it works fine for my use case, ensuring that my users enter dates and times correctly.

Side note: I don't want to offer a datetime picker, as they aren't very user friendly, they don't behave well for mobile solutions and there is no good out of the box solution for tailwind and stimulus. At least not to my knowledge.

The code

Without further ado, here's the code

import { Controller } from "@hotwired/stimulus"
import { formatDate, formatTime, registerCursorTracker } from 'cleave-zen'

// Connects to data-controller="cleave"
// data-cleave-formatter-value="date"
// data-cleave-options-value='{ delimiter: ".", datePattern: ["d", "m", "Y"] }'
export default class extends Controller {
  static values = {
    options: Object,
    formatter: String,
  }

  connect() {
    console.debug("Connecting cleave controller ...")
    this._parseFromDataAttributes();
  }

  disconnect() {
    this.element.removeEventListener('input');
  }

  _parseFromDataAttributes() {
    let input = this.element;

    // Add a listener to the input to format the value
    input.addEventListener('input', (e) => {
      if (this.formatterValue === "date") {
        input.value = formatDate(e.target.value, this.optionsValue);
      } else if (this.formatterValue === "time") {
        input.value = formatTime(e.target.value, this.optionsValue);
      } else {
        console.error("Cleave: No valid formatter specified. Supported: date, time", this.formatterValue);
      }
    })

	// without a cursor tracker, you cannot use backspace in
	// a form input to delete values.
    registerCursorTracker({
      input: input,
      delimiter: this.optionsValue.delimiter
    });
  }
}

Cleave-zen takes a different approach and no longer wants to control / do everything. Instead it basically just provides the required formatters and you have to register event listeners to the form inputs yourself. Or you could just use the formatter where ever you need to.

The controller is designed to be attachable to any form input field and having the configuration held at the input field.

Using cleave-zen for your inputs

As an example, to add guided formatting for a german date you'd define a Rails form input as follows:

<%= form.input :date,
  input_html: {
    placeholder: Time.zone.now.strftime("%d.%m.%Y"),
    data: {
      controller: "cleave",
      cleave_formatter_value: "date",
      cleave_options_value: {
        delimiter: ".",
        datePattern: ["d", "m", "Y"]
      }
    }
  }
%>

The resulting raw html of this looks as follows:

<input
  class="form-control"
  placeholder="01.02.2024"
  data-controller="cleave"
  data-cleave-formatter-value="date"
  data-cleave-options-value="{&quot;delimiter&quot;:&quot;.&quot;,&quot;datePattern&quot;:[&quot;d&quot;,&quot;m&quot;,&quot;Y&quot;]}"
  type="text"
  value="02.02.2024"
  name="appointment[date]"
  id="appointment_date"
>

As you can see, I am passing the formatter options for the formatter to be used as an optionsValue to the stimulus controller.

The formatterValue is being used to determine which cleave formatter should be used.

Setting up the scene

To use the code from above, first import cleave-zen to you project

./bin/importmap pin cleave-zen

This will add something like

pin "cleave-zen" # @0.0.17

to config/importmap.rb.

Now generate a stimulus controller to host the code. I named it cleave.

rails g stimulus cleave

This will create the boilerplate code for your stimulus controller at app/javascript/controllers/cleave_controller.js

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="cleave2"
export default class extends Controller {
  connect() {
  }
}

Wrapping up

Currently the controller supports the cleave formatTime and formatDate. Adding support for formatCreditCard can be added fairly easy.

Let me know what you think or if you consider using it.

I hope you found this article useful and that you learned something new.

If you have any questions or feedback, didn't understand something, or found a mistake, please send me an email or drop me a note on twitter / x. I look forward to hearing from you.

Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.