Component Declaration

Single File Component (a.k.a. SFC) - Most Common

<template>
  <p class="demo">
    <button class="btn-primary" @click.prevent="handleClick">
      <slot></slot>(clicked - {{count}})
    </button>
  </p>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    },
  },
};
</script>

<style scoped>
.btn-primary {
  display: inline-block;
  font-size: 1.2rem;
  color: #fff;
  background-color: #3eaf7c;
  padding: 0.8rem 1.6rem;
  border-radius: 4px;
  transition: background-color 0.1s ease;
  box-sizing: border-box;
  border-bottom: 1px solid #389d70;
}
</style>

String Template (or ES6 Template Literal)

Vue.component('my-btn', {
  template: `
    <button class="btn-primary" @click.prevent="handleClick">
      <slot></slot>(clicked - {{count}})
    </button>
  `,
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    },
  },
});

Render Function

Vue.component('my-btn', {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    },
  },
  render(h) {
    return h(
      'button',
      {
        attrs: {
          class: 'btn-primary',
        },
        on: {
          click: this.handleClick,
        },
      },
      this.$slots.default
    );
  },
});

JSX

Vue.component('my-btn', {
  data() {
    return {
      text: 'Click me',
    };
  },
  methods: {
    handleClick() {
      console.log('clicked');
    },
  },
  render() {
    return (
      <button class="btn-primary" @click.prevent="handleClick">
        {this.$slots.default}(clicked - {{count}})
      </button>
    );
  },
});

vue-class-component

<template>
  <button class="btn-primary" @click.prevent="handleClick">
    <slot></slot>(clicked - {{ count }})
  </button>
</template>

<script>
import Vue from 'vue';
import Component from 'vue-class-component';

@Component
export default MyBtn extends Vue {
  count = 0;

  handleClick() {
    this.count++;
    console.log('clicked', this.count);
  }
}
</script>

<style scoped>
.btn-primary {
  background-color: blue;
}
</style>

References:

Component Communication

Props and Events

Basically, vue components follow one-way data flow, that is props down (See official guide) and events up. Props are read-only data, so it's impossible to change props from child components. When props change, child components will be rerendered automatically (props are a reactive data source). Child components can only emit events to their direct parent, so that the parent component may change data, mapped to the child component's props.

<template>
  <button @click="$emit('click');">{{ text }}</button>
</template>

<script>
export default {
  name: 'v-btn',
  props: {
    text: String,
  },
};
</script>
<template>
  <v-btn :text="buttonText" @click="handleClick"></v-btn>
</template>

<script>
export default {
  data() {
    return {
      clickCount: 0,
      buttonText: 'initial button text',
    };
  },
  methods: {
    handleClick() {
      this.buttonText = `Button clicked ${++this.clickCount}`;
      console.log('clicked', this.buttonText);
    },
  },
};
</script>

References:

Component Events Handling

References:

Component Conditional Rendering

Directives (v-if / v-else / v-else-if / v-show)

v-if

<h1 v-if="true">Render only if v-if condition is true</h1>

v-if and v-else

<h1 v-if="true">Render only if v-if condition is true</h1>
<h1 v-else>Render only if v-if condition is false</h1>

v-else-if

<div v-if="type === 'A'">Render only if `type` is equal to `A`</div>
<div v-else-if="type === 'B'">Render only if `type` is equal to `B`</div>
<div v-else-if="type === 'C'">Render only if `type` is equal to `C`</div>
<div v-else>Render if `type` is not `A` or `B` or `C`</div>

v-show

<h1 v-show="true">
  Always rendered, but it should be visible only if `v-show` conditions is true
</h1>

If you want to conditionally render more than one element, you can use directives(v-if / v-else / v-else-if /v-show) on a <template> element. Notice that the <template> element is not actually rendered into the DOM. It is an invisible wrapper.

<template v-if="true">
  <h1>All the elements</h1>
  <p>will be rendered into DOM</p>
  <p>except `template` element</p>
</template>

Render Function or JSX

If you use render functions or JSX in your vue application, you can apply all the techniques, such as the if else and switch case statements and ternary and logical operators.

if else statement

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    if (this.isTruthy) {
      return <h1>Render value is true</h1>;
    } else {
      return <h1>Render value is false</h1>;
    }
  },
};

switch case statement

import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';

export default {
  data() {
    return {
      type: 'error',
    };
  },
  render(h) {
    switch (this.type) {
      case 'info':
        return <Info text={text} />;
      case 'warning':
        return <Warning text={text} />;
      case 'error':
        return <Error text={text} />;
      default:
        return <Success text={text} />;
    }
  },
};

or you can use object map to simplify switch case

import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';

const COMPONENT_MAP = {
  info: Info,
  warning: Warning,
  error: Error,
  success: Success,
};

export default {
  data() {
    return {
      type: 'error',
    };
  },
  render(h) {
    const Comp = COMPONENT_MAP[this.type || 'success'];

    return <Comp />;
  },
};

ternary operator

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    return (
      <div>
        {this.isTruthy ? (
          <h1>Render value is true</h1>
        ) : (
          <h1>Render value is false</h1>
        )}
      </div>
    );
  },
};

logical operator

export default {
  data() {
    return {
      isLoading: true,
    };
  },
  render(h) {
    return <div>{this.isLoading && <h1>Loading ...</h1>}</div>;
  },
};

References

Dynamic Component

<component> with is attribute

<component :is="currentTabComponent"></component>

With the above code example, the rendered component will be destroyed if a different component is rendered in <component>. If you want components to keep their instances without being destroyed within the <component> tag, you can wrap the <component> tag in a <keep-alive> tag like so:

<keep-alive> <component :is="currentTabComponent"></component> </keep-alive>

References

Functional Component

A functional component is a special SFC, it is basically a component that is stateless (meaning no script tag). It only accepts props in order to display data.

In order to make a SFC a functional one you add the the functional attribute to the <template> tag like this: <template functional>

fp-component.vue

<template functional>
  <h1>{{ props.title }}</h1>
  <p>{{ props.description }}</p>
</template>

index.vue

<template>
  <fp-component v-bind="{ title: 'FP Component', description: 'Only takes props' }" />
</template>

<script>
import FPComponent from './fp-component';

export default {
  components: {
    FPComponent
  }
}
</script>

The benefits of using a Functional Component over a Stateful Component:

  • Faster rendering
  • Lighter memory usage

Renderless Component

A renderless component is basically a component that does not render any HTML to the DOM but inside provides reusable JavaScript logic abstracted into a SFC.

A renderless component makes use of the Slots API in order to achieve what we want.

Renderless.vue

<script>
export default {
  render() {
    return this.$scopedSlots.default({ name: 'John' });
  }
};
</script>

The only job of Renderless.vue is to provide the prop name

App.vue

<template>
  <renderless v-slot="{ name }">
    <p>{{ name }}</p>
  </renderless>
</template>

<script>
import Renderless from './Renderless.vue';

export default {
  components: {
    Renderless,
  }
};
</script>

The neat thing about using a Renderless Component is that we can seperate our logic from our markup.

Composition

Library

Basic Composition

<template>
  <div class="component-b"><component-a></component-a></div>
</template>

<script>
import ComponentA from './ComponentA';

export default {
  components: {
    ComponentA,
  },
};
</script>

References

Extends

When you want to extend a single vue component

<template>
  <button class="button-primary" @click.prevent="handleClick">
    {{ buttonText }}
  </button>
</template>

<script>
import BaseButton from './BaseButton';

export default {
  extends: BaseButton,
  props: ['buttonText'],
};
</script>

References:

Mixins

// closableMixin.js
export default {
  props: {
    isOpen: {
      default: true,
    },
  },
  data: function() {
    return {
      shown: this.isOpen,
    };
  },
  methods: {
    hide: function() {
      this.shown = false;
    },
    show: function() {
      this.shown = true;
    },
    toggle: function() {
      this.shown = !this.shown;
    },
  },
};
<template>
  <div
    v-if="shown"
    class="alert alert-success"
    :class="'alert-' + type"
    role="alert"
  >
    {{ text }}
    <i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
  </div>
</template>

<script>
import closableMixin from './mixins/closableMixin';

