4 Components, Mixins, and Functional Components

Building a Vue application is like putting a puzzle together. Each piece of the puzzle is a component, and each piece has a slot to fill.

Components play a big part in Vue development. In Vue, each part of your code will be a component—it could be a layout, page, container, or button, but ultimately, it's a component. Learning how to interact with them and reuse them is the key to cleaning up code and performance in your Vue application. Components are the code that will, in the end, render something on the screen, whatever the size might be.

In this chapter, we will learn about how we can make a visual component that can be reused in many places. We'll use slots to place data inside our components, create functional components for seriously fast rendering, implement direct communication between parent and child components, and finally, look at loading your components asynchronously.

Let's put these all those pieces together and create the beautiful puzzle that is a Vue application.

In this chapter, we'll cover the following recipes:

  • Creating a visual template component

  • Using slots and named slots to place data inside your components

  • Passing data to your component and validating the data

  • Creating functional components

  • Accessing your children components data

  • Creating a dynamic injected component

  • Creating a dependency injection component

  • Creating a component mixin

  • Lazy loading your components

Technical requirements

In this chapter, we will be using Node.js and Vue-CLI.

Attention Windows users: you need to install an NPM package called windows-build-tools to be able to install the following required packages. To do so, open PowerShell as an administrator and execute the following command:

> npm install -g windows-build-tools

To install Vue-CLI, you need to open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> npm install -g @vue/cli @vue/cli-service-global

Creating a visual template component

Components can be data-driven, stateless, stateful, or a simple visual component. But what is a visual component? A visual component is a component that has only one purpose: visual manipulation.

A visual component could have a simple Scoped CSS with some div HTML elements, or it could be a more complex component that can calculate the position of the element on the screen in real-time.

We will create a card wrapper component that follows the Material Design guide.

Getting ready

The pre-requisite for this recipe is as follows:

  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start our component, we can use the Vue project with Vue-CLI, as we did in the 'Creating Your first project with Vue CLI' recipe in Chapter 2Introducing TypeScript and the Vue Ecosystem, or we can start a new one. 

To start a new project, open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> vue create visual-component

The CLI will ask some questions that will help with the creation of the project. You can use the arrow keys to navigate, the Enter key to continue, and the spacebar to select an option. Choose the default option:

? Please pick a preset: (Use arrow keys)
default (babel, eslint)
Manually select features

Now, let's follow these steps and create a visual template component:

  1. Let's create a new file called MaterialCardBox.vue in the src/components folder.

  2. In this file, we will start with the template of our component. We need to create the box for the card. By using the Material Design guide, this box will have a shadow and rounded corners:

<template>
<div class="cardBox elevation_2">
<div class="section">
This is a Material Card Box
</div>
</div>
</template>

  1. In the <script> part of our component, we will add just our basic name:

<script>
export default {
name: 'MaterialCardBox',
};
</script>

  1. We need to create our elevation CSS stylesheet rules. To do this, create a file named elevation.css in the style folder. There, we will create the elevations from 0 to 24, to follow all the elevations on the Material Design guide:

.elevation_0 {
    border: 1px solid rgba(0, 0, 0, 0.12);
}

.elevation_1 {
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2),
        0 1px 1px rgba(0, 0, 0, 0.14),
        0 2px 1px -1px rgba(0, 0, 0, 0.12);
}

.elevation_2 {
    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2),
        0 2px 2px rgba(0, 0, 0, 0.14),
        0 3px 1px -2px rgba(0, 0, 0, 0.12);
}

.elevation_3 {
    box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2),
        0 3px 4px rgba(0, 0, 0, 0.14),
        0 3px 3px -2px rgba(0, 0, 0, 0.12);
}

.elevation_4 {
    box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2),
        0 4px 5px rgba(0, 0, 0, 0.14),
        0 1px 10px rgba(0, 0, 0, 0.12);
}

.elevation_5 {
    box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
        0 5px 8px rgba(0, 0, 0, 0.14),
        0 1px 14px rgba(0, 0, 0, 0.12);
}

.elevation_6 {
    box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
        0 6px 10px rgba(0, 0, 0, 0.14),
        0 1px 18px rgba(0, 0, 0, 0.12);
}

.elevation_7 {
    box-shadow: 0 4px 5px -2px rgba(0, 0, 0, 0.2),
        0 7px 10px 1px rgba(0, 0, 0, 0.14),
        0 2px 16px 1px rgba(0, 0, 0, 0.12);
}

.elevation_8 {
    box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
        0 8px 10px 1px rgba(0, 0, 0, 0.14),
        0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

.elevation_9 {
    box-shadow: 0 5px 6px -3px rgba(0, 0, 0, 0.2),
        0 9px 12px 1px rgba(0, 0, 0, 0.14),
        0 3px 16px 2px rgba(0, 0, 0, 0.12);
}

.elevation_10 {
    box-shadow: 0 6px 6px -3px rgba(0, 0, 0, 0.2),
        0 10px 14px 1px rgba(0, 0, 0, 0.14),
        0 4px 18px 3px rgba(0, 0, 0, 0.12);
}

.elevation_11 {
    box-shadow: 0 6px 7px -4px rgba(0, 0, 0, 0.2),
        0 11px 15px 1px rgba(0, 0, 0, 0.14),
        0 4px 20px 3px rgba(0, 0, 0, 0.12);
}

.elevation_12 {
    box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
        0 12px 17px 2px rgba(0, 0, 0, 0.14),
        0 5px 22px 4px rgba(0, 0, 0, 0.12);
}

.elevation_13 {
    box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
        0 13px 19px 2px rgba(0, 0, 0, 0.14),
        0 5px 24px 4px rgba(0, 0, 0, 0.12);
}

.elevation_14 {
    box-shadow: 0 7px 9px -4px rgba(0, 0, 0, 0.2),
        0 14px 21px 2px rgba(0, 0, 0, 0.14),
        0 5px 26px 4px rgba(0, 0, 0, 0.12);
}

.elevation_15 {
    box-shadow: 0 8px 9px -5px rgba(0, 0, 0, 0.2),
        0 15px 22px 2px rgba(0, 0, 0, 0.14),
        0 6px 28px 5px rgba(0, 0, 0, 0.12);
}

.elevation_16 {
    box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
        0 16px 24px 2px rgba(0, 0, 0, 0.14),
        0 6px 30px 5px rgba(0, 0, 0, 0.12);
}

.elevation_17 {
    box-shadow: 0 8px 11px -5px rgba(0, 0, 0, 0.2),
        0 17px 26px 2px rgba(0, 0, 0, 0.14),
        0 6px 32px 5px rgba(0, 0, 0, 0.12);
}

.elevation_18 {
    box-shadow: 0 9px 11px -5px rgba(0, 0, 0, 0.2),
        0 18px 28px 2px rgba(0, 0, 0, 0.14),
        0 7px 34px 6px rgba(0, 0, 0, 0.12);
}

.elevation_19 {
    box-shadow: 0 9px 12px -6px rgba(0, 0, 0, 0.2),
        0 19px 29px 2px rgba(0, 0, 0, 0.14),
        0 7px 36px 6px rgba(0, 0, 0, 0.12);
}

.elevation_20 {
    box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2),
        0 20px 31px 3px rgba(0, 0, 0, 0.14),
        0 8px 38px 7px rgba(0, 0, 0, 0.12);
}

