kodadot/nft-gallery

View on GitHub
components/shared/BalanceInput.vue

Summary

Maintainability
Test Coverage
<template>
  <div class="arguments-wrapper">
    <NeoField
      :label="$t(label)"
      class="balance"
      :variant="checkZeroFailed ? 'danger' : ''"
    >
      <div class="field-body">
        <div class="field has-addons">
          <NeoInput
            ref="balance"
            v-model="inputValue"
            :required="required"
            type="number"
            :step="step"
            :min="minWithUnit"
            :max="maxWithUnit"
            :expanded="expanded"
            data-testid="balance-input"
            @input="handleInput"
          />
          <p class="control balance">
            <NeoSelect
              :value="selectedUnit"
              :disabled="!calculate"
              data-testid="balance-input-select"
              @input="handleUnitChange"
            >
              <option
                v-for="u in units"
                :key="u.value"
                :value="u.value"
              >
                {{ u.name }}
              </option>
            </NeoSelect>
          </p>
        </div>
      </div>
      <p
        v-if="checkZeroFailed"
        class="help is-danger"
      >
        {{ $t('tooltip.needToSetValidPrice') }}
      </p>
    </NeoField>
  </div>
</template>

<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core'
import { NeoField, NeoInput, NeoSelect } from '@kodadot1/brick'
import { units as defaultUnits } from '@/params/constants'
import type { Unit } from '@/params/types'

const props = defineProps({
  value: {
    type: Number,
    default: 0,
  },
  label: {
    type: String,
    default: 'amount',
  },
  calculate: {
    type: Boolean,
    default: true,
  },
  expanded: Boolean,
  step: {
    type: Number,
    default: 0.001,
  },
  min: {
    type: Number,
    default: 0,
  },
  max: {
    type: Number,
    default: Number.MAX_SAFE_INTEGER,
  },
  required: {
    type: Boolean,
    default: false,
  },
  hasToLargerThanZero: {
    type: Boolean,
    default: false,
  },
})

const emits = defineEmits(['input'])
const selectedUnit = ref(1)
const internalValue = ref(props.value || 0)
const checkZeroFailed = ref(false)
const { decimals, unit: chainUnit } = useChain()

const mapper = (unit: Unit) => {
  if (unit.name === '-') {
    return { ...unit, name: chainUnit.value }
  }
  return unit
}

const units = ref<Unit[]>(defaultUnits.map(mapper))

const minWithUnit = computed(() => props.min / selectedUnit.value)
const maxWithUnit = computed(() => props.max / selectedUnit.value)

watch(
  () => props.value,
  (newValue) => {
    internalValue.value = newValue
  },
)

const balance = ref(null)
const inputValue = computed({
  get: () => internalValue.value,
  set: (value) => {
    handleInput(value)
  },
})

const formatSelectedValue = (value: number): string => {
  return value ? String(value * 10 ** decimals.value * selectedUnit.value) : '0'
}

const handleInput = useDebounceFn((value: number) => {
  internalValue.value = value
  const valueInBaseUnit = internalValue.value * selectedUnit.value
  const formattedValue = props.calculate
    ? formatSelectedValue(valueInBaseUnit)
    : valueInBaseUnit

  emits('input', formattedValue)
  return formattedValue
}, 200)

const handleUnitChange = (unit: number) => {
  const valueInBaseUnit = internalValue.value * selectedUnit.value
  internalValue.value = valueInBaseUnit ? valueInBaseUnit / unit : 0
  selectedUnit.value = unit
  balance.value?.focus()
}

const checkValidity = () => {
  const valueEqualZero = inputValue.value.toString() === '0'
  checkZeroFailed.value = props.hasToLargerThanZero && valueEqualZero
  const balanceInputValid = balance.value?.checkHtml5Validity() ?? false
  return balanceInputValid && !checkZeroFailed.value
}

defineExpose({
  checkValidity,
})
</script>