Sharing logic between Vue components with mixins

It’s not uncommon to have two flows that are quite similar in terms of business logic but different in terms of UI. In Geoblink that happens in several places one of them our filter picker.

TM

Legend

There are two very different places where we use the filter picker:

  • In the map legend, to allow filtering POIs displayed in the map.
  • In the header of some modules like Territory Manager.

As you can check, when we display the filter picker in the legend, it displays a set of active filters inside the popup. However, they are displayed next to the popup toggle button when we use the filter picker in a larger container.

Managing filters is not a trivial flow as we support a complex set of actions and filters but even in a simple playground like the one below, the amount of logic common to all filter pickers is noticeable.

https://codepen.io/luzlurrun-geoblink/pen/gjKpXe

If we focus on compact-filters and regular-filters we can notice a lot of repeated stuff: the props inherited are the same and the computed properties are equal, too. Even some methods are repeated.

A possible way to prevent repeating ourselves could be refactoring those components into a single one but the template would get too complicated with extra logic to handle two completely different scenarios. It’s a clear no-no.

Fortunately there’s a better solution: mixins. Mixins allow us to inherit props, computed properties, partial data objects or methods, allowing us to get additional behaviours without repeating code.

In this specific case we could define a new object with all the common stuff of our two filter components:

const commonStuff = {
props: {
allFilters: {
type: Array,
required: true
},
activeFilters: {
type: Array,
required: true
}
},
computed: {
isFilterActive() {
const isFilterActive = {}
for (const filter of this.activeFilters) {
isFilterActive[filter] = true
}
return isFilterActive
},

availableFilters() {
return this.allFilters.filter(
filter => !this.isFilterActive[filter]
)
}
},
methods: {
addFilter(filter) {
this.$emit('add', filter)
},

removeFilter(filter) {
this.$emit('remove', filter)
}
}
}

Then we can add that common stuff to our components passing them in the `mixins` array when declaring the component:

Vue.component('compact-filters', { 
mixins: [commonStuff], // ...
})

Note that Vue is smart enough to properly merge complex objects like methods: in our compact-filters component we still have access to the two methods that are specific to that component and not part of the mixin. Same thing applies to data, computed or any other property of the mixin.

You can check the resulting playground after the refactor below:

https://codepen.io/luzlurrun-geoblink/pen/mjKJqp

Now it’s time to embrace composition over inheritance (or repetition!).

Lluís Ulzurrun de Asanza i Sàez — Senior Software Engineer