.elevation_21 {
    box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2),
        0 21px 33px 3px rgba(0, 0, 0, 0.14),
        0 8px 40px 7px rgba(0, 0, 0, 0.12);
}

.elevation_22 {
    box-shadow: 0 10px 14px -6px rgba(0, 0, 0, 0.2),
        0 22px 35px 3px rgba(0, 0, 0, 0.14),
        0 8px 42px 7px rgba(0, 0, 0, 0.12);
}

.elevation_23 {
    box-shadow: 0 11px 14px -7px rgba(0, 0, 0, 0.2),
        0 23px 36px 3px rgba(0, 0, 0, 0.14),
        0 9px 44px 8px rgba(0, 0, 0, 0.12);
}

.elevation_24 {
    box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
        0 24px 38px 3px rgba(0, 0, 0, 0.14),
        0 9px 46px 8px rgba(0, 0, 0, 0.12);
}

  1. For styling our card in the <style> part of the component, we need to set the scoped attribute inside the <style> tag to make sure that the visual style won't interfere with any other components within our application. We will make this card follow the Material Design guide. We need to import the Roboto font family and apply it to all elements that will be wrapped inside this component:

<style scoped>
  @import url('https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap');
  @import '../style/elevation.css';

  *{
    font-family: 'Roboto', sans-serif;
  }
  .cardBox{
      width: 100%;
  max-width: 300px;
    background-color: #fff;
    position: relative;
    display: inline-block;
    border-radius: 0.25rem;
  }
  .cardBox > .section {
    padding: 1rem;
    position: relative;
  }
</style>

  1. To run the server and see your component, you need to open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> npm run serve

Here is your component rendered and running:

How it works...

A visual component is a component that will wrap any component and place the wrapped data with custom styles. As this component mixes with others, it can form a new component without the need to reapply or rewrite any style in your code.

See also

You can find more information about Scoped CSS at https://vue-loader.vuejs.org/guide/scoped-css.html#child-component-root-elements.

You can find more information about Material Design cards at https://material.io/components/cards/.

Check out the Roboto font family at https://fonts.google.com/specimen/Roboto.

Using slots and named slots to place data inside your components

Sometimes the pieces of the puzzle go missing, and you find yourself with a blank spot. Imagine that you could fill that empty spot with a piece that you crafted yourself, not the original one that came with the puzzle box. That's a rough analogy for what a Vue slot is.

Vue slots are like open spaces in your component that other components can fill with text, HTML elements, or other Vue components. You can declare where the slot will be and how it will behave in your component. 

With this technique, you can create a component and, when needed, customize it without any effort at all.

Getting ready

The pre-requisite for this recipe is as follows:

  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start our component, we can create our Vue project with Vue-CLI, as we did in the Creating Your first project with Vue CLI recipe in Chapter 2Introducing TypeScript and the Vue Ecosystem, or use the project from the Creating a visual template component recipe.

Follow these instructions to create slots and named slots in components:

  1. Let's open the file called MaterialCardBox.vue in the components folder.

  2. In the <template> part of the component, we will need to add four main sections on the card. Those sections are based on the Material Design card anatomy and are the header, media, main section, and action areas. We will use the default slot for the main section, and the rest will all be named scopes. For some named slots, we will add a fallback configuration that will be displayed if the user doesn't choose any setting on the slot:

<template>
  <div class="cardBox elevation_2">
    <div class="header">
      <slot
        v-if="$slots.header"
        name="header"
      />
      <div v-else>
        <h1 class="cardHeader cardText">
          Card Header
        </h1>
        <h2 class="cardSubHeader cardText">
          Card Sub Header
        </h2>
      </div>
    </div>
    <div class="media">
      <slot
        v-if="$slots.media"
        name="media"
      />
      <img
        v-else
        src="https://via.placeholder.com/350x250"
      >
    </div>
    <div
      v-if="$slots.default"
      class="section cardText"
      :class="{
        noBottomPadding: $slots.action,
        halfPaddingTop: $slots.media,
      }"
    >
      <slot />
    </div>
    <div
      v-if="$slots.action"
      class="action"
    >
      <slot name="action" />
    </div>
  </div>
</template>

  1. Now, we need to create our text CSS stylesheet rules for the component. In the style folder, create a new file called cardStyles.css, and there we will add the rules for the card text and headers:

h1, h2, h3, h4, h5, h6{
    margin: 0;
}
.cardText{
    -moz-osx-font-smoothing: grayscale;
    -webkit-font-smoothing: antialiased;
    text-decoration: inherit;
    text-transform: inherit;
    font-size: 0.875rem;
    line-height: 1.375rem;
    letter-spacing: 0.0071428571em;
}
h1.cardHeader{
    font-size: 1.25rem;
    line-height: 2rem;
    font-weight: 500;
    letter-spacing: .0125em;
}
h2.cardSubHeader{
    font-size: .875rem;
    line-height: 1.25rem;
    font-weight: 400;
    letter-spacing: .0178571429em;
    opacity: .6;
}

  1. In the <style> part of the component, we need to create some CSS stylesheets to follow the rules of our design guide:

