Building a 90 Day Planner with Laravel and Vue.js

Alexander van Zyl
Jun 1, 2017 · 21 min read
90 Day Action Planner built with Laravel and Vue.js.

In this multi-part tutorial we will be building a 90 Day Action Planner using Laravel and Vue.js to create a single page application. Why 90 days? Well here is a snippet from the article on FastCompany where I got the inspiration from.

“90 days is the perfect unit of time to make headway on your big-picture goals–and to give them the focus they need”

The general idea is that you select 3 main goals to focus on for the next 90 days. You then define your criteria for success and what actions you should take, as well as by who and when.

Now that we have a general idea of what we are building, let’s jump into it!

Source code for this tutorial can be found here.

What will we cover?

  1. Getting set up — installing Laravel, Vuejs and other required front-end dependencies.
  2. Start building our front-end — creating our home page and a multi-step form.

Requirements:

  • Basic knowledge of Laravel and Vue.js
  • A local development environment; can be Homestead, Valet, my personal favorite Docker (I even created a simple dev. environment here) or something similar.
  • Your favorite text editor and console.
  • Vue.js Devtools extension to make debugging a breeze.

Getting Set Up

Step 1 — Install Laravel

composer create-project --prefer-dist laravel/laravel 90dayplanner

Let’s do a quick check to see if everything got pulled in. Pop open a browser and point it to the project (in my case that is http://localhost:8000/) and you should see the default welcome page.

Default Laravel welcome page after installation.

Step 2 — Install Vue.js and other frontend dependencies.

What are we installing?

  • vue-router: the official vue.js router to handle client-side rooting.
  • vuex: centralized state management, so we can easily share states between components.
  • foundation-sites: UI framework by Zurb, we will be using this over Bootstrap.
  • motion-ui: to handle UI transitions and animations.
  • font-awesome: icon font-pack.

I will make use yarn as I find it to be faster. Although npm 5 does boast performance improvements but I am not sure how they compare. Either way I will be referencing both yarn and npm commands.

Let’s open up a console make sure to cd into the applications root folder (should be 90dayplanner unless otherwise specified before) and run the following commands:

# Remove bootstrap 3
yarn remove bootstrap-sass
# or with npm
npm uninstall bootstrap-sass --save-dev
# Install all other required dependencies
yarn add vue-router vuex foundation-sites motion-ui \
font-awesome --dev
# or with npm
npm install vue-router vuex foundation-sites motion-ui \
font-awesome --save-dev

Okay now there is one caveat with we need to take care of. That is Vuex provides mapping helpers that make use of the object spread operator. But as of writing this is still part of the stage-3 ECMAScript proposal.

Here is an example use case:

computed: {
localComputed () { /* ... */ },
// mix this into the outer object with the object spread operator
...mapState({
// ...
})
}

To compile ECMAScript 6 to native JavaScript that can be understood by the browser, Laravel Mix makes use of Babel. By default object spread operator is not included in Babels ES2015 preset. To get this working will have to set up our own .babelrc file and define our own presets. Within the project root folder let’s create a .babelrc file.

File: ./.babelrc

{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 2%"],
"uglify": true
}
}],
"stage-3"
]
}

What’s going on here?

  • "modules": false: don’t transform any ECMAScript 6 module syntax to another module type.
  • "browsers": ["> 2%"]: when compiling to native JavaScript we want it to be compatible with browser that have more than 2% global usage (based off of Can I Use data).
  • "stage-3": include everything from the stage-3 preset.

With that set up we still need install the stage-3 preset . Let’s switch back to the console and run:

yarn add babel-preset-stage-3 --dev
# or with npm
npm install babel-preset-stage-3 --save-dev

Great! Now we can use object spread operator.

We don’t need to worry about installing the env preset, as it was installed when we set up Laravel Mix.

Tip: Apart from looking inside the ./package.json file. You can also see what packages have been installed via the console. yarn list --depth=0 will show all first level packages, basically everything you see in the ./node_modules folder. npm ls --depth=0 lists exactly whats in ./package.json

Finally we just need to set our project to use Foundation Sites over Bootstrap. Let’s open up ./resources/assets/js/bootstrap.js and change require('bootstrap-sass'); to require('foundation-sites');

