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:
- Official - Single File Component
- Official - Render Functions & JSX
- 7 Ways To Define A Component Template in VueJS
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:
- Official - Props
- Vue.js Component Communication Patterns
- Creating Custom Inputs With Vue.js
- Vue Sibling Component Communication
- Managing State in Vue.js
- Vue.js communication part 2: parent-child components
Component Events Handling
References:
- Official - Custom Events
- Leveraging Vue events to reduce prop declarations
- Vue.js Component Hooks as Events
- Creating a Global Event Bus with Vue.js
- Vue.js Event Bus + Promises
Component Conditional Rendering
v-if
/ v-else
/ v-else-if
/ v-show
)
Directives (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
- Official - Dynamic Components
- Official - Dynamic & Async Components
- Dynamic Component Templates with Vue.js
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:
- Official - Slot Content
- Understanding Component Slots with Vue.js
- Composing Custom Elements With Slots And Named Slots
- Writing Abstract Components with Vue.js
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:
- Official - Scoped Slots
- Getting Your Head Around Vue.js Scoped Slots
- Understanding scoped slots in Vue.js
- Scoped Component Slots in Vue.js
- The Trick to Understanding Scoped Slots in Vue.js
- The Power of Scoped Slots in Vue
- Building a list keyboard control component with Vue.js and scoped slots
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:
- Higher Order Components in Vue.js
- Do we need Higher Order Components in Vue.js?
- Higher-Order Components in Vue.js
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:
- Official API
- Official Guide
- Component Communication
- Dependency Injection in Vue.js App with TypeScript
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,
}
}