Building a 90 Day Planner with Laravel and Vue.js: Part 2

Alexander van Zyl
10 min readJun 12, 2017

--

This is the second part of the multi-part tutorial where we are building a 90 Day Action Planner using Laravel and Vue.js.

Source code for part 2 can be found here.

What will we cover?

Continuing from where we left off in part 1, in this post we will:

  1. add transition effects when going form step-to-step; and
  2. allow the user to add or remove additional metric and action fields.

Let’s fire up our application by running yarn watch or npm run watch if you using npm from the console.

Now before we jump in and start adding the transition effects there are two small things I would like us to address quickly.

  1. The name of the application.
Tab showing the default applications name “Laravel”.

If you recall inside of index.blade.php we set the title to the applications name. Like so <title>{{ config('app.name') }}</title>. Now by default this is set to “Laravel”. To change this we need to open up our environment file /.env and at the top change APP_NAME=Laravel to APP_NAME="90 Day Planner". If you happen to forget the quotes a 500 internal server error will be thrown.

It is also possible to override the default just in case someone forgets to update the APP_NAME. Let’s open up ./config/app.php and change the default from “Laravel” to “90 Day Planner”.

'name' => env('APP_NAME', '90 Day Planner'),

2. Add home button to multi-step form.

Multi-step form with added home button.

Right now there is no way for the user to go back to the home page without having to hit back arrow in the browser a few times. I think we can agree that this isn’t the best User Experience (UX). To fix this let’s open up ./resources/assets/js/pages/Create.vue and add a new router link above <router-view> like so:

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

<div class="row align-center">
<div class="column large-8">
<router-link class="mb-5 block"
:to="{ name: 'home' }"
>
<i class="fa fa-home"></i> Home
</router-link>
<router-view></router-view>
</div>
</div>
</div>
</template>

Noticed we have introduced a new helper class .block. All this does is set the CSS display to block. We use this because adding a bottom margin without the display set to block has no effect. We can add this new helper class at the bottom of our ./resources/assets/sass/_helpers.scss file:

.block { display: block };

Adding transition effect

To add our transition effect we will make use of Motion UI and the JavaScript utility that Foundation provides for it.

What is Motion UI ?

“Motion UI is a Sass library for creating CSS transitions and animations…”

Source

Step 1 — Import Motion UI Sass

First we will open up ./resources/assets/sass/app.scss, import Motion UI and then include all of its transitions:

// 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";

// Import Motion UI
@import "node_modules/motion-ui/src/motion-ui";

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

// Include all Motion UI transitions.
@include motion-ui-transitions;

// Components
@import "components/callout";

// Helpers
@import "helpers";

Step 2 — Initialize Foundation

To make use of Foundations JavaScript utility for Motion UI we first need to initialise Foundation.

There are few places we could initialise Foundation. One we could just create a new <script> tag after we include app.js inside of index.blade.php file, like so:

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

An alternative approach is to initialise Foundation inside app.js as soon as our Vue instance has been created, like so:

const app = new Vue({
router,
store,
created() {
$(document).foundation();
},
el: '#app'
});

Both approaches are valid and should work just fine. However I think a better place to initialise Foundation would be inside our bootstrap.js file right after we require it. So let’s open up ./resources/assets/js/bootstrap.js and after we require Foundation we will go ahead initialise it.

try {
window.$ = window.jQuery = require('jquery');
require('foundation-sites');
$(document).foundation();
} catch (e) {}

Step 3 — Add transition effects to each step

For our transition effects we are going to make each steps callout div slide in from the left and slide out to the right when going to the next step.

Transition effect going form step-to-step.

Let’s open up ./resources/assets/js/pages/Goal.vue within the <script> tags lets add our transition effects.

<script>
export default {
mounted() {
Foundation.Motion.animateIn(
this.$el,
'slide-in-left fast'
);
},

methods: {
next() {
Foundation.Motion.animateOut(
this.$el,
'slide-out-right fast',
() => {
this.$store.dispatch('goToNextStep')
}
);
}
}
}
</script>

What’s going on here ?

  • Foundation provides two utility methods Foundation.Motion.animateIn() for transitions that are entering the window and Foundation.Motion.animateOut() for transitions that are existing the window. Both methods take the same amount of parameters in the same order. The first parameter is the element we want to add the transition to, second the built-in transition we want to apply and thirdly an optional callback that gets fired only once the transition is complete.
  • $el is an instance property on our component that refers to the root DOM element. In this case this is our callout div.
$el property on Vue component.

Note: We could use jQuery to reference our callout div too. This can be done by giving our callout div an id/class and then referencing that id/class. For example we can give our callout div and id of “goal”:
<div id="goal" class="secondary callout shadow" ref="main">

then inside the transition method we reference the id using jQuery $('#goal') like so:
Foundation.Motion.animateIn($('#goal'), 'slide-in-left fast');

  • Once the component is mounted, the template is rendered and we have access to all DOM elements. At this point we tell Foundation that we want our callout div to slide in from the left fast.
  • We then modified our next() method to say when the next button is pressed slide the callout div out to the right fast. Once the transition is complete we proceed to the next step.

Here is the complete Goal.vue page after the changes we just made:

Goal.vue added transitions.

Let’s go ahead and add the transitions to./resources/assets/js/pages/Metrics.vue too.

Metrics.vue added transitions.

Finally for ./resources/assets/js/pages/Actions.vue we will just add the entering transition.

Actions.vue with entering transition only.

Note: It is also possible to make use of Vues’ ref attribute. In cases where you want to reference an element that is not the root element.

Ability to add/remove metrics and actions.

In this section we will give the user the ability to add or remove additional metrics or actions.

Add/Remove Metric

