Pt 5: Build a Wizard with Lucid Laravel and Vuex

in utopian-io •  7 years ago  (edited)

Pt 5: Build a Wizard with Lucid Laravel and Vuex

Today on our series, we'll be setting up the structure for the UI (user interface) for our wizard. We'll also setup a flexible architecture for our Vuex application. We'll also setup asset management tasks with Laravel Elixir.

Disclaimer

This tutorial is not an introduction to Vue or Vuex.

We're assuming a basic understanding of Vue (or any other component based framework) and Vuex (or an alternative state management solution).

If you'd like a friendlier introduction to Vue and Vuex, please visit these links:

Briefing

Previously in this series, we added some functionality to our API service. We also added some security measures to our API service while also validating the data passed to our API server.

Today, we'll have fun building out the user interface for our Wizard with Vue 2. We'll also be setting up asset management using Laravel Elixir for our styles and scripts.

Difficulty

  • Advanced

Requirements

What Will I Learn?

  1. Setting up Asset Management with Laravel Elixir.
  2. Setting Up the 7-in-1 SASS Structure.
  3. Setting Up Gulp.
  4. Setting Up Vue Structure.
  5. Setting Up Vuex Modules
  6. Setting Up Wizard Navigation

1. Setting up Asset Management with Laravel Elixir and Gulp.

We'll use modern frontend development methods in building out the UI for our app. Laravel Elixir is a frontend management solution bundled with Laravel 5 by the Laravel team. We'll use Sass as our CSS preprocessor and we'll be compiling our Sass with Elixir and Gulp.

2. Setting up the 7-in-1 SASS Structure

We'll be using Hugo Giraudel's 7-in-1 Sass architecture to structure our Sass code. This will help make our styles more modular and compact.

We'll create the following folders in the resources/assets/sass directory:

  • Layout
  • Modules
  • Pages
  • Theme
  • Utilities
  • Vendor

Jump into the terminal and issue this command

C:\xampp\htdocs\easywiz\resources\assets\sass>mkdir modules theme utilities 

We'll also create a file called easywiz.scss at resources\assets\sass. This file will help us bring all our Sass components together. If we've created this file, let's add some code to it. Our code will help us assemble the other modules together. We'll also add some documentation to help future team members.

easywiz.scss


// /* ==========================================================================
//    EASYWIZ.SCSS
//    ========================================================================== */
//
// /*
//  * easywiz.scss, by @creatrixity
//  *
//  * https://github.com/creatrixity | easywiz
//  */
//
// /*
//  *
//  *  Getting Started
//  *  ---------------
//  *
//  *  This is the master for all styles required for the EasyWiz Wizard. Sub-modules will be handled in their own
//  *  own master files.
//  *
//  *
//  *  Table Of Contents
//  *  -----------------
//  *
//  *  1. UTILITIES
//  *      Variables...........Framework-level specifications.
//  *
//  *  2. VENDOR DEPENDENCIES
//  *
//  *  3. MODULES
//  *      Modal...............Modal styles.
//  *      Step..............Wizard step styles abstracted for composability
//  *      Panel............Simple panels can be extended for specific use cases.
//  *      Input............Form control styling.
//  *
//  *  4. THEME
//  *      Default...............Cosmetic styling: colors, backgrounds, gradients.
//  *
//  */

// UTILITIES
@import 'utilities/variables';


// VENDOR DEPENDENCIES
@import 'node_modules/bootstrap-sass/assets/stylesheets/bootstrap',
        'node_modules/vue-touch-keyboard/dist/vue-touch-keyboard';


// MODULES
@import 'modules/modal',
        'modules/steps',
        'modules/panel',
        'modules/input';


// THEME
@import 'theme/default';

Next, we should customize our interface using Sass variables. Open /resources/assets/sass/utilities/_variables.scss and let's specify some variables.

variables.scss

//== Colors
//
//## Grays for use across Easywiz.

