6

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?

3 Answers 3

4

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.

Sign up to request clarification or add additional context in comments.

1 Comment

I guess it makes sense to force components to only expose what it has documented/mapped, even though it forces the dev to write consistent component API's. You both offered a solution and wrote a good answer as well. Thanks!
2

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

Comments

0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.