Skip to content

Instantly share code, notes, and snippets.

@AlexandreBonneau
Created November 8, 2016 00:03
Show Gist options
  • Save AlexandreBonneau/dd170d67e0649c970291f81b150ceab2 to your computer and use it in GitHub Desktop.
Save AlexandreBonneau/dd170d67e0649c970291f81b150ceab2 to your computer and use it in GitHub Desktop.
Directive cdz-auto-numeric : wrap an autoNumeric-managed input with Vue.js 2.0
//AutoNumeric 2 : https://rawgithub.com/BobKnothe/autoNumeric/master/autoNumeric-2.0/autoNumeric-2.0-BETA.js //FAIL
//AutoNumeric 1.9.46 : https://cdnjs.cloudflare.com/ajax/libs/autonumeric/1.9.46/autoNumeric.min.js //WORK
//Live example on http://codepen.io/AnotherLinuxUser/pen/VmwvOK
let cdzNumericDirective = {
//Known problems :
//Using __autoNumercic 2.0 BETA__, when using 2 inputs with the same v-model (or when one input updates the value of another, even with different v-models), when you enter a value in one input, both updates well, but when you focus on the second input, the initial value is shown (since the current autoNumeric 2.0 BETA keeps its own `rawValue`). You can solve this by using 1.9.46 for the time being.
bind(el, bindings, vnode, oldVnode) {
let jQueryElement = $(el);
let rawValue; //Hold the unformatted number
//Manage the numeric options
let autoNumericOptions = bindings.value;
if (null === autoNumericOptions || "" === autoNumericOptions || typeof autoNumericOptions === 'undefined') {
autoNumericOptions = { aSep: '.', aDec: ',', altDec: '.', aSign: ' €', pSign: 's', mRound: 'U' }; //Default options
}
//Init the autoNumeric field
jQueryElement.autoNumeric('init', autoNumericOptions);
//Manage the initial value, if it is set
if (el.value !== '' && el.value !== null) {
jQueryElement.autoNumeric('set', el.value);
updateVModel();
}
//Declare the functions
function getUnformattedValue() {
return Number(jQueryElement.autoNumeric('get'));
}
function onKeyup(e) {
updateVModel();
}
function onPaste(e) {
e.preventDefault();
let pasteText = e.clipboardData.getData('text/plain');
let selectionStart = el.selectionStart;
let selectionEnd = el.selectionEnd;
let tempResult;
if (selectionStart === selectionEnd) { //No selection, only a caret, insert the pasted text
tempResult = insertCharAtPosition(el.value, pasteText, selectionStart);
}
else { //A user selection is present, replace that with the pasted text
let firstPart = el.value.slice(0, selectionStart);
let lastPart = el.value.slice(selectionEnd, el.value.length);
tempResult = firstPart + pasteText + lastPart;
}
let finalResult;
try {
finalResult = unformatEuroNumber(tempResult);
}
catch (error) {
return;
}
jQueryElement.autoNumeric('set', finalResult);
updateVModel();
}
function updateVModel() {
//Keep the raw unformatted value always up-to-date
rawValue = getUnformattedValue();
//Update the v-model value attached to the element, by using the 'input' event (so that other inputs using the same v-model are updated)
el.dispatchEvent(new Event('input', { bubbles: true }));
console.log(`DOM value [${el.value}], unmasked value [${rawValue}]`); //DEBUG
}
//Add the event listeners
el.addEventListener("keyup", onKeyup, false);
el.addEventListener("paste", onPaste, false);
},
unbind(el, bindings, vnode, oldVnode) {
$(el).autoNumeric('destroy');
el.removeEventListener("keyup", onKeyup, false);
el.removeEventListener("paste", onPaste, false);
},
};
new Vue({
el : '#app',
data() {
return {
a : 11.11,
b : 22.222,
c : null,
months : 24,
}
},
computed: {
years : {
get() {
return this.months / 12;
},
set(newValue) {
this.months = Math.round(newValue * 12);
}
},
},
directives: {
'cdz-numeric' : cdzNumericDirective,
}
});
//-------------------------------------------------------------------------
//-- The following functions are helper function for this specific example,
//-- and can be swapped/replaced/deleted as needed.
/**
* Temporary function to unformat a Euro formatted number.
* "1.234.567,89 €" -> 1234567.89
*/
function unformatEuroNumber(strNumber) {
//Remove the thousands separators (dots)
strNumber = removeDotFromString(strNumber);
//Replace the comma by a dot
strNumber = replaceDecimalCharByPoint(strNumber, ',');
//Remove the currency symbol
strNumber = removeEndFromString(strNumber, ' €');
let result = Number(strNumber);
if (isNaN(result)) throw new Exception("Impossible to convert to a number.");
return result;
}
function removeStringPartFromString(str, strToRemove) {
if (strToRemove === '.') {
strToRemove = '\\.'; //Special case for the 'dot'
}
let regex = new RegExp(strToRemove, 'g');
return str.replace(regex, "");
}
function removeDotFromString(str) {
let regex = new RegExp('\\.', 'g');
return str.replace(regex, "");
}
function removeEndFromString(str, strAtTheEndToRemove) {
let regex = new RegExp(strAtTheEndToRemove+'$', 'g');
return str.replace(regex, "");
}
function replaceDecimalCharByPoint(val, decimalSeparatorForRegExp) {
if (decimalSeparatorForRegExp === '\\.') return val;
let regex = new RegExp(decimalSeparatorForRegExp, 'g');
return String(val).replace(regex, '.');
}
function isNumber(n) {
return !isArray(n) && !isNaN(parseFloat(n)) && isFinite(n);
}
function isArray(arr) {
if (Object.prototype.toString.call([]) === '[object Array]') {
return Array.isArray(arr) || (typeof arr === 'object' && Object.prototype.toString.call(arr) === '[object Array]');
}
else {
throw new Exception('toString message changed for Object Array');
}
}
function insertCharAtPosition(str, char, caretPosition) {
return str.slice(0, caretPosition)+char+str.slice(caretPosition);
}
function Exception(msg) {
this.message = msg || '';
this.name = "Exception";
}
Exception.prototype = new Error();
Exception.prototype.constructor = Exception;
<div class="container">
<div id="app">
<div class="problem">
<p>The problem is : whenever you input a number in the first 'a' input, the v-model gets updated in both 'a' inputs. However, if you click in the second 'a' input, it then shows its initial value, not the v-model value.</p>
<p>The same problem arise when two inputs do not share a `v-model`, but update each other via vue.js.</p>
<p class="solved">Ok, the culprit has been found ; the current <strong>autoNumeric 2.0 Beta</strong> version keeps a `rawValue` and use that on focus if the input has changed. This has been added in 2.0 to circumvent a weird Asp.net behavior. So, until there is a way to be able to use autoNumeric 2.0 like before, <strong class="reallyStrong">you'll have to use 1.9.46 for now</strong>.</p>
</div>
<div class="example example1">
<p>Example 1 : Two inputs <strong>sharing the same `v-model`</strong> :</p>
<div class="frow frow1">
<div>a:<input type="text" v-cdz-numeric v-model="a"></div>
<div>a:<input type="text" v-cdz-numeric v-model="a"></div>
</div>
<div class="frow frow2">
<div>b:<input class="unimportant" type="text" v-cdz-numeric="{ aPad: false }" v-model="b"></div>
<div>c:<input class="unimportant" type="text" v-cdz-numeric="{ aSep: '.', aDec: ',', altDec: '.', aSign: '', pSign: 's', mRound: 'U' }" v-model="c"></div>
</div>
<p class="vModelValues">v-model input values : [<span class="shared">{{a}}</span>] [<span class="shared">{{a}}</span>] [<span class="c1">{{b}}</span>] [<span>{{c}}</span>]</p>
</div>
<div class="example example2">
<div>
<p>Example 2 : Two inputs <strong>using different `v-model`</strong>, but having their values updated via vue.js, and not via <i>'normal'</i> user inputs :</p>
<div class="frow frow3">
<div>y:<input type="text" v-model="years" ref="years" v-cdz-numeric="{aSep: '', aDec: '.', altDec: ',', aSign: '', pSign: 's', mRound: 'U', vMin: '-9999999999.99', vMax: '9999999999.99', aPad: false}"></div>
<div>m:<input type="text" v-model="months" ref="months" v-cdz-numeric="{aSep: '', aDec: '.', altDec: ',', aSign: '', pSign: 's', mRound: 'U', vMin: '-9999999999.99', vMax: '9999999999.99', aPad: false}"></div>
</div>
</div>
<p class="vModelValues">v-model input values : [<span class="shared2">{{years}}</span>] [<span class="shared2">{{months}}</span>]</p>
</div>
<p><i class="notes">Watch out, do not use `v-model.number` here, otherwise this messes up with autoNumeric inserting 'Comma' inside the input value.</i></p>
</div>
</div>
#app {
display : flex;
flex-direction : column;
align-items : center;
}
.solved {
color : lime;
}
.reallyStrong {
text-decoration: underline;
}
.problem {
width : 55%;
margin-top : 1rem;
margin-bottom : 2rem;
}
.hello {
color : #eee;
height : 15vh;
line-height : 15vh;
user-select : none;
cursor : pointer;
transition : color 0.3s ease;
&:hover {
color: lime;
}
}
#normalInput {
margin-top : 4rem;
}
.example p:first-child {
color : red;
background-color : #222;
// margin-top : 2rem;
margin-bottom : 0.5rem;
padding : 0.5rem;
}
i.notes {
font-size : 0.8rem;
}
.frow {
display : flex;
justify-content : space-between;
font-size : 2rem;
& > div:first-child {
margin-right: 2rem;
}
}
.frow1 {
margin-bottom: 1rem;
input {
border : 1px solid yellow;
}
}
.frow3 input {
border : 1px solid cyan;
}
input[type="text"] {
text-align:right;
min-width: 150px;
min-height : 40px;
border-radius : 2px;
border : 1px solid #eee;
background-color : #ccc;
font-size : 2rem;
color : #333;
padding : 1rem;
&.unimportant {
background-color: #949494;
color : #555;
}
}
.vModelValues {
font-size: 1.4rem;
padding-left : 2rem;
margin-bottom : 3rem;
span {
color : magenta;
&.shared {
color : yellow;
}
&.shared2 {
color : cyan;
}
&.c1 {
color : lime;
}
}
}
//-------------------- Unimportant css rules --------------------
$transitionDuration : 0.3s;
* {
margin : 0;
padding : 0;
position : relative;
box-sizing : border-box;
}
button:-moz-focusring,
input:-moz-focusring {
outline: 0;
}
.container {
height : 100vh;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
}
.filler {
background-color : #12232F;
border : 1px solid #3F4245;
opacity : 0.80;
height : 20vh;
width : 100vw;
}
body {
background : #333; //#022238
color : white;
font-family : "Open sans", sans-serif;
font-weight : 100;
font-size : 1rem;
}
a.button {
// color : white;
color : yellow;
text-decoration : none;
transition : color $transitionDuration ease;
&:focus {
outline: none;
}
&:hover {
// color : deeppink;
color : lime;
}
}
.button2 {
background-color : deeppink;
color : white;
border : 1px solid red;
border-radius : 2px;
height : 30px;
cursor : pointer;
outline : none;
&:focus, &:active {
outline : none;
}
}
h1 {
margin : 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment