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 {
      text: 'Click me',
    };
  },
  methods: {
    data() {
      return {
        count: 0,
      };
    },
    methods: {
      handleClick() {
        this.count++;
        console.log('clicked', this.count);
      },
    },
  },
});

Render Function

Vue.component('my-btn', {
  methods: {
    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 component follows one-way data flow, that is props down(See official guide) and event up. Props are read-only data, so it's impossible to change props from child components. When props changes, child components will be rerendered automatically(props are reactive data source). Child components can only emit event to 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 <template> element is not actually rendered into 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 function or JSX in your vue application, you can apply all the techniques such as if else and switch case statement and ternary and logical operator.

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, 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 <component> tag, you can wrap the <component> tag in a <keep-alive> tag like so:

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

References

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:

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 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 slot="footer">
    <p>Here's some contact info</p>
  </template>
</base-layout>

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 slot-scope="{ 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: [
        { todo: 'todo 1', isComplete: true },
        { todo: 'todo 2', isComplete: false },
        { todo: 'todo 3', isComplete: false },
        { todo: '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 case.

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 child component without having to declare all child component's props. You can bind $attrs and $listeners in 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 parent component, you can do like 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:

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

<script>
export default {
  inject: {
    theme: {
      default: {},
    },
  },
  props: {
    primary: {
      type: Boolean,
      default: false,
    },
    secondary: {
      type: Boolean,
      default: false,
    },
  },
};
</script>
<theme-provider>
  <theme-button secondary>Themed Button</theme-button>
</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,
  }
}
Last Updated: 8/6/2018, 2:46:15 PM