$gray-base:              #000;
$gray-darkest:           lighten($gray-base, 6%);
$gray-darker:            lighten($gray-base, 16%);
$gray-dark:              lighten($gray-base, 26%);
$gray:                   lighten($gray-base, 46%);
$gray-light:             lighten($gray-base, 64%);
$gray-lighter:           lighten($gray-base, 81%);
$gray-lightest:          lighten($gray-base, 97%);

//
//## Brand colors for Easywiz.

$evergreen:              #36ba45;
$ripley:                 #eb4e4e;
$cleopatra:              #4ea7eb;
$peter-river:            #00a0da;

$brand-primary:          $evergreen;

$tw-top-bar-color:       $peter-river;

//== Forms
//
// //** Border color for inputs on focus

$input-border-focus:     $brand-primary;

We'll skip the rest of the styling for brevity and proceed to setting up dependencies.

Setting Up Dependencies

We now need to update our package.json file and add a few dependencies. We'll need these dependencies for everything from vendor code to automation tools. Add these lines to your package.json file.

package.json

  "devDependencies": {
      "gulp": "^3.9.1",
      "http-server": "^0.9.0",
      "laravel-elixir": "^6.0.0-9",
      "laravel-elixir-browsersync": "^0.1.5",
      "laravel-elixir-browsersync-official": "^1.0.0",
      "laravel-elixir-vue-2": "^0.2.0",
      "laravel-elixir-vueify": "^1.0.2",
      "laravel-elixir-webpack-official": "^1.0.2",
      "vue": "^2.0.1",
      "vue-resource": "^1.0.3"
    },
    "dependencies": {
      "bootstrap-sass": "^3.3.7",
      "jquery": "^3.1.0",
      "normalizr": "^3.2.3",
      "vee-validate": "next",
      "vue-router": "^2.7.0",
      "vuex": "^2.3.1",
      "vuex-router-sync": "^4.2.0"
    }

Next, install these dependencies by running the command below. Make sure you have Yarn installed. We now have to install our dependencies by running this command.

C:\xampp\htdocs\easywiz>yarn install

Setting Up Gulp

With our dependencies downloaded and ready for installation, we simply need to setup some tasks in our gulpfile.js. Let's add some code to our gulpfile.js. We'll be requiring our dependencies and setting up our project config.

gulpfile.js

const elixir = require('laravel-elixir');

const BrowserSync = require('laravel-elixir-browsersync');

const projectConfig = {
    serverAddress: '127.0.0.1:8000',
    sassEntry: 'easywiz.scss',
    jsEntry: 'app.js'
};

require('laravel-elixir-vue-2');

elixir.config.assetsPath = 'resources/assets';

We will use Elixir's fluent interface to run some tasks for us. Let's add some more code. This will setup Sass compilation, bundle our JavaScript and setup a development server that reloads on every change. That's pretty cool, I think.

elixir(mix => {
    mix.sass(projectConfig.sassEntry)
       .webpack(projectConfig.jsEntry)
       .browserSync({
           proxy: projectConfig.serverAddress,
       });
});

We'll run compilation of our assets by running gulp watch in our terminal. We should now have our easywiz.css file available at the public/dist directory.

We now need to create the view template for our wizard. Let's create a file called index.blade.php at resources/views/pages/wizard. We'll add some code that helps us provide a base to mount our Vue app on.

index.blade.php

<!DOCTYPE html>
<html lang="en">
    <head>
        <script>
            window.Laravel = {
                'csrfToken': "{{ csrf_token() }}",
                'siteName': "{{ config('app.name') }}",
                'apiDomain': "{{ config('app.url').'/api' }}",
                'URI': "/wizard/"
            }
        </script>
        <meta name="_token" content="{{ csrf_token() }}"/>
        <link rel="stylesheet" href="{{ asset('/assets/vendor/font-awesome/css/font-awesome.min.css') }}">
        <link rel="stylesheet" href="{{ asset('/css/easywiz.css') }}">
        <title>Wizard</title>
    </head>
    <body>
        <div class="app-wrapper">
            <section id="app">
                <app></app>
            </section>
        </div>
        <script src="{{ asset('/js/wizard.js') }}"></script>
    </body>