File: ./resources/assets/js/bootstrap.js

Full bootstrap.js file

Starting on the frontend

In this section we will start building out some of our frontend components. There is a fair amount to cover so we will focus developing our backend API in a future post.

Step 1 — Create an entry point page.

The entry page is just a view that includes all the boilerplate (like scripts, styles and so on) which allows Vue to render its components. So let’s do that now by creating a new file index.blade.php inside of the ./resources/views folder.

File: ./resources/views/index.blade.php

<!doctype html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no,
initial- scale=1.0, maximum-scale=1.0,
minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name') }}</title>

<!-- Styles -->
<link rel="stylesheet"
type="text/css" href="{{ mix('css/app.css') }}">
</head>
<body>
<div id="app">
<router-view></router-view>
</div>
</body>

<!-- Compressed JavaScript -->
<script src="{{ mix('js/app.js') }}"></script>
</html>

What’s going on here?

  • We are using mix()to pull in our assets because every time an asset is changed and recompiled for production it gets a suffixed with a new hash. It would be pretty annoying if we had to manually edit our view to include the updated assets every time. But luckily Laravel Mix creates a mapping for us (see ./public/mix-manifest.json) so we can just reference the file by its name and mix will pull in the hashed version for us. Why bother with hashing our assets in the first place? Have you ever made changes to your style or script files pushed to the sever, refreshed your browser and…nothing changes? By hashing our assets every time we make changes we bust the cache which then loads in the new changes.
  • router-view is where we will render the pages (vue components) for specific routes.

Let’s tell Laravel to use our new view. Let’s open up ./routes/web.php and instead of using the welcome view we will use our new index view, like so:

Route::get('/', function () {
return view('index');
});

While we at it let’s go ahead and delete ./views/welcome.blade.php, we no longer need it.

Step 2 — Create first route and home page.

To create our first route using vue-router we need to initialise it and then actually tell Vue to use it. So lets first create a new routes.js file inside of the ./resources/assets/js folder. This will house all our client-side routes.

File: ./resources/assets/js/routes.js

import Vue from 'vue';
import VueRouter from 'vue-router';

// Make sure vue is using the router
Vue.use(VueRouter);

// Initialize VueRouter
export default new VueRouter({
routes:[
{
path:'/',
name: 'home',
component: resolve => require(['./pages/Home.vue'], resolve)
}
]
});

What’s going on here?

  • Because we using a module builder (webpack) we have to explicitly tell Vue to use the router hence Vue.use(VueRouter) .
  • Next we initialise the router and create our home route. We give it a name of “home”. When the user visits the home page the router renders the Home.vue component.
  • Can’t we just use require('./pages/Home.vue') what’s all this fancy resolve stuff? By using resolve we can make use of lazy loading routes. How this works is for every page a separate .js file is created instead of being bundled with our main app.js file. Only when the user visits the home page does this file get pulled in. Basically we are just splitting the code to reduce the overall size of the app.js file. Admittedly at this point in time, it is a bit overkill. But thought it would be interesting to cover nonetheless. You could also import every single page at the top of routes.js like import Home from './pages/Home.vue'; and then component: Home. Feel free to use which method prefer.
  • Lastly if you haven't seen arrow functions before for example resolve => require([‘./pages/Home.vue’], resolve) it is the same as writing this:
function (resolve) {
require(['./pages/Home.vue'], resolve]);
}

Once you get used to arrow functions you might find they read better. What do you think?

Even though we’ve told Vue to use our router we still need to inject it into our Vue application. We do this inside ./resources/assets/js/app.js. At the very top we import router from './routes'; and then down at the bottom where we initialise our application we inject the router.

File: ./resources/assets/js/app.js

import router from './routes';/**
* First we will load all of this project's JavaScript dependencies
* which includes Vue and other libraries. It is a great starting
* point when building robust, powerful web applications using
* Vue and Laravel.
*/
require('./bootstrap');window.Vue = require('vue');/**
* Next, we will create a fresh Vue application instance and attach
* it to the page. Then, you may begin adding components to this
* application or customize the JavaScript scaffolding to fit
* your unique needs.
*/
const app = new Vue({
router,
el: '#app'
});

Next we need to create our home page this is nothing but a Vue component. For now we just going to include some HTML and the required export statement.

File: ./resources/assets/js/pages/Home.vue

Full Home.vue file

Right now if we go ahead and try to compile our assets we going to get a fat error. This is because we haven’t updated our app.scss file which is still trying to import Bootstraps styles and not Foundation Sites.

Error missing bootstrap module.

To fix this we need to open up ./resources/assets/sass/app.scss and delete everything and replace it with the below:

// Fonts
@import url('https://fonts.googleapis.com/css?family=Rubik');

// Settings
@import 'settings';

// Import Foundation and Font Awesome
@import "node_modules/foundation-sites/scss/foundation";
@import "node_modules/font-awesome/scss/font-awesome";

// Include everything from foundation and
// use flex-grid for our grid system.
@include foundation-everything($flex: true);

To get started with a _settings.scss file we can copy the one that came with Foundation Sites when we installed it. We can do this via the console like so:

cp node_modules/foundation-sites/scss/settings/_settings.scss \ resources/assets/sass

This will copy _settings.scss file to ./resources/assets/sass folder. All we need to do now is update the path to our _util.scss file and tell Foundation we want to use the Rubik font throughout the site.

// around line 44 change
// @import 'util/util'; to
@import 'node_modules/foundation-sites/scss/util/util';

// around line 66 change
// $body-font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif; to
$body-font-family: 'Rubik', Helvetica, Roboto, Arial, sans-serif;

We can also go ahead and delete the _variables.scss file, it’s no longer needed.

With any luck now we should be able to compile our assets without error. Let’s fire up a console and run yarn dev or if you using npm npm run dev.

Compiling assets with yarn,

