Skip to content

Commit

Permalink
Merge pull request #479 from probil/feature/#128-mask-placeholders
Browse files Browse the repository at this point in the history
Feature/#128 mask placeholders
  • Loading branch information
probil committed May 24, 2020
2 parents 6bfbaad + 85b241e commit 31b8f23
Show file tree
Hide file tree
Showing 10 changed files with 496 additions and 87 deletions.
178 changes: 169 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,177 @@ There is no reason to support using this lib for using without `v-model` but ope
<span>{{ '9999999999' | VMask('(###) ###-####') }}</span>
```

## :gear: Configs
## :gear: Configuration

List of supported placeholders:
Library provides several ways to apply the mask.

| Value | Format |
|-------|------------------------------|
| # | Number (0-9) |
| A | Letter in any case (a-z,A-Z) |
| N | Number or letter |
| X | Any symbol |
| ? | Optional (next character) |
The first and the easiest one is to use default placeholders.


### Default placeholders

This approach is good for simple cases. No configuration is required.

`app.js`:
```js
import Vue from 'vue'
import VueMask from 'v-mask'
Vue.use(VueMask)
```

`<your_component>.vue`:
```vue
<template>
<input type="text" v-mask="'####-##'" v-model="myInputModel">
</template>
<script>
export default {
data: () => ({
myInputModel: ''
})
}
</script>
```
Entering `56f473d4` in the input field will produce value `5647-34` in `myInputModel` variable.

Here is a list placeholders you can utilize by default:

| Placeholder | Format |
|-------------|--------------------------------|
| # | Number (0-9) |
| A | Letter in any case (a-z,A-Z) |
| N | Number or letter (a-z,A-Z,0-9) |
| X | Any symbol |
| ? | Optional (next character) |


### Custom placeholders
While default placeholders are easy to use and straightforward in reality we have to deal with more complex cases where validation can be a bit more complex and unpredictable. In such cases it makes sense to define custom placeholders specific to the project or the domain.

To define them you should pass them as an object while installing plugin. Where:
* `key` is the character in a mask
* `value` is regular expression used to verify entered symbol

You can disable any default placeholder by passing placeholder as a key and `null` as a value.

Any valid string character can be used as a placeholder (e.g. Cyrillic or Arabic)

`app.js`:
```js
import Vue from 'vue'
import VueMask from 'v-mask'

Vue.use(VueMask, {
placeholders: {
'#': null, // passing `null` removes default placeholder, so `#` is treated as character
D: /\d/, // define new placeholder
Я: /[\wа-яА-Я]/, // cyrillic letter as a placeholder
}
})
```
`<your_component>.vue`:
```vue
<template>
<input type="text" v-mask="'###-DDD-###-DDD'" v-model="myInputModel">
</template>
<script>
export default {
data: () => ({
myInputModel: ''
})
}
</script>
```
Entering `123456` in that input field will produce value `###-123-###-456` in `myInputModel` variable.

### Array of RegExp
In some cases you might not want to define global placeholders either because you are dealing with unique input or you are facing conflicts for placeholders in sevaral places.

In such cases you can supply array of per-char regular excursions or static characters to `v-mask`.

`app.js`:
```js
import Vue from 'vue'
import VueMask from 'v-mask'
Vue.use(VueMask)
```

`<your_component>.vue`:
```vue
<template>
<input type="text" v-mask="mask" v-model="myInputModel">
</template>
<script>
export default {
data: () => ({
mask: ['(', /\d/, /\d/, /\d/, ') ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/],
myInputModel: ''
})
}
</script>
```
In this example entering `5555551234` in the input field will produce value `(555) 555-1234` in `myInputModel` variable.

**Notice**: Keep in mind that library always verifies _one_ character per regular expression. Trying verify multiple charters in the same RegExp won't work.

### Function

If custom placeholder and array of RegExps can't fit your needs there is one more way you can use to mask a value.
The idea beneath is that you can write a function that is used by library to format the output.

This approach is super powerful but also more complex to write and understand so try previous ones first.

The function will be given a value from the input. It should return array of per-char regular expressions & static characters:

`app.js`:
```js
import Vue from 'vue'
import VueMask from 'v-mask'
Vue.use(VueMask)
```

