In a current Vue project, I have a couple different components that are almost just native elements, but with a slight wrapper around them that adds functionality. Examples include a date input that uses a particular date picker and a textarea that auto resizes.
For these components, I want them to behave as closely as possible to if they are native components - allowing me to transparently pass through attributes, add event listeners, and more.
Doing this in Vue is completely possible, but takes a bit of doing. Here are the steps I came to, using the autosizing textarea as an example.
Step 1: Implement v-model using :value and @input
According to the Vue Guide on components, v-model on a component essentially works by passing in a value
prop, and applying and input
event handler.
The first step to making our component then feel like a native element is to set these up with a template that looks like:
<textarea
:value="value"
@input="input"
>
</textarea>
and javascript that looks like
export default {
props: ['value'],
methods: {
input(event) {
this.$emit('input', event.target.value);
},
},
};
Now if someone uses v-model with our component, it will behave exactly as if they had applied v-model to the underlying textarea.
Step 2: Pass through other event listeners
We want to make sure input is working, because that is key to making v-model work, but we also want our component to handle other event listeners that we might apply. For example, we might want to apply a listener on focus in some locations, or something else.
Rather than try to think of all possible listeners, we're going to take advantage of the builtin $listeners
object on our component, which lets us get all listeners applied to the component.
Then we'll pick out the input one (we're handling that already) and apply the rest in bulk, using javascript that looks like:
computed: {
listeners() {
const { input, ...listeners } = this.$listeners;
return listeners;
},
},
and then in the template applying them in bulk:
<textarea
:value="value"
@input="input"
v-on="listeners"
>
</textarea>
Step 3: Pass through attributes
As well as event listeners, we also want to pass through any attributes that we might want to go in. By default, Vue will do that for us - all attributes set on a component are passed through to the root element of the component.
However, in this case we want to special case some of those attributes by setting defaults. To do this, we're going to disable this automatic attribute pass-through by setting inheritAttrs: false
, and then use a similar approach to listeners to reapply them after our defaults.
For an example in our autosizing textarea, we want to default rows to be 3, so we can do something like:
computed() {
rows() {
return this.$attrs.rows || 3;
},
attrs() {
const { rows, ...attrs } = this.$attrs;
return attrs;
},
},
and then in the template:
<textarea
:value="value"
@input="input"
v-on="listeners"
:rows="rows"
v-bind="attrs"
>
</textarea>
Apply custom functionality
Finally to simply add our autosizing capability, using the autosize npm package, we import autosize and add a quick mounted hook:
mounted() {
autosize(this.$el);
},
At the end of this, our full single file component template looks like this:
<textarea
:value="value"
@input="input"
v-on="listeners"
:rows="rows"
v-bind="attrs"
>
</textarea>
And our javascript looks like:
export default {
props: ['value'],
inheritAttrs: false,
computed: {
listeners() {
const { input, ...listeners } = this.$listeners;
return listeners;
},
rows() {
return this.$attrs.rows || 3;
},
attrs() {
const { rows, ...attrs } = this.$attrs;
return attrs;
},
},
methods: {
input(event) {
this.$emit('input', event.target.value);
},
},
mounted() {
autosize(this.$el);
},
};
P.S. — If you’re interested in these types of topics, I send out a weekly newsletter called the ‘Friday Frontend’. Every Friday I send out 15 links to the best articles, tutorials, and announcements in CSS/SCSS, JavaScript, and assorted other awesome Front-end News. Sign up here: https://zendev.com/friday-frontend.html
Top comments (0)