Vue Features to Remember

We at the Ptah team decided to go a little further than the usual SPA and tried to use Vue as a landing page designer. And now we want to share part of our experience.

This article is primarily for those who have just started working with Vue and want to know better its features and capabilities. Here I want to talk about some of the features of the framework, which often remain undeservedly forgotten by novice developers.

Render functions


Component templates are one of the things developers love Vue for. They are simple and logical, thanks to them the framework has a low entry threshold. Template syntax is enough in 90% of cases to write logical and beautiful code. But what to do if you are in the remaining 10%, and you can’t write a compact component? The render function will help you.

Let's see what it is using the example from the documentation:

Vue.component('anchored-heading', {
 render: function (createElement) {
   return createElement(
     'h' + this.level,   //  
     this.$slots.default //   
   )
 },
 props: {
   level: {
     type: Number,
     required: true
   }
 }
})

The anchored-heading component accepts the level property and draws the title tag. Thus recording

<anchored-heading :level="1">, !</anchored-heading>

Will be converted to

<h1>, !</h1>

If this component were described using a standard template, it would contain up to 6 v-if conditions that describe different levels of headers:

<h1 v-if="level === 1">
 <slot></slot>
</h1>
<h2 v-if="level === 2">
 <slot></slot>
</h2>
<h3 v-if="level === 3">
 <slot></slot>
</h3>
<h4 v-if="level === 4">
 <slot></slot>
</h4>
<h5 v-if="level === 5">
 <slot></slot>
</h5>
<h6 v-if="level === 6">
 <slot></slot>
</h6>

How it works?


The render method takes two arguments. The first argument to createElement is a function that describes which Vue element should create. In the community, it is customary to abbreviate createElement to one letter - h . The second argument is context , to access context data.

createElement takes three arguments:

  1. The item to create. This can be not only an HTML tag, but also a component name. This argument is required;
  2. An object with data. It may contain a list of classes, styles, input parameters for the component, methods for handling events, etc. For more details, see the documentation. Optional argument;
  3. Child virtual nodes. It can be either a string or an array. In the example above, this is this. $ Slots.default .

Render functions can help in the most unexpected situations. For example, in Ptah, we often need to use the style tag inside the page for some designer elements to work properly. However, Vue prohibits the use of this tag inside the template component. This limitation is easily bypassed thanks to a small wrapper:

Vue.component('v-style', {
 render: function (h) {
   return h('style', this.$slots.default)
 }
})

Now inside the templates, instead of the style tag, you can use v-style .

So, as soon as it starts to seem to you that the standard features of Vue templates are not enough - think about the Render functions. They look complicated only at first glance, but you can use all the features that JS provides in them.

Mixins


Do you have several similar components with duplicate code? Mixins or impurities will help to comply with the DRY principle - the functionality of mixins can be used in several components at once.

Let's take an example. Let's say we have 2 components with similar logic:

export default  {
  name: 'TextElement',

  data () {
    return {
      elementName: 'Text',
      showEditor: false,
      editor: null
    }
  },
  
  methods: {
    initEditor () {
      this.showEditor = true
      this.editor = new Editor(this.elementName)
    }
  }
}

export default  {
  name: 'ButtonElement',

  data () {
    return {
      elementName: 'Button',
      showEditor: false,
      editor: null
    }
  },
  
  methods: {
    initEditor () {
      this.showEditor = true
      this.editor = new Editor(this.elementName)
    }
  }
}

The components are different, but have the same logic. To make it, you need to create a regular js file. It would be logical to place it in the mixins directory next to the components.

// mixin.js
export default  {
  data () {
    return {
      showEditor: false,
      editor: null
    }
  },
  
  methods: {
    initEditor () {
      this.showEditor = true
      this.editor = new Editor(this.elementName)
    }
  }
}

// TextElement.vue
import mixin from './mixins/mixin'

export default  {
  name: 'TextElement',

  mixins: [mixin]  //  

  data () {
    return {
      elementName: 'Text',
    }
  },
}

// ButtonElement.vue
import mixin from './mixins/mixin'

export default  {
  name: 'ButtonElement',

  mixins: [mixin]

  data () {
    return {
      elementName: 'Button'
    }
  }
}

As you can see from the example, almost all logic migrated to the mixin. When using impurities inside the components, all their options merge. And in the component, you can freely call the initEditor () method , and, conversely, in the impurity, elementName from the component is used here . In this case, data objects will be merged recursively, and properties from the component will take precedence.

So, the benefits of impurities are obvious - this is code reuse. But there is a minus. This example is synthetic, just a couple of lines. Real components, such as those used in Ptah, can be written in a couple of hundred lines of code. It will not always be clear to a person who has not written this code how it works, especially if he overlooks adding mixins to the component. Unfortunately, getting rid of this minus will not work. I can recommend two things: describe the operation of the component in JSDoc and use special names for properties from the impurity (for example, you can add a prefix, which you will agree with the team in advance).

Provide / Inject


This pair of options is always used together and allows you to transfer data from the parent component to the entire hierarchy of its descendants. These options are primarily used for writing plugins, official documentation does not recommend using them in applications. In applications, communication between components builds very well on Vuex. However, this functionality still deserves attention.

How it works?


First, we need to define the data in the parent component, which we will pass on to its descendants.

// Parent.vue        
export default {
  provide: {
   device: 'is-desktop'
  }
}

Now the transferred data needs to be embedded in the child component.

// Child.vue    
export default {
  inject: ['device'],

  created () {
   console.log(this.device) // => "is-desktop"
  }
}

As you can see from the example, everything is quite simple. But one significant minus should be noted - the data from the provide / inject bundle is not reactive by default! However, this drawback is easily circumvented using Object.defineProperty :

provide () {
 let device = {}
 Object.defineProperty(device, 'type', { enumerable: true, get: () => this.device })
 return { device }
},

data () {
 return {
   device: 'is-desktop'
 }
}

Now changing this.device in the parent will change it in the descendants.

Component Meta


There are situations when it is not known in advance which component will be used in the code. Consider an example from our editor. The task is as follows: in the conditional section of FirstScreen, show the Text, Logo, Button elements, then add SocialIcons to these elements .

So, it is obvious that we will have a section component that will serve as a container for the elements and 4 components for the elements themselves. The structure will be approximately as follows:

/ sections
 -- FirstScreen.vue
/ elements
 -- Text.vue
 -- Logo.vue
 -- Button.vue
 -- SocialIcons.vue

To add all components of elements to the FirstScreen template at once, and then switch them using conditions would be an extremely unwise decision. There is a simple and wonderful tool for such tasks:

<component :is="%componentName%"/>

The component element with the : is attribute , which simply writes the name of the component. And thanks to him, our task is simply solved:

<script>
export default  {
  name: 'FirstScreen',

  data () {
    return {
      elements: [
        'Text',
        'Logo',
        'Button',
      ],
    }
  }
}
</script>

<template>
  <div class="first-screen">
    <component v-for="element in elements" :is="element"/>
  </div>
</template>

In the elements array, we wrote the names of the components and then simply output these components in a loop inside the FirstScreen template . Now, in order to add an element with social network icons to our section, we just need to execute this.elements.push ('SocialIcons') .

All Articles