</html>

3. Setting up our Vue structure

We'll setup our Vue structure now. Create api, components, config, directives and vuex directories at resources/assets/js. These directories will contain code performing a set of related functionality.

  • api: Code that sends HTTP requests to our API endpoints reside here.
  • components: This directory will house our Vue components.
  • config: Contains configuration files for routing and our application.
  • directives: Contains Vue directives (if we'll be using any).
  • vuex: This contains code for our Vuex architecture. Our actions, getters, mutations, stores and schemas will live here.

We'll edit our entry-point (app.js) and add some code to wire it all together. Edit resources/assets/js/app.js and add these lines.

app.js

/**
 * First we will load all of this project's JavaScript dependencies which
 * include Vue and Vue Resource. This gives a great starting point for
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

import Vue from 'vue';

import { sync } from 'vuex-router-sync';

import App from './components/App.vue';

import Vuex from 'vuex';

import store from './vuex/store.js';

import router from './routes.js';

require('./directives/index');

sync(store, router);

/**
 * Next, we will create a fresh Vue application instance and attach it to
 * the body of the page. From here, you may begin adding components to
 * the application, or feel free to tweak this setup for your needs.
 */

 Vue.component('app', App);

 const app = new Vue({
     router,
     store,
 }).$mount('#app');

5. Setting Up Vuex Modules

Let's create the store.js file at resources/assets/js/vuex. This will help us register the home Vuex module we will create.

store.js

import Vue from 'vue';
import Vuex from 'vuex';

import home from './modules/home/home-index';

Vue.use(Vuex);

export default new Vuex.Store({
    modules: {
        home
    },

    strict: true
});

We now need to create the home module. Let's create a file called home-index.js at resources/assets/js/vuex/home and add some code to it.

home-index.js

import { mutations } from './mutations';

import * as actions from './actions';
import * as getters from './getters';

const state = {
    currentstep: 1,

    isTeamModalOpen: false,
    isTeamCreateModalOpen: false,
    isCompetitionCreateModalOpen: false,

    loading: false,

    hasFetchedLeaderboards: false,
    hasFetchedTeams: false,
    hasFetchedCompetitions: false,

    steps: [
        {
            id: 1,
            label: 'Personal',
            title: 'Add Personal Information',
            icon_class: 'fa fa-user'
        },
        {
            id: 2,
            label: 'Team',
            title: 'Add Team',
            icon_class: 'fa fa-calendar'
        },
        {
            id: 3,
            label: 'Complete',
            title: 'Complete Wizard',
            icon_class: 'fa fa-space-shuttle'
        }
    ],

    leaderboards: [],

    competitions: [],

    teams: []

}

export default {
    actions,
    getters,
    state,
    mutations,
};

The above code basically helps us setup our default application state. We'd like to track which modals are open and we'd also like to store data we receive from the server in the state object. We now need to create our getters, actions and mutations classes. We'll create the following files at resources/assets/js/vuex/home:

  • getters.js: These contains methods that return particular properties in the global state object.

  • mutations.js: Contains methods that help us update the state of the application.

  • actions.js: Contains methods that trigger mutations by dispatching actions.

We'll add some code to getters.js below.

getters.js


export const getCompetitions = state => state.competitions;

export const getTeams = state => state.teams;

export const getTeamModalStatus = state => state.isTeamModalOpen;

export const getTeamCreateModalStatus = state => state.isTeamCreateModalOpen;

export const getCompetitionModalStatus = state => state.isCompetitionModalOpen;

export const getCompetitionCreateModalStatus = state => state.isCompetitionCreateModalOpen;

export const getCompetitionsFetchedStatus = state => state.hasFetchedCompetitions;

export const getTeamsFetchedStatus = state => state.hasFetchedTeams;

export const getLeaderboardsFetchedStatus = state => state.hasFetchedLeaderboards;

export const getSteps = state => state.steps;

export const getCurrentStep = (state) => state.currentstep;

export const getLoader = (state) => state.loading;

We've successfully added some methods to retrieve portions of the state. Let's add some actions to actions.js

actions.js

import { normalize, schema } from 'normalizr';

import api from '../../../api/index';

import { competitor, region } from '../../schema/home';

export const setLoader = ({commit}, loading) => {
    commit('SET_LOADER', {
        loading: loading
    });
};

export const setCompetitionsFetchedStatus = ({commit}, status) => {
    commit('SET_COMPETITIONS_FETCHED_STATUS', {
        status: status
    });
};

export const setTeamsFetchedStatus = ({commit}, status) => {
    commit('SET_TEAMS_FETCHED_STATUS', {
        status: status
    });
};

export const setLeaderboardsFetchedStatus = ({commit}, status) => {
    commit('SET_LEADERBOARDS_FETCHED_STATUS', {
        status: status
    });
};

export const setTeamModalStatus = ({commit}, status) => {
    commit('SET_TEAM_MODAL_STATUS', {
        status: status
    });
};

export const setTeamCreateModalStatus = ({commit}, status) => {
    commit('SET_TEAM_CREATE_MODAL_STATUS', {
        status: status
    });
};

export const setCompetitionModalStatus = ({commit}, status) => {
    commit('SET_COMPETITION_MODAL_STATUS', {
        status: status
    });
};

export const setCompetitionCreateModalStatus = ({commit}, status) => {
    commit('SET_COMPETITION_CREATE_MODAL_STATUS', {
        status: status
    });
};

export const setStep = ({commit}, step) => {
    commit('SET_STEP', {
        step: step
    });
};

export const getAllTeams = ({ commit }, callback) => {

    api.fetchData('/api/v1/teams', teams => {
        if (teams) {
            commit('GET_ALL_TEAMS', teams)

            callback(teams);
        }
    }, function () {
        alert('Failed to fetch teams. Try again');
    })

}

export const getAllCompetitions = ({ commit }, callback) => {
    api.fetchData('/api/v1/competitions', competitions => {

        if (events) {
            commit('GET_ALL_COMPETITIONS', competitions)

            callback(competitions);
        }

    }, function () {
        alert('Failed to fetch competitions. Try again');
    })
}

export const getAllLeaderboards = ({ commit }, callback) => {

    api.fetchData('/api/v1/leaderboards', leaderboards => {
        if (leaderboards) {
            commit('GET_ALL_COMPETITIONS', leaderboards);

            callback(leaderboards);
        }
    }, function () {
        alert('Failed to fetch leaderboards. Try again');
    });

}

export const persistCompetition = ({ commit }, payload) => {
    api.sendData('/api/v1/competitions', payload.data, (res) => {

        if (res) {
            payload.callback(res.data);
        }

    }, function () {
        alert('Failed to create competition. Try again');
    })
}

export const persistTeam = ({ commit }, payload) => {
    api.sendData('/api/v1/teams', payload.data, (res) => {

        if (res) {
            payload.callback(res.data);
        }

    }, function () {
        alert('Failed to create team. Try again');
    })
}


export const addCompetitionToStore = ({ commit }, payload) => {
    commit('ADD_COMPETITION', payload);
}

export const addTeamToStore = ({ commit }, payload) => {
    commit('ADD_TEAM', payload);
}

We now need to work on our mutations.js. We'll add some code to it.

mutations.js

export const mutations = {
    SET_STEP: function (state, payload) {
        state.currentstep = payload.step;
    },

    SET_TEAM_MODAL_STATUS: function (state, payload) {
        state.isTeamModalOpen = payload.status;
    },

    SET_TEAM_CREATE_MODAL_STATUS: function (state, payload) {
        state.isTeamCreateModalOpen = payload.status;
    },

    SET_COMPETITION_MODAL_STATUS: function (state, payload) {
        state.isCompetitionModalOpen = payload.status;
    },

    SET_COMPETITION_CREATE_MODAL_STATUS: function (state, payload) {
        state.isCompetitionCreateModalOpen = payload.status;
    },

    SET_EVENTS_FETCHED_STATUS: function (state, payload) {
        state.hasFetchedCompetitions = payload.status;
    },

    SET_LOADER: function (state, payload) {
        state.loading = payload.loading;
    },

    GET_ALL_COMPETITIONS: (state, payload) => {
        state.competitions = payload.data;
    },

    GET_ALL_TEAMS: (state, payload) => {
        state.teams = payload.data;
    },

    GET_ALL_LEADERBOARDS: (state, payload) => {
        state.leaderboards = payload.data;
    },

    ADD_TEAM: (state, payload) => {
        state.teams.push(payload.data);
    },

    ADD_COMPETITION: (state, payload) => {
        state.competitions.push(payload.data);
    },

}

We'll also create a routes.js file at the base of the resources/assets/js directory. This file will import our VueRouter and configure it for use.

routes.js

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

import { routes } from './config/router-config';

Vue.use(VueRouter);

export default new VueRouter({
    // hashbang: false,
    // history: true,
    linkActiveClass: 'active',
    mode: 'history',
    base: window.Laravel.URI,

    routes,
});

We defined a file called config/router-config.js but we are yet to create it. Let's create it and add some code. We are basically loading components if we are on a specified route. We have four components:

  • Home component: Our base component. This will serve the router view that allows us to load separate routes on the same page.
  • AddPersonal: This allows us update our personal information.
  • AddTeam: This allows us add a team we are interested in.
  • Completed: This shows us the completed page.
import Home from './../components/Home.vue';
import AddPersonal from './../components/profile/add-personal.vue';
import AddTeam from './../components/profile/add-team.vue';
import Completed from './../components/profile/completed.vue';

export const routes = [
    {
        path: '/',
        redirect: '/add-personal',
        component: Home,

        children: [
            {
                path: 'add-personal',
                component: AddPersonal
            },

            {
                path: 'add-team',
                component: AddTeam
            },

            {
                path: 'completed',
                component: Completed
            },
        ]
    },
];

We also need to create a file called app-config.js in our resources/assets/js/config directory. This config file specifies the title for each step in our wizard.

app-config.js

export default {
    stepData: [
        {
            'name': 'Add Personal',
            'location': '/add-personal'
        },

        {
            'name': 'Add Team',
            'location': '/add-team'
        },

        {
            'name': 'Complete wizard',
            'location': '/completed'
        }
    ]
}

We now need to create the first base component for our router-view. This will expose a view to dynamically load other sub views. We'll call it Index.vue. Create a file called Index.vue at resources/assets/js/components and add these lines.

Index.vue

<template>
    <div>
      <section class="w-top-bar u-mb-lg">

        <section class="container">
          <div class="row">
            <section class="col-md-12">
                <h1 class="w-top-bar__title text-center">Complete Your Profile</h1>
                <p class="lead w-top-bar__subtitle text-center">Step {{ getCurrentStep }} of {{ getSteps.length }} &mdash; {{ getSteps[getCurrentStep - 1].label }} </p>
             </section>
          </div>

          <step-navigation :steps="getSteps" :currentstep="getCurrentStep"></step-navigation>

        </section>

      </section>

      <router-view></router-view>

      <loader :loading="getLoader"></loader>
    </div>
</template>

<script>
  import Vue from 'vue';

  import { mapGetters } from 'vuex';

  import store from '../vuex/store.js';
  import router from '../routes.js';
  import config from '../config/app-config.js';

  Vue.component('step-navigation', require('./wizard/step-navigation.vue'));
  Vue.component('loader', require('./shared/loader.vue'));

  export default {
    methods: {
      setStep (step) {
        return store.dispatch('setStep', step);
      },

      moveToStep (step) {
        return router.push(config.stepData[step - 1]['location']);
      }
    },

    mounted () {},

    computed: {
      ...mapGetters([
        'getLoader',
        'getSteps',
        'getCurrentStep'
      ]),
    }
  };
</script>

The above code does a lot of stuff. Basically, we can add some HTML markup to the <template></template> tag. This template will be used to construct the Document Object Model (DOM) by Vue. In the template markup, we specify the <loader></loader> component and the <step-navigation></step-navigation> components.

We also write some JavaScript code in the <script></script>. We import the dependencies and components required for this class. We also have to specify a couple of methods to help us update the current step our wizard is on.

In the computed object, we're using the object spread ES7 operator to define a set of computed methods we can use to get the state of our components. We now need to create the dependencies we imported.

6. Setting up Wizard Navigation

In any wizard, navigation is required to give the user some perspective of his or her current progress in the process. We'll create the step-navigation.vue component at resources/assets/js/wizard and add some code to it.

step-navigation.vue

<template>
    <ol class="step-indicator">
        <li class="step-indicator__item" v-for="step in steps" is="step-navigation-step" :step="step" :currentstep="currentstep" :steps="steps" @step-change="stepChanged"></li>
    </ol>
</template>

<script>
    import Vue from 'vue';
    import store from '../../vuex/store';

    Vue.component('step-navigation-step', require('./step-navigation-step.vue'));

    export default {
        props: ['steps', 'currentstep'],

        mounted () {
        },

        methods: {
            stepChanged: function(step) {
                this.$parent.moveToStep(step);
            }
        }        
    }
</script>

Here, we define a component called step-navigation-step.vue that will contain every individual step in the wizard's navigation. We also define an event handler called stepChanged to help us update the store with the current step we should be on.

Let's create the step-navigation-step.vue component at resources/assets/js/components/wizard and add some code to it.

step-navigation-step.vue

<template>
    <li v-bind:class="indicatorClass" @click="skipToStep">
        <div class="step-indicator__step"> <b>{{ step.id}}</b> </div>
        <div class="step-indicator__caption hidden-xs hidden-sm">
            {{ step.title }}
        </div>
    </li>
</template>

<script>
    export default {
        props: ['step', 'currentstep', 'steps'],

        computed: {
            indicatorClass: function () {
                return {
                    'active': (this.step.id == this.currentstep),
                    'complete': (this.currentstep > this.step.id),
                    'last': (this.step.id === this.steps.length)
                }
            }
        },

        methods: {
            skipToStep: function () {
                this.$emit('step-change', this.step.id);
            }            
        }

    }
</script>

In our template, we have a single list-item styled to represent a step in the wizard. We define a method called skipToStep that changes the current step to the specified step. We also have a computed method called indicatorClass that returns a class name based on the step we are currently are on.

Conclusion

We've arrived at the end of this installment. In this episode, we set up Laravel Elixir. We also configure our gulpfile and added the base structure for our Vue and Vuex application.

In the next post, we will explore Normalizr schema's and how we can leverage them to make easywiz faster. We will also build out wizard modals and components.

Curriculum



Posted on Utopian.io - Rewarding Open Source Contributors

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Great tutorial, @creatrixity. I'm following this tutorial and I have problems bundling my app.js file with webpack. Some help will be nice.

Thanks for the contribution, it has been approved.


Need help? Write a ticket on https://support.utopian.io.
Chat with us on Discord.

[utopian-moderator]

Hey @creatrixity I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Utopian Witness!

Participate on Discord. Lets GROW TOGETHER!

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x