Skip to content

Commit daa1f9d

Browse files
author
Sarah Leventhal
committed
feat(package): update to use vue3
BREAKING CHANGE: consuming applications require vue3
1 parent 35a95ee commit daa1f9d

12 files changed

+262
-51
lines changed

demo/App.vue

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<!-- Demo vue3 app for testing purposes -->
2+
<!-- Examples copied from the component markdown -->
3+
<template>
4+
<div id="app">
5+
<div id="value-display-div">
6+
<checkbox v-model="masked" />
7+
</div>
8+
9+
<h1>Basic Usage</h1>
10+
<p><code>lazy</code> custom v-model modifier will only emit input event on change event.</p>
11+
<field label="US Phone Number">
12+
<input-facade mask="(###) ### - ####" v-model.lazy="basicUsageValue" :masked="masked" />
13+
</field>
14+
<display :value="basicUsageValue" />
15+
16+
<h1>Optional character</h1>
17+
<p>
18+
Use a question mark (?) to indicate that a character is optional. Similar to regular expression this means 0 or 1.
19+
</p>
20+
<field label="IP address">
21+
<input-facade
22+
name="ip"
23+
mask="##?#?.##?#?.##?#?.##?#?"
24+
v-model="optionalCharvalue"
25+
:masked="masked"
26+
:formatter="validateIP"
27+
/>
28+
</field>
29+
<display :value="optionalCharvalue" />
30+
31+
<h1>Repeating character</h1>
32+
<p>
33+
Use an asterisk (*) as a suffix to set a masking character as repeating, similar to regular expression. Note that
34+
this means that 0 or more of said character will match. If you need to match 1 or more than you must specify it.
35+
</p>
36+
<field label="One or more numbers">
37+
<input-facade mask="##* AA" v-model="repeatingCharValue" :masked="masked" />
38+
</field>
39+
<display :value="repeatingCharValue" />
40+
41+
<h1>Alternation (Pipe)</h1>
42+
<p>
43+
Use a pipe symbol to indicate altarnative <b>static</b> values that can be used in the mask. This is case
44+
insensitive and can match letters irregarless of accents. For example å = A. Android webview and Opera dont fully
45+
support that type of matching.
46+
<i>
47+
Note that because this only works with static values there is no need to escape characters that are also used as
48+
tokens.
49+
</i>
50+
</p>
51+
<field label="ID Code">
52+
<input-facade mask="A|B|C-####" v-model="alternationValue" :masked="masked" />
53+
</field>
54+
<display :value="alternationValue" />
55+
56+
<h1>Dynamic Masks</h1>
57+
<p>
58+
Accepts an array of masking pattern and dynamically chooses the appropriate one based on the number of characters
59+
in the field.
60+
</p>
61+
<field label="US Zip Code">
62+
<input-facade v-model="USPostal" :mask="['#####', '#####-####']" :masked="masked" />
63+
</field>
64+
65+
<field label="UK Postal Code">
66+
<input-facade v-model="UKPostal" :mask="['A# #AA', 'AXX #AA', 'AA#X #AA']" :masked="masked" />
67+
</field>
68+
69+
<display label="Zip Code" :value="USPostal" />
70+
<display label="Postal Code" :value="UKPostal" />
71+
72+
<h1>Custom Tokens</h1>
73+
<p>
74+
You can override the tokens on a per field basis. Just pass in your own token definition to the field. This can
75+
also be used to add internatilization support.
76+
</p>
77+
<field label="Hex Color">
78+
<input-facade mask="\#FFFFFF" :tokens="hexTokens" :masked="masked" v-model="customTokenValue" />
79+
</field>
80+
<display :value="customTokenValue" />
81+
82+
<h1>Post masking input formatter</h1>
83+
<p>
84+
Returning a string in the format function will re-run that value through the masker routine, Ensuring that the end
85+
result still confirms to the mask.
86+
</p>
87+
<field label="Date as MM/YY">
88+
<input-facade v-model="formatterValue" mask="##/##" :formatter="date" />
89+
</field>
90+
<display :value="formatterValue" />
91+
<p>
92+
Returning a boolean `true` will leave the masked or unmasked value as is, the value is passed by reference so if
93+
you modify them here, that will be their final value. However if a `false` is returned, the user's input will be
94+
ignored and the value will remain as it was prior.
95+
</p>
96+
<field label="Enter an even num">
97+
<input-facade v-model="boolFormatterValue" mask="#########" :formatter="evenMoney" masked />
98+
</field>
99+
<display :value="boolFormatterValue" />
100+
</div>
101+
</template>
102+
103+
<script>
104+
import InputFacade from '../src/component.vue'
105+
import Checkbox from '../styleguide/components/Checkbox.vue'
106+
import Display from '../styleguide/components/Display.vue'
107+
import Field from '../styleguide/components/Field.vue'
108+
109+
export default {
110+
name: 'App',
111+
components: {
112+
Checkbox,
113+
Display,
114+
Field,
115+
InputFacade
116+
},
117+
data() {
118+
return {
119+
basicUsageValue: '',
120+
optionalCharvalue: '',
121+
repeatingCharValue: '',
122+
alternationValue: '',
123+
USPostal: '',
124+
UKPostal: '',
125+
customTokenValue: '',
126+
formatterValue: '',
127+
boolFormatterValue: '',
128+
masked: true,
129+
hexTokens: {
130+
F: {
131+
pattern: /[0-9A-F]/i,
132+
transform: (v) => v.toLocaleUpperCase()
133+
}
134+
}
135+
}
136+
},
137+
methods: {
138+
date(value, event) {
139+
// do not format on deletion, this could leave the input in bad state
140+
// but allows user to delete the leading 0 if needed for some reason
141+
if (event.inputType !== 'deleteContentBackward') {
142+
const [month] = value.masked.split('/')
143+
144+
if (month > 12) {
145+
return '0' + value.unmasked
146+
}
147+
}
148+
},
149+
evenMoney(value, event) {
150+
if (event.data && event.data % 2 !== 0) {
151+
// odd number, ignore it
152+
return false
153+
} else if (value.unmasked) {
154+
const formatted = value.unmasked.match(/\d{1,3}/g).join(',')
155+
value.masked = `$${formatted}`
156+
return true
157+
}
158+
},
159+
validateIP(value) {
160+
const parts = value.masked.split('.')
161+
162+
if (parts.length < 4 && parts[parts.length - 1] > 25) {
163+
return value.masked + '.'
164+
}
165+
166+
return !parts.some((part) => part > 255)
167+
}
168+
}
169+
}
170+
</script>
171+
172+
<style scoped>
173+
#value-display-div {
174+
position: fixed;
175+
background-color: white;
176+
border: solid 0.75px black;
177+
border-radius: 5px;
178+
padding: 5px;
179+
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
180+
top: 15px;
181+
right: 15px;
182+
}
183+
code {
184+
padding: 2px;
185+
border-radius: 2px;
186+
display: inline-block;
187+
background-color: #eee;
188+
}
189+
</style>

