Click to share! ⬇️

Axios Powered VueJS Form Component

Almost the entire thread view page has been updated to use VueJS components. The only thing left to work on is the form to add a new reply to a thread. Currently, it does a full form request to add a new reply. In this tutorial, we will create a new Vue component using Axios to perform ajax requests to the Laravel backend. Via these ajax requests, we will submit new replies to the server, and on the client side VueJS will automatically re render the HTML on the page to reflect the new update.


Create A New .vue File

So let’s begin by creating a new file to hold the new component we want to create.
new vue file for component

With the new .vue file in place, the question becomes how do we start building out the <template> of the .vue file? Well, we can revisit the threads/show.blade.php file to see how this vue form is currently implemented. The markup we’re looking for is highlighted here.

@if(auth()->check())
    <form method="POST" action="{{$thread->path() . '/replies'}}">
        {{csrf_field()}}
        <div class="form-group">
            <textarea name="body" id="body" class="form-control" placeholder="What do you think?" rows="5"></textarea>
        </div>
        <button type="submit" class="btn btn-default">Post</button>
    </form>
@else
    <p class="text-center">Please <a href="{{ route('login') }}">login</a> to respond.</p>
@endif

So once again, we see the need to migrate the markup from a blade based syntax to purely JavaScript. So moving this into the <template> section of the .vue file, we can comment out the markup that is no longer valid. Blade auth checking is now gone, a actual <form> tag is now gone, as well as a few other items.

NewReply.vue


<template>
    <div><!--@if(auth()->check())-->
    <!--<form method="POST" action="{{$thread->path() . '/replies'}}">-->
    <!--{{csrf_field()}}-->
    <div class="form-group">
        <textarea name="body" id="body" class="form-control"
                  placeholder="What do you think?" rows="5"></textarea>
    </div>
    <button type="submit" class="btn btn-default">Post</button>
    <!--</form>-->
    <!--@else-->
    <!--<p class="text-center">Please <a href="{{ route('login') }}">login</a> to respond.</p>-->
    </div><!--@endif-->
</template>

<script>
    export default {}
</script>

Starting With v-model

The first thing we can do to start migrating over is to set up the v-model directive in the <template> along with the associated data property in the <script> area.


<template>
    <div><!--@if(auth()->check())-->
    <!--<form method="POST" action="{{$thread->path() . '/replies'}}">-->
    <!--{{csrf_field()}}-->
    <div class="form-group">
        <textarea name="body"
                  id="body"
                  class="form-control"
                  placeholder="What do you think?"
                  rows="5"
                  v-model="body">
        </textarea>
    </div>
    <button type="submit" class="btn btn-default">Post</button>
    <!--</form>-->
    <!--@else-->
    <!--<p class="text-center">Please <a href="{{ route('login') }}">login</a> to respond.</p>-->
    </div><!--@endif-->
</template>

<script>
    export default {
        data() {
            return {
                body: ''
            }
        }
    }
</script>

Adding A @click event to the button

Now, on the button in the <template> area, we need to add a @click handler to trigger a method. We’ll add a method named addReply() which of course will also be defined in the Vue instance. Note we’ll also add some browser side validation. In the addReply() method, we’ll now start to make use of Axios, a Promise based HTTP client for the browser and node.js, to power the ajax calls.


<template>
    <!--@if(auth()->check())-->
    <!--<form method="POST" action="{{$thread->path() . '/replies'}}">-->
    <!--{{csrf_field()}}-->
    <div class="form-group">
        <textarea name="body"
                  id="body"
                  class="form-control"
                  placeholder="What do you think?"
                  rows="5"
                  required
                  v-model="body">
        </textarea>
    </div>
    <button type="submit"
            class="btn btn-default"
            @click="addReply">Post
    </button>
    <!--</form>-->
    <!--@else-->
    <!--<p class="text-center">Please <a href="{{ route('login') }}">login</a> to respond.</p>-->
    <!--@endif-->
</template>

<script>
    export default {
        data() {
            return {
                body: ''
            }
        },

        methods: {
            addReply() {
                axios.post(this.endpoint, {body: this.body})
                    .then(({data}) => {
                        this.body = '';

                        flash('New reply added!');

                        this.$emit('created', data);
                    });
            }
        }
    }
</script>

Let’s talk about the addReply() method just a bit. First off, we see that axios is used to make a post request to a particular endpoint. That endpoint url is still to be determined. As the payload for that post request, we are sending through the data associated with the body property. This is whatever gets typed into the text area. After this, the .then construct is used to trigger an arrow function which resets the text area back to an empty string, triggers a flash message, then emits a ‘created’ event. So what is up with this firing of an event? Recall that the way Vue components communicate with their parent components if they have one is by firing an event which the parent can then listen for. So this NewReply.vue is a child of Replies.vue. Therefore we set up this ‘created’ event to notify the parent when a re render is needed. Also note, the second argument to the $emit function is the data response from the server.