<style scoped>
@import url("https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap");
@import "../style/elevation.css";
@import "../style/cardStyles.css";

* {
  font-family: "Roboto", sans-serif;
}

.cardBox {
  width: 100%;
  max-width: 300px;
  border-radius: 0.25rem;
  background-color: #fff;
  position: relative;
  display: inline-block;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14),
    0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.cardBox > .header {
  padding: 1rem;
  position: relative;
  display: block;
}
.cardBox > .media {
  overflow: hidden;
  position: relative;
  display: block;
  max-width: 100%;
}
.cardBox > .section {
  padding: 1rem;
  position: relative;
  margin-bottom: 1.5rem;
  display: block;
}
.cardBox > .action {
  padding: 0.5rem;
  position: relative;
  display: block;
}
.cardBox > .action > *:not(:first-child) {
  margin-left: 0.4rem;
}
.noBottomPadding {
  padding-bottom: 0 !important;
}
.halfPaddingTop {
  padding-top: 0.5rem !important;
}
</style>

  1. In the App.vue file, in the src folder, we need to add elements to those slots. Those elements will be added to each one of the named slots, and for the default slot. We will change the component in the <template> part of the file. To add a named slot, we need to use a directive called v-slot: and then the name of the slot we want to use:

<template>
  <div id="app">
    <MaterialCardBox>
      <template v-slot:header>
        <strong>Card Title</strong><br>
        <span>Card Sub-Title</span>
      </template>
      <template v-slot:media>
        <img src="https://via.placeholder.com/350x150">
      </template>
      <p>Main Section</p>
      <template v-slot:action>
        <button>Action Button</button>
        <button>Action Button</button>
      </template>
    </MaterialCardBox>
  </div>
</template>

For the default slot, we don't need to use a directive; it just needs to be wrapped in the component to be placed in the <slot /> part of the component.
  1. To run the server and see your component, you need to open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> npm run serve

Here is your component rendered and running:

How it works...

Slots are places where you can put anything that can be rendered into the DOM. We choose the position of our slot and tell the component where to render when it receives any information.

In this recipe, we used named slots, which are designed to work with a component that requires more than one slot. To place any information in that component within the Vue single file (.vue) <template> part, you need to add the v-slot: directive so that Vue is able to know where to place the information that was passed down.

See also

You can find more information about Vue slots at https://vuejs.org/v2/guide/components-slots.html.

You can find more information about the Material Design card anatomy at https://material.io/components/cards/#anatomy.

Passing data to your component and validating the data

You now know how to place data inside your component through slots, but those slots were made for HTML DOM elements or Vue components. Sometimes, you need to pass data such as strings, arrays, Booleans, or even objects.

The whole application is like a puzzle, where each piece is a component. Communication between components is an important part of it. The possibility to pass data to a component is the first step to connect the puzzle, and then validating the data is the final step to connect the pieces.

In this recipe, we will learn how to pass data to a component and validate the data that was passed to the component.

Getting ready

The pre-requisite is as follows:

  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start our component, we can create our Vue project with Vue-CLI, as we did in the recipe Creating Your first project with Vue CLI in Chapter 2, Introducing TypeScript and the Vue Ecosystemor use the project from the Using slots and name slots to place data inside your components recipe.

Follow these instructions to pass data to the component and validate it:

  1. Let's open the file called MaterialCardBox.vue in the src/components folder.

  2. In the <script> part of the component, we create a new property, called props. This property receives the component data, and that data can be used for visual manipulation, variables inside your code, or a function that needs to be executed. In this property, we need to declare the name of the attribute, the type, if it's required, and the validation function. This function will be executed at runtime to validate whether the passed attribute is a valid one:

<script>
export default {
  name: 'MaterialCardBox',
  inheritAttrs: false,
  props: {
    header: {
      type: String,
      required: false,
      default: '',
      validator: v => typeof v === 'string',
    },
    subHeader: {
      type: String,
      required: false,
      default: '',
      validator: v => typeof v === 'string',
    },
    mainText: {
      type: String,
      required: false,
      default: '',
      validator: v => typeof v === 'string',
    },
    showMedia: {
      type: Boolean,
      required: false,
      default: false,
      validator: v => typeof v === 'boolean',
    },
    imgSrc: {
      type: String,
      required: false,
      default: '',
      validator: v => typeof v === 'string',
    },
    showActions: {
      type: Boolean,
      required: false,
      default: false,
      validator: v => typeof v === 'boolean',
    },
    elevation: {
      type: Number,
      required: false,
      default: 2,
      validator: v => typeof v === 'number',
    },
  },
  computed: {},
};
</script>

  1. In the computed property, in the <script> part of the component, we need to create a set of visual manipulation rules that will be used for rendering the card. Those rules will be showMediaContent, showActionsButtons, showHeader, and cardElevation. Each rule will check the received props and the $slots objects to see whether the relevant card part needs to be rendered:

  computed: {
    showMediaContent() {
      return (this.$slots.media || this.imgSrc) && this.showMedia;
    },
    showActionsButtons() {
      return this.showActions && this.$slots.action;
    },
    showHeader() {
      return this.$slots.header || (this.header || this.subHeader);
    },
    showMainContent() {
      return this.$slots.default || this.mainText;
    },
    cardElevation() {
      return `elevation_${parseInt(this.elevation, 10)}`;
    },
  },

  1. After adding the visual manipulation rules, we need to add the created rules to the <template> part of our component. They will affect the appearance and behavior of our card. For example, if there is no header slot defined, and there is a header property defined, we show the fallback header. That header is the data that was passed down via props:

<template>
  <div
    class="cardBox"
    :class="cardElevation"
  >
    <div
      v-if="showHeader"
      class="header"
    >
      <slot
        v-if="$slots.header"
        name="header"
      />
      <div v-else>
        <h1 class="cardHeader cardText">
          {{ header }}
        </h1>
        <h2 class="cardSubHeader cardText">
          {{ subHeader }}
        </h2>
      </div>
    </div>
    <div
      v-if="showMediaContent"
      class="media"
    >
      <slot
        v-if="$slots.media"
        name="media"
      />
      <img
        v-else
        :src="imgSrc"
      >
    </div>
    <div
      v-if="showMainContent"
      class="section cardText"
      :class="{
        noBottomPadding: $slots.action,
        halfPaddingTop: $slots.media,
      }"
    >
      <slot v-if="$slots.default" />
      <p
        v-else
        class="cardText"
      >
        {{ mainText }}
      </p>
    </div>
    <div
      v-if="showActionsButtons"
      class="action"
    >
      <slot
        v-if="$slots.action"
        name="action"
      />
    </div>
  </div>
