Click to share! ⬇️

reply form vue component

In our application, we can create a new thread and leave a reply to any thread. We can also delete any thread or reply based on permissions allowed from the policies we had configured. Now we want to be able to edit a reply. Instead of deleting a reply entirely, we want to be able to just click a button, fix any text that we might not like, and then quickly save that edit. In this tutorial, we will use VueJS and textarea binding to create a reply component that will allow us to do just that.


threads/reply.blade.php

Beginning in the reply.blade.php view file, we will start by adding a simple edit button.


@can('update', $reply)
    <div class="panel-footer level">
        <button class="btn btn-xs mr-1">Edit</button>
        <form method="POST" action="/replies/{{$reply->id}}">
            {{ csrf_field() }}
            {{ method_field('DELETE') }}
            <button class="btn btn-danger btn-xs">Delete Reply</button>
        </form>
    </div>
@endcan

The goal is going to be that when a user clicks the edit button, the area seen highlighted below will morph into a text area where the user can update the text, and then send an ajax request upon completion to save the changes. This way there are no redirects and so on and so forth, making the edit action a very seamless experience.
dynamic text area vuejs


A Vue Inline Template

There are a few ways to set this up, but what makes sense here is to use a Vue Component but to make the template for that component inline. What that means is we’re kind of wrapping the existing view file right inside of a component. The markup for that looks like so.


<reply inline-template>
    <div id="reply-{{ $reply->id }}" class="panel panel-default">

        <div class="panel-body">
            <div class="level">
                <h5 class="flex">
                    <a href="{{ route('profile', $reply->owner) }}">
                        {{$reply->owner->name}}
                    </a> said {{ $reply->created_at->diffForHumans() }}
                </h5>

                <div>
                    <form method="POST" action="/replies/{{$reply->id}}/favorites">
                        {{csrf_field()}}
                        <button type="submit" class="btn btn-primary {{ $reply->isFavorited() ? 'disabled' : '' }}">
                            {{ $reply->favorites_count }} {{ str_plural('Favorite', $reply->favorites_count) }}
                        </button>
                    </form>
                </div>
            </div>
        </div>

        <div class="panel-body">
            {{ $reply->body }}
        </div>

        @can('update', $reply)
            <div class="panel-footer level">
                <button class="btn btn-xs mr-1">Edit</button>
                <form method="POST" action="/replies/{{$reply->id}}">
                    {{ csrf_field() }}
                    {{ method_field('DELETE') }}
                    <button class="btn btn-danger btn-xs">Delete Reply</button>
                </form>
            </div>
        @endcan
    </div>
</reply>

Loading the page in the browser would break at this point. You would see a message of “app.js:32407 [Vue warn]: Unknown custom element: – did you register the component correctly? For recursive components, make sure to provide the “name” option. (found in )”. That is because we still need to actually create that reply component.
Vue warn Unknown custom element


Register A New VueJS Component In Laravel

Now we can begin the process of registering and creating the <reply> component we need. In your IDE, you can visit resources/assets/js/app.js and add the new component.


require('./bootstrap');

window.Vue = require('vue');

Vue.component('flash', require('./components/Flash.vue'));

Vue.component('reply', require('./components/Reply.vue'));

const app = new Vue({
    el: '#app'
});

Notice that now we are registering two Vue components. You can see the registration for the flash component that we created in the example component tutorial. Now you are safe to create the Reply.vue file within the components directory like so.
new vue component file


Export The Vue Instance

The very first thing in the <script> of the component that we complete is to export the Vue instance.


<script>
    export default {

    }
</script>

Turn on file watching

Because we are now working with Vue and editing JavaScript files, we need to turn on file watching so that when we update files they will recompile automatically. Turn this on with either npm run watch or yarn run watch.

vagrant@homestead:~/Code/forumio$ yarn run watch

yarn run watch

Now with just the skeleton of the component in place, the error message we were seeing in Vue dev tools is cleared.


v-if v-else and data properties

When we click on the edit button, we want to somehow turn on the ability to edit. To accomplish this, we can attach an event listener to the button element in the view file. The markup here says, when the button is clicked, we want to set an editing property to true.


@can('update', $reply)
    <div class="panel-footer level">
        <button @click="editing = true" class="btn btn-xs mr-1">Edit</button>
        <form method="POST" action="/replies/{{$reply->id}}">
            {{ csrf_field() }}
            {{ method_field('DELETE') }}
            <button class="btn btn-danger btn-xs">Delete Reply</button>
        </form>
    </div>
@endcan

Determining visibility based on property value

Now, within the panel area where we display a reply, we will use both the v-if and v-else vuejs directives to determine if we will show one <div> or a different <div>. So below, if editing is true, then we see a text area. Otherwise, we will see the standard reply text of the reply on the page.


