dm4t2 / intl-number-input

Easy input of formatted numbers based on the ECMAScript Internationalization API (Intl.NumberFormat).
https://dm4t2.github.io/intl-number-input/
MIT License
12 stars 3 forks source link

Initializing with unformatted number / usage as a Vue custom component #4

Closed ragulka closed 2 years ago

ragulka commented 2 years ago

I'm trying to create a custom NumericInput component in Vue using this package and hit a snag, because it expects that the input element holds the formatted number on initialization. However, in my case, the number is supplied by the backend unformatted, as a regular number.

This is not a problem when using a locale that uses period . as the decimal separator, however it becomes an issue when the locale uses comma as the decimal separator: after I focus on the field and try to type into it, the period is removed and something like 123.45 becomes 12345 which is obviously not what I want.

Looks like the constructor tries to parse the input element's value here and there's no way to work around it.

Here's what I have so far:

<!-- NumericInput.vue -->
<template>
    <input ref="inputRef" :value="modelValue" />
</template>

<script setup>
import { defineProps, defineEmits, ref, onMounted } from 'vue'
import { NumberInput } from 'intl-number-input'

const props = defineProps({
    modelValue: {
        type: Number,
        default: null,
    },
})

const emit = defineEmits(['update:modelValue', 'change'])
const inputRef = ref(null)

onMounted(() => {
    if (inputRef.value) {
        const numberInput = new NumberInput({
            el: inputRef.value,
            options: {
                locale: 'et',
                formatStyle: 'decimal',
                precision: 2,
                hideNegligibleDecimalDigitsOnFocus: false,
            },
            onInput: value => emit('update:modelValue', value.number),
            onChange: value => emit('change', value.number),
        })
    }
})
</script>

<!-- usage -->
<NumericInput v-model="123.45" />

I guess I could use a computed formatted number for the element value instead, which would allow me to pass in an unformatted number & format it on component initialization. This would initialize the NumberInput instance correctly, at least.

However, there is another issue: I want to emit the raw number value on input & change. This causes a loop where the emitted value.number causes a recalculation of the computed formattedValue, which causes the caret to jump around in the input as I type.

Perhaps there's an easy way to fix this so forgive me if I'm missing something.

dm4t2 commented 2 years ago

You can call numberInput.setValue(props.modelValue) in the onMounted hook to set the initial value.

Sorry, that there are no docs at all. I'm currently working on that 🚀

ragulka commented 2 years ago

You can call numberInput.setValue(props.modelValue) in the onMounted hook to set the initial value.

Yeah, I realized that last night so I came up with the following:

<template>
    <input ref="inputRef" />
</template>

<script setup>
import { defineProps, defineEmits, ref, onMounted, watchEffect, useAttrs } from 'vue'
import { NumberInput } from 'intl-number-input'

const props = defineProps({
    modelValue: {
        type: Number,
        default: null,
    },
})

const emit = defineEmits(['update:modelValue'])
const inputRef = ref(null)
const attrs = useAttrs()

onMounted(() => {
    if (!inputRef.value) {
        return
    }

    const options = {
        el: inputRef.value,
        options: {
            locale: 'et',
            formatStyle: 'decimal',
            precision: 2,
            hideNegligibleDecimalDigitsOnFocus: false,
        },
    }

    // support the v-model.lazy modifier
    options[attrs.modelModifiers?.lazy ? 'onChange' : 'onInput'] = value => emit('update:modelValue', value.number)

    const numberInput = new NumberInput(options)

    // set the initial value
    numberInput.setValue(props.modelValue)

    // apply reactive changes to the value from outside, but only if the value is different (to prevent unwanted loops)
    watchEffect(() => {
        if (numberInput.getValue().number !== props.modelValue) {
            numberInput.setValue(props.modelValue)
        }
    })
})
</script>