</template>

  1. To run the server and see your component, you need to open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> npm run serve

Here is your component rendered and running:

How it works...

Each Vue component is a JavaScript object that has a render function. This render function is called when it is time to render it in the HTML DOM. A single file component is an abstraction of this object.

When we are declaring that our component has unique props that can be passed, it opens a tiny door for other components or JavaScript to place information inside our component. We are then able to use those values inside our component to render data, do some calculations, or make visual rules.

In our case, using the single file component, we are passing those rules as HTML attributes because vue-template-compiler will take those attributes and transform them into JavaScript objects.

When those values are passed to our component, Vue first checks whether the passed attribute matches the correct type, and then we execute our validation function on top of each value to see whether it matches what we'd expect.

After all of this is done, the component life cycle continues, and we can render our component.

See also

You can find more information about props at https://vuejs.org/v2/guide/components-props.html.

You can find more information about vue-template-compiler at https://vue-loader.vuejs.org/guide/.

Creating functional components

The beauty of functional components is their simplicity. They're a stateless component, without any data, computed property, or even a life cycle. They're just a render function that is called when the data that is passed changed. 

You may be wondering how this can be useful. Well, a functional component is a perfect companion for UI components that don't need to keep any data inside them, or visual components that are just rendered components that don't require any data manipulation.

As the name implies, they are simple function components, and they have nothing more than the render function. They are a stripped-down version of a component used exclusively for performance rendering and visual elements.

Getting ready

The pre-requisite is as follows:

  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start our component, create your Vue project with Vue-CLI, as we did in the recipe 'Creating Your first project with Vue CLI' in Chapter 2Introducing TypeScript and the Vue Ecosystemor use the project from the 'Passing data to your component and validating the data' recipe.

Now, follow these instructions to create a Vue functional component:

  1. Create a new file called MaterialButton.vue in the src/components folder.

  2. In this component, we need to validate whether the prop we'll receive is a valid color. To do this, install in the project the is-color module. You'll need to open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> npm install --save is-color

  1. In the <script> part of our component, we need to create the props object that the functional component will receive. As a functional component is just a render function with no state – it's stateless – the <script> part of the component is trimmed down to props, injections, and slots. There will be four props objects: backgroundColor, textColor, isRound, and isFlat. These won't be required when installing the component, as we will have a default value defined in props:

<script>
  import isColor from 'is-color';

  export default {
    name: 'MaterialButton',
    props: {
      backgroundColor: {
        type: String,
        required: false,
        default: '#fff',
        validator: v => typeof v === 'string' && isColor(v),
      },
      textColor: {
        type: String,
        required: false,
        default: '#000',
        validator: v => typeof v === 'string' && isColor(v),
      },
      isRound: {
        type: Boolean,
        required: false,
        default: false,
      },
      isFlat: {
        type: Boolean,
        required: false,
        default: false,
      },
    },
  };
</script>

  1. In the <template> part of our component, we first need to add the functional attribute to the <template> tag to indicate to the vue-template-compiler that this component is a functional component. We need to create a button HTML element, with a basic class attribute button and a dynamic class attribute based on the props object received. Different from the normal component, we need to specify the props property in order to use the functional component. For the style of the button, we need to create a dynamic style attribute, also based on props. To emit all the event listeners directly to the parent, we can call the v-on directive and pass the listeners property. This will bind all the event listeners without needing to declare each one. Inside the button, we will add a div HTML element for visual enhancement, and add <slot> where the text will be placed:

<template functional>
  <button
    tabindex="0"
    class="button"
    :class="{
      round: props.isRound,
      isFlat: props.isFlat,
    }"
    :style="{
      background: props.backgroundColor,
      color: props.textColor
    }"
    v-on="listeners"
  >
    <div
      tabindex="-1"
      class="button_focus_helper"
    />
    <slot/>
  </button>
</template>

  1. Now, let's make it pretty. In the <style> part of the component, we need to create all the CSS stylesheet rules for this button. We need to add the scoped attribute to <style> so that all the CSS stylesheet rules won't affect any other elements in our application:

<style scoped>
  .button {
    user-select: none;
    position: relative;
    outline: 0;
    border: 0;
    border-radius: 0.25rem;
    vertical-align: middle;
    cursor: pointer;
    padding: 4px 16px;
    font-size: 14px;
    line-height: 1.718em;
    text-decoration: none;
    color: inherit;
    background: transparent;
    transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
    min-height: 2.572em;
    font-weight: 500;
    text-transform: uppercase;
  }
  .button:not(.isFlat){
    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2),
    0 2px 2px rgba(0, 0, 0, 0.14),
    0 3px 1px -2px rgba(0, 0, 0, 0.12);
  }

  .button:not(.isFlat):focus:before,
  .button:not(.isFlat):active:before,
  .button:not(.isFlat):hover:before {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border-radius: inherit;
    transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
  }

  .button:not(.isFlat):focus:before,
  .button:not(.isFlat):active:before,
  .button:not(.isFlat):hover:before {
    box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
    0 5px 8px rgba(0, 0, 0, 0.14),
    0 1px 14px rgba(0, 0, 0, 0.12);
  }

  .button_focus_helper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    border-radius: inherit;
    outline: 0;
    opacity: 0;
    transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1),
    opacity 0.4s cubic-bezier(0.25, 0.8, 0.5, 1);
  }

  .button_focus_helper:after, .button_focus_helper:before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    border-radius: inherit;
    transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1),
    opacity 0.6s cubic-bezier(0.25, 0.8, 0.5, 1);
  }

  .button_focus_helper:before {
    background: #000;
  }

  .button_focus_helper:after {
    background: #fff;
  }

  .button:focus .button_focus_helper:before,
  .button:hover .button_focus_helper:before {
    opacity: .1;
  }

  .button:focus .button_focus_helper:after,
  .button:hover .button_focus_helper:after {
    opacity: .6;
  }

  .button:focus .button_focus_helper,
  .button:hover .button_focus_helper {
    opacity: 0.2;
  }

  .round {
    border-radius: 50%;
  }