`<your_component>.vue`:
```vue
<template>
<input type="text" v-mask="mask" v-model="myInputModel" placeholder="00:00-23:59">
</template>
<script>
/**
* Generate a time mask based on input value (23:59)
* @param {string} value
*/
export function timeMask(value) {
const hours = [
/[0-2]/,
value.charAt(0) === '2' ? /[0-3]/ : /[0-9]/,
];
const minutes = [/[0-5]/, /[0-9]/];
return value.length > 2
? [...hours, ':', ...minutes]
: hours;
}
/**
* Generate a time range mask based on input value (00:00-23:59)
* @param {string} value
*/
export function timeRangeMask(value) {
const numbers = value.replace(/[^0-9]/g, '');
if (numbers.length > 4) {
return [...timeMask(numbers.substring(0, 4)), '-', ...timeMask(numbers.substring(4))];
}
return [...timeMask(numbers)];
}
export default {
data: () => ({
mask: timeRangeMask,
myInputModel: ''
})
}
</script>
```
In this example entering `02532137` in the input field will produce valid time range `02:53-21:37` in `myInputModel` variable.

## :syringe: Tests

Expand Down
91 changes: 90 additions & 1 deletion src/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLocalVue, mount } from '@vue/test-utils';
import VueMask, { VueMaskDirective, VueMaskPlugin } from '../index';
import { timeRangeMask } from '../utils/timeRangeMask';

describe('plugin/directive registration', () => {
let Vue;
Expand Down Expand Up @@ -46,7 +47,7 @@ describe('directive usage', () => {
const wrapper = mountWithMask({
template: '<input />',
});
expect(wrapper.is('input')).toBe(true);
expect(wrapper.element.tagName).toBe('INPUT');
});

it('should update model value after directive bind', () => {
Expand All @@ -67,4 +68,92 @@ describe('directive usage', () => {
await wrapper.vm.$nextTick();
expect(wrapper.vm.$el.value).toBe('11.11.2011');
});

it('should accept an array of regular expressions directly', async () => {
const wrapper = mountWithMask({
data: () => ({ mask: ['(', /\d/, /\d/, /\d/, ') ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/], value: '5555551234' }),
template: '<input v-mask="mask" v-model="value"/>',
});
expect(wrapper.vm.$el.value).toBe('(555) 555-1234');
});

it('should allow for add/removal of global mask placeholders', async () => {
const localVue = createLocalVue();
localVue.use(VueMask, {
placeholders: {
'#': null,
D: /\d/,
},
});
const wrapper = mount({
data: () => ({ mask: '###-DDD-###-DDD', value: '123456' }),
template: '<input v-mask="mask" v-model="value"/>',
}, { localVue });
expect(wrapper.vm.$el.value).toBe('###-123-###-456');
});

it('should allow placeholders for uppercase and lowercase characters', async () => {
const localVue = createLocalVue();
localVue.use(VueMask, {
placeholders: {
u: /[A-Z]/,
l: /[a-z]/,
},
});
const wrapper = mount({
data: () => ({ mask: '###-###-###-ul-ul', value: '123123123AbAb' }),
template: '<input v-mask="mask" v-model="value"/>',
}, { localVue });
expect(wrapper.vm.$el.value).toBe('123-123-123-Ab-Ab');
});

it('should allow placeholders for cyrillic characters', async () => {
const localVue = createLocalVue();
localVue.use(VueMask, {
placeholders: {
Я: /[\wа-яА-Я]/,
},
});
const wrapper = mount({
data: () => ({ mask: 'ЯЯЯЯЯЯ ЯЯЯЯ', value: 'Доброеутро' }),
template: '<input v-mask="mask" v-model="value"/>',
}, { localVue });
expect(wrapper.vm.$el.value).toBe('Доброе утро');
});

it('should be possible to create a mask for accepting a valid time range', async () => {
const wrapper = mountWithMask({
data: () => ({
mask: timeRangeMask,
value: '02532137',
}),
template: '<input v-mask="mask" v-model="value"/>',
});
expect(wrapper.vm.$el.value).toBe('02:53-21:37');
});

it('should be possible to create a mask for rejecting an invalid time range', async () => {
const wrapper = mountWithMask({
data: () => ({
mask: timeRangeMask,
value: '23599999',
}),
template: '<input v-mask="mask" v-model="value"/>',
});
expect(wrapper.vm.$el.value).toBe('23:59-');
});

it('should have the ability to give two or multiple choices', async () => {
const localVue = createLocalVue();
localVue.use(VueMask, {
placeholders: {
P: /([67])/,
},
});
const wrapper = mount({
data: () => ({ mask: '0P-##-##-##-##', value: '0755555555' }),
template: '<input v-mask="mask" v-model="value"/>',
}, { localVue });
expect(wrapper.vm.$el.value).toBe('07-55-55-55-55');
});
});

0 comments on commit 31b8f23

Please sign in to comment.