<div class="panel-body">
    <div v-if="editing">
        <textarea></textarea>
    </div>

    <div v-else>
        {{ $reply->body }}
    </div>
</div>

Declaring Reactive Properties

We need to make sure that the property editing is defined on the Vue instance. Otherwise, we will see an error in Vue Dev tools of “[Vue warn]: Property or method “editing” is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.” With that in mind, we can now update Rely.vue like so:


<script>
    export default {
        data() {
            return {
                editing: false
            };
        }
    };
</script>

If a user clicks the edit button, the editing property gets set to true, and like magic we see the text box. Very cool!
v-if v-else example

We can see the relationship of how this works in this diagram as well.
vue js data property relationships


Passing Attribute Values From HTML To Vue Instance

A requirement you will almost always need is to be able to pass attribute values from the HTML side back into the Vue instance. This is done via props. In our case, we want access to the data held in the $reply variable from the Laravel side. How do we do this? Well, we are going to echo out the $reply to populate the value of an attribute in the HTML itself. We then set up the props property in the Vue instance to accept whatever is held in the particular attribute we are dealing with. In our case, we named the property attributes.

In our reply.blade.php file, we will specify this like so:

<reply :attributes="{{ $reply }}" inline-template>

The Vue instance will now be updated as well with props.


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

        data() {
            return {
                editing: false
            };
        }
    };
</script>

With the plumbing in place, Vue now has full access to all the data contained in the $reply variable. By opening up Vue developer tools, we can see that the <Reply> component now has that data.
vue developer tools inspect component


Populating HTML Elements using v-model

You will also need to be able to take data that is contained in the Vue instance, and use it to populate markup on the HTML side. This can be accomplished with v-model. For example, when we click on that edit button, a textarea is being presented. The textarea is blank however. What we actually want is the text of the reply so we can edit it. Well, now that we have access to that data right inside of Vue, we can bind it to the textarea with a v-model. Here is how we will do it.

Apply the v-model directive in reply.blade.php


<div class="panel-body">
    <div v-if="editing">
        <textarea v-model="body" class="form-control"></textarea>
    </div>

    <div v-else>
        {{ $reply->body }}
    </div>
</div>

Now we bind the data contained in the body property of the attributes object like so in Reply.vue.


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

        data() {
            return {
                editing: false,
                body: this.attributes.body
            };
        }
    };
</script>

The result is that now when the edit button is clicked, the text area is displayed. This time however, since v-model is in place and is looking for the data contained in the body property, the textarea gets populated with the text we are interested in as well. Again, very slick!
vue v-model binding example

Another diagram shows the relationship for v-model to data in the Vue instance.
v-model element binding example


Adding A Cancel Button

So you clicked the edit button, but you have decided that the reply is actually just fine. You don’t need to update anything, you just want to cancel the editing process. All you have to do is set the editing property back to false, and the editing textarea no longer displays. So we can add a button like we do below, set the @click event to assign the value of false to the editing property when it is clicked, and like magic once again the UI updates very quickly.


<div class="panel-body">
    <div v-if="editing">
        <div class="form-group">
            <textarea v-model="body" class="form-control"></textarea>
        </div>
        <button @click="editing = false" class="btn btn-xs btn-warning">Cancel</button>
    </div>

    <div v-else>
        {{ $reply->body }}
    </div>
</div>

data driven visibility vue


Adding A Save Button

There needs to be a button to actually save any changes you make if you decide to edit the text. We can add the markup as follows which says, when you click this button, call the update() method on the Vue instance. We have not yet created that method, but we will in a minute.


<div class="panel-body">
    <div v-if="editing">
        <div class="form-group">
            <textarea v-model="body" class="form-control"></textarea>
        </div>
        <button @click="update" class="btn btn-xs btn-success">Save</button>
        <button @click="editing = false" class="btn btn-xs btn-warning">Cancel</button>
    </div>

    <div v-else>
        {{ $reply->body }}
    </div>
</div>

Using Axios For Ajax With Vue

Now we get to the fun stuff. We need to take action when calling the update() method in Vue. What do we want to accomplish? Well, we want to save the data associated with the v-model back into the database via an ajax request to Laravel.


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

        data() {
            return {
                editing: false,
                body: this.attributes.body
            };
        },

        methods: {
            update() {
                axios.patch('/replies/' + this.attributes.id, {
                    body: this.body
                });
            }
        }
    };
</script>

Setting up the API on the Laravel side

The Vue code is ready to go. When we click on the save button, it triggers the update() method on the Vue instance. That method as we can see above, fires off an ajax request using axios to the replies endpoint with the id of the reply as a wildcard. In addition, the payload sent is the body property which contains the updated text entered in the textarea. This endpoint does not yet exist on the Laravel side, so we need to set that up.


Create A Test for the API