</style>

  1. To run the server and see your component, you need to open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> npm run serve

 

 

Here is your component rendered and running:

How it works...

Functional components are as simple as a render function. They don't have any sort of data, function, or access to the outside world.

They were first introduced in Vue as a JavaScript object render() function only; later, they were added to vue-template-compiler for the Vue single file application.

A functional component works by receiving two arguments: createElement and context. As we saw in the single file, we only had access to the elements as they weren't in the this property of the JavaScript object. This occurs because as the context is passed to the render function, there is no this property.

A functional component provides the fastest rendering possible on Vue, as it doesn't depend on the life cycle of a component to check for the rendering; it just renders each time data is changed. 

See also

You can find more information about functional components at https://vuejs.org/v2/guide/render-function.html#Functional-Components.

You can find more information about the is-color module at https://www.npmjs.com/package/is-color.

Accessing your children components data

Normally, parent-child communications are done via events or props. But sometimes, you need to access data, functions, or computed properties that exist in the child or the parent function.

Vue provides a way to interact in both ways, opening doors to communications and events, such as props and event listeners.

There is another way to access the data between the components: by using direct access. This can be done with the help of a special attribute in the template when using the single file component or a direct call of the object inside the JavaScript. This method is seen by some as a little lazy, but there are times when there really is no other way to do it than this.

Getting ready

The pre-requisite is as follows:

  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start your component, create your Vue project with Vue-CLI, as we did in the 'Creating Your first project with Vue CLI' recipe in Chapter 2Introducing TypeScript and the Vue Ecosystemor use the project from the 'Creating functional components' recipe.

We're going to separate the recipe into four parts. The first three parts will cover the creation of new components –  StarRatingInput, StarRatingDisplay, and StarRating  and the last part will cover the parent-child direct manipulation of the data and function access.

Creating the star rating input

We are going to create a star rating input, based on a five-star ranking system.

Follow these steps to create a custom star rating input:

  1. Create a new file called StarRatingInput.vue in the src/components folder.

  2. In the <script> part of the component, create a maxRating property in the props property that is a number, non-required, and has a default value of 5. In the data property, we need to create our rating property, with the default value of 0. In the methods property, we need to create three methods: updateRating, emitFinalVoting, and getStarName. The updateRating method will save the rating to the data, emitFinalVoting will call updateRating and emit the rating to the parent component through a final-vote event, and getStarName will receive a value and return the icon name of the star:

<script>
export default {
  name: 'StarRatingInput',
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
  },
  data: () => ({
    rating: 0,
  }),
  methods: {
    updateRating(value) {
      this.rating = value;
    },
    emitFinalVote(value) {
      this.updateRating(value);
      this.$emit('final-vote', this.rating);
    },
    getStarName(rate) {
      if (rate <= this.rating) {
        return 'star';
      }
      if (Math.fround((rate - this.rating)) < 1) {
        return 'star_half';
      }
      return 'star_border';
    },
  },
};
</script>

  1. In the <template> part of the component, we need to create a <slot> component to place the text before the star rating. We'll create a dynamic list of stars based on the maxRating value that we received via the props property. Each star that is created will have a listener attached to it in the mouseenter, focus, and click events. mouseenter and focus, when fired, will call the updateRating method, and click will call emitFinalVote:

<template>
  <div class="starRating">
    <span class="rateThis">
      <slot />
    </span>
    <ul>
      <li
        v-for="rate in maxRating"
        :key="rate"
        @mouseenter="updateRating(rate)"
        @click="emitFinalVote(rate)"
        @focus="updateRating(rate)"
      >
        <i class="material-icons">
          {{ getStarName(rate) }}
        </i>
      </li>
    </ul>
  </div>
</template>

  1. We need to import the Material Design icons into our application. Create a new styling file in the styles folder called materialIcons.css, and add the CSS stylesheet rules for font-family:

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-
Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2'); } .material-icons { font-family: 'Material Icons' !important; font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; -webkit-font-feature-settings: 'liga'; -webkit-font-smoothing: antialiased; }

  1. Open the main.js file and import the created stylesheet into it. The css-loader webpack will handle the processing of imported .css files in JavaScript files. This will help development because you don't need to re-import the file elsewhere:

import Vue from 'vue';
import App from './App.vue';
import './style/materialIcons.css';

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
}).$mount('#app');

  1. To style our component, we will create a common styling file in the src/style folder called starRating.css. There we will add the common styles that will be shared between the StarRatingDisplay and StarRatingInput components:

.starRating {
  user-select: none;
  display: flex;
  flex-direction: row;
}
.starRating * {
  line-height: 0.9rem;
}
.starRating .material-icons {
  font-size: .9rem !important;
  color: orange;
}

ul {
  display: inline-block;
  padding: 0;
  margin: 0;
}

ul > li {
  list-style: none;
  float: left;
}

  1. In the <style> part of the component, we need to create all the CSS stylesheet rules. Then, on the StarRatingInput.vue component file located in the src/components folder we need to add the scoped attribute to <style> so that all the CSS stylesheet rules won't affect any other elements in our application. Here, we will import the common styles that we created and add new ones for the input:

<style scoped>
  @import '../style/starRating.css';

  .starRating {
    justify-content: space-between;
  }

  .starRating * {
    line-height: 1.7rem;
  }

  .starRating .material-icons {
    font-size: 1.6rem !important;
  }

  .rateThis {
    display: inline-block;
    color: rgba(0, 0, 0, .65);
    font-size: 1rem;
  }
</style>

  1. To run the server and see your component, you need to open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> npm run serve

Here is your component rendered and running:

Creating the StarRatingDisplay component

Now that we have our input, we need a way to display the selected choice to the user. Follow these steps to create a StarRatingDisplay component:

  1. Create a new component called StarRatingDisplay.vue in the src/components folder.

  2. In the <script> part of the component, in the props property, we need to create three new properties: maxRating, rating, and votes. All three of them will be numbers and non-required and have a default value. In the methods property, we need to create a new method called getStarName, which will receive a value and return the icon name of the star:

