I needed to create an input mask for a Lit project I am working on. After a little research, i chooseIMask 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";importIMaskfrom"imask";/** * @typedef { import('lit').ElementPart } ElementPart **/exportclassInputMaskDirectiveextendsDirective{/** * @param {ElementPart} part * @param {[string]} [mask] directive arguments * @return {*} */update(part,[mask]){constinputEl=part.element.matches("input")?part.element:part.element.querySelector("input");if(!inputEl){console.warn("InputMaskDirective: input element not found");returnnoChange;}IMask(inputEl,{mask,});returnnoChange;}}exportconstinputMask=directive(InputMaskDirective);
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 overridesupdate
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 returnsnoChange
.
It should be used as a element part:
import{html}from"lit";import{inputMask}from"./input-mask.js";consttemplate=html`<input type="text"${inputMask("00/00/0000")} /> `;
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:
consttemplate=html` <div${inputMask("00/00/0000")}> <input type="text" /> </div>`;
Still, the directive will work as expected. But the actual usage is not exactly like that. I have aninput
function that renders the input element:
import{html}from"lit";import{inputMask}from"./input-mask.js";functioninput(attr,title){returnhtml`<label>${title}</label> <input name=${attr} type="text" />`;}consttemplate=html` <div${inputMask("00/00/0000")}>${input("name","Name")}</div>`;
This time, the directive will not work as expected. The problem is that at the timeupdate
is called, the child input element is not yet rendered. So,querySelector("input")
will returnnull
. Check thisLit 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:
exportclassInputMaskDirectiveextendsDirective{/** * @param {ElementPart} part * @param {[string]} [mask] directive arguments * @return {*} */update(part,[mask]){constinputEl=part.element.matches("input")?part.element:part.element.querySelector("input");if(inputEl){IMask(inputEl,{mask,});}else{constobserver=newMutationObserver((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});}returnnoChange;}}
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)
For further actions, you may consider blocking this person and/orreporting abuse