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:
- Official - Single File Component
- Official - Render Functions & JSX
- 7 Ways To Define A Component Template in VueJS
- Writing multiple Vue components in a single file
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:
- 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
- Design Patterns for Communication Between Vue.js 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
- Modifying component data with event emitters in Vue.js
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 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
- Official - Dynamic Components
- Official - Dynamic & Async Components
- Dynamic Component Templates with Vue.js
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:
- 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 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:
- 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
- How I finally got my head around Scoped Slots in Vue
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:
- Official - Render Functions & JSX
- Leveraging Render Props in Vue
- Using React-Style Callback Props With Vue: Pros and Cons
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:
- Higher Order Components in Vue.js
- Do we need Higher Order Components in Vue.js?
- Higher-Order Components in Vue.js
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:
- 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>
<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,
}
}