Extending Vue Components for Cleaner Code

Jo
Vue js

Background

One thing that makes developers happy is reusable code. It’s so satisfying to be able to build something out once, in an intelligent and organized way, and use it throughout your app. In Ruby, this concept is often referred to as “DRY” (don’t repeat yourself). Javascript, however, has a nasty reputation for being the opposite of this. This is most likely an unfair holdover from earlier days of spaghetti code and the wild west of the early web, but alas, that is its reputation.

Extending Components

In Vue, there are a number of techniques you can use to combat sprawling, unreadable code and create clean, reusable components. The pattern we will highlight today is the extends option (part of the Vue options API) that allows us to compose components and share functionality throughout different parts of your UI in a manageable, concise pattern.

This pattern works best for features that have similar core logic (often with complicated functions and data manipulation) with distinct differences in implementation. This could include vastly different markup or templates, as long as they share functionality. There is a caveat with markup (aka the template part of a vue single file component), however. The component that extends a base component can either inherit the entire template from the base or create its own. There is no way to append or prepend markup to the base template.

Let’s look at an example. For our example, the base will contain only the functions and data (the script part of a vue single file component) that will be shared throughout the extended components. The markup and UI will be handled separately in the different extended components.

Example

One use case we’ve run across here at Littlelines for the extended components pattern is search. Many of our clients request search functionality in their applications, often with multiple different types of records and in multiple places throughout the site, with unique markup and functionality requirements.

In our example scenario here, we will imagine that a client would like search on multiple different pages, and each will filter various attributes and fetch different kinds of records. We’ll assume that the search is being done on the back end, with elasticsearch or some other service, and being fed to the front end with several API endpoints.

We’ll start with a base component. This will contain only the script part of the component, as each of the components that extend it will want to customize the markup for each use case:

<script>
export default {
  name: 'base-search',
  data () {
    return {
      searchText: ''
    }
  },

  methods: {
    /* Add functions here that will be available in all components that inherit from this one */

    /* Here we can add complicated functions, that do things like parse a url for search params*/
    parseURLFilters () {
      const parsedFilters = Object.keys(this.filters).map(filter => {
        return { [filter]: `filters[${filter}]` }
      })
      const url = new URLSearchParams(window.location.search)

      parsedFilters.map(filterObj => {
        let dataProp = Object.keys(filterObj)[0]
        let filterName = Object.values(filterObj)[0]
         return this[dataProp] = url.get(filterName)
      })
    },

    submitSearch (searchUrl, filters = {}) {
      this.$http.get(searchUrl, {
        params: { query: this.searchText, filters }
      }).then(response => {
        /* emit event to handle results in extended components */
        this.$emit('setSearchResults', {searchResults: response.body})
      }, error => {
        /* handle errors */
      })
    }
  }
}
</script>

This base will contain the functionality that we want to share between all of the extended components. This will allow us to keep code duplication down and keep our code more DRY.

Here’s an example of how we can extend this component and make use of it in the UI.

<template>
<form id="authors_search" @submit="submit">
  <div class="input no-label pv3 bb1">
    <label for="search">Search</label>
    <input type="text" v-model="searchText" placeholder="Search">
  </div>

  <div class="bb1 mb3">
    <select v-model="selectedAuthorId">
      <option v-for = "author in authors" :value="author.id" >{{author.name}}</option>
    </select>
  </div>

  <button type="submit" class="btn5">Submit</button>
</form>
</template>

<script>
import SearchComponent from '../search'
import AuthorsService from '../../services/authors'
const booksUrl = '/api/v1/books/search'

export default {
  name: 'book-search',
  extends: SearchComponent,

  data () {
    return {
      authors: [],
      selectedAuthorId: null
    }
  },

  mounted () {
    this.getAuthors()
  },

  created() {
    /* calls parseURLFilters function from base component. */
    this.parseURLFilters()
    this.submit()
  },

  methods: {
    getAuthors () {
      /* call API to grab authors in db and fill in select box for filter. */
    },

    /* Submit artists search to api. Calls submitSearch function from base component with correct data and endpoint */
    submit () {
      this.submitSearch(booksUrl, { author_id: this.selectedAuthorId })
    }
  }
}
</script>

In this extended component, we can access all the functions, data properties, etc., in the base component and use these to fit the use case of each particular implementation. If a hook is defined in both the base and the extended component, it will run in both, with the extended component running first and the base component running directly afterward. That means if we call the created() hook in both, they will both run, with the extended component taking precedence.

Conclusion

This type of component composition is useful in a number of scenarios and is a tool we have in our VueJS toolbox to help keep code clean, DRY, and manageable. I hope this quick example and explanation of an often-overlooked part of the Vue options API has been helpful and you will experiment with it in your own projects soon. Thanks for reading.

Have a project we can help with?
Let's Talk

Get Started Today