Would you like to buy me a ☕️ instead?
Note: This is the first part of my “Advanced Vue.js Application Architecture” series on how to structure and test large scale Vue.js applications. Stay tuned, there’s more to come! Follow me on Twitter if you don’t want to miss any new article.
Next >
In today’s article we’ll learn how to build Vue.js Single File Components (SFC) which export multiple components at once using ES6 named exports. Furthermore we’ll utilize render functions to render the markup of our components via JSX. By combining these techniques, we are able to create UI components that consist of several separate components combined into a single file.
What we ultimately try to accomplish is the clean separation of our UI components (the styling) from components which contain logic or fetch data from an API.
Export multiple components from a Single File Component
Usually, a Vue.js SFC only exports a single component. Although the SFC specification requires that we have at least one
default
export, we’re not limited to
only
having a
default
export.
We can add as many additional named exports as we like.
<template>
<div class="grid">
<slot/>
</div>
</template>
<script>
export default {
name: 'UiGrid',
// ...
</script>
<style lang="scss">
/* ... */
</style>
In the example code snippet above you can see a regular Vue.js SFC component with a
default
export, exporting a single component.
<script>
export const UiGrid = {
// ...
render() {
return (
<div class="grid">
{this.$slots.default}
export const UiGridItem = {
// ...
render() {
return (
<div class="grid__item">
{this.$slots.default}
export default UiGrid;
</script>
<style lang="scss">
/* ... */
</style>
In this example you can see how we can modify our simple example component to not only export one but two components. Because we can’t have two
<template>
sections in one SFC file,
we‘ve changed the code to use a render function and JSX to render the markup of our components.
<template>
<UiGrid class="MyComponent">
<UiGridItem>
<!-- ... -->
</UiGridItem>
<UiGridItem>
<!-- ... -->
</UiGridItem>
</UiGrid>
</template>
<script>
// Look ma, I'm importing two
// components from a single file!
import {
UiGrid,
UiGridItem,
} from '../ui/UiGrid.vue'
export default {
name: 'MyComponent',
// ...
</script>
Here you can see how we can use the
UiGrid
component inside of another component to build a simple grid layout without having to deal with global CSS or duplicating the CSS styles for our grid layout in every component where we need it.
This concept is similar to how things are done in many React projects with styled components . By using JSX and named exports to export multiple components from one SFC we can keep all of our grid related styles in one single file instead of having to create multiple files each containing different parts of our grid layout implementation.
Keep in mind though, that it is not possible to use scoped styles or CSS Modules for multi export components (those will only work for the default export). You have to come up with your own approach of preventing your styles from leaking into the global scope. I recommend you to use namespacing in combination with the BEM syntax.
Building a Grid Component
Let’s take a closer look how we can build a real world Grid Component using the techniques outlined above.
<script>
import classnames from 'classnames';
export const UiGrid = {
props: {
columnGap: {
default: 'm',
type: String,
rowGap: {
default: 'm',
type: String,
tag: {
default: 'div',
type: String,
render() {
const Tag = this.tag;
return (
class={classnames(
'grid',
`grid--column-gap-${this.columnGap}`,
`grid--row-gap-${this.rowGap}`,
{this.$slots.default}
export const UiGridItem = {
// ...
export default UiGrid;
</script>
<style lang="scss">
/* ... */
</style>
The
UiGrid
wrapper component you can see above, has properties to control the gap between the columns and rows of its child grid items and we even make it possible to change the HTML tag of the component by providing a
tag
property. We use the
classnames
package to make it a little easier to provide multiple classes to our HTML elements.
<script>
import classnames from 'classnames';
export const UiGrid = {
// ...
export const UiGridItem = {
props: {
tag: {
default: 'div',
type: String,
width: {
default: () => [],
type: Array,
render() {
const Tag = this.tag;
return (
class={classnames(
'grid__item',
this.width.map(x => `grid__item--width-${x}`),
{this.$slots.default}
export default UiGrid;
</script>
<style lang="scss">
/* ... */
</style>
UiGridItem
elements take a
width
(an array of widths to be more precise) and also a
tag
property. We map over the given widths to create modifier classes for them. In the following code snippet you can see the CSS styles for our basic grid component.
$breakpoint-m: 32em;
$gap-m: 1em;
$gap-l: 2em;
.grid {
display: flex;
flex-wrap: wrap;
&--column-gap-m {
margin-left: -$gap-m;
&--column-gap-l {
margin-left: -$gap-l;
&--row-gap-m {
margin-top: -$gap-m;
&--row-gap-l {
margin-top: -$gap-l;
.grid__item {
box-sizing: border-box;
&--width-12\/12 {
width: 100%;
@media (min-width: $breakpoint-m) {
&--width-4\/12\@m {
width: 33.3333333%;
&--width-8\/12\@m {
width: 66.6666666%;
.grid--column-gap-m > & {
padding-left: $gap-m;
.grid--column-gap-l > & {
padding-left: $gap-l;
.grid--row-gap-m > & {
padding-top: $gap-m;
.grid--row-gap-l > & {
padding-top: $gap-l;
}
Building a Media Object Component
Next we also want to take a look at how we can utilize multi export Single File Components to build a the famous Media Object .
<script>
import classnames from 'classnames';
export const UiMedia = {
props: {
gap: {
default: 'm',
type: String,
tag: {
default: 'div',
type: String,
render() {
const Tag = this.tag;
return (
<Tag class={classnames(
'media',
`media--gap-${this.gap}`),
{this.$slots.default}
export const UiMediaFigure = {
// ...
export const UiMediaBody = {
// ...
export default UiMedia;
</script>
<style lang="scss">
/* ... */
</style>
As you can see in the example above we can use the same patterns we’ve used before to build our Media Object UI Component. The
UiMediaFigure
and
UiMediaBody
components in the following example snippet also follow the same principles.
<script>
import classnames from 'classnames';
export const UiMedia = {
// ...
export const UiMediaFigure = {
props: {
align: {
default: 'start',
type: String,
tag: {
default: 'div',
type: String,
render() {
const Tag = this.tag;
return (
class={classnames(
'media__figure',
`media__figure--align-${this.align}`,
{this.$slots.default}
export const UiMediaBody = {
props: {
align: {
default: 'start',
type: String,
tag: {
default: 'div',
type: String,
render() {
const Tag = this.tag;
return (
class={classnames(
'media__body',
`media__body--align-${this.align}`,
{this.$slots.default}
export default UiMedia;
</script>
<style lang="scss">
/* ... */
</style>
Last but not least you can take a look at the CSS styles in the next code block.
$gap-m: 1em;
$gap-l: 2em;
.media {
display: flex;
.media__figure {
&--align-center {
align-self: center;
&--align-end {
align-self: flex-end;
&:first-child {
.media--gap-m > & {
margin-right: $gap-m;
.media--gap-l > & {
margin-right: $gap-l;
&:last-child {
.media--gap-m > & {
margin-left: $gap-m;
.media--gap-l > & {
margin-left: $gap-l;
.media__body {
&--align-center {
align-self: center;
&--align-end {
align-self: flex-end;
}
Named slots
After seeing this implementation of the Media Object, you may wonder if this might not be an ideal use case for named slots.
<template>
<div class="media">
<div class="media__figure">
<slot name="figure"/>
</div>
<div class="media__body">
<slot/>
</div>
</div>
</template>
<template>
<div class="MyComponent">
<UiMedia>
slot="figure"
src="..."
alt="A nice image."