Dejan.S
Dejan.S

Reputation: 19158

Bind events to custom component

Using Vue3

I'm creating a custom component and would like to bind the events to the input inside it. So in this case I would like to do something similar as I do with the v-bind="inputProps". How is it done now $listeners are being removed? As you probably can see I'm not in a position to use v-bind="$attrs" as the properties are divided to where they "go".

I'm here to solve a problem but any hints on improvement is always a welcome bonus.

// c-text.vue

<template>
  <input
    v-if="useAsSingleElement"
    v-bind="inputProps"
    :class="['c-text', 'c-text--is-single-element']"
    @input="$emit('input', $event)"
    @blur="$emit('blur')"
    @focus="$emit('focus')">

  <c-form-item-shell
    v-else
    :id="id"
    :is-required="isRequired"
    :label-text="labelText"
    :icon="icon"
    :icon-class="iconClass"
    :errors="errors"
    :information="information">
    <template #element>
      <input
        v-bind="inputProps"
        class="c-text"
        @input="$emit('input', $event.target.value)"
        @blur="$emit('blur')"
        @focus="$emit('focus')">
    </template>
  </c-form-item-shell>
</template>

<script>
import { computed } from 'vue'
import CFormItemShell from '@/components/c-form-item-shell.vue'

export default {
  components: {
    CFormItemShell
  },
  props: {
    useAsSingleElement: { type: Boolean, required: false, default: false },
    name: { type: String, required: true },
    type: { type: String, required: false, default: 'text' },
    placeholder: { type: String, required: false, default: null },
    maxlength: { type: [String, Number], required: false, default: null },
    minxlength: { type: [String, Number], required: false, default: null },
    pattern: { type: String, required: false, default: null },
    readonly: { type: Boolean, required: false, default: false },
    spellcheck: { type: Boolean, required: false, default: false },
    autocomplete: { type: Boolean, required: false, default: true },
    id: { type: String, required: true },
    isRequired: { type: Boolean, required: false, default: false },
    labelText: { type: String, required: false, default: null },
    icon: { type: String, required: false, default: null },
    iconClass: { type: String, required: false, default: null },
    errors: { type: [String, Array], required: false, default: null },
    information: { type: [String, Array], required: false, default: null }
  },
  emits: ['input', 'blur', 'focus'],
  setup (props) {
    const inputProps = computed(() => {
      return {
        id: props.id,
        name: props.name ? props.name : props.id,
        type: props.type,
        placeholder: props.placeholder,
        maxlength: props.maxlength,
        minlength: props.minlength,
        pattern: props.pattern,
        readonly: props.readonly,
        spellcheck: props.spellcheck,
        autocomplete: props.autocomplete ? 'on' : 'off'
      }
    })

    return {
      inputProps
    }
  }
}
</script>

Upvotes: 1

Views: 1352

Answers (2)

Dejan.S
Dejan.S

Reputation: 19158

To answer my own question and more of a detailed answer in the hopes that it can help anybody in the future. Thanks to Boussadjra Brahim who's answer got me thinking. I did the following

  1. added inhertsAttrs: false
    Link to docs

This makes it so the events (meaning your @input, @click ect) on the component don't get applied to the parent node

  1. added emits: ['update:modelValue', 'blur', 'focus']
    Link to docs

This should be all of the emits that you want to capture and emit back

  1. "Collect" the emits manually to my propsAndListeners object like this. Then bind it to the input like <input v-bind="propsAndListeners" />

Keep in mind that you have to prefix your events with on so if you have a @click event that will have the name onClick in the $attrs (@ short is v-on)

const propsAndListeners = computed(() => {
  return {
    onInput: (event) => emit('update:modelValue', event.target.value)
  }
})

In most cases you probably would bind the $attrs to a element but the reason for the propsAndListeners is that I have if condition and binding the same things for an input in two places.



Here is the full script part

export default {
  components: {
    CFormItemShell
  },
  props: {
    useAsSingleElement: { type: Boolean, required: false, default: false },
    name: { type: String, required: true },
    type: { type: String, required: false, default: 'text' },
    modelValue: { type: [String, Number, Date], required: false, default: null },
    placeholder: { type: String, required: false, default: null },
    maxlength: { type: [String, Number], required: false, default: null },
    minxlength: { type: [String, Number], required: false, default: null },
    pattern: { type: String, required: false, default: null },
    readonly: { type: Boolean, required: false, default: false },
    spellcheck: { type: Boolean, required: false, default: false },
    autocomplete: { type: Boolean, required: false, default: true },
    id: { type: String, required: true },
    isRequired: { type: Boolean, required: false, default: false },
    labelText: { type: String, required: false, default: null },
    icon: { type: String, required: false, default: null },
    iconClass: { type: String, required: false, default: null },
    errors: { type: [String, Array], required: false, default: null },
    information: { type: [String, Array], required: false, default: null }
  },
  emits: ['update:modelValue', 'blur', 'focus'],
  setup (props, { emit }) {
    const propsAndListeners = computed(() => {
      return {
        id: props.id,
        name: props.name ? props.name : props.id,
        type: props.type,
        placeholder: props.placeholder,
        value: props.modelValue,
        maxlength: props.maxlength,
        minlength: props.minlength,
        pattern: props.pattern,
        readonly: props.readonly,
        spellcheck: props.spellcheck,
        autocomplete: props.autocomplete ? 'on' : 'off',
        onInput: (event) => emit('update:modelValue', event.target.value),
        onBlur: (event) => emit('blur', event),
        onFocus: (event) => emit('focus', event)
      }
    })

    return {
      propsAndListeners
    }
  }
}

Upvotes: 0

Boussadjra Brahim
Boussadjra Brahim

Reputation: 1

Based on the migration guide :

In Vue 3's virtual DOM, event listeners are now just attributes, prefixed with on, and as such are part of the $attrs object, so $listeners has been removed.

so just pass your events like onClick, onBlur ... and

export default {
  inheritAttrs: false
}

Upvotes: 1

Related Questions