0

I created a custom Input component.

I needed pass @blur from vee-validate form to my custom input component.

It works great in normal html input tag. I no idea how could we pass the @blur into custom Input component.

Example 1 work correctly, it triggered the validation after blur the input.

 <template>
  <form @submit="onSubmit">
    <input @blur="emailBlur" v-model="email" type="text" autocomplete="off" name="email" placeholder="email">
    <button type="submit" :disabled="isSubmitting">Submit</button>
  </form>
 </template>

Example 2 with My custom Input Component:

// src/components/Input.vue
<template>
  <div class="mt-2">
    <label :for="name" class="h5">Name</label>
    <input
      :type="type"
      :value="modelValue"
      @change="$emit('update:modelValue', $event.target.value)"
      :id="name"
      :placeholder="placeholder"
      />
  </div>
</template>

<script>
export default {
  name: 'Input',
  props: ["modelValue", 'name', 'type', 'placeholder'],
  setup(props) {
    console.log('props :>> ', props); // not receive the @blur 
  }
}
</script>

Parent Component (App.vue):

<template>
  <form>
   <Input @blur="emailBlur" v-model="email" type="text" name="email" placeholder="Custom input email" />
    <button type="submit" :disabled="isSubmitting">Submit</button>
  </form>

</template>

export default {
  name: 'App',
  components: {
    Input
  },
  setup() {
     // the vee validation values v-model to template
  }
}

1

2 Answers 2

2

Sending the handler as props from the parent is not a good practice instead need to trigger the handler(present in the parent) from the child component. By doing so you will have an advantage where you can bind different blur handlers based on your requirement inside different parent components

To do so you can follow the below approach

Custom Input Component

// src/components/Input.vue
<template>
  <div class="mt-2">
    <label :for="name" class="h5">Name</label>
    <input
      :type="type"
      :value="modelValue"
      @change="$emit('update:modelValue', $event.target.value)"
      :id="name"
      :placeholder="placeholder"
      @blur="$emit('blur')" //Change added
      />
  </div>
</template>

<script>
export default {
  name: 'Input',
  props: ["modelValue", 'name', 'type', 'placeholder'],
  emits: ['blur', 'update:modelValue'], // change added
}
</script>

Note:

for all v-models without arguments, make sure to change props and events name to modelValue and update:modelValue respectively

For Example:

Parent.vue

<ChildComponent v-model="pageTitle" />

and in Child.vue it should be like

export default {
  props: {
    modelValue: String // previously was `value: String`
  },
  emits: ['update:modelValue'],
  methods: {
    changePageTitle(title) {
      this.$emit('update:modelValue', title) // previously was `this.$emit('input', title)`
    }
  }
}
6
  • Thanks for solve my issue. And make me understanding how the vue logics.
    – Wilker
    Commented Oct 8, 2021 at 1:43
  • In my case, Vue warn me to add "update:modelValue" into emits array. Could i know why we need it?
    – Wilker
    Commented Oct 8, 2021 at 1:49
  • yes you need to add it, I have added the reason as a note in the answer itself. Pls check
    – Amaarockz
    Commented Oct 8, 2021 at 4:58
  • 1
    Forwarding the blur event up is technically working but it is not the correct way how to design input wrapper as you are forced to add new event handler (and into 'emits') for every possible event and new prop for every possible native input attribute (for example readonly or maxlength). Using inheritAttrs is the way to reduce the code needed and will make your life easier ... Commented Oct 8, 2021 at 6:15
  • @MichalLevý Since he asked for blur and change I have given that example. But. If you ask me I ll go with v-model itself
    – Amaarockz
    Commented Oct 8, 2021 at 6:24
0

What you are creating is usually called "transparent wrapper" component. What you want from this wrapper is to behave in almost every way as normal input component so the users of the component can work with it as it was normal input (but it is not)

In your case, you want to attach @blur event listener to your wrapper. But the problem is that blur is native browser event i.e. not a Vue event.

When you place event listener on a component for an event not specified in emits option (or v-bind an attribute that is not specified in component's props), Vue will treat it as Non-Prop Attribute. This means it take all such event listeners and non-prop attributes and place it on the root node of the component (div in your case)

But luckily there is a way to tell Vue "Hey, don't place those automatically on root, I know where to put it"

  1. Use inheritAttrs: false option on your component
  2. Put all non-prop attributes (including the event listeners) on the element you want - input in this case - using v-bind="$attrs"

Now you can even remove some props - for example placeholder (if you want), because if you use it directly on your component, Vue place it on input, which is what you want...

Also handling @change event is not optimal - you component allows to specify a type and different input types has different events. Nice trick around it is not to pass value and bind event explicitly, but instead use v-model with computed (see example below)

const app = Vue.createApp({
  data() {
    return {
      text: ""
    }
  },
  methods: {
    onBlur() {
      console.log("Blur!")
    }
  }
  
})

app.component('custom-input', {
  inheritAttrs: false,
  props: ["modelValue", 'name', 'type'],
  emits: ['update:modelValue'],
  computed: {
    model: {
      get() { return this.modelValue },
      set(newValue) { this.$emit('update:modelValue', newValue) } 
    }
  },
  template: `
  <div class="mt-2">
    <label :for="name" class="h5">{{ name }}:</label>
    <input
      :type="type"
      v-model="model"
      :id="name"
      v-bind="$attrs"
    />
  </div>
  `
})

app.mount("#app")
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
<div id='app'>
  <custom-input type="text" name="email" v-model="text" placeholder="Type something..." @blur="onBlur"></custom-input>
  <pre>{{ text }}</pre>
</div>

2
  • Cool. Your comment helps me better understanding the under hood what vue did.
    – Wilker
    Commented Oct 8, 2021 at 2:38
  • Thanks for pointing it out. It makes me to know more about vue.
    – Wilker
    Commented Oct 8, 2021 at 7:39

Not the answer you're looking for? Browse other questions tagged or ask your own question.