DEV Community

Luiz Américo
Luiz Américo

Posted on • Originally published at blikblum.dev on

Creating an input mask with Lit

I needed to create an input mask for a Lit project I am working on. After a little research, i choose IMask library. In this article, I'll show you how to create an input mask in a Lit template using a directive.

I could create a custom element to wrap the input element and handle the mask logic, but using a directive is a more elegant and flexible solution.

First, naive version

import { noChange } from "lit";
import { Directive, directive } from "lit/directive.js";
import IMask from "imask";

/**
 * @typedef { import('lit').ElementPart } ElementPart
 **/

export class InputMaskDirective extends Directive {
  /**
   * @param {ElementPart} part
   * @param {[string]} [mask] directive arguments
   * @return {*}
   */
  update(part, [mask]) {
    const inputEl = part.element.matches("input")
      ? part.element
      : part.element.querySelector("input");
    if (!inputEl) {
      console.warn("InputMaskDirective: input element not found");
      return noChange;
    }

    IMask(inputEl, {
      mask,
    });

    return noChange;
  }
}

export const inputMask = directive(InputMaskDirective);

Enter fullscreen mode Exit fullscreen mode

It's a simple directive that receives a mask string as an argument and applies it to the input element using the IMask library. The class overrides update method so it can access the part element, checks if element or any of its children is an input and apply the mask to it. Since it does not render anything, it returns noChange.

It should be used as a element part:

import { html } from "lit";
import { inputMask } from "./input-mask.js";

const template = html`<input type="text" ${inputMask("00/00/0000")} /> `;

Enter fullscreen mode Exit fullscreen mode

So far, so good. But there is a problem with this implementation. One of my requirements is to be able to be define the mask in a parent of the input element. Something like the below example:

const template = html`
  <div ${inputMask("00/00/0000")}>
    <input type="text" />
  </div>
`;

Enter fullscreen mode Exit fullscreen mode

Still, the directive will work as expected. But the actual usage is not exactly like that. I have an input function that renders the input element:

import { html } from "lit";
import { inputMask } from "./input-mask.js";

function input(attr, title) {
  return html`<label>${title}</label> <input name=${attr} type="text" />`;
}

const template = html`
  <div ${inputMask("00/00/0000")}>${input("name", "Name")}</div>
`;

Enter fullscreen mode Exit fullscreen mode

This time, the directive will not work as expected. The problem is that at the time update is called, the child input element is not yet rendered. So, querySelector("input") will return null. Check this Lit Playground to see the problem in action.

Second, improved version

Here is an improved version that uses a MutationObserver to wait for a input element to be added to the DOM:

export class InputMaskDirective extends Directive {
  /**
   * @param {ElementPart} part
   * @param {[string]} [mask] directive arguments
   * @return {*}
   */
  update(part, [mask]) {
    const inputEl = part.element.matches("input")
      ? part.element
      : part.element.querySelector("input");
    if (inputEl) {
      IMask(inputEl, {
        mask,
      });
    } else {
      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === 1 && node.matches("input")) {
              IMask(node, {
                mask,
              });

              observer.disconnect();
            }
          });
        });
      });
      observer.observe(part.element, { childList: true, subtree: true });
    }

    return noChange;
  }
}

Enter fullscreen mode Exit fullscreen mode

This works as expected in all cases.

Conclusion

This article shows how to create an input mask in a Lit template using a directive. It highlights a difference in the timing of the children rendering when using nested templates and how to handle it using a MutationObserver.

The same technique can be used to create other directives that need to access child elements.

Top comments (0)