Joachim Bjørge
Joachim Bjørge

Reputation: 123

How to expose wrapped <input> in Vue?

I'm trying to create a reusable styled input field in Vue. To make it styled (e.g. with an icon inside) I need to wrap it in another html-element.

Lets call the example below StyledInput

<div class="hasIcon">
    <input />
    <i class="someIcon"></i>
<div>

If I want to use StyledInput it might look like so:

<styled-input @keyup.enter="doSomething">
</styled-input>

But this would not work, due to the event listener being attached to the <div> instead of the <input>.

A workaround to that could be to emit all key-events from the input field:

<div class="hasIcon">
    <input @keyup="$emit('keyup', $event) />
    <i class="someIcon"></i>
<div>

But this will not scale well since it would have to be rewritten every time a developer uses an unmapped prop or event.

Is there a way to only make the inner element exposed to whomever uses it?

Upvotes: 5

Views: 1309

Answers (3)

agm1984
agm1984

Reputation: 17178

You can also use $attrs to pass props and events onto children elements:

<template>
    <div>
        <input v-bind="$attrs">
    </div>
</template>

In Vue 3, you can specify a second script tag:

<script setup>
</script>

<script>
export default {
    inheritAttrs: false,
};
</script>

https://vuejs.org/guide/components/attrs.html#disabling-attribute-inheritance

Upvotes: 1

Daniel Buckmaster
Daniel Buckmaster

Reputation: 7186

You could use slots to achieve this. If your <styled-input> template looks like this:

<div class="hasIcon">
    <slot><input></slot>
    <i class="someIcon"></i>
<div>

Then you can use it like this:

<styled-input>
    <input @keyup.enter="doTheThing">
</styled-input>

Or, in cases where you don't care about the input events, like this:

<styled-input></styled-input>

and the default slot content (a bare <input>) will be used. You can use CSS to style the <input> inside the component, but you can't add custom properties or classes to it, so this approach may or may not fit your requirements.

Upvotes: 0

craig_h
craig_h

Reputation: 32724

I'm not sure there is a Vue way to achieve this, because, as far as I'm aware there is no way to bind vue events dynamically, it is however possible to do this using vanilla javascript by passing all events as a prop then mapping them using addEventListener() to add your custom events:

Vue.component('my-input', {
  template: "#my-input",
  props: ['events'],
  mounted() {
    // get the input element
    let input = document.getElementById('styled-input');

    // map events
    this.events.forEach((event) => {
      let key = Object.keys(event);
      input.addEventListener(key, event[key]);
    });
  }
})

Then you can just pass through all events as a prop like so:

<my-input :events="events"></my-input>

View Model:

var app = new Vue({
  el: "#app",
  data: {
    events: [{
      focus: () => {
        console.log('focus')
      }
    }, {
      keyup: (e) => {
        console.log(e.which)
      }
    }]
  }
})

Heres the JSFiddle: https://jsfiddle.net/h1dnk40v/

Of course, this means any developer would have to do things like map key codes etc, so you will lose some of the convenience Vue provides.

One thing I will just mention is that Vue components aren't necessarily intended to be infinitely reusable, they are supposed to provide specific functionality and encapsulate complex logic, so you would probably do better to implement the most likely use cases, and if the component doesn't fit you can extend it or write a new one for that particular event.

Upvotes: 4

Related Questions