export default {
  mixins: [closableMixin],
  props: ['text'],
};
</script>

References:

2.6.0+

If you use Vue version above 2.6.0, Vue introduces new unified slot api, which is v-slot. It replaces the slot and slot-scope attributes, which are deprecated, but have not been removed and are still documented here. You can refer to deprecated API here.

Slots (Default)

<template>
  <button class="btn btn-primary">
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'VBtn',
};
</script>
<template>
  <v-btn>
    <span class="fa fa-user"></span> Login
  </v-btn>
</template>

<script>
import VBtn from './VBtn';

export default {
  components: {
    VBtn,
  },
};
</script>

References:

Named Slots

BaseLayout.vue

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

App.vue

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

Vue provides shorthand syntax for named slots. You can replace v-slot: with #.

References

Scoped Slots

<template>
  <ul>
    <li v-for="todo in todos" v-bind:key="todo.id">
      <!-- We have a slot for each todo, passing it the -->
      <!-- `todo` object as a slot prop. -->
      <slot v-bind:todo="todo"> {{ todo.text }} </slot>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'TodoList',
  props: {
    todos: {
      type: Array,
      default: () => [],
    },
  },
};
</script>
<template>
  <todo-list v-bind:todos="todos">
    <template v-slot:default="{ todo }">
      <span v-if="todo.isComplete"></span>
      {{ todo.text }}
    </template>
  </todo-list>
</template>

<script>
import TodoList from "./TodoList";

export default {
  components: {
    TodoList
  },
  data() {
    return {
      todos: [
        { text: "todo 1", isComplete: true },
        { text: "todo 2", isComplete: false },
        { text: "todo 3", isComplete: false },
        { text: "todo 4", isComplete: true }
      ]
    };
  }
};
</script>

References:

Render Props

In most cases, you can use scoped slots instead of render props. But, it might be useful in some cases.

with SFC

<template>
  <div id="app"><Mouse :render="__render" /></div>
</template>

<script>
import Mouse from './Mouse.js';
export default {
  name: 'app',
  components: {
    Mouse,
  },
  methods: {
    __render({ x, y }) {
      return (
        <h1>
          The mouse position is ({x}, {y})
        </h1>
      );
    },
  },
};
</script>
<style>
* {
  margin: 0;
  height: 100%;
  width: 100%;
}
</style>

with JSX

const Mouse = {
  name: 'Mouse',
  props: {
    render: {
      type: Function,
      required: true,
    },
  },
  data() {
    return {
      x: 0,
      y: 0,
    };
  },
  methods: {
    handleMouseMove(event) {
      this.x = event.clientX;
      this.y = event.clientY;
    },
  },
  render(h) {
    return (
      <div style={{ height: '100%' }} onMousemove={this.handleMouseMove}>
        {this.$props.render(this)}
      </div>
    );
  },
};

export default Mouse;

References:

Passing Props & Listeners

Sometimes, you may want to pass props and listeners to a child component without having to declare all props for the child component. You can bind $attrs and $listeners in the child component and set inheritAttrs to false (otherwise both, div and child-component will receive the attributes)

PassingProps.vue

<template>
  <div>
    <h1>{{title}}</h1>
    <passing-props-child v-bind="$attrs" v-on="$listeners"></passing-props-child>
  </div>
</template>

<script>
import PassingPropsChild from './PassingPropsChild';

export default {
  components: {
    PassingPropsChild,
  },
  inheritAttrs: false,
  props: {
    title: {
      type: String,
      default: 'Hello, Vue!',
    },
  },
};
</script>

From the parent component, you can do this:

PassedProps.vue

<template>
  <p class="demo">
    <passing-props
      title="This is from <passing-props />"
      childPropA="This is from <passing-props-child />"
      @click="handleClickPassingPropsChildComponent"
    >
    </passing-props>
  </p>
</template>

<script>
import PassingProps from './PassingProps';

export default {
  components: {
    PassingProps,
  },
  methods: {
    handleClickPassingPropsChildComponent() {
      console.log('This event comes from `<passing-props-child />`');
      alert('This event comes from `<passing-props-child />`');
    },
  },
};
</script>