<script>
export default {
  name: 'StarRatingDisplay',
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
    rating: {
      type: Number,
      required: false,
      default: 0,
    },
    votes: {
      type: Number,
      required: false,
      default: 0,
    },
  },
  methods: {
    getStarName(rate) {
      if (rate <= this.rating) {
        return 'star';
      }
      if (Math.fround((rate - this.rating)) < 1) {
        return 'star_half';
      }
      return 'star_border';
    },
  },
};
</script>

  1. In <template>, we need to create a dynamic list of stars based on the maxRating value that we received via the props property. After the list, we need to display that we received votes, and if we receive any votes, we will display them too:

<template>
  <div class="starRating">
    <ul>
      <li
        v-for="rate in maxRating"
        :key="rate"
      >
        <i class="material-icons">
          {{ getStarName(rate) }}
        </i>
      </li>
    </ul>
    <span class="rating">
      {{ rating }}
    </span>
    <span
      v-if="votes"
      class="votes"
    >
      ({{ votes }})
    </span>
  </div>
</template>

  1. In th<style> part of the component, we need to create all the CSS stylesheet rules. We need to add the scoped attribute to <style> so that all the CSS stylesheet rules won't affect any other elements in our application. Here, we will import the common styles that we created and add new ones for the display:

<style scoped>
  @import '../style/starRating.css';

  .rating, .votes {
    display: inline-block;
    color: rgba(0,0,0, .65);
    font-size: .75rem;
    margin-left: .4rem;
  }
</style>

  1. To run the server and see your component, you need to open Terminal (macOS or Linux) or the Command Prompt/PowerShell (Windows) and execute the following command:

> npm run serve

Here is your component rendered and running:

Creating the StarRating component

After creating the input and the display, we need to join both together in a single component. This component will be the final component that we'll use in the application.

Follow these steps to create the final StarRating component:

  1. Create a new file called StarRating.vue in the src/components folder.

  2. In the <script> part of the component, we need to import the StarRatingDisplay and StarRatingInput components. In the props property, we need to create three new properties: maxRatingratingand votes. All three of them will be numbers and non-required, with a default value. In the data property, we need to create our rating property, with a default value of 0, and a property called voted, with a default value of false. In the methods property, we need to add a new method called vote, which will receive rank as an argument. It will define rating as the received value and define the inside variable of the voted component as true:

<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';

export default {
  name: 'StarRating',
  components: { StarRatingDisplay, StarRatingInput },
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
    rating: {
      type: Number,
      required: false,
      default: 0,
    },
    votes: {
      type: Number,
      required: false,
      default: 0,
    },
  },
  data: () => ({
    rank: 0,
    voted: false,
  }),
  methods: {
    vote(rank) {
      this.rank = rank;
      this.voted = true;
    },
  },
};
</script>

  1. In the <template> part, we will place both the components, displaying the input of the rating:

<template>
  <div>
    <StarRatingInput
      v-if="!voted"
      :max-rating="maxRating"
      @final-vote="vote"
    >
      Rate this Place
    </StarRatingInput>
    <StarRatingDisplay
      v-else
      :max-rating="maxRating"
      :rating="rating || rank"
      :votes="votes"
    />
  </div>
</template>

Data manipulation on child components

Now that all of our components are ready, we need to add them to our application. The base application will access the child component, and it will set the rating to 5 stars.

Now, follow these steps to understand and manipulate the data in the child components:

  1. In the App.vue file, in the <template> part of the component, remove the main-text attribute of the MaterialCardBox component and place it as the default slot of the component. 

  2. Before the placed text, we will add the StarRating component. We will add a ref attribute to it. This attribute will indicate to Vue to link this component directly to a special property in the this object of the component. In the action buttons, we will add the listeners for the click event—one for resetVote and another for forceVote:

<template>
  <div id="app">
    <MaterialCardBox
      header="Material Card Header"
      sub-header="Card Sub Header"
      show-media
      show-actions
      img-src="https://picsum.photos/300/200"
    >
      <p>
        <StarRating
          ref="starRating"
        />
      </p>
      <p>
        The path of the righteous man is beset on all sides by the 
iniquities of the selfish and the tyranny of evil men. </p> <template v-slot:action> <MaterialButton background-color="#027be3" text-color="#fff" @click="resetVote" > Reset </MaterialButton> <MaterialButton background-color="#26a69a" text-color="#fff" is-flat @click="forceVote" > Rate 5 Stars </MaterialButton> </template> </MaterialCardBox> </div> </template>

  1. In the <script> part of the component, we will create a methods property, and add two new methods: resetVote and forceVote. Those methods will access the StarRating component and reset the data or set the data to a 5-star vote, respectively:

<script>
import MaterialCardBox from './components/MaterialCardBox.vue';
import MaterialButton from './components/MaterialButton.vue';
import StarRating from './components/StarRating.vue';

export default {
  name: 'App',
  components: {
    StarRating,
    MaterialButton,
    MaterialCardBox,
  },
  methods: {
    resetVote() {
      this.$refs.starRating.rank = 0;
      this.$refs.starRating.voted = false;
    },
    forceVote() {
      this.$refs.starRating.rank = 5;
      this.$refs.starRating.voted = true;
    },
  },
};
</script>

How it works...

When the ref property is added to the component, Vue adds a link to the referenced element to the $refs property inside the this property object of JavaScript. From there, you have full access to the component.

This method is commonly used to manipulate HTML DOM elements without the need to call for document query selector functions. 

However, the main function of this property is to give access to the Vue component directly, enabling you the ability to execute functions and see the computed properties, variables, and changed variables of the component—like full access to the component from the outside.

There's more...

In the same way that a parent can access a child component, a child can access a parent component by calling $parent on the this object. An event can access the root element of the Vue application by calling the $root property.

See also

You can find more information about parent-child communication at https://vuejs.org/v2/guide/components-edge-cases.html#Accessing-the-Parent-Component-Instance.

Creating a dynamic injected component

There are some cases where your component can be defined by the kind of variable you are receiving or the type of data that you have; then, you need to change the component on the fly, without the need to set a lot of Vue v-if, v-else-if, and v-else directives.

In those cases, the best thing to do is to use dynamic components, when a computed property or a function can define the component that will be used to be rendered, and the decision is taken in real time.

