A couple of months ago we started a Vue app that lists and searches for web development tutorials. It works nicely but was missing some key things. All of the data was hard-coded into the application and couldn’t be changed. Today we’ll be changing that.
For our backend, we first considered json-server, which makes it extremely simple to set up a REST server by giving it a JSON file with all of the data. We decided we wanted to test a different open source back end, though, and we settled on Kinto, which is developed by Mozilla. It offers offline-first functionality with its JavaScript client, a web-based admin, built-in synchronization, and real-time capabilities via Pusher (needs a plugin).
With Kinto handling our server-side storage and API, we’ll be updating the Tutorial Search application in a few ways:
- Instead of directly pulling tutorial data directly from
data.js
, Kinto will provide it to us. - Vuex will directly use the Kinto API as well as provide the API for our components to consume.
- We will be able to add and delete tutorials from the list.
In order to save time, we will not be adding the ability to edit tutorials. Consider it a personal challenge for you to complete on your own before moving on to the next part of the tutorial.
Getting Started
Before we start plopping code into our editors we’ll need to do a bit of housecleaning. First, update all of your npm dependencies to the latest versions. My preferred way to accomplish this task is with npm-check. It shows you which packages are out-of-date, which version you have installed, and which version is latest. It then lets you select which packages to update. After you’ve done that, we need to install a few new dependencies:
1 2 |
npm install -S vuex kinto |
That gives us Vuex and Kinto for the client side of the application, but we also need to get our backend set up. There are numerous ways to install Kinto onto a server, but the simplest for quick setup of prototyping apps would be to use one of the 3 buttons on their installation instruction page for deploying on cloud providers. For the sake of this tutorial, we used Heroku via the “Deploy to Heroku” button.
Once you’ve got it installed on a server, navigate to the admin interface at http(s?)://YOURDOMAIN/v1/admin/. If you’re using Heroku, it’ll be https://PROJECTNAME.herokuapp.com/v1/admin/. Here you can log in with whatever username and password you want since the BasicAuth authentication it comes with by default won’t put up any fight no matter what credentials you use. For this tutorial, everything will be public anyway, though we may get into securing things in the next article.
You should be on the main dashboard now, so click “Create Bucket”. Give the bucket an ID of “tut-search”. Within that bucket, click “Create Collection”. We’ll call this collection “tutorials” and you can use the following JSON schema:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
{ "<span class="hljs-attribute">id</span>": <span class="hljs-value"><span class="hljs-string">"http://example.com/example.json"</span></span>, "<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"object"</span></span>, "<span class="hljs-attribute">$schema</span>": <span class="hljs-value"><span class="hljs-string">"http://json-schema.org/draft-04/schema#"</span></span>, "<span class="hljs-attribute">properties</span>": <span class="hljs-value">{ "<span class="hljs-attribute">url</span>": { "<span class="hljs-attribute">id</span>": <span class="hljs-string">"/properties/url"</span>, "<span class="hljs-attribute">type</span>": <span class="hljs-string">"string"</span> }, "<span class="hljs-attribute">tech</span>": { "<span class="hljs-attribute">id</span>": <span class="hljs-string">"/properties/tech"</span>, "<span class="hljs-attribute">type</span>": <span class="hljs-string">"array"</span>, "<span class="hljs-attribute">items</span>": { "<span class="hljs-attribute">id</span>": <span class="hljs-string">"/properties/tech/items"</span>, "<span class="hljs-attribute">type</span>": <span class="hljs-string">"string"</span> } }, "<span class="hljs-attribute">title</span>": { "<span class="hljs-attribute">id</span>": <span class="hljs-string">"/properties/title"</span>, "<span class="hljs-attribute">type</span>": <span class="hljs-string">"string"</span> }, "<span class="hljs-attribute">description</span>": { "<span class="hljs-attribute">id</span>": <span class="hljs-string">"/properties/description"</span>, "<span class="hljs-attribute">type</span>": <span class="hljs-string">"string"</span> }, "<span class="hljs-attribute">datePublished</span>": { "<span class="hljs-attribute">id</span>": <span class="hljs-string">"/properties/datePublished"</span>, "<span class="hljs-attribute">type</span>": <span class="hljs-string">"string"</span> } }</span>, "<span class="hljs-attribute">definitions</span>": <span class="hljs-value">{} </span>} |
And the following UI schema:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "<span class="hljs-attribute">ui:order</span>": <span class="hljs-value">[ <span class="hljs-string">"title"</span>, <span class="hljs-string">"url"</span>, <span class="hljs-string">"datePublished"</span>, <span class="hljs-string">"description"</span>, <span class="hljs-string">"tech"</span> ]</span>, "<span class="hljs-attribute">description</span>": <span class="hljs-value">{ "<span class="hljs-attribute">ui:widget</span>": <span class="hljs-string">"textarea"</span> } </span>} |
The rest of the settings can be as you see fit. I sorted by “-datePublished” and my columns were “title”, “url”, and “datePublished”. Once you’re done with that, click “Create collection.” Now you can enter all the tutorial records manually using the admin interface, but to save you some time, we’ll write a script to do it for us. Also, go to “Permissions” for the tutorials collection and make sure that an anonymous user can read, write, and create records.
Importing Data into Kinto
To import the data into Kinto quickly, we’ll work inside of main.js
to write a little code that will import the data from data.js/
and use the Kinto API to put the records into our new Kinto database. First, we need to comment out all the code that is currently in main.js
because we just want to run the import code. Then we’ll add this to the file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import Kinto from <span class="hljs-string">'kinto'</span> import { tutorials } from <span class="hljs-string">'./data'</span> <span class="hljs-keyword">let</span> db = <span class="hljs-keyword">new</span> Kinto({ remote: <span class="hljs-string">'https://PROJECTNAME.herokuapp.com/v1/'</span>, bucket: <span class="hljs-string">'tut-search'</span> }) <span class="hljs-keyword">let</span> tutorialAPI = db.collection(<span class="hljs-string">'tutorials'</span>) tutorials .reduce((prev, tut) => prev.then(() => tutorialAPI.create(tut)), tutorialAPI.sync()) .then(() => tutorialAPI.sync()) .then(({ ok, errors, published }) => <span class="hljs-built_in">console</span>.log({ ok, errors, published })) |
Here we’re creating a Kinto collection connected to our backend. Then we iterate through our tutorials with reduce
, waiting for the previous API call to finish and then telling Kinto to create
a new tutorial. After all of them have been created, we need to sync
so that it actually synchronizes with the backend, otherwise, this data will only be on the client. The final line is a debug line, so make sure your console is showing that ok
is true
and that published
has 22 elements. If there are any errors, you can look in the errors
array for information on what went wrong. Make sure you edit the remote
URL for your own application.
Use npm start
to run the script in your browser and be sure to only run it once, otherwise, you’ll end up with duplicate records (you can always use the admin or API to delete the duplicates, but it’s just a bit of hassle you should try to avoid).
Now delete that code and uncomment the old code in main.js
so we can move on to updating the actual application.
Creating a Vuex Store
We’ve got our tutorial data on the server now, so we need to start taking advantage of that. The first thing we need to do is remove the tutorial data from data.js
, so it should only contain the technologies
data:
1 2 3 4 5 6 7 8 9 10 |
export <span class="hljs-keyword">let</span> technologies = { angular: { label: <span class="hljs-string">'AngularJS'</span>, color: <span class="hljs-string">'#dd0031'</span> }, node: { label: <span class="hljs-string">'NodeJS'</span>, color: <span class="hljs-string">'#8bc849'</span> }, python: { label: <span class="hljs-string">'Python'</span>, color: <span class="hljs-string">'#fdcd3d'</span> }, react: { label: <span class="hljs-string">'React'</span>, color: <span class="hljs-string">'#06c4f9'</span> }, ror: { label: <span class="hljs-string">'Ruby on Rails'</span>, color: <span class="hljs-string">'#961122'</span> }, vue: { label: <span class="hljs-string">'VueJS'</span>, color: <span class="hljs-string">'#41b883'</span> }, webpack: { label: <span class="hljs-string">'Webpack'</span>, color: <span class="hljs-string">'#1d71b2'</span> } } |
For the sake of this tutorial, we won’t be putting this data onto the server. Now we need to start using Vuex, so create the store.js
file in the src
folder with the following contents:
1 2 3 4 5 6 7 8 9 10 11 12 |
import Vue from <span class="hljs-string">'vue'</span> import Vuex from <span class="hljs-string">'vuex'</span> import tutorials from <span class="hljs-string">'./stores/tutorials.js'</span> Vue.use(Vuex) export <span class="hljs-keyword">default</span> <span class="hljs-keyword">new</span> Vuex.Store({ strict: process.env.NODE_ENV !== <span class="hljs-string">'production'</span>, modules: { tutorials } }) |
This sets Vue up to use the Vuex plugin and creates a store with the tutorials
module. We’ll create that module shortly, but first, we need to need to go into main.js
and make the store available to all of the components:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import Vue from <span class="hljs-string">'vue'</span> import App from <span class="hljs-string">'./components/App'</span> import store from <span class="hljs-string">'./store'</span> <span class="hljs-comment">// Add this line</span> import <span class="hljs-string">'bootstrap/dist/css/bootstrap.css'</span> Vue.config.productionTip = <span class="hljs-literal">false</span> <span class="hljs-keyword">new</span> Vue({ el: <span class="hljs-string">'#app'</span>, render: (h) => h(App), store <span class="hljs-comment">// Add this line. Don't forget the comma above it too.</span> }) |
Now let’s create that module for the Vuex store. Create a folder called stores
and add the tutorials.js
file to it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
import Kinto from <span class="hljs-string">'kinto'</span> <span class="hljs-keyword">let</span> db = <span class="hljs-keyword">new</span> Kinto({ remote: <span class="hljs-string">'https://PROJECTNAME.herokuapp.com/v1/'</span>, bucket: <span class="hljs-string">'tut-search'</span> }) <span class="hljs-keyword">let</span> tutorialAPI = db.collection(<span class="hljs-string">'tutorials'</span>) export <span class="hljs-keyword">default</span> { namespaced: <span class="hljs-literal">true</span>, state: { data: [] }, getters: { tutorials(state) { <span class="hljs-keyword">return</span> state.data }, filtered(state) { <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">({ searchTerm, tech })</span> </span>{ <span class="hljs-keyword">let</span> result = state.data <span class="hljs-keyword">if</span> (searchTerm) { result = result.filter( (tutorial) => tutorial.title.toLowerCase().search(searchTerm) >= <span class="hljs-number">0</span> || tutorial.description.toLowerCase().search(searchTerm) >= <span class="hljs-number">0</span> ) } <span class="hljs-keyword">if</span> (tech) { result = result.filter((tutorial) => tutorial.tech.indexOf(tech) >= <span class="hljs-number">0</span>) } <span class="hljs-keyword">return</span> result } } }, mutations: { setTutorials(state, payload) { state.data = payload } }, actions: { load({ dispatch }) { <span class="hljs-keyword">return</span> dispatch(<span class="hljs-string">'sync'</span>) }, addTutorial({ dispatch }, tutorial) { <span class="hljs-keyword">return</span> tutorialAPI.create(tutorial).then(() => dispatch(<span class="hljs-string">'sync'</span>)) }, sync({ commit }) { <span class="hljs-keyword">return</span> tutorialAPI .sync() .then(() => tutorialAPI.list()) .then((response) => { commit(<span class="hljs-string">'setTutorials'</span>, response.data) }) .catch(<span class="hljs-built_in">console</span>.error) }, <span class="hljs-keyword">delete</span>({ dispatch }, id) { <span class="hljs-keyword">return</span> tutorialAPI.delete(id).then(() => dispatch(<span class="hljs-string">'sync'</span>)) } } } |
The first few lines should look familiar since they’re quite similar to the ones we used in our import script. Once again, don’t forget to update the URL. The rest is setting up a namespaced store module. If this doesn’t make much sense to you, I recommend going through the excellent documentation for Vuex.
The store is designed in a way that you’ll always use the getters and actions, and the actions will use the mutations, but they shouldn’t be used by outside code. The actions all use the Kinto API to do the manipulations, sync them up to the server, and then update the Vuex store’s data with the synchronized data. In the import script, we saw create
and sync
, but here we’re also using delete
, which will delete a record, and list
, which will give us the data that is stored on the client so we can put it into the store for the getters to access.
Notice that the only error handling we have here is a simple console.error
. You’ll probably want to do a lot more than that in a real user-facing application. This can be done by removing the catch
here and letting the error propagate up to the calling components and they can handle the error cases.
A final note here: Kinto uses the fetch
API, so if you need to support any browsers that don’t have it implemented, you’ll need to add a polyfill and this is the place to add it since this is the place where Kinto is used.
Updating the App to Use Vuex
Now we need to tell our app to use the data from Vuex, since it’s no longer available in data.js
. To do that, open up components/App.vue
and update the script
portion to the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import TutorialList from <span class="hljs-string">'./TutorialList'</span> import Pagination from <span class="hljs-string">'./Pagination'</span> import SearchBox from <span class="hljs-string">'./SearchBox'</span> import RadioGroup from <span class="hljs-string">'./RadioGroup'</span> import getArraySection from <span class="hljs-string">'../utilities/get-array-section'</span> <span class="hljs-comment">// Removed import of data.js</span> export <span class="hljs-keyword">default</span> { name: <span class="hljs-string">'app'</span>, components: { TutorialList, Pagination, SearchBox, RadioGroup }, data: () => ({ searchTerm: <span class="hljs-string">''</span>, tech: <span class="hljs-string">''</span>, page: <span class="hljs-number">1</span> <span class="hljs-comment">// `tutorials` array removed; Refactored to be under `computed`</span> }), computed: { pageOfTutorials() { <span class="hljs-keyword">return</span> getArraySection(<span class="hljs-keyword">this</span>.tutorials, <span class="hljs-keyword">this</span>.page, <span class="hljs-number">10</span>) }, <span class="hljs-comment">// tutorials added here</span> tutorials() { <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.$store.getters[<span class="hljs-string">'tutorials/filtered'</span>]( { searchTerm: <span class="hljs-keyword">this</span>.searchTerm.toLowerCase(), tech: <span class="hljs-keyword">this</span>.tech }) } }, watch: { <span class="hljs-comment">/* `tutorials` as a computed property removes need to watch `searchTerm` `tech`, but we still need to reset the page to 1 when the list of tutorials updates, so we've added that. */</span> tutorials() { <span class="hljs-keyword">this</span>.page = <span class="hljs-number">1</span> } }, <span class="hljs-comment">/* Vuex store now offers the filtered list, plus the refactor made it unnecessary to use a method on the component, so it is removed. Also, the `created` hook doesn't require `filterTutorials` to be called because of the refactor, but we need to force vuex to download the tutorials */</span> created() { <span class="hljs-keyword">this</span>.$store.dispatch(<span class="hljs-string">'tutorials/load'</span>) } } |
The changes are noted in the comments so it should be simple to make the changes. Note that refactoring tutorials
into the computed
section wasn’t actually due to updating to use Vuex. It is simply a better way of writing the code that I didn’t think of when writing it for the first iteration of the application.
Now it should be displaying the tutorials from the backend as expected, but we have a problem: sometimes it can take a while to make that request for the tutorials, and while users are waiting for the tutorials to load, the app is telling them there are zero tutorials and doesn’t give any indication that we’re trying to download them. Let’s rectify this by creating a component that wraps around sections of our app and overlays them with a loading spinner during asynchronous operations.
To do this, we’ll create components/LoadingOverlay.vue
and fill it with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
<span class="hljs-tag"><<span class="hljs-title">template</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"loading-overlay-container"</span>></span> <span class="hljs-tag"><<span class="hljs-title">slot</span>></span><span class="hljs-tag"></<span class="hljs-title">slot</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"overlay"</span> <span class="hljs-attribute">v-if</span>=<span class="hljs-value">"isLoading"</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"loader"</span>></span> <span class="hljs-tag"><<span class="hljs-title">svg</span> <span class="hljs-attribute">version</span>=<span class="hljs-value">"1.1"</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"loader-spinner"</span> <span class="hljs-attribute">xmlns</span>=<span class="hljs-value">"http://www.w3.org/2000/svg"</span> <span class="hljs-attribute">xmlns:xlink</span>=<span class="hljs-value">"http://www.w3.org/1999/xlink"</span> <span class="hljs-attribute">x</span>=<span class="hljs-value">"0px"</span> <span class="hljs-attribute">y</span>=<span class="hljs-value">"0px"</span> <span class="hljs-attribute">width</span>=<span class="hljs-value">"30px"</span> <span class="hljs-attribute">height</span>=<span class="hljs-value">"30px"</span> <span class="hljs-attribute">viewBox</span>=<span class="hljs-value">"0 0 50 50"</span> <span class="hljs-attribute">style</span>=<span class="hljs-value">"enable-background:new 0 0 50 50;"</span> <span class="hljs-attribute">xml:space</span>=<span class="hljs-value">"preserve"</span>></span> <span class="hljs-tag"><<span class="hljs-title">path</span> <span class="hljs-attribute">fill</span>=<span class="hljs-value">"#fff"</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M43.935,25.145c0-10.318-8.364-18.683-18.683-18.683c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615c8.072,0,14.615,6.543,14.615,14.615H43.935z"</span>></span><span class="hljs-tag"></<span class="hljs-title">path</span>></span> <span class="hljs-tag"></<span class="hljs-title">svg</span>></span> <span class="hljs-tag"><<span class="hljs-title">span</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"loader-text"</span>></span>{{ loadingText }}<span class="hljs-tag"></<span class="hljs-title">span</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"></<span class="hljs-title">template</span>></span> <span class="hljs-tag"><<span class="hljs-title">script</span>></span><span class="javascript"> export <span class="hljs-keyword">default</span> { props:{ isLoading: { type: <span class="hljs-built_in">Boolean</span>, <span class="hljs-keyword">default</span>: <span class="hljs-literal">false</span> }, loadingText: { type: <span class="hljs-built_in">String</span>, <span class="hljs-keyword">default</span>: <span class="hljs-string">'Loading'</span> } } } </span><span class="hljs-tag"></<span class="hljs-title">script</span>></span> <span class="hljs-tag"><<span class="hljs-title">style</span> <span class="hljs-attribute">scoped</span>></span><span class="css"> <span class="hljs-class">.loading-overlay-container</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">position</span>:<span class="hljs-value"> relative</span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-class">.overlay</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">background</span>:<span class="hljs-value"> <span class="hljs-function">rgba</span>(<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,.<span class="hljs-number">5</span>)</span></span>; <span class="hljs-rule"><span class="hljs-attribute">position</span>:<span class="hljs-value"> absolute</span></span>; <span class="hljs-rule"><span class="hljs-attribute">top</span>:<span class="hljs-value"> <span class="hljs-number">0</span></span></span>; <span class="hljs-rule"><span class="hljs-attribute">right</span>:<span class="hljs-value"> <span class="hljs-number">0</span></span></span>; <span class="hljs-rule"><span class="hljs-attribute">bottom</span>:<span class="hljs-value"> <span class="hljs-number">0</span></span></span>; <span class="hljs-rule"><span class="hljs-attribute">left</span>:<span class="hljs-value"> <span class="hljs-number">0</span></span></span>; <span class="hljs-rule"><span class="hljs-attribute">color</span>:<span class="hljs-value"> white</span></span>; <span class="hljs-rule"><span class="hljs-attribute">border-radius</span>:<span class="hljs-value"> inherit</span></span>; <span class="hljs-rule"><span class="hljs-attribute">display</span>:<span class="hljs-value"> flex</span></span>; <span class="hljs-rule"><span class="hljs-attribute">align-items</span>:<span class="hljs-value"> center</span></span>; <span class="hljs-rule"><span class="hljs-attribute">justify-content</span>:<span class="hljs-value"> center</span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-class">.loader</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">background</span>:<span class="hljs-value"> <span class="hljs-function">rgba</span>(<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,.<span class="hljs-number">5</span>)</span></span>; <span class="hljs-rule"><span class="hljs-attribute">padding</span>:<span class="hljs-value"> <span class="hljs-number">20px</span></span></span>; <span class="hljs-rule"><span class="hljs-attribute">border-radius</span>:<span class="hljs-value"> <span class="hljs-number">5px</span></span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-class">.loader-text</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">vertical-align</span>:<span class="hljs-value"> middle</span></span>; <span class="hljs-rule"><span class="hljs-attribute">font-size</span>:<span class="hljs-value"> <span class="hljs-number">20px</span></span></span>; <span class="hljs-rule"><span class="hljs-attribute">text-transform</span>:<span class="hljs-value"> uppercase</span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-class">.loader-spinner</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">vertical-align</span>:<span class="hljs-value"> middle</span></span>; <span class="hljs-rule"><span class="hljs-attribute">animation</span>:<span class="hljs-value"> spinner <span class="hljs-number">0.6s</span> linear infinite</span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-at_rule">@<span class="hljs-keyword">keyframes</span> spinner </span>{ 0% <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">transform</span>:<span class="hljs-value"> <span class="hljs-function">rotate</span>(<span class="hljs-number">0</span>)</span></span>; <span class="hljs-rule">}</span></span> 100% <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">transform</span>:<span class="hljs-value"> <span class="hljs-function">rotate</span>(<span class="hljs-number">360deg</span>)</span></span>; <span class="hljs-rule">}</span></span> } </span><span class="hljs-tag"></<span class="hljs-title">style</span>></span> |
The spinner is an adaptation of a CodePen example by Aurer (example 3). You can customize the text that is displayed beside the spinner and you control whether it is shown or not simply by passing in a Boolean to the isLoading
prop.
Now let’s use this in components/App.vue
to show the spinner while the app is starting up. First, wrap the Pagination
and TutorialList
section with the LoadingOverlay
:
1 2 3 4 5 6 7 8 |
<span class="hljs-comment"><!-- ... --></span> <span class="hljs-tag"><<span class="hljs-title">LoadingOverlay</span> <span class="hljs-attribute">:isLoading</span>=<span class="hljs-value">"listIsLoading"</span>></span> <span class="hljs-tag"><<span class="hljs-title">Pagination</span> <span class="hljs-attribute">v-model</span>=<span class="hljs-value">"page"</span> <span class="hljs-attribute">:items</span>=<span class="hljs-value">"tutorials.length"</span> <span class="hljs-attribute">:perPage</span>=<span class="hljs-value">"10"</span> /></span> <span class="hljs-tag"><<span class="hljs-title">TutorialList</span> <span class="hljs-attribute">:tutorials</span>=<span class="hljs-value">"pageOfTutorials"</span> /></span> <span class="hljs-tag"><<span class="hljs-title">Pagination</span> <span class="hljs-attribute">v-model</span>=<span class="hljs-value">"page"</span> <span class="hljs-attribute">:items</span>=<span class="hljs-value">"tutorials.length"</span> <span class="hljs-attribute">:perPage</span>=<span class="hljs-value">"10"</span> /></span> <span class="hljs-tag"></<span class="hljs-title">LoadingOverlay</span>></span> <span class="hljs-comment"><!-- ... --></span> |
We’re using the listIsLoading
property to signal when the overlay should show, so let’s edit the script part to import LoadingOverlay
and control listIsLoading
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<span class="hljs-comment">// ...</span> import LoadingOverlay from <span class="hljs-string">'./LoadingOverlay'</span> <span class="hljs-comment">// ... Add LoadingOverlay to list of components</span> components: { TutorialList, Pagination, SearchBox, RadioGroup, LoadingOverlay }, data: () => ({ <span class="hljs-comment">// ... add listIsLoading property to data</span> listIsLoading: <span class="hljs-literal">false</span> }), <span class="hljs-comment">// ...</span> created() { <span class="hljs-comment">// Set listIsLoading appropriately during loading</span> <span class="hljs-keyword">this</span>.listIsLoading = <span class="hljs-literal">true</span> <span class="hljs-keyword">this</span>.$store.dispatch(<span class="hljs-string">'tutorials/load'</span>) .then(() => <span class="hljs-keyword">this</span>.listIsLoading = <span class="hljs-literal">false</span>) .catch(() => <span class="hljs-keyword">this</span>.listIsLoading = <span class="hljs-literal">false</span>) } |
Now if you try running it again, you should see a loading overlay until the records show up. If it’s happening too quickly, you can use Chrome dev tools’ network throttling feature to slow things down.
Deleting Tutorials
Let’s add the ability to delete tutorials now. In components/Tutorial.vue
, we’ll add a small overlay that appears when you hover over the tutorial and contains the delete button. This will make sure the delete button doesn’t clog the screen visually while you’re looking through the tutorials. We’ll also wrap the tutorial with LoadingOverlay
because deleting a tutorial will be asynchronous again. Your file should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
<span class="hljs-tag"><<span class="hljs-title">template</span>></span> <span class="hljs-comment"><!-- Replace div with LoadingOverlay --></span> <span class="hljs-tag"><<span class="hljs-title">LoadingOverlay</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"tutorial-list-item"</span> <span class="hljs-attribute">:isLoading</span>=<span class="hljs-value">"isLoading"</span>></span> <span class="hljs-tag"><<span class="hljs-title">h4</span>></span><span class="hljs-tag"><<span class="hljs-title">a</span> <span class="hljs-attribute">:href</span>=<span class="hljs-value">"item.url"</span>></span>{{ item.title }}<span class="hljs-tag"></<span class="hljs-title">a</span>></span><span class="hljs-tag"></<span class="hljs-title">h4</span>></span> <span class="hljs-tag"><<span class="hljs-title">p</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"meta"</span>></span> Published <span class="hljs-tag"><<span class="hljs-title">span</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"meta-bit"</span>></span>{{ item.datePublished }}<span class="hljs-tag"></<span class="hljs-title">span</span>></span> at <span class="hljs-tag"><<span class="hljs-title">span</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"meta-bit"</span>></span>{{ domainOf(item.url) }}<span class="hljs-tag"></<span class="hljs-title">span</span>></span> <span class="hljs-tag"></<span class="hljs-title">p</span>></span> <span class="hljs-tag"><<span class="hljs-title">p</span>></span>{{ item.description }}<span class="hljs-tag"></<span class="hljs-title">p</span>></span> <span class="hljs-tag"><<span class="hljs-title">p</span>></span> <span class="hljs-tag"><<span class="hljs-title">TechLabel</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"tech-label"</span> <span class="hljs-attribute">v-for</span>=<span class="hljs-value">"tech in item.tech"</span> <span class="hljs-attribute">:tech</span>=<span class="hljs-value">"tech"</span> <span class="hljs-attribute">:key</span>=<span class="hljs-value">"tech"</span> /></span> <span class="hljs-tag"></<span class="hljs-title">p</span>></span> <span class="hljs-comment"><!-- Add this section --></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"actions-overlay"</span>></span> <span class="hljs-tag"><<span class="hljs-title">button</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"btn btn-danger btn-xs"</span> @<span class="hljs-attribute">click</span>=<span class="hljs-value">"deleteTutorial"</span>></span><span class="hljs-tag"><<span class="hljs-title">span</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"glyphicon glyphicon-trash"</span> <span class="hljs-attribute">aria-hidden</span>=<span class="hljs-value">"true"</span>></span><span class="hljs-tag"></<span class="hljs-title">span</span>></span> Delete<span class="hljs-tag"></<span class="hljs-title">button</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-comment"><!-- Replace this div too --></span> <span class="hljs-tag"></<span class="hljs-title">LoadingOverlay</span>></span> <span class="hljs-tag"></<span class="hljs-title">template</span>></span> <span class="hljs-tag"><<span class="hljs-title">script</span>></span><span class="javascript"> import TechLabel from <span class="hljs-string">'./TechLabel'</span> <span class="hljs-comment">// Import LoadingOverlay</span> import LoadingOverlay from <span class="hljs-string">'./LoadingOverlay'</span> <span class="hljs-keyword">let</span> parser = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'a'</span>) export <span class="hljs-keyword">default</span> { <span class="hljs-comment">// ...</span> <span class="hljs-comment">// Add isLoading data</span> data() { <span class="hljs-keyword">return</span> { isLoading: <span class="hljs-literal">false</span> } }, <span class="hljs-comment">// Add LoadingOverlay</span> components: { TechLabel, LoadingOverlay }, methods: { domainOf(){ <span class="hljs-comment">/* ... */</span> }, <span class="hljs-comment">// Add this method</span> deleteTutorial() { <span class="hljs-keyword">this</span>.isLoading = <span class="hljs-literal">true</span> <span class="hljs-keyword">this</span>.$store.dispatch(<span class="hljs-string">'tutorials/delete'</span>, <span class="hljs-keyword">this</span>.item.id).then(() => <span class="hljs-keyword">this</span>.isLoading = <span class="hljs-literal">false</span>) } } } </span><span class="hljs-tag"></<span class="hljs-title">script</span>></span> <span class="hljs-tag"><<span class="hljs-title">style</span> <span class="hljs-attribute">scoped</span>></span><span class="css"> <span class="hljs-comment">/* ... */</span> <span class="hljs-comment">/* Add styles for showing/hiding the delete button */</span> <span class="hljs-class">.actions-overlay</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">border-top</span>:<span class="hljs-value"> <span class="hljs-number">1px</span> solid <span class="hljs-hexcolor">#aaa</span></span></span>; <span class="hljs-rule"><span class="hljs-attribute">padding-top</span>:<span class="hljs-value"> <span class="hljs-number">10px</span></span></span>; <span class="hljs-rule"><span class="hljs-attribute">transform</span>:<span class="hljs-value"> <span class="hljs-function">scaleY</span>(<span class="hljs-number">0</span>)</span></span>; <span class="hljs-rule"><span class="hljs-attribute">transform-origin</span>:<span class="hljs-value"> top left</span></span>; <span class="hljs-rule"><span class="hljs-attribute">transition</span>:<span class="hljs-value"> transform .<span class="hljs-number">2s</span> ease-out</span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-class">.tutorial-list-item</span><span class="hljs-pseudo">:hover</span> <span class="hljs-class">.actions-overlay</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">transform</span>:<span class="hljs-value"> <span class="hljs-function">scaleY</span>(<span class="hljs-number">1</span>)</span></span>; <span class="hljs-rule">}</span></span> </span><span class="hljs-tag"></<span class="hljs-title">style</span>></span> |
Adding New Tutorials
Of course, if all we can do is delete the tutorials, that means we’ll eventually run out of tutorials, so we need to be able to add some to the list as well. Let’s start by creating a form that can be used to create new tutorials. Add components/TutorialForm.vue
with the following contents:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
<span class="hljs-tag"><<span class="hljs-title">template</span>></span> <span class="hljs-tag"><<span class="hljs-title">form</span> @<span class="hljs-attribute">submit.prevent</span>=<span class="hljs-value">"emitTutorial"</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"row"</span>></span> <span class="hljs-tag"><<span class="hljs-title">FormInput</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"col-sm-4"</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"title"</span> <span class="hljs-attribute">v-model</span>=<span class="hljs-value">"formTutorial.title"</span> <span class="hljs-attribute">required</span>></span>Title<span class="hljs-tag"></<span class="hljs-title">FormInput</span>></span> <span class="hljs-tag"><<span class="hljs-title">FormInput</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"col-sm-4"</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"url"</span> <span class="hljs-attribute">v-model</span>=<span class="hljs-value">"formTutorial.url"</span> <span class="hljs-attribute">required</span>></span>URL<span class="hljs-tag"></<span class="hljs-title">FormInput</span>></span> <span class="hljs-tag"><<span class="hljs-title">FormInput</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"col-sm-4"</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"date"</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"date"</span> <span class="hljs-attribute">v-model</span>=<span class="hljs-value">"formTutorial.datePublished"</span> <span class="hljs-attribute">required</span>></span>Date Published<span class="hljs-tag"></<span class="hljs-title">FormInput</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"row"</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"form-group col-sm-6"</span>></span> <span class="hljs-tag"><<span class="hljs-title">label</span> <span class="hljs-attribute">for</span>=<span class="hljs-value">"description"</span>></span>Description<span class="hljs-tag"></<span class="hljs-title">label</span>></span> <span class="hljs-tag"><<span class="hljs-title">textarea</span> <span class="hljs-attribute">v-model</span>=<span class="hljs-value">"formTutorial.description"</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"description"</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"form-control"</span> <span class="hljs-attribute">required</span>></span><span class="hljs-tag"></<span class="hljs-title">textarea</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"form-group col-sm-6"</span>></span> <span class="hljs-tag"><<span class="hljs-title">label</span> <span class="hljs-attribute">for</span>=<span class="hljs-value">"description"</span>></span>Technologies<span class="hljs-tag"></<span class="hljs-title">label</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"tech-options"</span>></span> <span class="hljs-tag"><<span class="hljs-title">label</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"tech-option"</span> <span class="hljs-attribute">v-for</span>=<span class="hljs-value">"option in techOptions"</span>></span> <span class="hljs-tag"><<span class="hljs-title">input</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"checkbox"</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"tech-check"</span> <span class="hljs-attribute">v-model</span>=<span class="hljs-value">"formTutorial.tech"</span> <span class="hljs-attribute">:value</span>=<span class="hljs-value">"option"</span>></span> <span class="hljs-tag"><<span class="hljs-title">TechLabel</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"tech-label"</span> <span class="hljs-attribute">:tech</span>=<span class="hljs-value">"option"</span> /></span> <span class="hljs-tag"></<span class="hljs-title">label</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"><<span class="hljs-title">input</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"submit"</span> <span class="hljs-attribute">value</span>=<span class="hljs-value">"Add Tutorial"</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"btn btn-primary btn-lg btn-block"</span>></span> <span class="hljs-tag"></<span class="hljs-title">form</span>></span> <span class="hljs-tag"></<span class="hljs-title">template</span>></span> <span class="hljs-tag"><<span class="hljs-title">script</span>></span><span class="javascript"> import FormInput from <span class="hljs-string">'./FormInput'</span> import TechLabel from <span class="hljs-string">'./TechLabel'</span> import { technologies } from <span class="hljs-string">'../data'</span> export <span class="hljs-keyword">default</span> { components: { FormInput, TechLabel }, model: { event: <span class="hljs-string">'submit'</span> }, props: { tutorial: { type: <span class="hljs-built_in">Object</span>, <span class="hljs-keyword">default</span>: () => ({}) } }, data() { <span class="hljs-keyword">return</span> { formTutorial: <span class="hljs-keyword">this</span>.generateTutorial(<span class="hljs-keyword">this</span>.tutorial), techOptions: [...Object.keys(technologies)] } }, watch: { tutorial(value) { <span class="hljs-keyword">this</span>.formTutorial = <span class="hljs-keyword">this</span>.generateTutorial(value) } }, methods: { emitTutorial() { <span class="hljs-keyword">this</span>.$emit(<span class="hljs-string">'submit'</span>, <span class="hljs-keyword">this</span>.formTutorial) }, <span class="hljs-comment">// Generates defaults and strips out any properties we don't want</span> generateTutorial({ title=<span class="hljs-string">''</span>, datePublished=<span class="hljs-string">''</span>, description=<span class="hljs-string">''</span>, tech=[], url=<span class="hljs-string">''</span> }) { <span class="hljs-keyword">return</span> { title, datePublished, description, tech, url } } } } </span><span class="hljs-tag"></<span class="hljs-title">script</span>></span> <span class="hljs-tag"><<span class="hljs-title">style</span> <span class="hljs-attribute">scoped</span>></span><span class="css"> <span class="hljs-class">.tech-options</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">display</span>:<span class="hljs-value"> flex</span></span>; <span class="hljs-rule"><span class="hljs-attribute">flex-flow</span>:<span class="hljs-value"> row wrap</span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-class">.tech-option</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">padding</span>:<span class="hljs-value"> <span class="hljs-number">0</span> <span class="hljs-number">10px</span></span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-class">.tech-check</span>, <span class="hljs-class">.tech-label</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">vertical-align</span>:<span class="hljs-value"> middle</span></span>; <span class="hljs-rule">}</span></span> <span class="hljs-class">.tech-check</span> <span class="hljs-rules">{ <span class="hljs-rule"><span class="hljs-attribute">margin</span>:<span class="hljs-value"> <span class="hljs-number">0</span></span></span>; <span class="hljs-rule">}</span></span> </span><span class="hljs-tag"></<span class="hljs-title">style</span>></span> |
We’re emitting the submit
event so the parent can handle the creation of a new tutorial in the store instead of adding that logic here. This allows it to be more generic and reusable. With a few tweaks like making the label of the button dynamic, we could make this truly generic so it could also be used for editing tutorials, but this will do for now.
You may have noticed the use of the FormInput
component, which doesn’t exist yet. This is a helper component to remove some repetition and boilerplate for the forms. Let’s create that component now in components/FormInput.vue
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<span class="hljs-tag"><<span class="hljs-title">template</span>></span> <span class="hljs-tag"><<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"form-group"</span>></span> <span class="hljs-tag"><<span class="hljs-title">label</span> <span class="hljs-attribute">:for</span>=<span class="hljs-value">"name"</span>></span><span class="hljs-tag"><<span class="hljs-title">slot</span>></span><span class="hljs-tag"></<span class="hljs-title">slot</span>></span><span class="hljs-tag"></<span class="hljs-title">label</span>></span> <span class="hljs-tag"><<span class="hljs-title">input</span> <span class="hljs-attribute">:type</span>=<span class="hljs-value">"type"</span> <span class="hljs-attribute">:name</span>=<span class="hljs-value">"name"</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"form-control"</span> <span class="hljs-attribute">:value</span>=<span class="hljs-value">"inputValue"</span> @<span class="hljs-attribute">change</span>=<span class="hljs-value">"updateValue"</span> @<span class="hljs-attribute">input</span>=<span class="hljs-value">"updateValue"</span> <span class="hljs-attribute">:required</span>=<span class="hljs-value">"required"</span>></span> <span class="hljs-tag"></<span class="hljs-title">div</span>></span> <span class="hljs-tag"></<span class="hljs-title">template</span>></span> <span class="hljs-tag"><<span class="hljs-title">script</span>></span><span class="javascript"> export <span class="hljs-keyword">default</span> { props: { name: { type: <span class="hljs-built_in">String</span>, required: <span class="hljs-literal">true</span> }, type: { type: <span class="hljs-built_in">String</span>, <span class="hljs-keyword">default</span>: <span class="hljs-string">'text'</span> }, required: { type: <span class="hljs-built_in">Boolean</span>, <span class="hljs-keyword">default</span>: <span class="hljs-literal">false</span> }, value: {} }, data () { <span class="hljs-keyword">return</span> { inputValue: <span class="hljs-keyword">this</span>.value } }, watch: { value(value) { <span class="hljs-keyword">this</span>.inputValue = value }, inputValue(value) { <span class="hljs-keyword">this</span>.$emit(<span class="hljs-string">'change'</span>, value) <span class="hljs-keyword">this</span>.$emit(<span class="hljs-string">'input'</span>, value) } }, methods: { updateValue(e) { <span class="hljs-keyword">this</span>.inputValue = e.target.value } } } </span><span class="hljs-tag"></<span class="hljs-title">script</span>></span> |
Sweet! Now there’s only one thing left to do: add this form to the app. Open up components/App.vue
one more time and make the following edits:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<span class="hljs-tag"><<span class="hljs-title">template</span>></span> <span class="hljs-comment"><!-- ... --></span> <span class="hljs-comment"><!-- Add the following above the LoadingOverlay with the TutorialList --></span> <span class="hljs-tag"><<span class="hljs-title">LoadingOverlay</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"well"</span> <span class="hljs-attribute">:isLoading</span>=<span class="hljs-value">"formIsLoading"</span>></span> <span class="hljs-tag"><<span class="hljs-title">h3</span>></span>Add A Tutorial<span class="hljs-tag"></<span class="hljs-title">h3</span>></span> <span class="hljs-tag"><<span class="hljs-title">TutorialForm</span> @<span class="hljs-attribute">submit</span>=<span class="hljs-value">"addTutorial"</span> <span class="hljs-attribute">:tutorial</span>=<span class="hljs-value">"tutorial"</span> /></span> <span class="hljs-tag"></<span class="hljs-title">LoadingOverlay</span>></span> <span class="hljs-comment"><!-- ... --></span> <span class="hljs-tag"></<span class="hljs-title">template</span>></span> <span class="hljs-tag"><<span class="hljs-title">script</span>></span><span class="javascript"> <span class="hljs-comment">// ... Import TutorialForm</span> import TutorialForm from <span class="hljs-string">'./TutorialForm'</span> export <span class="hljs-keyword">default</span> { <span class="hljs-comment">// Add the TutorialForm component</span> components: { TutorialList, TutorialForm, Pagination, SearchBox, RadioGroup, LoadingOverlay }, data: () => ({ <span class="hljs-comment">// ... Add the following properties</span> tutorial: {}, formIsLoading: <span class="hljs-literal">false</span> }), <span class="hljs-comment">// ...</span> methods: { <span class="hljs-comment">// Add the following methods</span> addTutorial(tutorial) { <span class="hljs-built_in">window</span>.tut = tutorial <span class="hljs-keyword">this</span>.setIsLoading(<span class="hljs-string">'form'</span>, <span class="hljs-keyword">this</span>.$store.dispatch(<span class="hljs-string">'tutorials/addTutorial'</span>, tutorial).then(() => <span class="hljs-keyword">this</span>.tutorial = {})) }, <span class="hljs-comment">// Add the following helper for controlling loading spinners</span> setIsLoading(sectionName, promise) { <span class="hljs-keyword">this</span>[sectionName + <span class="hljs-string">'IsLoading'</span>] = <span class="hljs-literal">true</span> promise .then(() => <span class="hljs-keyword">this</span>[sectionName + <span class="hljs-string">'IsLoading'</span>] = <span class="hljs-literal">false</span>) .catch(() => <span class="hljs-keyword">this</span>[sectionName + <span class="hljs-string">'IsLoading'</span>] = <span class="hljs-literal">false</span>) } }, created() { <span class="hljs-comment">// Update to use the setIsLoading helper</span> <span class="hljs-keyword">this</span>.setIsLoading(<span class="hljs-string">'list'</span>, <span class="hljs-keyword">this</span>.$store.dispatch(<span class="hljs-string">'tutorials/load'</span>)) } } </span><span class="hljs-tag"></<span class="hljs-title">script</span>></span> |
And that’s everything! We’ve now updated our app to pull data from a Kinto back end, use Vuex as the store, and add/delete tutorials from the list! It was a quick and wild ride, but we made it and hopefully, we learned a little bit by the end of it.