Categories
JavaScript Vue

Introduction to Vue.js Render Functions

Vue.js is an easy to use web app framework that we can use to develop interactive front end apps.

In this article, we’ll look at how to define and use Vue.js render functions.

Basics

We can define and use render functions to programmatically display elements in our Vue app.

An example that can benefit from using render functions are components where there’re lots of v-if cases.

For example, if we want to display headings of various sizes with one component, we can define the following component:

src/index.js :

Vue.component("variable-heading", {  
  template: "#variable-heading-template",  
  props: {  
    size: {  
      type: Number,  
      required: true  
    }  
  }  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <script type="text/x-template" id="variable-heading-template">  
      <h1 v-if="size === 1">  
        <slot></slot>  
      </h1>  
      <h2 v-else-if="size === 2">  
        <slot></slot>  
      </h2>  
      <h3 v-else-if="size === 3">  
        <slot></slot>  
      </h3>  
      <h4 v-else-if="size === 4">  
        <slot></slot>  
      </h4>  
      <h5 v-else-if="size === 5">  
        <slot></slot>  
      </h5>  
      <h6 v-else-if="size === 6">  
        <slot></slot>  
      </h6>  
    </script>  
    <div id="app">  
      <variable-heading :size="1">foo</variable-heading>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

The code above works by passing in the size value to the variable-heading component as a prop and then the v-if in component template will determine which element to display depending on the size prop’s value.

It’s very long and has duplicate slot elements.

We can use render functions to reduce the code above to:

src/index.js :

Vue.component("variable-heading", {  
  props: {  
    size: {  
      type: Number,  
      required: true  
    }  
  },  
  render(createElement) {  
    return createElement(`h${this.size}`, this.$slots.default);  
  }  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <variable-heading :size="1">foo</variable-heading>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

The code above works by us adding a render method to our variable-heading component, which returns a Virtual DOM node, or VNode for short, by calling createElement function with the tag name as the first argument, and the children HTML nodes as an array as the second argument.

A Virtual DOM Node tells Vue what to render on the screen, which will be turned into a real DOM node.

The this.$slot.default have the child HTML nodes passed in between the opening and closing tags of variable-heading .

Nodes, Trees, and the Virtual DOM

HTML code are rendering dynamically by rendering a dynamic tree of elements called the DOM tree. DOM standards for the Document Object Model, which abstracts the HTML structure into a tree.

A tree has nodes and some nodes like text and HTML nodes are displayed in our browsers’ screens.

Vue manipulates and renders the DOM tree for us so that we don’t have to do it ourselves.

For example, we can create a component with a render function as follows:

Vue.component("variable-heading", {  
  data() {  
    return { msg: "hi" };  
  },  
  render(createElement) {  
    return createElement("p", this.msg);  
  }  
});

createElement Arguments

The createElement takes a few arguments.

The first argument is the HTML tag name, component options or async function that resolves to one of the 2 kinds of items mentioned before. It’s required.

The second argument is an object that has data corresponding to the attributes that we want to add to the template. It’s an optional argument.

The third and last argument is a string or array of the child VNodes built using createElement . This is also optional.

The Data Object

The data object has many properties that can control the attributes, classes, event handlers, inline styles of the created VNode.

The following properties are available:

class

It’s an object which has the class names that we want to include in the class attribute of the HTML element we render.

For example, we can write the following:

class: {  
  foo: true,  
  bar: false  
}

Then the foo class is included in the class attribute and bar isn’t.

style

An object with the styles that we want to include. For example, we can write the following:

style: {  
  color: 'yello',  
  fontSize: '12px'  
},

Then the styles above will be included with the element that’s rendered.

attrs

attrs object has the attributes and values that we want to include in the rendred HTML element. For example, we can write:

attrs: {  
  id: 'foo'  
}

Then our rendered HTML element will have the ID foo .

props

The props object has the component’s props with the corresponding value. For instance, we can write:

props: {  
  foo: 'bar'  
}

Then we can pass in the foo prop with the value 'bar' .

domProps

domProps object have the DOM properties that we want to pass in. For example, we can write:

domProps: {  
  innerHTML: 'foo'  
}

Then our rendered HTML element has the text foo inside.

on

With the on object, we can set event handlers for our rendered HTML element. It’s used for listening to events that are emitted via $emit .

For example, we can write:

on: {  
  click: this.clickHandler  
}

to listen to Vue click events.

nativeOn

We can listen to native browser events with the nativeOn object. It haves the same object as on expect that we have native event handlers instead of Vue event handlers.

directives

We can pass in the directives array to pass in one or more directives with their values, arguments, modifiers, etc.

For example, we can write:

directives: [  
  {  
    name: 'custom-directive',  
    value: '2',  
    expression: '1 + 1',  
    arg: 'foo',  
    modifiers: {  
      bar: true  
    }  
  }  
],

Then we can apply the custom-directive directive with modifiers, arguments, values, and expressions into the VNode.

scopedSlots and slot

We can pass in slots into our directive by setting it to a function that returns an element.

For scoped slots, we can write:

scopedSlots: {  
  default: props => createElement('span', props.text)  
}

For slots, we can just set the value as the name of the slot.

Other special top-level properties

key takes the key, ref takes a string ref name, refInFor is a boolean that is true if the same ref name is applied to multiple element.

Full Example

We can set the properties above as in the example below:

src/index.js :

Vue.component("anchor-text-block", {  
  render(createElement) {  
    const [slot] = this.$slots.default;  
    const headingId = slot.text.toLowerCase().replace(/ /g, "-");return createElement("div", [  
      createElement(  
        "a",  
        {  
          attrs: {  
            name: headingId,  
            href: `#${headingId}`  
          }  
        },  
        this.$slots.default  
      ),  
      createElement(  
        "p",  
        {  
          style: {  
            color: "green",  
            fontSize: this.fontSize  
          }  
        },  
        this.paragraphText  
      )  
    ]);  
  },  
  props: {  
    fontSize: {  
      type: Number,  
      required: true  
    },  
    paragraphText: {  
      type: String,  
      required: true  
    }  
  }  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <anchor-text-block paragraph-text="foo" :font-size="12"  
        >link</anchor-text-block  
      >  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, we created an anchor-text-block component that 2 props, paragraph-text and font-size as indicated in the props property that we have in the component’s code.

The kebab case in the template is automatically mapped to camelCase in the JavaScript code.

In the render method, we called createElement , which creates a div , and then in the second argument, we have an array with the a and p elements created. This is where we passed in both props.

We also set the attrs property to #link since we have link as the slot’s inner text since that’s what we added between the opening and closing tags.

In the p element, we set the fontSize style from the prop and set the color to green.

Conclusion

We can use render functions to create components that are more flexible than our usual components.

It can have everything that components usually have like event handlers, directives, styles, attributes, nested elements and components and more.

Categories
JavaScript Vue

Vue.js Render Functions — Advanced Features

Vue.js is an easy to use web app framework that we can use to develop interactive front end apps.

In this article, we’ll look at more advanced features of render functions.

A render function is a method of a component that gives us an alternative way to create HTML elements via the creation of Virtual DOM Nodes.

VNodes Must Be Unique

VNodes must be unique in the array that we pass into the second argument of the VNode function.

We can’t create VNodes that have duplicate child elements. For example, the following is invalid:

Vue.component("text-block", {  
  render(createElement) {  
    const dupNode = createElement("p", "hi");  
    return createElement("div", [dupNode, dupNode]);  
  }  
});

…since we have 2 dupNode in the array.

Instead, we should write:

Vue.component("text-block", {  
  render(createElement) {  
    return createElement(  
      "div",  
      Array.apply(null, { length: 5 }).map(() => {  
        return createElement("p", "hi");  
      })  
    );  
  }  
});

to call createElement multiple times to create fresh VNodes with the same content repeatedly.

Replacing Template Features with Plain JavaScript

We can replace v-if with if statements and v-for with map .

For example, we can create a person component as follows:

src/index.js:

Vue.component("persons", {  
  props: ["persons"],  
  render(createElement) {  
    if (this.persons.length) {  
      return createElement(  
        "ul",  
        this.persons.map(p => {  
          return createElement("li", p);  
        })  
      );  
    } else {  
      return createElement("p", "Persons list is empty");  
    }  
  }  
});

new Vue({  
  el: "#app",  
  data: {  
    persons: ["Joe", "Jane", "Mary"]  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="[https://cdn.jsdelivr.net/npm/vue/dist/vue.js](https://cdn.jsdelivr.net/npm/vue/dist/vue.js)"></script>  
  </head>  
  <body>  
    <div id="app">  
      <persons :persons="persons"></persons>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

Then we get a list of persons rendered.

This is the same as:

src/index.js :

Vue.component("persons", {  
  props: ["persons"],  
  template: `      
    <ul v-if='persons.length'>  
      <li v-for='p of persons'>{{p}}</li>  
    </ul>  
    <p v-else>Persons list is empty</p>  
  `  
});

new Vue({  
  el: "#app",  
  data: {  
    persons: ["Joe", "Jane", "Mary"]  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <persons :persons="persons"></persons>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

v-model

There’s no equivalent to v-model in render functions. We can implement equivalent logic as follows:

src/index.js :

Vue.component("custom-input", {  
  props: ["value"],  
  render(createElement) {  
    return createElement("input", {  
      domProps: {  
        value: this.value  
      },  
      on: {  
        input: event => {  
          this.$emit("input", event.target.value);  
        }  
      }  
    });  
  }  
});

new Vue({  
  el: "#app",  
  data: {  
    msg: ""  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <custom-input v-model="msg"></custom-input>  
      {{msg}}  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, we emit the input event in our custom-input component and takes the value prop. This is the same as v-model .

Therefore, we can bind to a model of the parent with v-model and when we type something into the input, the typed in data will show below the input.

Event & Key Modifiers

To add .passive, .capture and .once event modifiers, we can prefix the events handlers with the following prefixes to add the modifiers to our event handlers:

  • .passive — prefix with &
  • .capture — prefix with !
  • .once — prefix with ~
  • .capture.once or .once.capture — prefix with ~!

For example, we can write:

on: {  
  '!click': this.doThisInCapturingMode,  
  '~keyup': this.doThisOnce,  
  '~!mouseover': this.doThisOnceInCapturingMode  
}

Other modifiers don’t need a prefix since they have equivalents in plain JavaScript. They’re the following:

  • .stop — use event.stopPropagation()
  • .prevent — use event.preventDefault()
  • .self — use if (event.target !== event.currentTarget) return
  • Keys — e.g. .enter, or .13 — use if (event.keyCode !== 13) { //... }
  • Modifiers Keys — e.g. .ctrl, .alt, .shift, .meta — use if (!event.ctrlKey) { //... }

For example, we can add a keyup event handler as follows:

Vue.component("custom-input", {  
  props: ["value"],  
  render(createElement) {  
    return createElement("input", {  
      domProps: {  
        value: this.value  
      },  
      on: {  
        keyup: event => {  
          event.preventDefault();  
          this.$emit("input", event.target.value);  
        }  
      }  
    });  
  }  
});

Then the input event is emitted when a keyboard key is released.

We can use the modifiers that have special prefixes as follows:

src/index.js :

Vue.component("custom-button", {  
  render(createElement) {  
    return createElement("button", {  
      domProps: {  
        innerHTML: "Click Me",  
        id: "child"  
      },  
      on: {  
        "!click": event => {  
          alert(`${event.target.id} clicked`);  
        }  
      }  
    });  
  }  
});

new Vue({  
  el: "#app",  
  methods: {  
    onClick() {  
      alert(`parent clicked.`);  
    }  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <custom-button></custom-button>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

Then we get child clicked alert popup displayed since we used the capture modifier on click.

Conclusion

If we want to write components with v-if and v-for but using render functions, we can replace them with if statements and array’s map method respectively.

For adding Vue event handlers, we can add modifiers to some of them with special prefixes and others by using plain JavaScript.

Also, we can add duplicate elements in an array of child VNodes.

Categories
JavaScript Vue

How to create more complex slots in Vue

Vue.js is an easy to use web app framework that we can use to develop interactive front end apps.

In this article, we’ll look at named and scoped slots.

Named Slots

Sometimes we want to have multiple slots. To distinguish them, then we have to name them.

We can define a component with named slots with the name attribute as follows:

Vue.component("layout", {  
  template: `  
  <div>  
    <header>      
      <slot name="header"></slot>  
    </header>  
    <main>        
      <slot></slot>  
    </main>  
    <footer>        
      <slot name="footer"></slot>  
    </footer>  
  </div>  
  `  
});

Then we can use the component above together as follows:

src/index.js :

Vue.component("layout", {  
  template: `  
  <div>  
    <header>      
      <slot name="header"></slot>  
    </header>  
    <main>        
      <slot></slot>  
    </main>  
    <footer>        
      <slot name="footer"></slot>  
    </footer>  
  </div>  
  `  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <layout>  
        <template v-slot:header>  
          <h1>Header</h1>  
        </template> <p>Main</p> <template v-slot:footer>  
          <p>Footer</p>  
        </template>  
      </layout>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

The slot with the name header is filled with:

<template v-slot:header>  
   <h1>Header</h1>  
</template>

The slot with the name footer is filled with:

<template v-slot:footer>  
   <p>Footer</p>  
</template>

The slot with no name is filled with:

<p>Main</p>

If we want to wrap the content for the slot with no name with v-slot:default as follows:

<template v-slot:default>  
  <p>Main</p>  
</template>

Either way, the rendered HTML will be the same.

Scoped Slots

We can use scoped slots to access data from the child component.

To make data from the child component available in the parent, we can use the v-bind directive.

A simple example, to get child component data from the parent would be as follows:

src/index.js :

Vue.component("user", {  
  data() {  
    return {  
      user: {  
        firstName: "Joe",  
        lastName: "Smith"  
      }  
    };  
  },  
  template: `<p>  
    <slot v-bind:user="user"></slot>  
  </p>`  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <user>  
        <template v-slot:default="slotProps">  
          {{ slotProps.user.firstName }}  
        </template>  
      </user>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, we have:

<p>  
   <slot v-bind:user="user"></slot>  
</p>

to make user available from within the root Vue instance.

Then in the root template, we have:

<user>  
  <template v-slot:default="slotProps">  
    {{ slotProps.user.firstName }}  
  </template>  
</user>

to access user ‘s data via slotProps slotProps have access to all the data that are made available via v-bind in the child component.

Abbreviated Syntax for Lone Default Slots

If there’s only the default slot with provided content, we can shorten v-slot:default or v-slot on the component as follows:

<user v-slot:default="slotProps">  
   {{ slotProps.user.firstName }}  
</user>

or:

<user v-slot="slotProps">  
   {{ slotProps.user.firstName }}  
</user>

If there’re other named slots, then the syntax above can’t be used because of potential ambiguity.

If we have multiple slots, then we have to write something like the following:

src/index.js :

Vue.component("user", {  
  data() {  
    return {  
      user: {  
        firstName: "Joe",  
        lastName: "Smith"  
      }  
    };  
  },  
  template: `<p>  
    <slot v-bind:user="user" name='first-name'></slot>  
    <slot v-bind:user="user" name='last-name'></slot>  
  </p>`  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <user>  
        <template v-slot:first-name="slotProps">  
          {{ slotProps.user.firstName }}  
        </template>  
        <template v-slot:last-name="slotProps">  
          {{ slotProps.user.lastName }}  
        </template>  
      </user>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, we have to name the slots explicitly, and then we can access the child component’s data via v-slot:first-name=”slotProps” and v-slot:last-name=”slotProps” respectively.

Also, we wrapped the slot content in template .

Destructuring Slot Props

We can destructure the properties of the slot props with the destructuring assignment operator.

For example, we can use it as follows:

src/index.js :

Vue.component("user", {  
  data() {  
    return {  
      user: {  
        firstName: "Joe",  
        lastName: "Smith"  
      }  
    };  
  },  
  template: `<p>  
    <slot v-bind:user="user" name='first-name'></slot>  
    <slot v-bind:user="user" name='last-name'></slot>  
  </p>`  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <user>  
        <template v-slot:first-name="{ user }">  
          {{ user.firstName }}  
        </template>  
        <template v-slot:last-name="{ user }">  
          {{ user.lastName }}  
        </template>  
      </user>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, instead of writing slotProps , we changed it to { user } . { user } is the same as slotProps.user .

Conclusion

We can use named and scoped slots to create multiple slots and access data from the child component in the parent respectively.

Named slots prevent ambiguity and lets us use multiple slots.

Also, we can use v-bind in the child component and then use slotProps in the component to access child component’s data from the parent component.

Categories
JavaScript Vue

Vue.js Components — Parent, Child and Root

In this article, we’ll look at how to access various parts of a parent, child, or root component. Also, we look at dependency inject in Vue.js

Element & Component Access

Sometimes we have to access other components from a given component. However, it’s a bad idea to do this often since it gets messy fast.

Accessing the Root Instance

We can access the root instance of from a child component with the $root property.

For example, we can access the root Vue instance’s foo property as follows:

src/index.js :

Vue.component("foo", {  
  template: `<p>{{rootFoo}}</p>`,  
  computed: {  
    rootFoo() {  
      return this.$root.foo;  
    }  
  }  
});

new Vue({  
  el: "#app",  
  data: {  
    foo: "bar"  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <foo></foo>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

The code above created a rootFoo computed property from the root Vue instance’s foo component and displayed it inside the foo component.

So we get foo displayed.

The data returned from $root can also be set and its methods can so be called.

So we can write:

this.$root.foo = 2

or:

this.$root.baz()

from any subcomponent in addition to accessing the properties’ values.

Accessing the Parent Component Instance

Similar to $root , we can access the parent component’s properties with the $parent property from a child component.

This shouldn’t be used frequently because it’s hard to trace when the parent’s data was set, so we should pass in props to the child instead of accessing the $parent property directly.

For example, we can use it as follows:

src/index.js :

Vue.component("child", {  
  template: `<p>{{parentFoo}}</p>`,  
  computed: {  
    parentFoo() {  
      return this.$parent.foo;  
    }  
  }  
});

Vue.component("parent", {  
  data() {  
    return {  
      foo: "bar"  
    };  
  },  
  template: `  
    <p><slot></slot></p>  
  `  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <parent>  
        <child></child>  
      </parent>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, we accessed the parent component’s foo property from the child component by using this.$parent in the computed property parentFoo. So, we get bar displayed on the screen.

Accessing Child Component Instances & Child Elements

We can access a child component directly in JavaScript. To do this, we can add a ref attribute to the child component and assign a name as the value.

Then we can use this.$refs.name to access the component where name is the name that we set.

For example, we can use it as follows:

src/index.js :

new Vue({  
  el: "#app",  
  mounted() {  
    this.$refs.input.focus();  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <input ref="input" />  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the example above, we added a ref with name input in the template and then we accessed the input element with:

this.$refs.input

And then called focus on it in the mounted hook.

This lets us focus the input as soon as the input element has loaded.

We can also access a child component’s methods as follows:

src/index.js :

Vue.component("base-input", {  
  methods: {  
    focus() {  
      this.$refs.input.focus();  
    }  
  },  
  template: `<input ref='input' />`  
});

new Vue({  
  el: "#app",  
  mounted() {  
    this.$refs.baseInput.focus();  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <base-input ref="baseInput"></base-input>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, our root Vue instance called base-input ‘s focus method when it’s mounted.

Therefore, we see that the input is in focus when base-input loaded.

Dependency Injection

To prevent accessing the parent component and root Vue instance directly, we can use dependency injection to get the component that we want to get.

We can use it as follows:

src/index.js :

Vue.component("foo-button", {  
  methods: {},  
  inject: ["toggleFoo"],  
  template: `<button @click='toggleFoo'>Toggle Foo</button>`  
});

new Vue({  
  el: "#app",  
  data: {  
    foo: ""  
  },  
  provide() {  
    return {  
      toggleFoo: this.toggleFoo  
    };  
  },  
  methods: {  
    toggleFoo() {  
      this.foo = this.foo === "foo" ? "" : "foo";  
    }  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <foo-button></foo-button>  
      {{foo}}  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, we added the provide method to return an object with the items that we want to make available to child components.

We made the toggleFoo method from the root Vue instance available to our foo-button component.

Then in the foo-button component, we injected the toggleFoo method from the parent to foo-button and then we call it when click events are triggered on the button.

We included foo-button in the template as usual.

In the end, we should see that we toggle the word foo on the screen when we click on the button.

Conclusion

We can access the root Vue instance with $root , and the parent component with $parent .

To access a child component from a parent, we can assign a ref with a name to the child component and then use this.$refs to access it.

These properties shouldn’t be used too often because they create a mess fast.

To keep accessing external components clean and traceable, we can use dependency injection by defining the provide method to expose certain parts of a component and use inject to get the provided parts.

Categories
JavaScript Vue

Vue.js Components — Slots and Dynamic Components

Vue.js is an easy to use web app framework that we can use to develop interactive front end apps.

In this article, we’ll look at distributing components with slots and dynamic components.

Distributing Components with Slots

We can pass content in components by putting text between the opening and closing tags.

To do this, we can add a slot to our component with the slot element.

For example, we can use it as follows:

src/index.js :

Vue.component("error-box", {  
  template: `  
    <div v-bind:style="{color: 'red'}">  
      <slot></slot>  
    </div>  
  `  
});

new Vue({  
  el: "#app",  
  data: {  
    text: ""  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head> <body>  
    <div id="app">  
      <error-box>Error</error-box>  
    </div> <script src="src/index.js"></script>  
  </body>  
</html>

The text Error that we put between the error-box tags replace the slot tag, so we get the word ‘Error’ in red displayed.

Dynamic Components

We can switch dynamically between components by using v-bind:is .

The value of v-bind:is can contain either the name of a registered component or a component’s options object.

For example, we can use it as follows:

src/index.js :

Vue.component("tab-foo", {  
  template: "<div>Foo</div>"  
});

Vue.component("tab-bar", {  
  template: "<div>Bar</div>"  
});

Vue.component("tab-baz", {  
  template: "<div>Baz</div>"  
});

new Vue({  
  el: "#app",  
  data: {  
    currentTab: "foo"  
  },  
  computed: {  
    currentTabComponent() {  
      return `tab-${this.currentTab.toLowerCase()}`;  
    }  
  }  
});

In the code above, we created 3 components, and in the root Vue instance, we have the currentTab field so that we can add buttons to change the content in the HTML template.

The currentTabComponent will be updated as this.currentTab changes, which we’ll set the value of with buttons in the template.

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head> <body>  
    <div id="app">  
      <button @click='currentTab = "foo"'>Foo</button>  
      <button @click='currentTab = "bar"'>Bar</button>  
      <button @click='currentTab = "baz"'>Baz</button>  
      <component v-bind:is="currentTabComponent"></component>  
    </div> <script src="src/index.js"></script>  
  </body>  
</html>

In the template above, we have 3 buttons with click event handlers to set the currentTab value, which will also set the currentTabComponent property as currentTab changes.

Since we passed currentTabComponent to v-bind:is , the component element will replace the component with the name set in currentTabComponent .

So when we click the Foo button we get Foo, when we click the Bar button we get Bar, and when we click the Baz button we get Baz.

We can pass in an object to v-bind:is as follows:

src/index.js :

new Vue({  
  el: "#app",  
  data: {  
    tabBar: {  
      template: "<div>Bar</div>"  
    }  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head> <body>  
    <div id="app">  
      <component v-bind:is="tabBar"></component>  
    </div> <script src="src/index.js"></script>  
  </body>  
</html>

We passed in the tabBar object as the value of v-bind:is directive.

DOM Template Parsing Issues

Some HTML elements have restrictions on what can be inside them. For example, ul must have li elements inside them.

We can nest whatever element we want with the is attribute.

For example, we can use it as follows:

src/index.js :

Vue.component("list-item", {  
  template: "<div>Foo</div>"  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head> <body>  
    <div id="app">  
      <ul>  
        <li is="list-item"></li>  
      </ul>  
    </div> <script src="src/index.js"></script>  
  </body>  
</html>

The code above sets the list-item component as the component inside the li element.

This limitation doesn’t apply if we’re using string templates from one of the following sources:

  • String templates (e.g. template: '...')
  • Single-file (.vue) components
  • <script type="text/x-template">

Conclusion

We can get the content of items in between the opening and closing tags of our component with the slot element.

The component element let us change the component dynamically by passing in a string name for the component.

We can pass an object or a string with the component name to v-bind:is .