These decisions sometimes can be simple if there are two responses, but they can be more complex with a long switch case, where you may have a long list of possible components to be used.

Getting ready

The pre-requisite is as follows:
  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start our component, we can create our Vue project with Vue-CLI, as we did in the 'Creating Your first project with Vue CLI' recipe in Chapter 2Introducing TypeScript and the Vue Ecosystem, or use the project from the 'Accessing your children components data' recipe.

Follow these steps to create a dynamic injected component:

  1. Open the StarRating.vue component.

  2. In the <script> part of the component, we need to create a computed property with a new computed value called starComponent. This value will check whether the user has voted. If they haven't, it will return the StarRatingInput component; otherwise, it will return the StarRatingDisplay component:

<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';

export default {
  name: 'StarRating',
  components: { StarRatingDisplay, StarRatingInput },
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
    rating: {
      type: Number,
      required: false,
      default: 0,
    },
    votes: {
      type: Number,
      required: false,
      default: 0,
    },
  },
  data: () => ({
    rank: 0,
    voted: false,
  }),
  computed: {
    starComponent() {
      if (!this.voted) return StarRatingInput;
      return StarRatingDisplay;
    },
  },
  methods: {
    vote(rank) {
      this.rank = rank;
      this.voted = true;
    },
  },
};
</script>

  1. In the <template> part of the component, we will remove both of the existing components and replace them with a special component called <component>. This special component has a named attribute that you can point to anywhere that returns a valid Vue component. In our case, we will point to the computed starComponent property. We will take all the bind props that were defined from both of the other components and put them inside this new component, including the text that is placed in <slot>:

<template>
  <component
    :is="starComponent"
    :max-rating="maxRating"
    :rating="rating || rank"
    :votes="votes"
    @final-vote="vote"
  >
    Rate this Place
  </component>
</template>

How it works...

Using the Vue special <component> component, we declared what the component should render according to the rules set on the computed property. 

Being a generic component, you always need to guarantee that everything will be there for each of the components that can be rendered. The best way to do this is by using the v-bind directive with the props and rules that need to be defined, but it's possible to define it directly on the component also, as it will be passed down as a prop.

See also

You can find more information about dynamic components at https://vuejs.org/v2/guide/components.html#Dynamic-Components.

Creating a dependency injection component

Accessing data directly from a child or a parent component without knowing whether they exist can be very dangerous.

In Vue, it's possible to make your component behavior like an interface and have a common and abstract function that won't change in the development process. The process of dependency injection is a common paradigm in the developing world and has been implemented in Vue also.

There are some pros and cons to using the internal Vue dependency injection, but it is always a good way to make sure that your children's components know what to expect from the parent component when developing it.

Getting ready

The pre-requisite is as follows:
  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start our component, we can create our Vue project with Vue-CLI, as we did in the 'Creating Your first project with Vue CLI' recipe in Chapter 2Introducing TypeScript and the Vue Ecosystemor use the project from the 'Creating a dynamic injected component' recipe.

Now, follow these steps to create a dependency injection component:

  1. Open the StarRating.vue component.

  2. In the <script> part of the component, add a new property called provide. In our case, we will just be adding a key-value to check whether the component is a child of the specific component. Create an object in the property with the starRating key and the true value:

<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';

export default {
  name: 'StarRating',
  components: { StarRatingDisplay, StarRatingInput },
  provide: {
    starRating: true,
  },
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
    rating: {
      type: Number,
      required: false,
      default: 0,
    },
    votes: {
      type: Number,
      required: false,
      default: 0,
    },
  },
  data: () => ({
    rank: 0,
    voted: false,
  }),
  computed: {
    starComponent() {
      if (!this.voted) return StarRatingInput;
      return StarRatingDisplay;
    },
  },
  methods: {
    vote(rank) {
      this.rank = rank;
      this.voted = true;
    },
  },
};
</script>

  1. Open the StarRatingDisplay.vue file.

  2. In the <script> part of the component, we will add a new property called inject. This property will receive an object with a key named starRating, and the value will be an object that will have a default() function. This function will log an error if this component is not a child of the StarRating component:

