Telerik blogs
VueT2 Light_1200x303

Let's look at component composition. I'm going to attempt to unravel the crazy that goes on in my head when designing components in a step-by-step article where we will build a Search Bar component together. 

A good component is like an 🥑, it seems like it's hit or miss and most of the time you are going to get angry, sad, or a mixture of both at it.

But fear not! I'm going to attempt to unravel the crazy that goes on in my head when designing components in a step-by-step article where we will build a Search Bar component together. Keep in mind that I will be assuming you have a fair amount of knowledge of the framework to be able to follow this article.

Getting into the Mindset

Component composition is more often than not a process of trial and error to find the sweet spot on where to make, or break, a piece of code into a house of reusable goodness.

Bear with me and picture your favorite video game controller — for me it was the N64 tri-fork of blister making. Ready? Imagine this controller represents a website, some code, a collection of inputs and outputs.

Now, I'm going to ask you to think about it in terms of pieces and parts. How would you tear it apart? I can picture a container, the actual plastic holding the buttons, and the buttons themselves.

The controller itself is not that interesting, but let's take a look at the buttons only. What kinds does your controller have? Do some of them share similar properties? What about similar functionality?

I could describe the buttons on the N64 controller as being part of two groups — the round ones like A, B and the yellow buttons, and the raised ones like the shoulder buttons and the Z trigger.

Both these groups share a common pattern: they are both buttons and they both emit a button press when I press them that the N64 can interpret. They all share the property of having a color, which varies on each instance of a button.

I don't really care at this point how they work internally. There are some workings there for the button to bounce back after it's pushed, for example. But for creating better, more reusable components, I want you to focus on how they communicate with the outside world, with other components, via properties and events.

When you start working on a component, if you focus on these two aspects (also known as the public API of the component), you can almost guarantee that this component is going to be highly reusable because it is neatly packed inside a black box. No one other than itself needs to know how it works.

Now that being said, let's unleash the madness. Are you ready? ARE YOU? 🦄!!!!!!111!

Creating the Base Components

One of the things I like doing whenever I start working on a new set of components is to figure out what their most basic form is, and how I can capture that into a component. When thinking about a search bar in a website, I can think about two main pieces — the input and a search button.

Let's start by creating a BaseButton component. It will be super simple, as a good base component should be, but it'll lay out the way for more specific components in the future.

    <template>
      <button
        v-on="$listeners"
        class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
        <slot />
      </button>
    </template>

The BaseButton component exposes a single default slot and uses v-on="$listeners" to make sure that any event listeners added to the instance are set on the button element. I've gone ahead and added some Tailwind classes as well to make it look nice; we will come back to these later on.

Let's stop a second to talk about slots. In their simplest form, slots are a very powerful tool. They allow us to set aside a part of a component that will be defined by whoever implements it — you can think about it as a type of placeholder for your code.

In this particular example with the button, what will happen is that whatever is set in the inner part of the element, will be rendered inside the placeholder. Consider the following example.

    <BaseButton>Praise the Magical 🥑 </BaseButton>
    <BaseButton>Search</BaseButton>
    <BaseButton><i class="text-lg">🔥</i></BaseButton>

All three cases above are completely valid code. The <slot/> inside BaseButton will take care of rendering whatever we place in between the <BaseButton> tags into this placeholder. This is a very powerful tool that allows us to make components super flexible, and it's a must-have for every Vue developer's toolkit.

Similar to the BaseButton, we are going to build a BaseInput component that will be the simplest form of an input we can muster for this application.

    <template>
      <input
        @input="$emit('input', $event.target.value)"
        @change="$emit('change', $event.target.value)"
        class="bg-white focus:outline-none focus:shadow-outline border border-gray-300 rounded-lg py-2 px-4 block w-full appearance-none leading-normal"
      />
    </template>

Now, you could argue, based on what we stated earlier, that these components are too specific, that they are defining a very strict set of classes and colors and that they could be made even more basic by removing the color classes. If you noticed this, congratulations — you are starting to the get in the right mindset.

How flexible or not a component is in the context of your application highly depends on your application’s needs. As the sole and single responsible developer for this example, I know that we won't be using a lot of different types of buttons, so I can overlook this and simply use the component as-is. But if we wanted to make the button more dynamic for example's sake, we could easily create a color property that dynamically changes the classes as is needed by the user.

    <template>
      <button
        v-on="$listeners"
        :class="[`bg-${color}-500`, `hover:bg-${color}-700`]"
        class="text-white font-bold py-2 px-4 rounded">
        <slot />
      </button>
    </template>

    <script>
    export default {
      props: {
        color: { type: String, default: 'blue' }
      }
    }
    </script>

Autoloading our Base Components

You may be wondering at this point why I am so adamant on naming everything so far with a prefix of Base. Have you ever found yourself at a point in your application where you have a set of components that you just keep using over and over again? And having to import them over and over again?

I know what you're thinking: Marina, I can import all of those as global components and be done with it. But what if there was a nicer, cleaner way?

Go into your components folder and create a globals.js file. Inside of it, place the following code.

    import Vue from 'vue'

    const requireComponent = require.context(
      '.', // The relative path of the components folder
      true, // Whether or not to look in subfolders
      /Base[A-Z]\w+\.(vue|js)$/ // The regular expression used to match base component filenames
    )

    requireComponent.keys().forEach(fileName => {
      const componentConfig = requireComponent(fileName)

      const componentName = fileName
        .split('/')
        .pop() // Get last part - filename
        .replace(/\.\w+$/, '') // Removes .vue

      // Register component globally
      Vue.component(
        componentName,
        // Look for the component options on `.default`, which will
        // exist if the component was exported with `export default`,
        // otherwise fall back to module's root.
        componentConfig.default || componentConfig
      )
    })

What this is going to do is recursively find and automagically import all the components that have the Base prefix from your components folder. You can go ahead into main.js and import '@/components/globals.js — that way you never ever again have to worry about adding them into a long, hard-to-read list of global components. Neat, right?!

This trick (sans a couple of simplifications I made) I picked up from Chris Fritz's Vue Enterprise Boilerplate. If you get a chance, check it out!

Putting the SearchBar Together

Now that we have some basic components, putting together a SearchBar should be a pretty simple task. Let's think about this in terms of a component. What do we want for the user to have available when they use a <SearchBar /> in their code?

I know I want them to be able to listen to input events. I also want them to be able to set a delay for the input event to be fired after the user stops typing. This makes sense in a SearchBar because we don't want it to be called after every single keystroke!

Let's start with a simple component, and we can add these features later on. Create a SearchBar.vue file for our new component.

    <template>
      <div class="flex items-center">
        <BaseInput type="text" class="mr-4" />
        <BaseButton color="green">Search</BaseButton>
      </div>
    </template>

Now that we have the base for our component, we can start thinking about how we want this component to communicate the input events to the outside world. I only want to emit the input when the button is clicked, so we have to listen for that event.

    <template>
      <div class="flex items-center">
        <BaseInput v-model="search" type="text" class="mr-4" />
        <BaseButton color="green" @click="startSearch">Search</BaseButton>
      </div>
    </template>

    <script>
    export default {
      data () {
        return {
          search: ''
        }
      },
      methods: {
        startSearch () {
          this.$emit('input', this.search)
        }
      }
    }
    </script>

Enhancing the SearchBar

What if we wanted to take this component further? I want to be able to use it in a way that the search button is not present. I want to receive input events directly when the user is typing, but only after a delay.

We have two options here: continue to make this component bigger (which can start becoming problematic the more and more logic we add), or create a new component that uses this one with some modifications. For fun's sake, let's go with door number two.

First, we need to make a new component AutomaticSearch (spare me, I struggle with naming just like every other dev 😅 — just be happy not everything is named after noms).

    <template>
      <SearchBar />
    </template>

    <script>
    import SearchBar from '@/components/SearchBar'
    export default {
      components: { SearchBar }
    }
    </script>

Not very impressive so far. What I want to do next is modify SearchBar so that I can hide the search button with a prop, and for it to emit typing events that I can capture for this new component. Notice that none of these changes will modify my current component API, they will just enhance it.

    <template>
      <div class="flex items-center">
        <BaseInput @input="searchChange" type="text" class="mr-4" />
        <BaseButton v-if="!hideButton" color="green" @click="startSearch">Search</BaseButton>
      </div>
    </template>

    <script>
    export default {
      props: {
        hideButton: {
          type: Boolean,
          default: false
        }
      },
      data () {
        return {
          search: ''
        }
      },
      methods: {
        searchChange (val) {
          this.search = val
          this.$emit('search-change', val)
        },
        startSearch () {
          this.$emit('input', this.search)
        }
      }
    }
    </script>

Notice that we added the hideButton property, which is a boolean we can toggle on our component to completely remove the search button, as shown by the v-if statement. We also added an @input event listener to the BaseInput and removed the v-model since we want to manually listen to these events, store the value into the state search as before, but also $emit a new event searchChange.

If the user of the component doesn't care about this event, they can safely ignore it, but we can leverage it for our AutomaticSearch component. Let's take a look at that one now.

    <template>
      <SearchBar
        hideButton
        @search-change="startSearch"
      />
    </template>

    <script>
    import SearchBar from '@/components/SearchBar'
    export default {
      components: { SearchBar },
      props: {
        inputEventDelay: {
          type: Number,
          default: 0
        }
      },
      data () {
        return {
          inputTimer: null
        }
      },
      methods: {
        startSearch (search) {
          if (this.inputEventDelay === 0) {
            this.$emit('input', search)
            return
          }

          const self = this
          clearTimeout(this.inputTimer)
          this.inputTimer = setTimeout(function () {
            self.$emit('input', search)
          }, self.inputEventDelay)
        }
      }
    }
    </script>

This component first of all implements a single SearchBar as the root element, and forcefully applies the hideButton prop to true so that we can get rid of the pesky button. We are also going to listen to the @searchChange event that we just created.

When the searchChangeevent happens, we are going to check if the inputEventDelay property has been set to a value greater than 0. If it hasn't, we are just going to emit the input as is.

If the value, however, is greater than 0, we are going to clear any old timeouts that may have been started by the user typing into the box, and then create a new timeout in its place. When this timer is done, we finally fire the input event.

This type of approach is very good for when you have an autocomplete service, for example, and you are going to be making calls to an API every time the user is done typing something into the box BUT you want to give it some room in between keystrokes so that you are not flooding your API with a high number of requests.

I've set myself up with a nice little playground to test how these two components behave by adding the following code to my App.vue.

    <template>
      <div class="flex flex-col items-center">
        <img alt="Vue logo" src="./assets/logo.png">
        <SearchBar v-model="searchBar" class="mb-4" />
        <AutomaticSearch v-model="automaticSearch" :inputEventDelay="1000" />

        <p>Search bar search: {{ searchBar }}</p>
        <p>Automatic bar search: {{ automaticSearch }}</p>
      </div>
    </template>

    <script>
    import SearchBar from '@/components/SearchBar'
    import AutomaticSearch from '@/components/AutomaticSearch'
    export default {
      components: { SearchBar, AutomaticSearch },
      data () {
        return {
          searchBar: '',
          automaticSearch: ''
        }
      }
    }
    </script>

If you want the complete code for this madness, you can find it here: https://gitlab.com/marinamosti/mindset-component-composition.

Wrapping Up

The process of creating new components, the planning and the trial and error, breaking and building, and the rewarding feeling of it actually not exploding like a waffle nuke when you're done is one of my favorite parts of being a developer. I hope you enjoyed this little trip through the land of crazy that is my head, and got a little something out of it to implement in your own projects.

As always, thanks for reading and share with me your OWN crazy on Twitter: @marinamosti.

P.S. All hail the magical avocado! 🥑

P.P.S. ❤️🔥🐶☠️


About the Author

Marina Mosti

Marina Mosti is a frontend web developer with over 18 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community. In her spare time, she enjoys playing bass, drums and video games.

Related Posts

Comments

Comments are disabled in preview mode.