Skip to content

Input mask on form inputs #1385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Tracked by #6736 ...
thamibn opened this issue Oct 16, 2021 · 14 comments · May be fixed by #6841
Open
Tracked by #6736 ...

Input mask on form inputs #1385

thamibn opened this issue Oct 16, 2021 · 14 comments · May be fixed by #6841

Comments

@thamibn
Copy link

thamibn commented Oct 16, 2021

This function solves the problem (这个功能解决的问题)

Avoids users to enter wrong data from the get go

Expected API (期望的 API)

This will allow us to mask inputs to specific format e.g (##)-(####)

Means a user will enter the data in that format.

Find below pure JavaScript example
https://imask.js.org/

@github-actions github-actions bot added the feature request New feature or request label Oct 16, 2021
@07akioni
Copy link
Collaborator

07akioni commented Oct 17, 2021

I've done some experiments and find it's not easy to integrate it whether inside or outside the library.

The main problems are that they don't offer a DOM independant API and the handling of selection range issues of the input element.

However this is a somehow useful feature. I'll try to make it work later.

@B3nsten
Copy link

B3nsten commented Dec 6, 2021

Might not be the optimal solution, but vue-the-mask offers a directive, that can easily be ported to Vue3 and is indeed working (at least for my purposes) with the existing NInput element. With a wrapper component and some tweaking, it's also possible to change between masked/unmasked value (while the displayed value is masked) and also get selection persistence.

./directives/mask.js

function event(name, detail) {
  const evt = new Event(name, { bubbles: true, cancelable: true })
  return evt
}

function dynamicMask(maskit, masks, tokens) {
  masks = masks.sort((a, b) => a.length - b.length)
  return function (value, mask, masked = true) {
    let i = 0
    while (i < masks.length) {
      const currentMask = masks[i]
      i++
      const nextMask = masks[i]
      if (!(nextMask && maskit(value, nextMask, true, tokens).length > currentMask.length)) {
        return maskit(value, currentMask, masked, tokens)
      }
    }
    return '' // empty masks
  }
}

function maskit(value, mask, masked = true, tokens) {
  value = value || ''
  mask = mask || ''
  let iMask = 0
  let iValue = 0
  let output = ''
  while (iMask < mask.length && iValue < value.length) {
    let cMask = mask[iMask]
    const masker = tokens[cMask]
    const cValue = value[iValue]
    if (masker && !masker.escape) {
      if (masker.pattern.test(cValue)) {
        output += masker.transform ? masker.transform(cValue) : cValue
        iMask++
      }
      iValue++
    } else {
      if (masker && masker.escape) {
        iMask++ // take the next mask char and treat it as char
        cMask = mask[iMask]
      }
      if (masked) output += cMask
      if (cValue === cMask) iValue++ // user typed the same char
      iMask++
    }
  }

  // fix mask that ends with a char: (#)
  let restOutput = ''
  while (iMask < mask.length && masked) {
    const cMask = mask[iMask]
    if (tokens[cMask]) {
      restOutput = ''
      break
    }
    restOutput += cMask
    iMask++
  }

  return output + restOutput
}

export function masker(value, mask, masked = true, tokens) {
  return Array.isArray(mask)
    ? dynamicMask(maskit, mask, tokens)(value, mask, masked, tokens)
    : maskit(value, mask, masked, tokens)
}

export const tokens = {
  '#': { pattern: /\d/ },
  X: { pattern: /[0-9a-zA-Z]/ },
  S: { pattern: /[a-zA-Z]/ },
  A: { pattern: /[a-zA-Z]/, transform: v => v.toLocaleUpperCase() },
  H: { pattern: /[0-9a-fA-F]/, transform: v => v.toLocaleUpperCase() },
  a: { pattern: /[a-zA-Z]/, transform: v => v.toLocaleLowerCase() },
  '!': { escape: true }
}

export default function (el, binding, vnode) {
  let config = binding.value
  if (Array.isArray(config) || typeof config === 'string') {
    config = {
      mask: config,
      tokens: tokens
    }
  } else return

  if (el.tagName.toLocaleUpperCase() !== 'INPUT') {
    const els = el.getElementsByTagName('input')
    if (els.length !== 1) {
      throw new Error("v-mask directive requires 1 input, found " + els.length)
    } else {
      el = els[0]
    }
  }

  let position = el.selectionEnd
  const digit = el.value[position - 1]
  const newDisplay = masker(el.value, config.mask, true, config.tokens)
  if (newDisplay !== el.value) {
    el.value = newDisplay
    el.dispatchEvent(event('input'))
    while (position < el.value.length && el.value.charAt(position - 1) !== digit) {
      position++
    }
    if (el === document.activeElement) {
      el.setSelectionRange(position, position)
      setTimeout(function () {
        el.setSelectionRange(position, position)
      }, 0)
    }
  }
}

./main.js

import mask from './directives/mask'
app.directive('mask', mask)

./components/MaskedInput.vue

<template>
  <n-input :value="display" v-mask="mask" @input="refresh" />
</template>

<script setup>
import { masker, tokens } from "@/directives/mask";

const emit = defineEmits(["update:value"]);

const props = defineProps({
  value: [String, Number],
  mask: {
    type: [String, Array],
    required: true,
  },
  masked: {
    type: Boolean,
    default: false,
  },
});
const { mask, masked, value } = toRefs(props);

const display = ref(value.value);
const lastValue = ref(null);

watch(
  () => value.value,
  (newValue) => {
    if (newValue !== lastValue.value) {
      display.value = newValue;
    }
  }
);

watch(
  () => masked.value,
  () => refresh(display.value)
);

const refresh = (value) => {
  display.value = value;
  const val = masker(value, mask.value, masked.value, tokens);
  if (val !== lastValue.value) {
    lastValue.value = val;
    emit("update:value", val);
  }
};
</script>

Use somewhere

<masked-input v-model:value="value" :mask="'##/##/####'" />

@brunotourinho
Copy link

Hey @B3nsten did you get it to work somehow? I'm in a project that demands masks, including date and currency.

@B3nsten
Copy link

B3nsten commented Jan 27, 2023

Hey @B3nsten did you get it to work somehow? I'm in a project that demands masks, including date and currency.

The posted code was working. What's erroring for you?

@brunotourinho
Copy link

I'll give it a try, now! Have to adapt a bit because Im working with Nuxt 3!

@brunotourinho
Copy link

It worked ❤️! Now I'm missing currency mask. Any chance you have it already?

@B3nsten
Copy link

B3nsten commented Jan 27, 2023

Well, depending on your needs you might get away with something like:

<masked-input v-model:value="value" :mask="['#.##', '##.##', '###.##']" />

You'd have to change a lot of things if you need negative numbers, want to handle the money prefix inside the input (I'd use a naive input group, tho), etc. This approach is not the best for handling variable input lengths and in this state is not suitable to handle anything but strings.