Rendering A Child Component From a Parent Component

Now we can reference the component up in Replies.vue like so:


<template>
    <div>
        <div v-for="(reply, index) in items" :key="reply.id">
            <reply :data="reply" @deleted="remove(index)"></reply>
        </div>

        <new-reply></new-reply>
    </div>
</template>

<script>
    import Reply from './Reply.vue';
    import NewReply from './NewReply.vue';

    export default {
        props: ['data'], // accepts data for replies

        components: {Reply, NewReply},

        data() {
            return {
                items: this.data
            }
        },

        methods: {
            add(reply) {
                this.items.push(reply);

                this.$emit('added');
            },

            remove(index) {
                this.items.splice(index, 1);

                this.$emit('removed');

                flash('Reply was deleted!');
            }
        }
    }
</script>

Also note that in the component above, the v-for is being used as a tool for a type of list rendering. We are basically rendering out a list of replies. To make sure that when we remove an item from the collection, the correct one is actually removed, we need to use a unique key attribute. This way if we delete the second reply on a page, it will delete that item and not say the 5th reply.

This should be enough to at least render the new reply form, and it does in fact render at this point.
vue form component

In addition, check out the Vue dev tools to see the new component. As we type text into the form, the body property gets updated in real time thanks to v-model.
vue dev tools v-model


Listen for an event in the parent component

Let’s now set up the event listening in the parent component of Replies.vue. What we want to say is, when a new reply has been ‘created’, then it should trigger an add() method. That is represented with this markup in Replies.vue. In addition, we’ll highlight the new add() method in the methods object below. The main purpose of this method is to use a JavaScript push to add a new reply to the Vue collection which will re render it on the page.


<template>
    <div>
        <div v-for="(reply, index) in items" :key="reply.id">
            <reply :data="reply" @deleted="remove(index)"></reply>
        </div>

        <new-reply @created="add"></new-reply>
    </div>
</template>

<script>
    import Reply from './Reply.vue';
    import NewReply from './NewReply.vue';

    export default {
        props: ['data'], // accepts data for replies

        components: {Reply, NewReply},

        data() {
            return {
                items: this.data
            }
        },

        methods: {
            add(reply) {
                this.items.push(reply);

                this.$emit('added');
            },

            remove(index) {
                this.items.splice(index, 1);

                this.$emit('removed');

                flash('Reply was deleted!');
            }
        }
    }
</script>

Calculate the endpoint on the Vue client side

We have not yet set the endpoint for adding a reply. If we try to add a reply now, that POST request is going to get sent off into the abyss. This part is a little bit tricky because the NewReply.vue component is self contained and is a child component, yet, it needs to know what url to post to. This means that we need to rely on the parent to determine the endpoint. So what we can do is add an ‘endpoint’ attribute to the <new-reply> component up in the Replies.vue parent.


<template>
    <div>
        <div v-for="(reply, index) in items" :key="reply.id">
            <reply :data="reply" @deleted="remove(index)"></reply>
        </div>

        <new-reply :endpoint="endpoint" @created="add"></new-reply>
    </div>
</template>

<script>
    import Reply from './Reply.vue';
    import NewReply from './NewReply.vue';

    export default {
        props: ['data'], // accepts data for replies

        components: {Reply, NewReply},

        data() {
            return {
                items: this.data,
                endpoint: location.pathname + '/replies'
            }
        },

        methods: {
            add(reply) {
                this.items.push(reply);

                this.$emit('added');
            },

            remove(index) {
                this.items.splice(index, 1);

                this.$emit('removed');

                flash('Reply was deleted!');
            }
        }
    }
</script>

This means the child NewReply.vue must accept that data via a prop.

<template>
    <div>
        <div class="form-group">
        <textarea name="body"
                  id="body"
                  class="form-control"
                  placeholder="What do you think?"
                  rows="5"
                  required
                  v-model="body">
        </textarea>
        </div>
        <button type="submit"
                class="btn btn-default"
                @click="addReply">Post
        </button>
    </div>
</template>

<script>
    export default {
        props: ['endpoint'],

        data() {
            return {
                body: ''
            }
        },

        methods: {
            addReply() {
                axios.post(this.endpoint, {body: this.body})
                    .then(({data}) => {
                        this.body = '';

                        flash('New reply added!');

                        this.$emit('created', data);
                    });
            }
        }
    }