First we can set up a new test in our ParticipateInForumTest class.

public function test_authorized_users_can_update_replies()
{
    $this->signIn();

    $reply = create('AppReply', ['user_id' => auth()->id()]);

    $this->patch('/replies/' . $reply->id, ['body' => 'Text has been edited!']);

    $this->assertDatabaseHas('replies', ['id' => $reply->id, 'body' => 'Text has been edited!']);
}

Then we can add the proper route to our routes file in web.php.

<?php

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

Route::get('/threads', 'ThreadsController@index');
Route::get('/threads/create', 'ThreadsController@create');
Route::get('/threads/{channel}/{thread}', 'ThreadsController@show');
Route::delete('/threads/{channel}/{thread}', 'ThreadsController@destroy');
Route::post('/threads', 'ThreadsController@store');
Route::get('/threads/{channel}', 'ThreadsController@index');
Route::post('/threads/{channel}/{thread}/replies', 'RepliesController@store');
Route::patch('/replies/{reply}', 'RepliesController@update');
Route::delete('/replies/{reply}', 'RepliesController@destroy');
Route::post('/replies/{reply}/favorites', 'FavoritesController@store');
Route::get('/profiles/{user}', 'ProfilesController@show')->name('profile');

And finally, we can add the update() method to the RepliesController.

<?php

namespace AppHttpControllers;

use AppReply;
use AppThread;
use IlluminateHttpRequest;

class RepliesController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

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

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

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

    public function update(Reply $reply)
    {
        $reply->update(request(['body']));
    }

    public function destroy(Reply $reply)
    {
        $this->authorize('update', $reply);

        $reply->delete();

        return back();
    }
}

Running the test shows we are good to go.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_authorized_users_can_update_replies
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 893 ms, Memory: 10.00MB

OK (1 test, 1 assertion)

We should also add a test for unauthorized users. We do not want them updating replies they should not.

public function test_unauthorized_users_can_not_update_replies()
{
    $reply = create('AppReply');

    $this->patch('/replies/' . $reply->id)
        ->assertRedirect('/login');

    $this->signIn()
        ->patch('/replies/' . $reply->id)
        ->assertStatus(403);
}

So for this, we’ll need to also make use of the policy we had created during this series. This will make sure only authorized users can update a reply.


public function update(Reply $reply)
{
    $this->authorize('update', $reply);
    
    $reply->update(request(['body']));
}

Updating The View Once Complete

Rest assured, the code above will update the database via the ajax request we are making. Now, we want to make sure that we see the updated reply immediately without the need for a page refresh. How to do this? Inside the update() method, once everything finishes, then the editing property should get set back to false. We can also add a quick call to the flash() function so that even though there is no full page reload, a flash message will still appear.


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

        data() {
            return {
                editing: false,
                body: this.attributes.body
            };
        },

        methods: {
            update() {
                axios.patch('/replies/' + this.attributes.id, {
                    body: this.body
                });
                this.editing = false;
                flash('Updated!');
            }
        }
    };
</script>

What that does is cause the v-else to trigger in the view file once the update() is complete. There’s just one problem however. The contents of the body are echoed out via Laravel and that is the text prior to editing.

<div v-else>
    {{ $reply->body }}
</div>

We don’t want to see the old text, we want to see the new updated text. That means we should not be manually populating the HTML, we should have Vue populate the HTML dynamically via it’s binding abilities. If we change this markup to use v-text like so, then we should be good.

<div v-else v-text="body"></div>

It looks like editing in real time with no page reload is in fact working great!
vue instant update on page


Getting Rid Of Odd Page Loads

You might notice if you are following along that when you reload the page, the textarea is loading kind of oddly. Watch the textarea as we manually reload the page. Not good.
strange element rendering


v-cloak to the rescue

v-cloak allows you to add an attribute to an element until everything is fully loaded. It kind of reminds me of like when you put all your jquery in a .ready() function and tells the code to hold off until this page is fully loaded and ready to go. So what we want to do is actually just not display the element as the page is loading. This is what is causing that odd element jumping on the page. So at the very top level of the component, we’ll just add the v-cloak directive like so.

<reply :attributes="{{ $reply }}" inline-template v-cloak>

And in our css, we can just define as follows:


<style>
    [v-cloak] {
        display: none;
    }
</style>

Now, as things are loading, the dynamic component will not even be displayed. When loading is complete, Vue will automatically remove that bit of css defined by v-cloak and you will see the element on the page with no artifacts or page jumps.
v-cloak example


VueJS Textarea Binding Summary

This was a fun and pretty involved tutorial. We saw exactly how to work with VueJS in the browser in concert with Laravel acting as the API on server to create real time page editing and updating with no need for any manual page reloads. This gives the end user a really smooth and slick user experience, and was quite fun to set up for us the developers.

Click to share! ⬇️