Update 2016-11-28 The
$dispatch
and$broadcast
methods have been deprecated in Vue 2.0, so if you've upgraded, be sure to read the migration guide that covers the changes.
Introduction
If, like myself, you find yourself new to the world of VueJS and not had much to do with Javascript in the past, aside from perhaps jQuery, having to change the way you think about working with a reactive JavaScript framework may be a bit overwhelming at first.
I recently found myself in a situation where I was repeating myself between components. This becomes unmaintainable very quickly, especially as your use of that code snippet finds its way into three or four or more components. Any time you make a change, you find yourself updating multiple locations.
Parent-Child communication in VueJS
Given a root Vue instance is accessible by all descendants via this.$root
, a parent component can access child components via the this.$children
array, and a child component can access it's parent via this.$parent
, your first instinct might be to access these components directly.
The VueJS documentation warns against this specifically for two very good reasons:
- It tightly couples the parent to the child (and vice versa)
- You can't rely on the parent's state, given that it can be modified by a child component.
Not one to create more work for myself later, particularly when the documentation warns against it, I continued on to learn about Vue's custom event interface.
Vue's custom event interface
The event interface implemented by Vue allows you to communicate up and down the component tree. Leveraging the custom event interface gives you access to four methods:
-
$on()
- allows you to declare a listener on your Vue instance with which to listen to events -
$emit()
- allows you to trigger events on the same instance (self
) -
$dispatch()
- send an event upward along the parent chain -
$broadcast()
- send an event downward along the descendants chain
An example using pagination
The situation I found myself in that led to me learning about events was a simple paginator. Laravel provides this functionality out of the box when querying the database, so you only need to deal with the JSON response generated by the query.
Note that in this example, I'm using Laravel Elixir with Jeffrey Way's Vueify wrapper and Bootstrap's Pager component.
The Paginator component
1// resources/assets/js/components/Paginator.vue 2<template> 3 <div id="paginator" v-show="previous || next"> 4 <nav> 5 <ul class="pager"> 6 <li v-show="previous" class="previous"> 7 <a @click.prevent="paginate('previous')" class="page-scroll" href="#"> 8 < Previous 9 </a>10 </li>11 <li v-show="next" class="next">12 <a @click.prevent="paginate('next')" class="page-scroll" href="#">13 Next >14 </a>15 </li>16 </ul>17 </nav>18 </div>19</template>
The template will only display if the pagination has a truth-y previous
or next
value. Likewise, the previous and next button will only be visible in the event the corresponding property is truth-y.
Next, we define the JavaScript that belongs to this component.
1// resources/assets/js/components/Paginator.vue 2<script> 3 export default { 4 5 data() { 6 return { 7 page: 1, 8 next: false, 9 previous: false10 }11 },12 13 methods: {14 paginate(direction) {15 if (direction === 'previous') {16 --this.page;17 } else if (direction === 'next') {18 ++this.page;19 }20 21 this.$dispatch('pageUpdated', this.page);22 this.$dispatch('paginationTriggered');23 },24 25 updatePagination(paginator) {26 this.next = paginator.next_page_url ? true : false;27 this.previous = paginator.prev_page_url ? true : false;28 }29 },30 31 events: {32 'responsePaginated': 'updatePagination'33 }34 35 };36</script>
What we've done now, is define some pretty basic methods, that we'll use to update the state of our paginator template, based on external events.
Whenever the paginate
method is triggered, we'll increment or decrement the page being tracked in the instance and $dispatch
an event. The first notifies the component tree that the page was updated, along with the new page value, as well as letting any component that's listening that pagination was triggered.
The component also has an events
object, which maps an event name to a method (updatePagination
) that should be executed when handling the event. Vue will automatically pass broadcast arguments to the receiving methods.
Note: I wasn't able to use an anonymous function in the events
object. When doing so, Vue does not appear to pass along any variables. If you know more about this, let me know in the comments.
A parent component leveraging the Paginator
Next, I'll show you how to handle the two events we dispatched from the Paginator
component, as well as how to broadcast the responsePaginated
event down to the descendants.
1// resources/assets/js/components/Parent.vue 2<template> 3 // Your paginated content will go here, perhaps in a table 4 5 // Add the tag for the paginator component 6 <paginator></paginator> 7</template> 8 9<script>10 import Paginator from './Paginator.vue';11 12 export default {13 14 components: { Paginator },15 16 data() {17 return {18 paginatedData: [],19 page: 120 }21 },22 23 created() {24 this.fetchPaginatedData();25 },26 27 methods: {28 fetchPaginatedData() {29 this.$http.get('/api/paginated/data', { page: this.page }).then(30 function (response) {31 this.paginatedData = response.data.data;32 33 this.$broadcast('responsePaginated', response.data);34 }35 );36 },37 38 updatePage(page) {39 this.page = page;40 }41 },42 43 events: {44 'pageUpdated': 'updatePage',45 'paginationTriggered': 'fetchPaginatedData'46 }47}
Now that we extracted the pagination to its own component, the component responsible from loading paginated data from the Laravel backend is quite simple. We leverage vue-resource to make a GET
request to the endpoint, update the instance data, and $broadcast
the responsePaginated
event with the response.data
.
response.data
contains an object with the current page number, previous and next page URLs, total results, and from and to values - that is to say the paginated result contains results 1 - 25 of 50, for example.
The broadcasted event makes its way down the descendant chain, where it will be processed by the paginator component's updatePagination
method. This will reactively update the display of the list items, showing the next and previous links as necessary.
The main component also handles two events itself - the two events we fired with the $dispatch
method in the paginator. The pageUpdated
event fires the updatePage
method, which simply sets the page
property on the instance, which will be used as a query parameter in subsequent requests to the Laravel backend, whilst the paginationTriggered
event will trigger the fetchPaginatedData
method to fire another GET
request to the Laravel backend.
Conclusion
This brings to a close my first attempt at handling communication between parent and child components within a Vue, with the aim at reducing duplication of code and separating of pagination concerns from the fetching component.
As a first pass, I'm quite happy with the results but eager to know if there is something I've missed or if there's other ways to tackle this in the future.
You'll note I only used Bootstrap's simple paginator with previous and next links. You could easily adjust this to render page links and pass the new page value along with the pageUpdated
event.
One other thing that I did do in this instance, but have not shown in the sample code above is displaying the paginated counts and totals in the parent component. This is as straightforward as rendering the extra data from the GET
request to the Laravel backend, along the following lines:
1<template> 2 <span v-show="! loading && pagination.total > 0"> 3 Showing results {{ pagination.from }} to {{ pagination.to }} of {{ pagination.total }} 4 </span> 5 6 // Rest of your view logic 7</template> 8 9<script>10 export default {11 12 data() {13 return {14 pagination: {15 total: 0,16 from: 0,17 to: 018 }19 }20 },21 22 methods: {23 fetchPaginatedData() {24 this.$http.get('/api/paginated/data', { page: this.page }).then(25 function (response) {26 pagination.total = response.data.total;27 pagination.from = response.data.from;28 pagination.to = response.data.to;29 30 // Rest of your fetch logic31 }32 );33 }34 }35 };36</script>
Update 5th March, 2016
Shortly after my original post, the VueJS Twitter account responded to my original tweet about it:
.@michaeldyrynda events work when the scenario is simple to comprehend, but when there's a lot of shared state, it's time to checkout vuex.
— Vue.js (@vuejs) March 5, 2016
What is Vuex?
Vuex is an application architecture for centralized state management in Vue.js applications. It is inspired by Flux and Redux, but with simplified concepts and an implementation that is designed specifically to take advantage of Vue.js' reactivity system.
I've not looked into Vuex in great detail just yet, but it certainly looks like an interesting extension to VueJS if your application starts to reach the medium-to-large-scale SPA its documentation refers to. For the paginator example in this post, I don't believe it qualifies, but this is certainly something to keep in mind as your knowledge increases and your application grows.