This tutorial is out of date and no longer maintained.
Note: This is part 2. Be sure to read part 1, the introduction to this tutorial series:
One of my favorite things about Vue.js is how approachable it is. We can simply drop the library into an existing project, create a Vue
instance with an element or ID of our choosing as a selector, and we’re all set to add reactivity to the page. This simplicity is great and comes in handy if we just want to use a few of Vue’s features, but there’s actually a lot more we can do with the library that some people may not be aware of.
Surrounding the core Vue.js library is a rich ecosystem of tools and plugins that allow us to create full single-page applications. Vue also offers full support for ES2015 and comes with its own file type: the .vue
component, which is great because it allows us to have our template, scripts, and styles all in the same file. While some might say that this could be cumbersome and file sizes could get huge, I would argue that the number of clicks and amount of mental bandwidth (even if small) that we save by using this kind of format makes it quite valuable.
We’re going to use several libraries and plugins from the Vue ecosystem to:
.vue
file type to have our templates, scripts, and styles all in one spotI thought it might be interesting to revisit the time tracker app tutorial from last year. In that tutorial, we built a (slightly feature lacking) time tracker with Angular 1.x and Laravel 5. Creating a similar app with Vue.js and Node should be a good way to contrast the two stacks if you’re interested in that kind of thing.
Note: The app we’re going to build is going to be pretty simplistic as far as tracking time goes. That’s because we want to focus more on how all the pieces of a Vue.js app fit together and less on making a real-world time tracker.
So let’s get to work – you’ll be logging your hours like a champ in no time.
The app we build will use Webpack for module bundling, preprocessing, and hot module reloading. If you’re not familiar with Webpack, it basically gives us a way to automatically string together and then serve our various bits of JavaScript from a single file. This is great for production because it means we don’t need to worry about making multiple HTTP requests for our various component files. But there’s more than that going on here: we need Webpack so that we can have our .vue
files handled properly. Without a loader to convert the files to proper JavaScript, HTML, and CSS, the browser won’t be able to understand what’s going on. However, with the appropriate Webpack loaders in place, the .vue
files get converted to something the browser can understand.
Hot module reloading is a powerful feature that gives us a lot of convenience, and it’s available to us out of the box with Vue. Normally when we refresh the page after making a change to our code, all of the application’s state is lost. This can be a pain when we’re working on a part of the app that requires us to be a few clicks deep or otherwise have some kind of state be active. With hot module reloading, the code that we change is swapped in automatically and our state is preserved.
Vue also makes CSS preprocessing available to us automatically, so we can choose to write LESS or SASS instead of plain CSS if we like.
It used to be the case that we had to install a bunch of dependencies from npm to get a proper Webpack setup, but thankfully we now have the option of using vue-cli. This is a great addition to the Vue ecosystem because it means we no longer need to scaffold our projects by hand, but rather can have them generated for us very quickly.
First, install vue-cli.
Then create a new webpack
project and install the dependencies for it.
The generated app comes with a command that lets us run the application with hot module reloading.
This command is an alias for node build/dev-server.js
, and is found in package.json
on the scripts
object. The dev-server.js
file that gets run is basically what makes Webpack do its thing. That is, it configures all of the Webpack pieces, parses and compiles all the project files, and serves everything up to be viewed in the browser. We can take a look at localhost:8080
to see the starter app.
Everything is scaffolded, but we’re actually going to need a couple more dependencies for our time tracker app. We need a way to set up routing and make XHR requests, and for that, we can use some additional libraries from the Vue ecosystem.
Taking a look at the app file, we can see that the src
directory root has an App.vue
file and a main.js
file. It’s the main.js
file that is the starting point where things get kicked off. Inside that file, we’re importing both Vue
and App
, and then creating a new Vue instance on the body
element. While this is fine if we don’t want to use any routing, we’re going to need to change things up a bit to support the setup we want.
To support this new setup, we just need to wrap the <app></app>
element in index.html
within a div
.
Our application now supports routing, and if we refresh, we see that the default /hello
route is reached which displays the same message as before.
The initial setup is done, now let’s get on to creating our own components!
Let’s kick things off here by applying a navigation bar to the app. But first, we’ll need to add Bootstrap for some styling. A quick way to do this is to grab the CSS from Bootstrap’s CDN.
The best place for the navbar is our App.vue
file, so let’s edit it to have a new template. At the same time, we can get rid of the script
and style
sections that are there right now.
In addition to the navigation bar, we’ve also got a container for the rest of the application. There’s a smaller div
that will be used for our sidebar, and a larger one that will show other components through the router-view
tag. Just in the same way that other frameworks, such as AngularJS, will display partial content through a special tag, vue-router does this as well through router-view
.
The Home
component for our app really just needs to show a simple message. Let’s create a new file called Home.vue
and give it a template with that message.
We also need to tell the router about this new component in App.vue
by importing it and mapping it. This is an easy change because essentially all we need to do here is replace Hello
with Home
and we’re good to go.
So what’s going on with that blank space in the App
component? Well, we’ll later have a sidebar
going in there which will display the value for the sum of all of our time entries, but first, we need to create some. For that, we’ll need to get started on components that let users log and display individual entries.
Here’s the plan: we’ll make a component that lists out existing time entries, called TimeEntries
, and then another one for entering new time entries, called LogTime
. We’ll add a link from TimeEntries
to LogTime
so the user can jump to the spot for adding new time entries quickly. The TimeEntries
component is going to be a little large, so let’s approach the template, script, and style sections separately. Keep in mind that all of these parts will go in the same file in the end, but we’ll split them out here.
Note: The data that is used in this template hasn’t been set up yet, but we’ll do that afterward.
Let’s take a look at the Vue.js-specific items in the template to get a feel for what’s going on. The first one we encounter is a v-if
on both the Log Time button and an h3
that says “Log Time”. The v-if
is checking where we are in our routing, and we’re saying that we don’t want the button to be displayed if the route we’re currently at is /time-entries/log-time
.
We’re going to create the LogTime
component later and we’ll see how this all fits together at that point, but essentially once we’re at this route, we don’t want to show the button for navigating to it. The same goes for the “Log Time” h3
, but in reverse. We’ve also got a v-link
in the button at the top, which we’ve actually seen previously. This directive is from vue-router
and will instruct the router to go to the /time-entries/log-time
route when clicked.
The next thing we see is another router-view
below the hr
tag, and this is because we’ll be registering a sub-route for logging time entries. Essentially we’re nesting one route within another, so the LogTime
component will be two levels deep. This is the cool thing about routing–we can just keep putting router-view
elements in our templates, and as long as we register a component for them, they will keep nesting further down.
The first p
tag we see has a v-if
condition on it which checks for whether an array called timeEntries
has anything in it. If it’s empty, a message indicating so will be displayed. Next, we get to our first repeater. The form for Vue.js repeaters is v-for="alias in collection"
, which is very similar to the pattern we see in other frameworks.
If you’re unfamiliar with repeaters, what this does is loop over an array and outputs some HTML for each item in the array, starting with the HTML element that the repeat statement is placed on, up to the element that closes it.
We’re using a new syntax shorthand in the img
tag for binding the user’s profile picture to the src
attribute. Using the colon is the same as doing v-bind
and saves us a few keystrokes. By binding the image URL to the src
attribute like we are, we don’t need to template it out with the curly braces. Rather, the binding gets evaluated and resolves to the URL provided in our data (which we’ll see below).
We’re templating out the information for the time entry throughout the rest of the template, and near the end, we have a button that uses another of Vue’s shorthands–@click
. This binds a click event to the button element which calls the deleteTimeEntry
method (which we’ve yet to create), passing in the currently iterated-over time entry.
Next, let’s put in the script.
We’re starting out with one-time entry that has a user
object, as well as a comment
, totalTime
value, and date
. The method for deleting time entries is fairly simple – we’re just looking for the index of the clicked time entry in the array and then splicing it out. Remember that we’re just working with local data in this part of the tutorial series, but in later installments, we’ll see how to work with remote data that is persisted to a database. We’re also calling $dispatch
as part of the delete method. We’ll see more on this later, but essentially what we want to do here is dispatch an event with the data that we deleted so we can listen for it in other components. This will come into play when we get to coding the sidebar.
Finally, we’ve got a timeUpdate
method on the events
key. This method is going to come into play when we make the LogTime
component and is what will be emitted when the user saves a time entry. When that happens, a timeEntry
object will come through and be pushed onto the timeEntries
array in this component. This is how we will be facilitating communication between different components for now.
Let’s not forget some simple styles for this component.
We just need to add routing for this component to main.js
and we’ll be able to see it in the browser.
Next, we need a component that provides a screen for the user to log time entries. This one is a bit smaller, so let’s take a look at the whole file all at once.
We’ve got input elements in place for the user to enter a date, number of hours, and a comment for a new time entry. In the data()
function, we’re initializing the timeEntry
model with some data for the user so that we can have a name and profile photo. There’s an obvious limitation with this simple app: it’s only really good for one user. That’s by design at this stage though, and we’ll adapt the application to multiple users with login in a later installment.
We’ve got a single method in this component called save
which takes the object that has the user’s input and dispatches it out of the component with $dispatch
. Now in our other components, we can listen for the dispatched timeUpdate
event and grab the timeEntry
object that comes through with it. So why are we doing this? So that we can have some method of communicating data between our components. Since data for a given component is scoped to it, we need some mechanism to communicate from that component to the outside world. This method works well enough but can get messy when our app grows and needs to communicate a lot of data between many components. We’ll see how to fix this by using Vuex for state management later on in the series. We’ve already got our event listener set up on the TimeEntries
component, so when we save a new entry, we can see it get added to the list.
We need to add the LogTime
component as a sub-route of TimeEntries
in our router configuration. By doing this, the router will know that LogTime
is a child of TimeEntries
, and the appropriate URI structure will be generated when we navigate to it.
We’re almost done with the first part of this series. The only thing left to do now is to add a Sidebar
component which will hold the total number of hours for all of our time entries. Before we code the actual component, we need to make some adjustments to App.vue
so that we can handle the data that is dispatched from the LogTime
component when new entries are saved, and from the TimeEntries
component when they are deleted.
First, place the sidebar
element into the template.
We’ve got a property binding on sidebar
called time
that points to totalTime
. By binding the property in this way, we’ll be able to pick it up in the actual Sidebar
component as a prop
. If you’ve used React at all, passing properties into elements in this way will no doubt feel familiar.
We need some logic to calculate the values to pass down in the time
property.
First, we import the Sidebar
component (which we’ll create next) and then we initialize the totalTime
property with the value from our hard-coded time entry. This obviously isn’t a very robust way of picking up that initial value, but we’ll just leave it like this for now because things will change when we modify the app to consume remote data in the next article anyway. We’re listening for two events: timeUpdate
and deleteTime
. Then we’re simply incrementing and decrementing totalTime
based on the values that come through from the data in those events.
With that in place, we can create the actual Sidebar.vue
component.
The props
array is where we can specify any properties that we want to use which are passed into the component, and here we are getting the time
prop which is placed on the sidebar
element in App.vue
. The template simply prints out the total number of hours.
There we have it! We’ve created the most basic of time tracking apps that will really never be very useful in the real world. However, we were able to get a good sense of how to build a full single-page application with Vue.js, and that’s the name of the game for this series. We’ve now got a solid foundation for how to use Vue components to build a SPA, and with that knowledge in place, we can move on to more complex concepts.
The Vue.js ecosystem is maturing all the time and Evan is constantly introducing additions and improvements to it, including new tooling and additional plugins. Vue.js and its associated libraries really do make it easy to develop single-page apps with modern JavaScript, and the component system with .vue
files is great to work with because the template, scripts, and styles for a given component are nicely encapsulated.
This is the first article in our series on building a SPA with Vue.js. Stay tuned for the next one where we’ll build the backend for this app and see how we can use vue-resource to work with remote data.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!