Working Example:

This is from <passing-props />

References:

Higher Order Component (a.k.a. HOC)

References:

Provider / Consumer

The Provider / Consumer pattern is very simple, it aims at separating stateful logic from the presentation. We need two components to create this pattern.

Provider.vue

<template>
  <div>
    <slot v-bind="{ state, actions }" />
  </div>
</template>

<script>
export default {
  computed: {
    state() {
      return {
        label: 'button',
      };
    },
    actions() {
      return {
        click: this.click,
      };
    },
  },
  methods: {
    click() {
      console.log('Clicked');
    },
  },
}
</script>

Provider.vue is responsible for containing all the stateful logic, we are successfully separating it from the presentation. We are making use of the Slots API as a data provider.

Consumer.vue

<template functional>
  <div>
    <p>{{ props.state.label }}</p>
    <button @click="props.actions.click">CLICK</button>
  </div>
</template>

Consumer.vue is responsible for containing the presentation, note that we are using a Functional Component.

App.vue

<template>
  <provider v-slot="{ state, actions }">
    <consumer v-bind="{ state, actions }" />
  </provider>
</template>

<script>
import Provider from './Provider.vue';
import Consumer from './Consumer.vue';

export default {
  components: {
    Provider,
    Consumer,
  },
};
</script>

This pattern provides a neat way of allowing us to compose clean and decoupled components. Check out the example on CodeSandbox

Dependency injection

Vue supports provide / inject mechanism to provide object into all its descendants, regardless of how deep the component hierarchy is, as long as they are in the same parent chain. Notice that provide and inject bindings are not reactive, unless you pass down an observed object.

<parent-component>
  <child-component>
    <grand-child-component></grand-child-component>
  </child-component>
</parent-component>

With the above example component hierarchy, in order to derive data from parent-component, you should pass down data(object) as props to child-component and grand-child-component. However, if parent-component provide data(object), grand-child-component can just define inject provided object from parent-component.

References:

Provide / Inject

TIP

You can also use vue-property-decorator's @Provide, @Inject

ThemeProvider.vue

<script>
export default {
  provide: {
    theme: {
      primaryColor: '#3eaf7c',
      secondaryColor: '#1FA2FF'
    },
  },
  render(h) {
    return this.$slots.default[0];
  },
};
</script>

ThemeButton.vue

<template>
  <button class="btn" :style="{ color: '#fff', backgroundColor: (primary && theme.primaryColor) || (secondary && theme.secondaryColor)  }">
    <slot></slot>
  </button>
</template>

<script>
export default {
  inject: {
    theme: {
      default: {},
    },
  },
  props: {
    primary: {
      type: Boolean,
      default: false,
    },
    secondary: {
      type: Boolean,
      default: false,
    },
  },
};
</script>
<theme-provider>
  <p class="demo">
    <button class="btn">Normal Button</button>
    <theme-button secondary>Themed Button</theme-button>
  </p>
</theme-provider>

Working Example:

Handling Errors

errorCaptured Hook

ErrorBoundary.vue

<script>
export default {
  name: 'ErrorBoundary',
  data() {
    return {
      error: false,
      errorMessage: '',
    };
  },
  errorCaptured(err, vm, info) {
    this.error = true;
    this.errorMessage = `Sorry, error occured in ${info}`;

    return false;
  },
  render(h) {
    if (this.error) {
      return h('p', { class: 'demo bg-danger' }, this.errorMessage);
    }

    return this.$slots.default[0];
  },
};
</script>

ThrowError.vue

<template>
  <p class="demo">
    <button class="btn btn-danger" @click.prevent="throwError()">Error Thrown Button ({{count}})</button>
  </p>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  watch: {
    count() {
      throw new Error('error');
    },
  },
  methods: {
    throwError() {
      this.count++;
    },
  },
};
</script>
<error-boundary> <throw-error></throw-error> </error-boundary>

Working Example:

References

Productivity Tips

watch on create

// don't
created() {
  this.fetchUserList();
},
watch: {
  searchText: 'fetchUserList',
}
// do
watch: {
  searchText: {
    handler: 'fetchUserList',
    immediate: true,
  }
}