</script>

Checking in the browser by way of Vue dev tools shows that the data is being passed down from the Parent (Replies.vue) to the child (NewReply.vue) as we see it here.
parent passing data to child component vue


Update the endpoint on the Laravel back end

Initially we had build our controller methods without consideration for a component json powered front end. This means we need to refactor a bit in the store() method of the RepliesController to make sure it can account for ajax requests using the expectsJson() method. Below we first store a newly added reply in the $reply variable. Then with the new if() branch, we check to see if the request that was made was an ajax request, and if it was we directly return the $reply variable while also eager loading the user which is automatically cast to json so the Vue front end can make easy use of it.


public function store($channelId, Thread $thread)
{
    $this->validate(request(), [
        'body' => 'required'
    ]);

    $reply = $thread->addReply([
        'body' => request('body'),
        'user_id' => auth()->id()
    ]);

    if (request()->expectsJson()) {
        return $reply->load('owner');
    }

    return back()->with('flash', 'Your reply was published!');
}

Hide user email

In addition to the change above, we need to update User.php model to hide a user’s email address like so.

protected $hidden = [
    'password', 'remember_token', 'email'
];

This will prevent an end user being able to open up developer tools in their browser and see a user’s email address.


Preventing Non Authenticated Users From Seeing A New Reply Form

Once again, since we lost the ability to use simple blade directives to decide whether to display a particular element on the page, we’ll need to re create that functionality on the JavaScript side. The way we do this is to set up a v-if directive on a wrapping div that fully contains the new reply form and button. Then, we set up a computed property like we did before to look at the JavaScript variable of signedIn to see if the user is signed in or not. In addition, we also make use of a v-else directive to allow the user to login if they like. NewReply.vue is now updated like we see here.


<template>
    <div>
        <div v-if="signedIn">
            <div class="form-group">
                <textarea name="body"
                          id="body"
                          class="form-control"
                          placeholder="What do you think?"
                          rows="5"
                          required
                          v-model="body">
                </textarea>
            </div>
            <button type="submit"
                    class="btn btn-default"
                    @click="addReply">Post
            </button>
        </div>

        <p class="text-center" v-else>
            Please <a href="/login">login</a> to respond.
        </p>

    </div>
</template>

<script>
    export default {
        props: ['endpoint'],

        data() {
            return {
                body: ''
            }
        },

        computed: {
            signedIn() {
                return window.App.signedIn;
            }
        },

        methods: {
            addReply() {
                axios.post(this.endpoint, {body: this.body})
                    .then(({data}) => {
                        this.body = '';

                        flash('New reply added!');

                        this.$emit('created', data);
                    });
            }
        }
    }
</script>

Real Time Reply Count Updating

Another thing we want to do is update the reply count on the page. Since a new reply is now added to the database using ajax, there is no page reload to reflect the updated reply count in the database. In the days before these new MVVM frameworks like Vue, that meant you needed to set up a jQuery like function which fired on a successful ajax request. You would dive into the dom, find the element that has the reply count, and manually update it. Kind of cumbersome. Now, with the data being reactive in our page, we can update the reply count more easily. So what we can do in threads/show.blade.php is to make sure we have an increment and decrement attribute like so.

<replies :data="{{ $thread->replies }}" @added="repliesCount++" @removed="repliesCount--"></replies>

The snippet above means this component is listening for an ‘added’ or ‘removed’ event. If one of those is picked up on, the count gets increased or reduced. This means that we need to emit an event somewhere else. This happens in Replies.vue as part of either the add() or remove() functions.


methods: {
    add(reply) {
        this.items.push(reply);

        this.$emit('added');
    },

    remove(index) {
        this.items.splice(index, 1);

        this.$emit('removed');

        flash('Reply was deleted!');
    }
}

The Moment Of Truth!

Now that we have built everything out, we can add, edit, favorite, or delete a reply all with a very slick single page application like feel. Components are updating in real time on the page with no need for page refreshes which gives a great user experience.
axios vuejs form component


Axios Powered VueJS Form Component Summary

In this tutorial, we took upon the task of setting up a NewReply.vue component which makes use of axios to post new replies to the database. How did we do it?

  • Create a new child component (NewReply.vue)
  • Set up data capture with v-model on the child
  • Set up a click event listener on the child
  • Reference the child (NewReply) in the parent (Replies.vue)
  • Listen for ‘created’ event on parent, trigger add() if so
  • Determine how to populate endpoint url to post to
  • Ensure Laravel backend is supporting ajax requests
  • Set up authentication checks on the client side
  • emit events to allow for real time page updates
Click to share! ⬇️