Implementing the Builder Pattern in Vue.js Part 1: Listings

  You block advertising 😢
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.

References


Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.



Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

☕️ Support Me on Ko-fi