How to add input masks in LiveView

What is the proper way to add a custom input field to a LiveView form in Elixir that can use a decimal mask or credit card format? Ideally, this would behave like a standard input field, without requiring extra handlers on the Elixir side to process the information, other than the usual form validation handler.

I have discovered that Phoenix has a method called pushInput to submit forms. It would be wonderful if we could easily add this functionality to enhance our forms with reusable components, leading to an improved user experience.

3 Likes

@chrismccord do you have any suggestion how can I achieve this?

The js interop guide has a controlled input example to do exactly this :slight_smile:

For example, the markup for a controlled input for phone-number formatting could be written
like this:

    <input type="text" name="user[phone_number]" id="user-phone-number" phx-hook="PhoneNumber" />

Then a hook callback object could be defined and passed to the socket:

    let Hooks = {}
    Hooks.PhoneNumber = {
      mounted() {
        this.el.addEventListener("input", e => {
          let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
          if(match) {
            this.el.value = `${match[1]}-${match[2]}-${match[3]}`
          }
        })
      }
    }

    let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})

https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook

6 Likes

Thanks @chrismccord. That is was I look for :grin:.

const formatNumber = (input) => {
  const sanitizedInput = String(input).replace(/,|\./g, "").padStart(3, "0")
  return sanitizedInput.replace(/^0*(\d+)(\d{2})$/, "$1.$2")
}

/**
 * @type {import("phoenix_live_view").ViewHook}
 */
export const DecimalInput = {
  /**
   * Some times the server sends a float value with a single decimal place, like 1.0
   * This function formats the value to 1.00 so the user can edit the value with 2 decimal places
   */
  _formatSeverValue() {
    if (this.el.value) {
      const value = parseFloat(this.el.value.replace(/,/g, ""))
      this.el.value = formatNumber(value.toFixed(2))
    }
  },
  mounted() {
    this._formatSeverValue()
    this.el.addEventListener("input", (_event) => (this.el.value = formatNumber(this.el.value)))
  },
  updated() {
    this._formatSeverValue()
  },
}

Can I assume if I change a hidden input value using javascript, the value also will be propagated to the server?

Nope, you’ll want to manually trigger a phx-change event as described below:

Triggering phx- form events with JavaScript

Often it is desirable to trigger an event on a DOM element without explicit user interaction on the element. For example, a custom form element such as a date picker or custom select input which utilizes a hidden input element to store the selected state.

In these cases, the event functions on the DOM API can be used, for example to trigger a phx-change event:

document.getElementById("my-select").dispatchEvent(
  new Event("input", {bubbles: true})
)

When using a client hook, this.el can be used to determine the element as outlined in the “Client hooks” documentation.

source: Form bindings — Phoenix LiveView v0.19.3

2 Likes

Thanks. :smiley: