Pt:7 Build a Wizard with Lucid Laravel and Vuex

in utopian-io •  7 years ago  (edited)

vue.jpg

Previously on the series, we built out the add-team and add-personal components`. We'll also setup modal dialogs for various actions in our wizard.

Today, we will continue work on the remaining modal dialog components and we'll also work on the completed.vue component that displays a greeting to the user upon successful completion of the wizard steps.

Disclaimer

As started previously, 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

Last time on the series, we commenced work on a handful of modal dialog components. In the course of development of this components, we explored the breadth of the Vuex architecture we employed. We also built the add-team and add-personal components.

Today, we'll get our hands dirty building out some more modal dialogs for our Wizard. We'll need modal dialogs that can help our users perform the following tasks:

  1. Create a team of their own.
  2. Create a competition they'd like to partake in.
  3. Create a leaderboard for their friends.

Also, some of the tasks we described above also contain certain 'sub-tasks' of their own. For example, when creating a team, our user should be able to upload artwork of their team as part of the team creation data.

With all said, let's get some work done! Let's dive right in!

Difficulty

  • Advanced

Requirements

  • PHP version 5.6.4 or greater
  • Composer version 1.4.1 or greater
  • Lucid Laravel version 5.3.*
  • Yarn package manager
  • Previous code on the Github repository.
  • A little patience and love for new ideas.

What Will I Learn?

  1. Building the Team Create Modal Component.
  2. Building the Competition Create Modal Component.
  3. Building the Leaderboard Create Modal Component.
  4. Building the User Preferences Vue Component.
  5. Building the Wizard Completion Component.

1. Building the Team Create Modal Component.

We now need to build out the team-create-modal.vue component. This component has some required functionality. We must implement artwork upload capability. We can do this by leveraging the FileReader JavaScript API to help read the image data into the memory buffer. We can then turn this image data (which happens to be in binary form) into a Base64 string that can be understood by a browser. Ideally, we shouldn't be saving the image as a Base64 string going into production as a straight up binary encoded file would be much better for performance.

We'll write some code to help us create the functionality we described above.

add-team-modal.js

<template>
    <section>
        <div>
            <div class="modal fade" :class="{ in: open, visible: open }" v-show="open">
                <section class="modal-dialog">
                    <div class="modal-content">
                        <section class="modal-header">
                            <h3 class="text-uppercase text-center">
                                Create Team
                                <button @click.prevent="closeModal" type="button" class="close btn btn-default" data-dismiss="modal">&times;</button>
                            </h3>
                        </section>

                        <section class="modal-body">
                            <form @submit.prevent="saveTeam" action="">
                                <section class="modal-body">
                                
                                    <div class="form-group">
                                        <label for="">Name</label>
                                        <input autofocus @keydown="updateAlias" v-validate data-vv-rules="required" v-focus v-model="team.name" class="form-control" type="text" name="team_name">
                                        <p class="text-danger" v-if="errors.has('team_name')">{{ errors.first('team_name') }}</p>
                                    </div>

                                    <div class="form-group">
                                        <label for="">Alias</label>
                                        <input placeholder="Just fill the above field ..." v-validate data-vv-rules="required" v-focus v-model="team.alias" class="form-control" type="text" name="team_alias">
                                        <p class="text-danger" v-if="errors.has('team_alias')">{{ errors.first('team_alias') }}</p>
                                    </div>

                                    <div v-if="!team.artwork">
                                        <h3 class="text-center">Select Team Artwork</h3>

                                        <div class="form-group text-center">
                                            <label class="btn btn-info btn-file">
                                                <i class="fa fa-upload"></i> &nbsp; Choose Team Artwork...
                                                <input type="file" class="hidden" @change="onFileChange">
                                            </label>
                                        </div>
                                    </div>
                                    
                                    <div class="text-center" v-else>
                                        <img :src="team.artwork" style="width: 100px;" class="u-mb-md img-thumbnail" />
                                        <section class="clearfix">
                                            <button class="btn btn-danger" @click="removeImage"><i class="fa fa-trash"></i> Remove Team Artwork</button>
                                        </section>

                                    </div>

                                </section>

                                <section class="modal-footer">
                                    <button @click.prevent="closeModal" class="btn btn-default pull-left">&larr; Cancel and go back</button>
                                    <button type="submit" class="btn btn-primary">Create team and continue &rarr;</button>
                                </section>
                            </form>
                        </section>

                    </div>
                </section>
            </div>
            <div class="modal-backdrop" v-show="open"></div>
        </div>

    </section>
</template>

<script>
    import Vue from 'vue';

    import { mapGetters } from 'vuex';
    import { mapActions } from 'vuex';

    export default {
        directives: { focus },

        data: () => {
            return {
                team: {
                    'id': 0,
                    'alias': '',
                    'artwork': null,
                }
            }
        },

        props: {
            open: {
                type: Boolean,
                default() {
                    return true;
                },
            },
        },

        computed: {
            ...mapGetters([
                'getTeamModalStatus'
            ]),
        },

        methods: {
            ...mapActions ([
                'setLoader',
                'setTeamCreateModalStatus',
                'persistTeam',
                'addTeamToStore',
            ]),

            updateAlias (e) {
                this.team.alias = e.target.value;
            },

            onFileChange (e) {
                var files = e.target.files || e.dataTransfer.files;

                if (!files.length) return;

                this.createImage(files[0]);
            },

            createImage (file) {
                var image = new Image(),
                    fileReader = new FileReader(),
                    self = this;

                fileReader.onload = (e) => {
                    self.team.artwork = e.target.result;
                }

                fileReader.readAsDataURL(file);
            },


            removeImage () {
                this.team.artwork = '';
            },

            closeModal () {
                this.setLoader(false);
                this.setTeamCreateModalStatus(false);
            },

            clearTeam () {
                this.team = {
                    'id': 0,
                    'alias': '',
                    'artwork': null,
                };
            },

            saveTeam () {
                var self = this;

                this.$validator.validateAll().then(result => {
                    if (result) {
                        self.setLoader(true);

                        this.persistTeam({
                            callback: (payload) => {
                                this.addTeamToStore(payload);

                                this.clearTeam();

                                this.$emit('teamSaved', payload.data.id);

                                this.closeModal();
                            },

                            data: this.team
                        });

                        return;
                    }

                    alert('Please fix the errors on this form');
                });

            }
        }
    }
</script>

<style scoped>
    .modal-backdrop
    {
        background: rgba(0, 0, 0, 0.8);
    }

    .visible
    {
        display: block;
    }
</style>

Let's get a grasp of what's going on here. In the name form group, we listen for the keydown event on the input box and we assign the updateAlias method as its handler. Basically, we are updating the alias input box with the value from the name field. This prefilling functionality helps our users get their tasks done faster.

For our artwork upload, we hide a file input dialog in a button. When the button is clicked, an OS level file system dialog is launched prompting the user to select the artwork file. We also listen to the onChange event and run the onFileChange event handler. We also provide functionality for getting rid of any uploaded artwork.

In our <script/> we define some state defaults for our team data. We define the properties types our component will accept and we also set some defaults.

The onFileChange handler gets any files uploaded by the user and tries to create an image out of the uploaded binary file. The method that creates the image (aptly called createImage) instantiates a new FileReader instance. If a file was uploaded, we set the team artwork to the results of the FileReader. We finally get to read the binary data as a base64 string.

The removeTeam method simply sets our team.artwork state property to an empty string.

Our saveTeam method validates the user input and then persists the entered data to the server. It also adds the returned team to the application store upon which it resets the team data defaults and closes the modal.

2. Building the Competition Create Modal Component.

We'll be providing an outlet for our user to be able to create their favorite competitions.
We'll build out the competition-create-modal.vue component. This component is relatively simpler compared to the other components we've created. The only required functionality here is the ability to provide a name and an alias for the competition. We'll basically be validating user input, persisting the newly created competition to our Laravel server and then adding the competition to the store.

Let's get to writing some code to help us accomplish our goal.

competition-create-modal.vue

<template>
    <section>
        <div>
            <div class="modal fade" :class="{ in: open, visible: open }" v-show="open">
                <section class="modal-dialog">
                    <div class="modal-content">
                        <section class="modal-header">
                            <h3 class="text-uppercase text-center">
                                Create new competition
                                <button @click.prevent="closeModal" type="button" class="close btn btn-default" data-dismiss="modal">&times;</button>
                            </h3>
                        </section>

                        <form @submit.prevent="saveCompetition" method="post" action="">
                            <section class="modal-body">
                                <div class="form-group">
                                    <label for="">Name</label>
                                    <input v-validate data-vv-rules="required" v-focus v-model="competition.name" class="form-control" placeholder="Example: FIFA Confederations Cup" type="text" name="competition_name">
                                    <p class="text-danger" v-if="errors.has('competition_name')">{{ errors.first('competition_name') }}</p>
                                </div>
                                <div class="form-group">
                                    <label for="">Alias</label>
                                    <input v-validate data-vv-rules="required" v-model="competition.alias" class="form-control" placeholder="Example: FIFA CC" type="text" name="competition_alias" value="">
                                    <p class="text-danger" v-if="errors.has('competition_alias')">{{ errors.first('competition_alias') }}</p>
                                </div>

                            </section>

                            <section class="modal-footer">
                                <button @click.prevent="closeModal" class="btn btn-default pull-left">&larr; Cancel and go back</button>
                                <button type="submit" class="btn btn-primary">Create competition and continue &rarr;</button>
                            </section>
                        </form>
                    </div>
                </section>
            </div>
            <div class="modal-backdrop" v-show="open"></div>
        </div>

    </section>
</template>

<script>
    import Vue from 'vue';
    import VeeValidate from 'vee-validate';

    Vue.use(VeeValidate, {
        events: 'input|blur'
    });

    import { mapGetters } from 'vuex';
    import { mapActions } from 'vuex';

    export default {
        directives: { focus },

        data: () => {
            return {
                competition: {}
            }
        },

        props: {
            open: {
                type: Boolean,
                default() {
                    return true;
                },
            },
        },

        computed: {
            ...mapGetters([
                'getCompetitionModalStatus',
            ]),
        },

        methods: {
            ...mapActions ([
                'setLoader',
                'setCompetitionModalStatus',
                'persistCompetition',
                'addCompetitionToStore',
            ]),

            triggerRegionModal () {
                this.setCompetitionModalStatus(false);
            },

            closeModal () {
                this.setLoader(false);
                this.setCompetitionModalStatus(false);
            },

            saveCompetition () {
                var self = this;

                this.$validator.validateAll().then(result => {
                    if (result) {
                        self.setLoader(true);

                        this.persistCompetition({
                            callback: (payload) => {
                                this.addCompetitionToStore(payload);

                                this.$emit('competitionSaved', payload.data);

                                this.closeModal();
                            },

                            data: this.competition
                        });

                        return;
                    }

                    alert('Please fix the errors on this form');
                });

            }
        }
    }
</script>

<style scoped>
    .modal-backdrop
    {
        background: rgba(0, 0, 0, 0.8);
    }

    .visible
    {
        display: block;
    }
</style>

This is basically a rehash of our previous modal components. We have our name and alias form groups. We're handling the submit action on our form through the saveCompetition handler method. We're showing the modal only when the Boolean prop, open is set to true.

In our <script/>, we have the closeModal method. This method simply hides any loader instances still visible and then it hides the modal by setting its store property value to false.

In the saveCompetition submit event handler method, we attempt validation of the user supplied information. If our validation was passed successfully, we show a loader and persist the competition to the server. Next, we add the persisted competition to the store. We then get to emit a competitionSaved event that can be caught by the parent component and we close the modal.

3. Building the Leaderboard Create Modal Component.

Finally, we need to create a modal that allows us to add a public accessible leaderboard. This is going to be a piece of cake. All we need to do is provide a text input box that allows our user fill out a leaderboard. In an ideal production application, we'd like to make our leaderboard unique and only allow users add new leaderboards and join previously existing leaderboards.

<template>
    <section>
        <div>
            <div class="modal fade" :class="{ in: open, visible: open }" v-show="open">
                <section class="modal-dialog">
                    <div class="modal-content">
                        <section class="modal-header">
                            <h3 class="text-uppercase text-center">
                                Create new leaderboard
                                <button @click.prevent="closeModal" type="button" class="close btn btn-default" data-dismiss="modal">&times;</button>
                            </h3>
                        </section>

                        <form @submit.prevent="saveLeaderboard" method="post" action="">
                            <section class="modal-body">
                                <div class="form-group">
                                    <label for="">Name</label>
                                    <input v-validate data-vv-rules="required" v-focus v-model="leaderboard.name" class="form-control" type="text" name="leaderboard_name">
                                    <p class="text-danger" v-if="errors.has('leaderboard_name')">{{ errors.first('leaderboard_name') }}</p>
                                </div>

                            </section>

                            <section class="modal-footer">
                                <button @click.prevent="closeModal" class="btn btn-default pull-left">&larr; Cancel and go back</button>
                                <button type="submit" class="btn btn-primary">Create leaderboard and continue &rarr;</button>
                            </section>
                        </form>
                    </div>
                </section>
            </div>
            <div class="modal-backdrop" v-show="open"></div>
        </div>

    </section>
</template>

<script>
    import Vue from 'vue';
    import VeeValidate from 'vee-validate';

    Vue.use(VeeValidate, {
        events: 'input|blur'
    });

    import { mapGetters } from 'vuex';
    import { mapActions } from 'vuex';

    export default {
        directives: { focus },

        data: () => {
            return {
                leaderboard: {}
            }
        },

        props: {
            open: {
                type: Boolean,
                default() {
                    return true;
                },
            },
        },

        computed: {
            ...mapGetters([
                'getLeaderboardModalStatus',
            ]),
        },

        methods: {
            ...mapActions ([
                'setLoader',
                'setLeaderboardModalStatus',
                'persistLeaderboard',
                'addLeaderboardToStore',
            ]),

            closeModal () {
                this.setLoader(false);
                this.setLeaderboardModalStatus(false);
            },

            saveLeaderboard () {
                var self = this;

                this.$validator.validateAll().then(result => {
                    if (result) {
                        self.setLoader(true);

                        this.persistLeaderboard({
                            callback: (payload) => {
                                this.addLeaderboardToStore(payload);

                                this.$emit('leaderboardSaved', payload.data);

                                this.closeModal();
                            },

                            data: this.leaderboard
                        });

                        return;
                    }

                    alert('Please fix the errors on this form');
                });

            }
        }
    }
</script>

<style scoped>
    .modal-backdrop
    {
        background: rgba(0, 0, 0, 0.8);
    }

    .visible
    {
        display: block;
    }
</style>

We have our name form group. We're handling the submit action on our form through the saveLeaderboard handler method.

In our <script/>, we have the closeModal method. This method simply hides any loader instances still visible and then it hides the modal by setting its store property value to false.

In the saveLeaderboard submit event handler method, we attempt validation of the user supplied information (i.e the name field). Upon successfully validation, a loader is displayed and we persist the leaderboard to the server. Next, we add the leaderboard to the store. We then get to emit a leaderboardSaved event that can be caught by the parent component and then we close the modal.

4. Building the User Preferences Component.

We should provide a means for our users to customize their preferences. Currently, we're only allowing functionality that helps us update our preferred leaderboards. Our core functional requirement for this component is the ability to create and update leaderboards. This means we get to trigger the create-leaderboard-modal dialog component we just created.

Let's get straight to coding what we've just discussed.

add-preferences.vue


<template>
    <section>
        <div class="container">
            <div class="row">
                <section class="clearfix">
                    <div class="col-md-6 col-md-offset-3 form-container u-mb-lg" style="margin-bottom: 30px;">
                        <form class="panel panel-default" method="post" @submit.prevent="nextStep">
                            <section class="panel-heading">
                                <h3 class="u-mb-md text-center">
                                    Update Preferences
                                </h3>
                            </section>

                            <section class="panel-body">

                                <section class="text-center">
                                    <h4 class="text-bold text-uppercase u-mb-sm">Just need your preferences</h4>
                                </section>

                                <div class="form-group u-mb-md">
                                    <section>
                                        <label class="" for="">
                                            Select Leaderboard
                                        </label>
                                        <a href="#" @click.prevent="triggerLeaderboardModal" class="btn btn-link pull-right">Create a new leaderboard <b class="caret"></b></a>
                                    </section>

                                    <select v-validate data-vv-rules="required" v-model="leaderboard_id" name="leaderboard_id" class="form-control clearfix">
                                        <option v-for="leaderboard in getLeaderboards" :value="leaderboard.id">{{ leaderboard.name }}</option>
                                    </select>
                                    <p class="text-warning" v-if="errors.has('leaderboard_id')">{{ errors.first('leaderboard_id') }}</p>
                                </div>

                                <div class="form-group clearfix">
                                    <button type="submit" class="btn btn-lg btn-block btn-info pull-right">
                                        Complete Profile (3/3)
                                    </button>
                                </div>
                            </section>
                        </form>

                    </div>
                </section>

            </div>
        </div>

    </section>
</template>

<script>
    import Vue from 'vue';

    import VeeValidate from 'vee-validate';

    Vue.use(VeeValidate, {
        events: 'input|blur'
    });

    import { mapGetters } from 'vuex';
    import { mapActions } from 'vuex';

    Vue.component('leaderboard-modal', require('./dialog/leaderboard-modal.vue'));

    export default {
        props: [],

        data: function () {
            return {
            }
        },

        mounted () {
            var self = this;

            this.$parent.setStep(2);

            if (!this.getLeaderboards.length) {
                self.setLoader(true);

                this.getAllLeaderboards(function () {
                    self.setLoader(false)
                });
            }

        },

        computed: {
            ...mapGetters([
                'getLeaderboards',
                'getLeaderboardModalStatus',
                'getCurrentStep'
            ]),

            leaderboardModalOpen () { return this.isLeaderboardModalOpen}
        },

        methods: {
            ...mapActions ([
                'setLoader',
                'setLeaderboardModalStatus',
                'getAllLeaderboards',
            ]),

            triggerLeaderboardModal () {
                this.setLeaderboardModalStatus(true);
            },

            setLeaderboardID (data) {
                this.leaderboard_id = data.id;
            },

        }
    }
</script>

We have our leaderboard select box form group. We're handling the submit action on our form through the saveLeaderboard handler method.

In our <script/>, we have the triggerLeaderboardModal method. This method simply sets the leaderboard modal state value to open:true

In the setLeaderboardID method,we simply set the leaderboard_id state property to the provided id.

5. Building the Wizard Completion Component.

Finally, our user has completed his profile information with the help of our wizard and we'd love to give him (or her) a pat on the back. Let's create a component that does just that.

completed.vue

<template>
    <div class="container container--area">
        <div class="row">
            <section class="clearfix">
                <div class="col-md-12">
                    <h3 class="text-center heading heading--completed u-mb-md">Way to go! You've successfully completed your info!</h3>
                    <p class="lead text-muted u-mb-lg text-center">
                        Just want to let you know that you've successfully added your profile info. <br>
                        Have fun!
                    </p>

                </div>
            </section>
        </div>
    </div>
</template>
<script>
    import store from '../../vuex/store.js';

    export default {
        props: [],

        mounted () {
            var self = this;

            store.dispatch('setStep', 4);
        },

    }

</script>

<style>
    .container--area
    {
        padding-bottom: 60px;
    }

    .u-mb-lg
    {
        margin-bottom: 60px;
    }

    .heading--completed
    {
        color: #08ABA6;

    }
</style>

This is probably the simplest component we have had to create throughout the duration of our series. This component simply has a greeting message for the user upon completion of the wizard. In production, you'd probably like to display a friendly animation or some other special effect to help keep your app memorable.

Conclusion

We've arrived at the end of the series. In this installment, we set up our individual wizard modal components for the create-competition-modal and the create-leaderboard-modal dialogs. We also setup our add-preferences component as a base for further customization by our users.

We have completed the profile wizard. This is the starting point. Go and explore countless use cases for wizards in your own applications.

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:  

Thanks for the contribution.


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

[utopian-moderator]

Hey @creatrixity! Thank you for the great work you've done!

We're already looking forward to your next contribution!

Fully Decentralized Rewards

We hope you will take the time to share your expertise and knowledge by rating contributions made by others on Utopian.io to help us reward the best contributions together.

Utopian Witness!

Vote for Utopian Witness! We are made of developers, system administrators, entrepreneurs, artists, content creators, thinkers. We embrace every nationality, mindset and belief.

Want to chat? Join us on Discord https://discord.me/utopian-io