Tutorial build a wizard with vuex

in #utopian-io2 years ago

Repository

https://github.com/vuejs/vuex
7b8b5acb31defd60918ce7dabdb25082b128vue.jpg

What Will I Learn?

I will learn:
• Team Create Modal Component.
• Competition Create Modal Component.
• Leaderboard Create Modal Component.
• User Preferences Vue Component.
• Wizard Completion Component.

Requirements

• PHP version 5.6.4 or greater
• Composer version 1.4.1 or greater
• Lucid Laravel version 5.3.*
• Yarn package manager
Github

Difficulty

• Advanced

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

    <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 @dolar="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 dolar 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.

Competition Create Modal Component.

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

    <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.

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.

    <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.

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

    <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.

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

    <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.

Resource

Proof of Work Done

Github

Sort:  

Is this tutorials some sort of series? Because I don't find Laravel code here.
If yes, then there is also validation part in the server I guess.

About global state (Vuex related), can I ask your opinion about storing info and token at the global states? Some people say that storing it in global states (Vuex or maybe local storage) can be attack-factor for doing XSS

Last thing, many people that are new to state management easily confused and sometimes misunderstood what it's for (sometimes they use it like Service Pattern in OOP paradigm). Maybe adding some diagram and illustration can bring more value to your reader :)

Hey @drsensor
Here's a tip for your valuable feedback! @Utopian-io loves and incentivises informative comments.

Contributing on Utopian
Learn how to contribute on our website.

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Thank you for your contribution.

  • There is a lot of information on this subject, try to find something more innovative and contribute to the open source community.
  • There is already a utopian contributor who talks about this subject, here is the link.

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