Formatting Inputs with Cleave.js in a Vue/Typescript/Rails Project

Cleave.js is a super cool JavaScript library that lets you force the formatting of an input. It works for credit card numbers, dates, and several other kinds of formatted text. If you haven't heard of it before, check out the demos on the Cleave.js homepage.

I had a little bit of trouble getting Cleave to work in my project because I'm using Vue and Typescript in a Rails app. I've documented most of the steps i took to get these all workign together mostly as a reference for myself, but i hope other people find it useful too.

Install Cleave.js

The first thing that needs to be done is installing cleave.js and the type definition files for it. (Correct me if I'm wrong; I think they're called type definiton files.) Since I'm using yarn with webpacker, this is pretty straightforward:

yarn add cleave.js
yarn add @types/cleave.js --dev

Create a Reusable Cleave.js Component

Next, I created a basic reusable component to which I can pass in my Cleave.js options. I'll explain some of the more important lines of this component below.

// text-box.vue

<template>
  <div class="form-group">
    <label
      :for="id"
      class="form-control-placeholder" 
    >
      {{ label }}
    </label>
    <input
      :id="id"
      :value="value"
      type="text"
      class="form-control"
      @input="input"
    >
  </div>
</template>

<script lang="ts">
import Cleave = require('cleave.js');

export default {
  mounted () {
    if (this.cleaveOptions) {
      new Cleave(this.$el.querySelector("input"), this.cleaveOptions);
      this.$el.oninput = (event: Event) => {
        const value = (event.target as HTMLInputElement).value
        this.$emit('input', value)
      }
    }
  },

  props: {
    cleaveOptions: {
      type: Object,
    },
    id: {
      type: String,
      required: true,
    },
    label: {
      type: String,
      required: true,
    },
    value: {
      type: String,
    },
  },

  methods: {
    input(event: Event) {
      const value = (event.target as HTMLInputElement).value;
      if (!this.cleaveOptions) {
        this.$emit('input', value);
      }
    },
  }
}
</script>

<style scoped>
</style>

Line-by-Line Explanation of Cleave.js Component

Setting the script type to "ts" is necessary to get Typescript to work:

<script lang="ts">

And I used the import method listed in the Cleave.js repository:

import Cleave = require('cleave.js');

My input is not the root element in this component, so I select it with .querySelector("input"). I then add a new event listener that will pass the formated value of the input up to the paret component in an event. This is done in the mounted section of the component because we want to hook that event listener onto the input element as early as possible.

However, this is only done if cleaveOptions are passed into this component. If no options are passed in, then we don't attach a Cleave.js instance or emit any events from here.

  mounted () {
    if (this.cleaveOptions) {
      new Cleave(this.$el.querySelector("input"), this.cleaveOptions);
      this.$el.oninput = (event: Event) => {
        const value = (event.target as HTMLInputElement).value
        this.$emit('input', value)
      }
    }
  },

Finally, I have a way to emit input events on the form field as a fallback in case no options are passed in for Cleave.js. This method is basic Vue + Typescript: it emits the data value in an input event when the input value chagnes, and everything is typed for Typescript.

event: Event indicates that the function parameter is of type Event, and event.target as HTMLInputElement indicates that the target of the event should be cast to an HTMLInputElement before we call its value method.

    input(event: Event) {
      const value = (event.target as HTMLInputElement).value;
      if (!this.cleaveOptions) {
        this.$emit('input', value);
      }
    },

When I tried to get the above code to work, I ran into some Typescript compiler errors, so I add to set the compiler module setting to commonjs to get everything to work:

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    ...
  },
  ...
}

Have you been working with Vue and Typescript? Let me know if this was helpful for you in the comments!

Photo by Tom Barrett