<script>
export default {
  name: 'StarRatingDisplay',
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
    rating: {
      type: Number,
      required: false,
      default: 0,
    },
    votes: {
      type: Number,
      required: false,
      default: 0,
    },
  },
  inject: {
    starRating: {
      default() {
        console.error('StarRatingDisplay need to be a child of 
StarRating'); }, }, }, methods: { getStarName(rate) { if (rate <= this.rating) { return 'star'; } if (Math.fround((rate - this.rating)) < 1) { return 'star_half'; } return 'star_border'; }, }, }; </script>

  1. Open the StarRatingInput.vue file.

  2. In the <script> part of the component, we will add a new property called inject. This property will receive an object with a key named starRating, and the value will be an object that will have a default() function. This function will log an error if this component is not a child of the StarRating component:

<script>
export default {
  name: 'StarRatingInput',
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
  },
  inject: {
    starRating: {
      default() {
        console.error('StarRatingInput need to be a child of 
StarRating'); }, }, }, data: () => ({ rating: 0, }), methods: { updateRating(value) { this.rating = value; }, emitFinalVote(value) { this.updateRating(value); this.$emit('final-vote', this.rating); }, getStarName(rate) { if (rate <= this.rating) { return 'star'; } if (Math.fround((rate - this.rating)) < 1) { return 'star_half'; } return 'star_border'; }, }, }; </script>

How it works...

At runtime, Vue will check for the injected property of starRating in the StarRatingDisplay and StarRatingInput components, and if the parent component does not provide this value, it will log an error on the console.

Using component injection is commonly used to maintain a way of a common interface between bounded components, such as a menu and an item. An item may need some function or data that is stored in the menu, or we may need to check whether it's a child of the menu.

The main downside of dependency injection is that there is no more reactivity on the shared element. Because of that, it's mostly used to share functions or check component links.

See also

You can find more information about component dependency injection at https://vuejs.org/v2/guide/components-edge-cases.html#Dependency-Injection.

Creating a component mixin

There are times where you find yourself rewriting the same code over and over. However, there is a way to prevent this and make yourself far more productive. 

You can use what is called a mixin, a special code import in Vue that joins code parts from outside your component to your current component.

Getting ready

The pre-requisite is as follows:

  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start our component, we can create our Vue project with Vue-CLI, as we did in the recipe 'Creating Your First Project with Vue CLI' in Chapter 2Introducing TypeScript and the Vue Ecosystem, or use the project from the 'Creating a dependency injection component' recipe.

Let's follow these steps to create a component mixin:

  1. Open the StarRating.vue component.

  2. In the <script> part, we need to extract the props property into a new file called starRatingDisplay.js that we need to create in the mixins folder. This new file will be our first mixin, and will look like this:

export default {
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
    rating: {
      type: Number,
      required: false,
      default: 0,
    },
    votes: {
      type: Number,
      required: false,
      default: 0,
    },
  },
};

  1. Back in the StarRating.vue component, we need to import this newly created file and add it to a new property called mixin:

<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
import StarRatingDisplayMixin from '../mixins/starRatingDisplay';

export default {
  name: 'StarRating',
  components: { StarRatingDisplay, StarRatingInput },
  mixins: [StarRatingDisplayMixin],
  provide: {
    starRating: true,
  },
  data: () => ({
    rank: 0,
    voted: false,
  }),
  computed: {
    starComponent() {
      if (!this.voted) return StarRatingInput;
      return StarRatingDisplay;
    },
  },
  methods: {
    vote(rank) {
      this.rank = rank;
      this.voted = true;
    },
  },
};
</script>

  1. Now, we will open the StarRatingDisplay.vue file.

  2. In the <script> part, we will extract the inject property into a new file called starRatingChild.js, which will be created in the mixins folder. This will be our mixin for the inject property:

export default {
  inject: {
    starRating: {
      default() {
        console.error('StarRatingDisplay need to be a child of 
StarRating'); }, }, }, };

  1. Back in the StarRatingDisplay.vue file, in the <script> part, we will extract the methods property into a new file called starRatingName.js, which will be created in the mixins folder. This will be our mixin for the getStarName method:

export default {
  methods: {
    getStarName(rate) {
      if (rate <= this.rating) {
        return 'star';
      }
      if (Math.fround((rate - this.rating)) < 1) {
        return 'star_half';
      }
      return 'star_border';
    },
  },
};

  1. Back in the StarRatingDisplay.vue file, we need to import those newly created files and add them to a new property called mixin:

<script>
import StarRatingDisplayMixin from '../mixins/starRatingDisplay';
import StarRatingNameMixin from '../mixins/starRatingName';
import StarRatingChildMixin from '../mixins/starRatingChild';

export default {
  name: 'StarRatingDisplay',
  mixins: [
    StarRatingDisplayMixin,
    StarRatingNameMixin,
    StarRatingChildMixin,
  ],
};
</script>

  1. Open the StarRatingInput.vue file.

  2. In the <script> part, we remove the inject properties and extract the props property into a new file called starRatingBase.js, which will be created in the mixins folder. This will be our mixin for the props property:

export default {
  props: {
    maxRating: {
      type: Number,
      required: false,
      default: 5,
    },
    rating: {
      type: Number,
      required: false,
      default: 0,
    },
  },
};

  1. Back in the StarRatingInput.vue file, we need to rename the rating data property to rank, and in the getStarName method, we need to add a new constant that will receive either the rating props or the rank data. Finally, we need to import the starRatingChild mixin and the starRatingBase mixin:

<script>
import StarRatingBaseMixin from '../mixins/starRatingBase';
import StarRatingChildMixin from '../mixins/starRatingChild';

export default {
  name: 'StarRatingInput',
  mixins: [
    StarRatingBaseMixin,
    StarRatingChildMixin,
  ],
  data: () => ({
    rank: 0,
  }),
  methods: {
    updateRating(value) {
      this.rank = value;
    },
    emitFinalVote(value) {
      this.updateRating(value);
      this.$emit('final-vote', this.rank);
    },
    getStarName(rate) {
      const rating = (this.rating || this.rank);
      if (rate <= rating) {
        return 'star';
      }
      if (Math.fround((rate - rating)) < 1) {
        return 'star_half';
      }
      return 'star_border';
    },
  },
};
</script>

How it works...

Mixins work as an object merge, but do make sure you don't replace an already-existing property in your component with an imported one. 

The order of the mixins properties is important as well, as they will be checked and imported as a for loop, so the last mixin won't change any properties from any of their ancestors. 

Here, we took a lot of repeated parts of our code and split them into four different small JavaScript files that are easier to maintain and improve productivity without needing to rewrite code.

 

See also

You can find more information about mixins at https://vuejs.org/v2/guide/mixins.html.

Lazy loading your components

webpack and Vue were born to be together. When using webpack as the bundler for your Vue project, it's possible to make your components load when they are needed or asynchronously. This is commonly known as lazy loading.

Getting ready

The pre-requisite is as follows:

  • Node.js 12+

The Node.js global objects that are required are as follows:

  • @vue/cli

  • @vue/cli-service-global

How to do it...

To start our component, we can create our Vue project with Vue-CLI, as we did in the 'Creating Your first project with Vue CLI' recipe in Chapter 2Introducing TypeScript and the Vue Ecosystem, or use the project from the 'Creating a component mixin' recipe.

Now, follow these steps to import your component with a lazy loading technique:

  1. Open the App.vue file.

  2. In the <script> part of the component, we will take the imports at the top of the script and transform them into lazy load functions for each component:

<script>
export default {
  name: 'App',
  components: {
    StarRating: () => import('./components/StarRating.vue'),
    MaterialButton: () => import('./components/MaterialButton.vue'),
    MaterialCardBox: () => 
import('./components/MaterialCardBox.vue'), }, methods: { resetVote() { this.$refs.starRating.rank = 0; this.$refs.starRating.voted = false; }, forceVote() { this.$refs.starRating.rank = 5; this.$refs.starRating.voted = true; }, }, }; </script>

How it works...

When we declare a function that returns an import() function for each component, webpack knows that this import function will be code-splitting, and it will make the component a new file on the bundle.

The import() function was introduced as a proposal by the TC39 for module loading syntax. The base functionality of this function is to load any module that is declared asynchronously, avoiding the need to place all the files on the first load.

See also

You can find more information about async components at https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components.

You can find more information about the TC39 dynamic import at https://github.com/tc39/proposal-dynamic-import.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.161.193