demo/main.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createApp } from 'vue'
2+
import App from './App.vue'
3+
4+
createApp(App).mount('#app')

jest.config.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
module.exports = {
2-
preset: '@vue/cli-plugin-unit-jest',
2+
moduleFileExtensions: ['vue', 'js', 'json', 'jsx'],
3+
testEnvironment: 'jsdom',
4+
testEnvironmentOptions: {
5+
customExportConditions: ['node', 'node-addons']
6+
},
7+
transform: {
8+
'^.+\\.js$': 'babel-jest',
9+
'^.+\\.vue$': '@vue/vue3-jest'
10+
},
311
testMatch: ['**/tests/**/*.test.js'],
412
collectCoverageFrom: ['src/*.{js,vue}', '!src/plugin.js'],
513
coverageThreshold: {

package.json

+12-5
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,41 @@
1515
"build:plugin": "vue-cli-service build --target=lib src/plugin.js",
1616
"coveralls": "cat ./coverage/lcov.info | coveralls",
1717
"semantic-release": "semantic-release",
18+
"serve": "vue-cli-service serve",
1819
"dev": "vue-cli-service styleguidist --config styleguide/config.js --mode development",
1920
"build:docs": "vue-cli-service styleguidist:build --config styleguide/config.js",
2021
"test": "jest --coverage",
22+
"test:filter": "func() { yarn test \"$1\" --coverage-reporters=\"lcov\" --coverageThreshold={} ${@:2}; }; func",
2123
"test:watch": "jest --coverage --watchAll"
2224
},
2325
"main": "dist/vue-input-facade.umd.min.js",
2426
"files": [
2527
"dist/*.js"
2628
],
2729
"devDependencies": {
30+
"@babel/core": "^7.14.5",
31+
"@babel/preset-env": "^7.14.7",
2832
"@vue/cli-plugin-babel": "^4.5.15",
2933
"@vue/cli-plugin-eslint": "^4.5.15",
30-
"@vue/cli-plugin-unit-jest": "^4.5.15",
3134
"@vue/cli-service": "^4.5.15",
35+
"@vue/compiler-dom": "^3.2.40",
3236
"@vue/eslint-config-prettier": "^5.0.0",
33-
"@vue/test-utils": "^1.3.0",
37+
"@vue/test-utils": "^2.1.0",
38+
"@vue/vue3-jest": "^28.1.0",
39+
"babel-core": "^7.0.0-bridge.0",
3440
"babel-eslint": "^10.1.0",
41+
"babel-jest": "^28.1.3",
3542
"core-js": "^3.19.3",
3643
"cz-conventional-changelog": "^3.3.0",
3744
"eslint": "^5.16.0",
3845
"eslint-plugin-prettier": "^3.4.1",
3946
"eslint-plugin-vue": "^5.0.0",
4047
"husky": "^4.3.8",
48+
"jest": "^28.1.3",
49+
"jest-environment-jsdom": "^29.1.2",
4150
"prettier": "^1.18.2",
4251
"semantic-release": "^17.4.7",
43-
"vue": "^2.6.14",
44-
"vue-cli-plugin-styleguidist": "^4.44.15",
45-
"vue-template-compiler": "^2.6.14"
52+
"vue": "^3.2.40"
4653
},
4754
"config": {
4855
"commitizen": {

src/component.vue

+16-20
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,6 @@ export default {
3535
* @since v1.3
3636
*/
3737
formatter: Function,
38-
/**
39-
* Vue's v-model .lazy modifier does not currently work with custom components. If you wish to have your v-model
40-
* updated only during the change event instead of on input, enable this property. <b>Note: This works by supressing
41-
* input events and only emitting a single input event at the same time as the change event.</b>
42-
* @since v1.3
43-
*/
44-
lazy: {
45-
type: Boolean,
46-
default: false
47-
},
4838
/**
4939
* The mask pattern for this input, it could be a single pattern or multiple patterns when its an array.
5040
*/
@@ -80,7 +70,10 @@ export default {
8070
* The input's value
8171
* @model
8272
*/
83-
value: [String, Number]
73+
modelValue: [String, Number],
74+
modelModifiers: {
75+
default: () => ({})
76+
}
8477
},
8578
directives: { facade: directive },
8679
data() {
@@ -90,18 +83,21 @@ export default {
9083
}
9184
},
9285
watch: {
93-
value(newValue) {
86+
modelValue(newValue) {
9487
// avoid trigering the directive's update hook when we emit
9588
// the unmasked value to the parent component
9689
if (newValue !== this.emittedValue) {
9790
this.maskedValue = newValue
9891
}
9992
},
100-
mask(newMask) {
101-
if (!newMask && !this.masked) {
102-
// when removing the masking rule, set the displayed value to the unmasked
103-
// to remove any unwanted masking characters from the input
104-
this.maskedValue = this.unmaskedValue
93+
mask: {
94+
deep: true,
95+
handler(newMask) {
96+
if (!newMask && !this.masked) {
97+
// when removing the masking rule, set the displayed value to the unmasked
98+
// to remove any unwanted masking characters from the input
99+
this.maskedValue = this.unmaskedValue
100+
}
105101
}
106102
},
107103
masked() {
@@ -128,7 +124,7 @@ export default {
128124
this.maskedValue = target.value
129125
this.unmaskedValue = target.unmaskedValue
130126
131-
if (!this.lazy) {
127+
if (!this.modelModifiers.lazy) {
132128
this.emitInput()
133129
}
134130
},
@@ -139,7 +135,7 @@ export default {
139135
*/
140136
this.$emit('change', this.emittedValue)
141137
142-
if (this.lazy) {
138+
if (this.modelModifiers.lazy) {
143139
this.emitInput()
144140
}
145141
},
@@ -148,7 +144,7 @@ export default {
148144
* Fires when the value of the input has been changed.
149145
* @param {String} value The input's current value, masked or unmasked.
150146
*/
151-
this.$emit('input', this.emittedValue)
147+
this.$emit('update:modelValue', this.emittedValue)
152148
}
153149
}
154150
}

src/core.js

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export function updateValue(el, vnode, { emit = true, force = false } = {}, even
156156
currentValue = currentValue || ''
157157

158158
if (force || oldValue !== currentValue) {
159+
// to keep the string as short as possible (not append extra chars at the end)
159160
if (['deleteByCut', 'deleteContent', 'deleteContentBackward', 'deleteContentForward'].includes(event?.inputType)) {
160161
config = { ...config, short: true }
161162
}

src/directive.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as core from './core'
22
const CONFIG_KEY = core.CONFIG_KEY
33

44
export default {
5-
bind: (el, { value, modifiers }, vnode) => {
5+
beforeMount: (el, { value, modifiers }, vnode) => {
66
el = core.getInputElement(el)
77
const config = core.normalizeConfig(value, modifiers)
88
el[CONFIG_KEY] = { config }
@@ -11,7 +11,7 @@ export default {
1111
core.updateValue(el, vnode, { force: config.prefill })
1212
},
1313

14-
inserted: (el) => {
14+
mounted: (el) => {
1515
el = core.getInputElement(el)
1616
const config = el[CONFIG_KEY]
1717
// prefer adding event listener to parent element to avoid Firefox bug which does not
@@ -52,7 +52,7 @@ export default {
5252
}
5353
},
5454

55-
update: (el, { value, oldValue, modifiers }, vnode) => {
55+
updated: (el, { value, oldValue, modifiers }, vnode) => {
5656
el = core.getInputElement(el)
5757

5858
if (value !== oldValue) {
@@ -63,7 +63,7 @@ export default {
6363
}
6464
},
6565

66-
unbind: (el) => {
66+
unmounted: (el) => {
6767
core.getInputElement(el)[CONFIG_KEY].cleanup()
6868
}
6969
}

0 commit comments

Comments
 (0)