Search code examples
vue.jseventsevent-handlingvuejs3v-model

Can i refactoring from repeating code this vue custom modifiers in multi v-model?


I made multiple v-model binding in vue 3. but got some probel in code quality when i must code emit value function repeating with 90% the function code is same. Is this possible to refactoring this code

<script lang="ts" setup>
const props = defineProps({
  firstName: String,
  lastName: String,
  firstNameModifiers: {
    default: {
      default: () => ({}),
      capitalize: () => ({}),
    },
  },
  lastNameModifiers: {
    default: {
      default: () => ({}),
      capitalize: () => ({}),
    },
  },
});
const emit = defineEmits(["update:firstName", "update:lastName"]);

// ! TODO: Make Function to handle modifiers - its repeating below
function firstNameEmitValue(e: Event) {
  const target = e.target as HTMLInputElement;
  let value = target.value;
  if (props.firstNameModifiers["capitalize"]) {
    value = value.charAt(0).toUpperCase() + value.slice(1);
  }
  emit(`update:firstName`, value);
}

function lastNameEmitValue(e: Event) {
  const target = e.target as HTMLInputElement;
  let value = target.value;
  if (props.firstNameModifiers["capitalize"]) {
    value = value.charAt(0).toUpperCase() + value.slice(1);
  }
  emit(`update:lastName`, value);
}
</script>

<template>
  <input type="text" :value="firstName" @input="firstNameEmitValue" />
  <input type="text" :value="lastName" @input="lastNameEmitValue" />
</template>

Its ok when just using 1 modifiers. But its will become annoying if i want to add other modifiers. for example toUppercase, toLowerCase etch. Maybe the solution is just separate the component for firstname input and lastname input and make it as single v-model and emit.

But i just want to try this approach bcause vue put it in, in their documentation.

Events Documentation


Solution

  • This should do it:

    helpers.ts

    export const names = ["first", "last"]
    

    your component

    <script lang="ts" setup>
      import { reactive, computed } from "vue"
      import { names } from '../path/to/helpers'
    
      const modifiers = {
        default: {
          default: () => ({}),
          capitalize: () => ({})
        }
      }
    
      const props = defineProps(
        Object.assign(
          {},
          ...names.map((name) => ({
            [name + "Name"]: String,
            [name + "NameModifiers"]: modifiers
          }))
        )
      )
      const emit = defineEmits(names.map((name) => `update:${name}Name`))
      const emitValue = ({ target }: Event, name: string) => {
        if (target instanceof HTMLInputElement) {
          let { value } = target
          if (props[`${name}NameModifiers`]["capitalize"]) {
            value = value.charAt(0).toUpperCase() + value.slice(1)
          }
          emit(`update:${name}Name`, value)
        }
      }
      const state = reactive(
        Object.assign(
          {},
          ...names.map((name) => ({ [name]: computed(() => props[name]) }))
        )
      )
    </script>
    
    <template>
      <input
        type="text"
        v-for="name in names"
        :key="name"
        :value="state[name]"
        @input="emitValue($event, name)"
      />
    </template>
    

    If you add 'middle' to names, it should work out of the box.

    Note: haven't tested it, since you haven't provided a sandbox. If it doesn't work, I'll make one and test it.


    I tend to avoid @input + :value (although it works), in favor of v-model with computed setter. Has the advantage of not having to cast the input type and also allows assigning to model programmatically, should you ever need it. I also find the resulting syntax cleaner, hence more readable:

    <script lang="ts" setup>
      import { reactive, computed } from "vue"
      import { names } from '../path/to/helpers'
    
      const modifiers = {
        default: {
          default: () => ({}),
          capitalize: () => ({})
        }
      }
    
      const props = defineProps(
        Object.assign(
          {},
          ...names.map((name) => ({
            [name + "Name"]: String,
            [name + "NameModifiers"]: modifiers
          }))
        )
      )
      const emit = defineEmits(names.map((name) => `update:${name}Name`))
      const state = reactive(
        Object.assign(
          {},
          ...names.map((name) => ({
            [name]: computed({
              get: () => props[name],
              set: (val) =>
                emit(
                  `update:${name}Name`,
                  props[`${name}NameModifiers`]["capitalize"]
                    ? val.charAt(0).toUpperCase() + val.slice(1)
                    : val
                )
            })
          }))
        )
      )
    </script>
    
    <template>
      <input type="text" v-for="name in names" :key="name" v-model="state[name]" />
    </template>