Step 1 —Create the ability add/remove metrics.

To get started let’s open up ./resources/assets/js/pages/Metrics.vue file. Inside our <script> tags we will define an array of metrics and we will add two new methods add() and remove() .

<script>
export default {
data() {
return {
metrics: [{ name: '' }]
}
},
...

methods: {
...
add() {
this.metrics.push({ name: '' });
},

remove(index) {
if (this.metrics.length > 1) {
this.metrics.splice(index, 1)
}
}
}
}
</script>

What’s going on here ?

  • metrics: [{ name: '' }]: we define an array of metrics inside our data object. Within that array we have a single metric object with a property of name which defaults to an empty string.
  • add(): when triggered we push a new metric object onto the metrics array.
  • remove(): we always want to have at least one metric input field. So we only remove a metric if there is more than one in our metrics array. To actually remove a metric from the array we make use of the splice() function. The first parameter splice() takes is the starting point at which it will remove an item from the array. In our case we use the index of a metric. The second parameter is the number of items we wish to remove. Since we only want to remove the metric itself we set this to one.

Note: There are some caveats when it comes to Vue detecting changes in arrays this is covered in the documentation here.

Inside the <template> tags let’s replace the form with this one below:

<form>
<div class="input-group" v-for="(metric, index) in metrics">
<span class="input-group-label">
<i class="fa fa-line-chart"></i>
</span>
<input class="input-group-field"
type="text"
v-model="metric.name"
@keydown.enter.prevent="add"
placeholder="Metric"
v-focus
>
<div class="input-group-button" v-show="metrics.length > 1">
<button type="button"
class="button alert"
@click="remove(index)"
>
<i class="fa fa-minus"></i>
</button>
</div>
</div>
<button class="button expanded tiny"
type="button"
@click="add"
title="Add another metric"
>
<i class="fa fa-plus"></i>
</button>
<hr>

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

What’s going on here ?

  • v-for="(metric, index) in metrics": We create a new input-group for every metric and reference its index.
  • v-model="metric.name": we make use of two-way data binding. This will update the name property on the metric object as the user inputs the name of the metric.
  • v-focus: is a custom directive. We haven’t created it yet so let’s do it now. For now we will add this to our resources/assets/js/app.js file after we require Vue.
window.Vue = require('vue');

// Register a global custom directive called v-focus
Vue.directive('focus', {
inserted: (el) => el.focus()
})
  • basically this says as soon as the element that contains the v-focus directive is inserted into the DOM, focus on it.
  • We replaced the “Add” button with a “Remove” button, which when pressed removes the respective metric.
  • v-show="metrics.length > 1": only show the remove button when there is more than one metric in the array.
  • We also added a new “Add” button after our input-group. When clicked a new input field is displayed.
Updated Metrics.vue file (Add/Remove metrics).

Step 2— Create the ability Add/Remove Actions.

In terms of functionality adding and removing actions will work exactly as it does for metrics. However we will be making some noticeable changes to form on our Actions page. First let’s open up ./resources/assets/js/pages/Actions.vue and within our <tempalte> tags we will update the form like so:

<form @submit.prevent="save">
<div class="row" v-for="(action, index) in actions">

<div class="medium-6 small-12 columns">
<div class="input-group">
<span class="input-group-label">
<i class="fa fa-list"></i>
</span>
<input class="input-group-field"
type="text"
placeholder="Action..."
v-model="action.name"
v-focus
>
</div>
</div>

<div class="medium-6 small-12 columns">
<div class="input-group">
<span class="input-group-label">
<i class="fa fa-user"></i>
</span>
<input class="input-group-field"
type="text"
placeholder="Who will perform the action?"
v-model="action.person_responsible"
>
<div class="input-group-button">
<button type="button"
class="button alert"
title="Remove action"
v-show="actions.length > 1"
@click="remove(index)"
>
<i class="fa fa-minus"></i>
</button>
</div>
</div>
</div>
</div>

<button type="button"
class="button expanded tiny warning"
title="Add another action"
@click="add"
>
<i class="fa fa-plus"></i>
</button>
<hr>

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

We haven’t introduced any new concepts here so let’s quickly go through the visual changes we made to our form.

What changes were made ?

  • We now have a row that contains two columns each containing an input field. For small display screens these fields are displayed one after the other small-12. For medium displays and up we display them side-by-side medium-6.
  • A new text field has been introduced to allow the user to specify who is responsible for performing the action.
  • We then removed the “Add” button from the input-group and replaced it with the “Remove” button.
  • Finally just like in our metrics form we added a new “Add” button but this time after the row.

Let’s go ahead and add in the actions array and create the add and remove methods now, like so:

<script>
export default {
data() {
return {
actions: [{ name: '', person_responsible: '' }]
}
},

mounted() {
Foundation.Motion.animateIn(this.$el, 'slide-in-left fast');
},

methods: {
save() {
alert('Saving...');
},

add() {
this.actions.push({ name: '', person_responsible: ''});
},

remove(index) {
if (this.actions.length > 1) {
this.actions.splice(index, 1)
}
}
}
}
</script>

After everything compiles we should now have a form that look like this:

Action form after compiled changes.

Here is the complete Action.vue file with the changes we made:

Actons.vue add/remove actions.

Let’s wrap things up here for now and go over the changes we just made. First we added a transition effect when going from step-to-step. We also gave the user the ability to add or remove as many metrics or actions they see fit.

At the moment we do have some shared functionality between forms and depending how thing progress it may be worth extracting them to a mixin.

In the next post we can look at creating a new shared state to keep track of the goal, metrics and actions. We can also look at how we can use local storage in the event the user loses connection to the internet or refreshes the page by mistake.

Until next time, happy coding and thanks for reading!

--

--