Courses
Vue Foundations Vue Fullstack Vue Enterprise
Articles
Latest Topics
More
Newsletter Sponsorship Discord About

Extending Vue Component Templates

Anthony Gore

Anthony Gore | February 24th, 2020 | 4 min read
vue.js components composition api design patterns templates

Do you find yourself wanting to extend a component's template? Vue provides several means of extracting and reusing the component logic and state, but templates are deemed single-use only.

In this article, I'll present a solution using the HTML template pre-processor Pug. It's a bit of a hack, but it does work!

If you're looking to extend a component's logic and state, you probably want to read my other article Extending Vue.js Components which covers composition functions, mixins, higher-order components, etc. Here we're talking about the template.

Table of contents:


    Case study: a survey app

    Let's say you're trying to make a component-based survey app with Vue which looks like this:

    Notice that each survey question has a different input type:

    1. Text input
    2. Select input
    3. Radio input

    An obvious architecture would be to make each question into a separate component, which is what we're going to do. Well name these components:

    1. SurveyInputText
    2. SurveyInputSelect
    3. SurveyInputRadio

    Base component

    Let's first create a "base" component called SurveyInput.vue. Notice the following about it:

    • The question prop is going to be common across each component. This captures the question text e.g. "What is your name".
    • We don't have any shared logic, but you could add it e.g. validation and error logic
    • I'm leaving a gap where the input element will need to go. This is where we'll extend the template as you'll shortly see.

    SurveyInput.vue

    <template>
      <div class="wrapper">
        <h4>{{ question }}</h4>
        <!--the appropriate input element should go here-->
      </div>
    </template>
    <script>
    export default {
      props: {
        question: String  
      }
    }
    </script>
    

    State and logic reuse

    Ignoring the template for a moment, what will be our strategy for extraction and reuse of the state/logic? What we choose here will not affect our ability to extend the component template, so feel free to use mixins, higher-order components, or even the new composition API. I'm going to keep it simple and use the extends component option.

    Again, there's still a hanging question about how we get the base template into this subcomponent, as the Vue.extends API doesn't apply to templates!

    SurveyInputText.vue

    <template>
      <!--The base template needs to be here-->
      <input :placeholder="placeholder">
    </template>
    <script>
    import SurveyInput from './SurveyInput.vue';
    export default {
      extends: SurveyInput,
      props: {
        placeholder: String
      }
    }
    </script>
    

    Conventional options for extending the template (and why they aren't ideal)

    A reasonable Vue user would first consider the following design patterns for the template:

    • Props-driven template logic
    • Slots

    But as I'll show, both have downsides for the use case we're considering, so there is a legitimate case for wanting to use the template-extension hack I'm going present.

    Props-driven template logic

    Rather than making separate components for each input type, you could create a single mega-component and then specify the template using conditional logic fed by a prop called question-type i.e.

    SurveyInput.vue

    <template>
      <div class="wrapper">
        <h4>{{ question }}</h4>
        <div v-if="questionType === 'text'">
          <input type="text" v-model="input" />
        </div>
        <div v-else-if="questionType === 'select'">
          <select v-model="input">
            <!--etc etc-->
          </select>
        </div>
        <!--etc etc-->
      </div>
    </div>
    

    Now you can declare your questions in the parent, Survey.vue, like this:

    Survey.vue

    <template>
      <SurveyInput question-type="text" />
      <SurveyInput question-type="select" />
      <!--etc etc-->
    </template>
    

    The downside of this approach is that it doesn't scale well. Once you get, say, 3-4 different question types the template will become big and messy.

    Slots

    Another conventional approach that could work is to put a slot where the input belongs.

    SurveyInput.vue

    <template>
      <div class="wrapper">
        <h4>{{ question }}</h4>
        <slot />
      </div>
    </div>
    

    Using this approach the parent can provide the correct markup for each input like this:

    Survey.vue

    <template>
      <SurveyInput>
        <input type="text" v-model="q1" />
      </SurveyInput>
      <SurveyInput>
        <select v-model="q2">
          <!--etc etc-->
        </select>
      </SurveyInput>
      <!--etc etc-->
    </template>
    

    The downside to slots, though, is that you now have to organize the data model differently. Now, the parent owns the state of each input and would have to communicate it with each child component using props/events, adding complex logic and verbosity.

    Survey.vue

    <template>
      <SurveyInput :input="q1" @input-validated="updateInput">
        <input type="text" v-model="q1" />
      </SurveyInput>
      <!--etc etc-->
    </template>
    

    Hopefully, I've convinced you that template logic and slots aren't ideal, so now let's look at how we can extend a component's template like we can with its state/logic.

    To do this, we're going to need an HTML template pre-processor.

    Pug HTML pre-processor

    By using vue-loader and the lang property of the template in a single-file component, we can use HTML template pre-processing tools like Slim, Handlebars, etc.

    My hack for extending Vue templates is to use the include and extends options provided by Pug (previously Jade).

    <template lang="pug">
      ...
    </template>
    

    First, add Pug to your project. Vue Loader should pick this up without any further config needed.

    $ npm i -S pug
    

    Now, let's convert our base component's template to Pug syntax:

    <template lang="pug">
      div.wrapper
        h4 {{ question }}
        block input
    </template>
    

    Notice that we use block input to declare an "outlet" where the subcomponent content will be distributed.

    Important: the outlet is kind of like a slot but the important difference is that template processing occurs at compile-time, not at run-time as it would with slots.

    Creating a base template

    So here's where it gets slightly messy. If we want our child components to extend the template we first need to put it into its own file SurveyInput.pug:

    SurveyInput.pug

    div.wrapper
      h4 {{ question }}
      block input
    

    Now we can include this file in our base component so it can still be used as a normal single-file component:

    SurveyInput.vue

    <template lang="pug">
      include SurveyInput.pug
    </template>
    <script>
    export default {
      props: {
        question: String
      }
    }
    </script>
    

    It's a shame to have to do that since it kind of defeats the purpose of "single file" components. Probably someone could make a custom webpack loader to avoid having to do this.

    Extending to a subcomponent

    To extend the subcomponent's template from the base, you'll need to covert its template to Pug as well.

    The subcomponents use the extends feature of Pug which includes the base component and outputs any custom content in the input block (again, similar to slots, but it happens at compile-time).

    SurveyInputText.vue

    <template lang="pug">
      extends SurveyInput.pug
      block input
        input(type="text" :placeholder="placeholder")
    </template>
    <script>
    import SurveyInput from './SurveyInput.vue';
    export default {
      extends: SurveyInput,
      props: {
        placeholder: String  
      }
    }
    </script>
    

    Here's what the subcomponent's template would effectively look like after extending the base and being translated to a regular HTML Vue template:

    <div class="wrapper">
      <h4>{{ question }}</h4>
      <input type="text" :placeholder="placeholder">
    </div>
    

    Bring it all together

    Using this strategy we can go ahead and create the other two subcomponents SurveyInputSelect and SurveyInputRadio. If we then use them in a project our main template might look like this:

    Survey.vue

    <survey-input-text
      question="1. What is your name?"
      placeholder="e.g. John Smith"
    ></survey-input-text>
    
    <survey-input-select
      question="2. What is your favorite UI framework?"
      :options="['React', 'Vue.js', 'Angular']"
    ></survey-input-select>
    
    <survey-input-radio
      question="3. What backend do you use?"
      :options="['Node.js', 'Laravel', 'Ruby']"
      name="backend"
    >
    </survey-input-radio>
    

    And here's how the rendered markup would look:

    <div class="wrapper">
      <h4>1. What is your name?</h4>
      <input type="text" placeholder="e.g. John Smith">
    </div>
    <div class="wrapper">
      <h4>2. What is your favorite UI framework?</h4>
      <select>
        <option>React</option>
        <option>Vue.js</option>
        <option>Angular</option>
      </select>
    </div>
    <div class="wrapper">
      <h4>3. What backend do you use?</h4>
      <div><input type="radio" name="backend" value="Node.js">Node.js</div>
      <div><input type="radio" name="backend" value="Laravel">Laravel</div>
      <div><input type="radio" name="backend" value="Ruby">Ruby</div>
    </div>
    

    Happy hacking!


    Anthony Gore

    About Anthony Gore

    I'm Anthony Gore and I'm a web developer with a crush on Vue.js. I'm a Vue Community Partner, curator of the weekly Vue.js Developers Newsletter, and the creator of Vue.js Developers.

    If you enjoyed this article, show your support by buying me a coffee. You might also enjoy taking one of my online courses!

    Related posts

    Click to load comments...

    Courses
    • Vue Foundations
    • Vue Fullstack
    • Vue Enterprise
    Articles
    • Latest
    • Topics
    Newsletter
    • Join
    • Sponsorship
    More
    • Discord
    • Vue.js jobs
    • About

    Vue.js Developers © 2021. View our privacy policy .

    • RSS
    • |
    • Atom