Open the browser navigate to the project (in my case this is on http://localhost:8000) and let’s see what we have!

90 Day Planner home page.

Step 3 — Add basic styling

Without focusing too much on design. Let’s try make things look better with a bit of styling. If we open up Home.vue and look at the HTML there are a few CSS classes we need to implement:

  • mt-50
  • shadow
  • callout-header
  • circle-bullets

It will become obvious what each one does as we implement them. First let’s create a _helpers.scss file inside of the ./resources/assets/sass folder.

File: ./resources/assets/sass/_helpers.scss

@for $i from 1 through 50 {
// margin helper classes
.m-#{$i} {margin: #{$i}px;}
.mt-#{$i} {margin-top: #{$i}px;}
.mb-#{$i} {margin-bottom: #{$i}px;}
.ml-#{$i} {margin-left: #{$i}px;}
.mr-#{$i} {margin-right: #{$i}px;}

// padding helper classes
.p-#{$i} {padding: #{$i}px;}
.pt-#{$i} {padding-top: #{$i}px;}
.pb-#{$i} {padding-bottom: #{$i}px;}
.pl-#{$i} {padding-left: #{$i}px;}
.pr-#{$i} {padding-right: #{$i}px;}
}

.shadow {
box-shadow: 0 0 6px 0 $dark-gray;
}

.circle-bullets > li {
list-style: none;
margin-bottom: 5px;

> span {
color: $white;
font-weight: bold;
display: inline-block;
height: rem-calc(40px);
width: rem-calc(40px);
background-color: $primary-color;
border: 2px solid scale-color($primary-color, $lightness: 60%);
border-radius: 50%;
vertical-align: middle;
line-height: rem-calc(40px);
text-align: center;
margin-right: rem-calc(10px);
}
}

What’s going on here?

  • First we create margin and padding helpers with a range of 1 to 50 pixels by making use of the Sass @for directive. As an example m-10 would give us an all-round margin of 10 pixels, mt-50 gives us just a top margin of 50 pixels and so forth. You’ve probably already guessed mb-* is for bottom margin, ml-* is for left margin, and mr-* is for right margin. Padding works in same way.
  • .shadow creates a dark-gray box-shadow with a 6 pixel blur.
  • .circle-bullets changes the style of our unordered list of steps and creates a circle around each step. rem-cal() takes a pixel value and returns the equivalent rem unit. scale-color() can be used to lighten or darken a color by specifying a percentage.

Let’s add our new _helper.scss file to ./resources/assets/sass/app.scss. So after @include foundation-everything($flex: true); we add the following:

// Helpers
@import "helpers";

To make our lives a bit easier let’s use browserSync to automatically refresh the browser every time we make changes. Let’s add browserSync to ./webpack.mix.js after our sass function:

mix.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css')
.browserSync({
proxy: '127.0.0.1:8000',
browser: 'google-chrome'
});

Note:

  • proxy: in my case I am telling browserSync to proxy to localhost on port 8000 as this is where I am running my server. Make sure to change this to reflect your environment. If you have a local domain like my-app.dev you could pass it through like so browserSync({proxy: 'my-app.dev'}) or simply browserSync('my-app.dev'). If you don’t specify a proxy to my understanding it defaults to app.dev. This is unique to Laravel Mix.
  • browser: here I am explicitly telling browserSync to open up in Chrome as my default browser is set to Firefox. Don’t set this parameter if you want to use your default system browser.

To start up browserSync all we need to do is run yarn watch or npm run watch from the console. It should open up the browser at localhost port 3000 and we’ll see out updated home page.

Home page after running yarn watch.

The next thing we going to do is extend the callout component given to use by Foundation Sites. Create a new directory inside of the sass folder called components inside this directory create a new file called _callout.scss.

Sass directory structure.

File: ./resources/assets/sass/components/_callout.scss

Full _callout.scss file.

What’s going on here?

  • We have two mixins. callout-header-size() which sets the padding and margin size of our callout header. Then we have callout-header-style() which is used to set the background, text color and the top left and right border radius.
  • All the variables starting with $callout-* are the default variables set in our _settings.scss file. The only new variable we have introduced here is$callout-header-background which sets the background color to secondary as the default.
  • Next we have an @each directive that loops over the colors in our $foundation-palette map . For each color it creates a class that we can use to set the callout headers text a background color.
// Foundation color palette map
$foundation-palette: (
primary: #1779ba,
secondary: #767676,
success: #3adb76,
warning: #ffae00,
alert: #cc4b37,
);
  • Finally in the last section we are saying if the parent <div> with class .callout also has the class .small or .large adjust the size of our .callout-header accordingly. You will notice the ampersand (‘&’) has been placed to the right of .callout.large and .callout.small this allows us to reference our parent class even though we nested inside .callout-header. When this compiles to CSS we get the below:
.callout.large .callout-header { // styling for large header }
.callout.small .callout-header { // styling for small header }

Now we just need to import _callout.scss file into app.scss just like we did before with _helpers.scss file.

File: ./resources/assets/sass/app.scss

// Fonts
@import url('https://fonts.googleapis.com/css?family=Rubik');

// Settings
@import 'settings';

// Import Foundation and Font Awesome
@import "node_modules/foundation-sites/scss/foundation";
@import "node_modules/font-awesome/scss/font-awesome";

// Include everything from foundation and
// use flex-grid for our grid system.
@include foundation-everything($flex: true);

// Components
@import "components/callout";

// Helpers
@import "helpers";

It’s a good idea to keep all our variables inside of _settings.scss. This gives us a single place where we can change variable settings without having to hunt them down. Especially as the project grow.

So let’s add our new $callout-header-background variable to our _settings.scss file. Around line 272 you should see all the variables for the Callout component. Let’s add our new variable after $callout-link-tint Then let’s set $callout-header-background color to $primary-color and $callout-radius to 5 pixels.

// 13. Callout
// -----------

$callout-background: $white;
$callout-background-fade: 85%;
$callout-border: 1px solid rgba($black, 0.25);
$callout-margin: 0 0 1rem 0;
$callout-padding: 1rem;
$callout-font-color: $body-font-color;
$callout-font-color-alt: $body-background;
$callout-radius: 5px;
$callout-link-tint: 30%;
$callout-header-background: $primary-color;

Since we have browserSync watching for changes in the background it will compile our assets and we should be able to see our new changes. We probably not going to win any design awards but it does look better!

Home page after adding callout-header style.

Step 4— Build the step-bar

Step bar showing the users progress.

For now our step-bar will have a 3 states:

  • complete state (green),
  • active state (blue) and
  • neutral state (gray).

Before we start styling our step-bar we actually need somewhere to put it. When we click the “Get Started” button on the home page it will re-route us to /create. Right now this page doesn’t exist and we haven’t told Vue how to handle it, so let’s do the now.

Inside ./resources/assets/js/routes.js and after the home route we will add our new route.

routes:[
{
path:'/',
name: 'home',
component: resolve => require(['./pages/Home.vue'], resolve)
},
{
path:'/create',
name: 'create',
component: resolve => require(['./pages/Create.vue'], resolve)
}
]

Next let’s create our new Create.vue component.

File: ./resources/assets/js/pages/Create.vue

<template>
<div>
<step-bar :steps="steps"></step-bar>

<div class="row align-center">
<div class="column large-8">
<router-view></router-view>
</div>
</div>
</div>
</template>

<script>
import StepBar from '../components/StepBar';

export default {
data() {
return {
steps: [
{label: 'Goal', route: 'create.goal'},
{label: 'Metrics', route: 'create.metrics'},
{label: 'Actions', route: 'create.actions'}
]
}
},

components: {
StepBar
}
}
</script>

What’s going on here?

  • Starting from the top within our <template> tag we have a <step-bar> tag this corresponds to our StepBar component. The StepBar component expects an array of steps to be passed. This is done via the steps property.
Step-bar HTML component explanation.
  • <router-view> is where the page for each step will be rendered.
  • Inside our script tag the data object has an array of step objects which consists of two properties label : the name of the step and route : the name of route where the page for that step will be loaded.

It should all make sense when we create our StepBar component. We can do this now. Let’s create a new directory inside ./resources/assets/js and call it components. Now inside that directory we create a new file StepBar.vue.

File: ./resources/assets/js/components/StepBar.vue

Full StepBar.vue file.

What’s going on here?

  • Let’s start with what happening between the <script> tags. Here we are defining a required property of “steps” which must be of type “Array”.
  • In our <template> tags all we doing here is looping through each step using Vues v-for directive to create an unordered list of steps. If the steps route name matches the current route we give it a class of .active .
  • For the first time we are introducing <style> tags inside of our component. You will notice we define a lang attribute which we set to SCSS. This tells webpack that we are using SCSS and it should use the Sass preprocessor when compiling to CSS.
  • Next we have to import our SCSS settings. Allowing us to make reference to our variables and utilities like rem-cal(). Now for the actual styling I am not going to explain line-for-line but the diagram below should give you a pretty clear idea of what each section is doing.
Styling the step-bar.

With the styling done let’s head back to the browser. When we click on the “Get Started” button on the home page it should route us to /create. Given everything compiled successfully we should see out step-bar.

Step-bar after compiling.

If we open up Developer Tools in Chrome F12 or if you are on a mac Cmd + Opt + I and click on Vue to open up Vue Devtools. We can actually see all the properties on our StepBar component. In this case it has only one “steps” which we can see is the array of steps we passed to it. Awesome!

StepBar component props.

Step 5 — Create a shared state.

A shared state gives us a single point of truth that we can share across multiple components.

To achieve this we could of course knockout our own state manager. But as the application grows this can become a debugging nightmare. Vuex on the other hand interrogates very well with Vue Devtools. Giving us the ability see what mutations are taking place as we perform each action. If you new to Vuex and want to get a better understanding, I would recommend reading the introduction on the official Vuex.

Without further ado let’s create our state. We going to create a new folder and file like so ./resources/assets/js/store/store.js

File: ./resources/assets/js/store/store.js

import Vue from 'vue';
import Vuex from 'vuex';
import router from '../routes';

Vue.use(Vuex);

export default new Vuex.Store({
state: {
currentStepRoute: '',
steps: []
},

mutations: {
SET_STEPS (state, steps) {
state.steps = steps;
// Set the current route to the first steps route.
state.currentStepRoute = steps[0].route;
},

UPDATE_CURRENT_STEP_ROUTE (state, route) {
state.currentStepRoute = route;
}
},

actions: {
setSteps({commit}, steps) {
commit('SET_STEPS', steps);
},

updateCurrentStepRoute({commit}, route) {
commit('UPDATE_CURRENT_STEP_ROUTE', route);
},

goToNextStep({state}) {
// Find the current steps index using the route
// name and add one for the next steps index
let nextStepIndex = _.findIndex(state.steps, (step) => {
return step.route === state.currentStepRoute;
}) + 1;

// Only go to the next step if it isn't the last one...
if (nextStepIndex !== state.steps.length) {
router.push({name: state.steps[nextStepIndex].route});
}
}
}
});

What’s going on here?

  • We import our router allowing us to route to different location inside of our store. We also tell Vue that we want to use Vuex.
  • state is a data object that serves as single point of truth. Right now its purpose is to keep track of the current steps route. It also contains an array of all the steps. Any change to our state will reflect throughout our application.
  • mutations are responsible for making changes to our state. The general idea is the state shouldn’t be changed outside of the store. When defining mutation function the first argument will be the state object.
  • actions on the other hand can only commit mutations and are not responsible for actually performing the change on the state object. We don’t always have to commit a mutation though. We can also perform relative tasks in our actions like we have done in the goToNextStep() function.
  • In our action functions we are passing arguments using curly brackets like {state} and {commit} this is known as argument destructing. The first argument an action function receive is a context object. As an example the setSteps() function can also be written like so:
setSteps(context, steps) {
context.commit('SET_STEPS', steps);
}
Example context object.
  • since we are only interested in using the commit function we can use argument destructing {commit} to grab just that off of the context object.
  • _.findIndex() is a Lodash method that returns the first truthy item. In our case the index of the first step where its route name matches the currentStepRoute.

Finally just like with our router we need actually inject the store into our application.

File: ./resources/assets/js/app.js

import router from './routes';
import store from './store/store';

/**
* First we will load all of this project's JavaScript dependencies
* which includes Vue and other libraries. It is a great starting
* point when building robust, powerful web applications using
* Vue and Laravel.
*/

require('./bootstrap');

window.Vue = require('vue');

/**
* Next, we will create a fresh Vue application instance and attach
* it to the page. Then, you may begin adding components to this
* application or customize the JavaScript scaffolding to fit
* your unique needs.
*/

const app = new Vue({
router,
store,
el: '#app'
});

Step 6 — Update the StepBar component.

With the shared state out the way we need to update our StepBar component to make use of it.

File: ./resources/assets/js/components/StepBar.vue

<script>
export default {
props: {
steps: {
type: Array,
required: true
}
},

created() {
this.$store.dispatch('setSteps', this.steps)
},

watch: {
$route({name}) {
this.$store.dispatch('updateCurrentStepRoute', name);
}
}
}
</script>

What’s going on here?

  • As soon as the StepBar component is created we are setting the steps on our state object by dispatching the setSteps action this.$store.dispatch('setSteps', this.steps).
  • We then watch for any changes to the $route. Such as when the user press back on the browser of clicks next to go to then next step. When this happens we update the currentStepRoute property on the state by dispatching updateCurrentStepRoute action. Again we make use of argument destructing to grab the just the name off of the route object.
Example route object.

It would also be great if when we click on a steps link we are taken to the relevant page. To get this working all we need to do is replace <a href="#">{{ step.label }}</a> with <router-link :to="{name: step.route}">{{ step.label }}</router-link>. The complete template should look like this:

File: ./resources/assets/js/components/StepBar.vue

<template>
<div class="row align-center mt-50">
<div class="column large-8">
<ul class="step-bar">
<li v-for="step in steps"
:class="{'active': step.route == $route.name}"
>
<router-link :to="{name: step.route}">
{{ step.label }}
</router-link>
</li>
</ul>
</div>
</div>
</template>

Note: Vue is probably going to throw an error at this point because we haven’t defined the routes for each step just yet.

Step 7 — Create each steps page.

We need to create three pages that correspond to our steps this will be:

  • Goal.vue: where we define our focus area
  • Metrics.vue: where we set what criteria or metric(s) will be used to measure success; and finally
  • Actions.vue: what steps and by who need to be taken to achieve the goal.

For now we not going to focus on submitting any data to our backend (we’ll cover this in a future post) but rather on getting our multi-step form to function.

Let’s go ahead and create the pages for each of our steps.

File: ./resources/assets/js/pages/Goal.vue

<template>
<div class="secondary callout shadow">
<div class="callout-header">
<h4>Focus area</h4>
<p>What area you would like to focus on within the
next 90 days?</p>
</div>

<form>
<label>What area would you like to focus on?</label>
<div class="input-group">
<span class="input-group-label">
<i class="fa fa-trophy"></i>
</span>
<input class="input-group-field"
type="text"
placeholder="Title for your focus area"
>
</div>
<button class="button expanded"
type="submit"
@click.prevent="next"
>Next</button>
</form>
</div>
</template>

<script>
export default {
methods: {
next() {
this.$store.dispatch('goToNextStep')
}
}
}
</script>

File: ./resources/assets/js/pages/Metrics.vue

<template>
<div class="callout secondary shadow">
<div class="callout-header primary">
<h4>Metrics</h4>
<p>
What criteria or metric(s) will be used to measure success.
</p>
</div>

<form>
<div class="input-group">
<span class="input-group-label">
<i class="fa fa-line-chart"></i>
</span>
<input class="input-group-field" type="text">
<div class="input-group-button">
<button type="button" class="button">
<i class="fa fa-plus"></i>
</button>
</div>
</div>

<button class="button expanded" type="submit" @click.prevent="next">Next</button>
</form>
</div>
</template>

<script>
export default {
methods: {
next() {
this.$store.dispatch('goToNextStep')
}
}
}
</script>

File: ./resources/assets/js/pages/Actions.vue

<template>
<div class="callout secondary shadow">
<div class="callout-header primary">
<h4>Action Steps</h4>
<p>What actions need to be taken to achieve your goal?</p>
</div>

<form @submit.prevent="save">
<div class="input-group">
<span class="input-group-label">
<i class="fa fa-list"></i>
</span>
<input class="input-group-field" type="text">
<div class="input-group-button">
<button type="button" class="button">
<i class="fa fa-plus"></i>
</button>
</div>
</div>

<button class="button expanded" type="submit">Done</button>
</form>
</div>
</template>

<script>
export default {
methods: {
save() {
alert('Saving...')
}
}
}
</script>

What’s going on here?

  • For the Goal and Metrics page when the user clicks the “Next” button they are taken next step by dispatching the goToNextStep action on our store object.
  • For the action page we just alert “Saving…” when the form is submit.

Step 8— Update routes.

Each steps route will be a child of the create route, basically nested routes. As an example the URL for each steps page would be:

  • Goal: /create/goal
  • Metrics: /create/metrics
  • Actions: /create/actions

So lets update our routes inside routes.js.

routes:[
{
path:'/',
name: 'home',
component: resolve => require(['./pages/Home.vue'], resolve)
},
{
path:'/create',
name: 'create',
component: resolve => require(['./pages/Create.vue'], resolve),
redirect: { name: 'create.goal' },
children: [
{
path: 'goal',
name: 'create.goal',
component: resolve =>
require(['./pages/Goal.vue'], resolve),
},
{
path: 'metrics',
name: 'create.metrics',
component: resolve =>
require(['./pages/Metrics.vue'], resolve),
},
{
path: 'actions',
name: 'create.actions',
component: resolve =>
require(['./pages/Actions.vue'], resolve),
}
]
}
]

When we navigate to http://localhost:3000/#/create in the browser it will automatically redirect us to the first step http://localhost:3000/#/create/goal.

As we navigate form one step to the other our Step-bar updates to reflect the page we on. Sweet!

Step-bar in action.

I think we have made some great progress so we will end things here for now.

Let’s recap what we have achieved so far. First we created an introduction page the lets the user know what steps need to be taken. And we made it look a bit better with some styling. We then created our Step-bar and laid the foundation for our multi-step form.

In part 2 will continue building on top of what we have and add more functionality to each step. It would also be pretty cool if we added a transition effect when going form step-to-step (Hint: We can use Motion UI).

Until next time, happy coding and thanks for reading!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade