Creating a reusable tabbed card component in Vue
I built a thing for Buttondown that lets you embed an iFrame to handle subscriptions really nicely:
Changelog entry coming shortly, but pretty pumped about this — embeddable subscription widgets! pic.twitter.com/uf2UjQILZt
— Buttondown (@buttondownemail) August 6, 2017
This thing necessitated a change in the UI: The current share page was pretty gross (below), and this was going to make it grosser:
I am admittedly not a great designer, and whenever I need to change UI I go spelunking in Bootstrap to see if I can find a paradigm that I like.
And I found something great! Bootstrap cards with tabbed navigation ended up looking really great:
I don’t actually include Bootstrap in Buttondown, though. A lot of its design cues are taken from it (and some things, like modals, are almost identically styled), but I didn’t want to deal with the additional CSS + JS overhead, especially because the jQuery-esque way of handling interactivity seems anti-Vue.
This seemed like a job for a Vue components: I really wanted to be able to just have a tabbed-card
component that I could then add various dynamic tabs with transclusion.
Turns out, you can! The code isn’t even that complicated: the trick is using dynamic slot
names to handle the work for you. You still have to pass in a list of tab names as a prop, but I think that’s totally reasonable.
Here’s what the TabbedCard component looks like:
<template>
<div class="card">
<div class="card-header">
<ul class="card-nav nav-tabs card-header-tabs">
<li class="nav-item" v-for="tab in tabs">
<a
:class="activeTab === tab ? 'nav-link active' : 'nav-link'"
href="#"
@click="(e) => activeTab = tab"
>
</a>
</li>
</ul>
</div>
<div class="card-block">
<slot :name="tab" v-for="tab in tabs" v-if="tab === activeTab" />
</div>
</div>
</template>
<script>
export default {
props: {
tabs: Array,
},
data() {
return {
activeTab: this.tabs[0],
};
},
};
</script>
And here’s how I invoke it, in the Share screen:
<tabbed-card :tabs="['as a link', 'as a form', 'as an iFrame']">
<div slot="as a link">
<!-- Stuff. -->
</div>
<div slot="as a form">
<!-- Stuff. -->
</div>
<div slot="as an iFrame">
<!-- Stuff.-->
</div>
</tabbed-card>
This ends up being simple, and just leverages props and Vue’s transclusion functionality: we provide a tabs
array, which creates both the actual tabs and a bunch of slots for content to go into. This is nice separation of concerns: the screen declaring the tabbed card doesn’t know about how the tabbed card is formed, it just supplies a list of tabs and content for each tab.
And here’s the finished product:
This is nothing groundbreaking, to be sure, but it’s really elegant and re-usable. It makes me happy when Vue makes it easy to create clean, maintainable components — it makes me feel like I’m doing the right thing, which can be an elusive feeling in software.