Would you like to buy me a ☕️ instead?
Recently I’ve seen a great talk by Jacob Schatz about Phenomenal Design Patterns in Vue. One of the patterns he mentioned in his talk was the Builder Pattern. I found his example very interesting, so it was clear to me that I had to experiment with this pattern myself.
In this article, we take a look at a possible implementation of the Builder Pattern that is custom tailored to how I typically structure my Vue.js applications. In the first part of this two-part series, we use this pattern to create highly reusable listing views (think of an app with a lot of listing views for a multitude of different content types) and in the second part, we will explore a possible solution for creating many generic forms using this approach.
The Builder Pattern with Vue.js components
The builder pattern is a creational pattern in Object Oriented Programming. “Creational” means it is typically used for simplifying the process of creating new objects. But in Vue.js applications it’s all about components. So in our case we wan’t the builder class to create a component for us (in fact a component is actually nothing else than an object).
Creating listing views with a builder class
Before we take a look at the code let’s start with defining the problem we want to solve. Think of the following scenario: we have to build an application which is used to manage a lot of different content types (e.g. users, products, messages, articles, tags,…). For everyone of those content types we need a listing view. Some of the content types are better suited for table views and others should be displayed in a grid view. Again others need a filter system and most also need a pagination navigation.
In the following code snippet you can see a possible solution for a ListingBuilder
class, which can be used to build all these different variants of list views for us.
// src/builders/ListingBuilder.js
import EntityListing from '../components/EntityListing.vue';
export default class ListingBuilder {
constructor() {
this.props = {};
}
withProvider(provider) {
this.provider = provider;
return this;
}
withListingItem(item) {
this.item = item;
return this;
}
showFilter() {
this.props.showFilter = true;
return this;
}
showPagination() {
this.props.showPagination = true;
return this;
}
view(view) {
this.props.view = view;
return this;
}
build() {
const Provider = this.provider;
const Item = this.item;
const props = this.props;
return {
render(h) {
return h(Provider, [
h(
EntityListing,
{
props,
scopedSlots: { default: props => h(Item, { props }) },
},
[Item],
),
]);
},
};
},
};
As you can see above, our ListingBuilder
has multiple methods which we can use to control which kind of listing view is going to be built for us as soon as we call the build()
method.
<template>
<div id="app">
<h2>Product Listing</h2>
<ProductListing/>
<h2>User Listing</h2>
<UserListing/>
</div>
</template>
<script>
// src/App.vue
import ListingBuilder from './builders/ListingBuilder';
import ProductListingItem from './components/ProductListingItem.vue';
import ProductProvider from './components/ProductProvider.vue';
import UserListingItem from './components/UserListingItem.vue';
import UserProvider from './components/UserProvider.vue';
export default {
name: 'App',
components: {
ProductListing: new ListingBuilder()
.withProvider(ProductProvider)
.withListingItem(ProductListingItem)
.showFilter()
.showPagination()
.view('grid')
.build(),
UserListing: new ListingBuilder()
.withProvider(UserProvider)
.withListingItem(UserListingItem)
.showPagination()
.view('table')
.build(),
},
};
</script>
In this example you can see how we can use the ListingBuilder
class to quickly create two different listing components. The ProductListing
component has its own ProductProvider
and ProductListingItem
implementations, it has filters and a pagination. Furthermore it uses the grid
view of the generic EntityListing
component. The UserListing
component, on the other hand, uses a slightly different configuration.
In the following code snippet you can see how we could achieve the same result by using a separate ProductListing
component instead.
<template>
<ProductProvider>
<EntityListing
view="grid"
show-filter
show-pagination
v-slot="{ entity }"
>
<ProductListingItem :entity="entity"/>
</EntityListing>
</ProductProvider>
</template>
<script>
// src/components/ProductListing.vue
import EntityListing from './EntityListing.vue';
import ProductListingItem from './ProductListingItem.vue';
import ProductProvider from './ProductProvider.vue';
export default {
name: 'ProductListing',
components: {
EntityListing,
ProductListingItem,
ProductProvider,
},
};
</script>
The result of the code you can see above is exactly the same as what we get when using our ListingBuilder
class for creating the ProductListing
component. If you are interested in seeing the rest of the code, you can take a look at the following CodeSandbox.
Do you want to learn more about advanced Vue.js techniques?
Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.
Enhance reusability with the Director Pattern
Although we have seen that we can save ourselves a lot of work initially by letting the ListingBuilder
class do the heavy lifting, this pattern could lead to more work in the end if we have to reuse the component ProductListing
in several places. With the separate component, we can simply import and reuse the component, but when we use the ListingBuilder
, we have to repeat the initialization logic over and over again wherever we need the component.
But there is a solution to this problem: the Director Pattern.
// src/builders/ListingDirector.js
import ProductListingItem from '../components/ProductListingItem.vue';
import ProductProvider from '../components/ProductProvider.vue';
import UserListingItem from '../components/UserListingItem.vue';
import UserProvider from '../components/UserProvider.vue';
export default class ListingDirector {
constructor(builder) {
this.builder = builder;
}
makeProductListing() {
return this.builder
.withProvider(ProductProvider)
.withListingItem(ProductListingItem)
.showFilter()
.showPagination()
.view('grid')
.build();
}
makeUserListing() {
return this.builder
.withProvider(UserProvider)
.withListingItem(UserListingItem)
.showPagination()
.view('table')
.build();
}
}
<script>
// src/App.vue
import ListingBuilder from './builders/ListingBuilder';
import ListingDirector from './builders/ListingDirector';
export default {
name: 'App',
components: {
ProductListing: new ListingDirector(
new ListingBuilder()
).makeProductListing(),
UserListing: new ListingDirector(
new ListingBuilder()
).makeUserListing(),
},
};
</script>
Above you can see how we can use the ListingDirector
for reusing existing configurations of listing components.
Wrapping it up
Although I can definitely see the strengths of this pattern, it feels like a foreign body in a typical Vue.js application. I don’t think it’s a pattern that you will use very often in every app you build, but rather a pattern that you only reach for in very specific situations.
But this was only the first part of this two-part series. You can read the second part of this article series to learn more about how to use the Builder Pattern to generate highly reusable forms.