@brunotourinho
Copy link

Thanks a bunch! I've been working on this feature for a week! You saved me!

@07akioni 07akioni mentioned this issue Feb 5, 2023
@rom-rzic
Copy link

Hello. There is an easier way, it is not necessary to create a new MaskedInput component, you can use the package https://github.com/beholdr/maska , and to set the mask, use as
<n-input placeholder="Phone number *" v-model:value="form.phone" :input-props="{ vMaska: true, 'data-mask': '+# ### ###-##-##' }"/>

@golddeitys
Copy link

@rom-rzic does it work? Tried but nothing happened

@rassimjhan
Copy link

rassimjhan commented Sep 20, 2023

Hello. There is an easier way, it is not necessary to create a new MaskedInput component, you can use the package https://github.com/beholdr/maska , and to set the mask, use as <n-input placeholder="Phone number *" v-model:value="form.phone" :input-props="{ vMaska: true, 'data-mask': '+# ### ###-##-##' }"/>

Hello! Could you show how register vMaska in component? I have tried your example but in doesn't work. I register maska in script tag import { vMaska } from "maska"

@rassimjhan
Copy link

rassimjhan commented Sep 20, 2023

Hello. There is an easier way, it is not necessary to create a new MaskedInput component, you can use the package https://github.com/beholdr/maska , and to set the mask, use as <n-input placeholder="Phone number *" v-model:value="form.phone" :input-props="{ vMaska: true, 'data-mask': '+# ### ###-##-##' }"/>

Hello! Could you show how register vMaska in component? I have tried your example but in doesn't work. I register maska in script tag import { vMaska } from "maska"

I found the solution!

 <n-input
   v-model:value="formData.user.phone"
   placeholder="+7 700 777 77 77"
   v-maska
   :input-props="{
     'data-maska': '+# ### ### ## ##',
   }"
/>

The reason why @rom-rzic example didn't work because 'data-mask' and he didn't set v-maska directive in n-input also you don't have to set vMaska: true in input-props. You have to use 'data-maska' and don't forget to set v-maska directive!

Welcome 🎉   

@rassimjhan
Copy link

@rom-rzic does it work? Tried but nothing happened

Solution #1385 (comment)

@gglazewskigran
Copy link

someone got it working for n-input pair?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants