Building A Comments System With Vue.js, Laravel, and Tailwind CSS Part III

Nick Basile • May 3, 2018

Heads up! This post is over a year old, so it might be out of date.

Now that we have our comments functionality in place with Vue.js and our components styled by Tailwind CSS, it's time to start using Laravel to persist our comments.

The Set Up

Let's get this show on the road by setting up our Comment model. We'll run php artisan make:model Comment -a to set up the model and all of the other files that we need to manage our comments.

As we can see, this command will create a migration, factory, controller, and model for us. Now, that we have our files let's start working on our migration.

The Migration

Looking in our database/migrations directory, we can see that we've added a migration to create a comments_table. With our migration, we'll be able to define the database schema for our comments.

Opening up our file, we can see that Laravel has provided us with some boilerplate out of the box. We'll need to add a field for the user_id so we can keep track of who wrote a comment and a body field for the comment itself.

public function up()
{
    Schema::create('comments', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('user_id');
        $table->longText('body');
        $table->timestamps();
    });
}

Just like that, we have our schema defined! Now we can hop to the command line and run php artisan migrate to add that table to our database.

With our database taken care of, let's focus on our factory so we can create some dummy data.

The Factory

Factories let us quickly spin up "dummy" data that we can use with our applications in development. They're incredibly useful if you wanted to write tests for our code - but that's a blog post for another day.

Let's open up database/factories/CommentFactory.php. Once again, Laravel has provided us with some boilerplate to get us started. Since our comments have a relationship with a user, we'll need to create a User to define the user_id. Then we can use the built-in Faker library to generate some dummy text for the body.

<?php

use Faker\Generator as Faker;

$factory->define(App\Comment::class, function (Faker $faker) {
    $user = factory(App\User::Class)->create();

    return [
        'user_id' => $user->id,
        'body' => $faker->sentence,
    ];
});

We're cooking with gas today! If we head to our command line, we can run php artisan tinker to test out our new factory. Inside of tinker, we can write PHP or Laravel code and then execute it in the context of our project. This is such an awesome feature for rapidly testing new features or ideas.

We'll go ahead and run factory(\App\Comment::class, 5)->create(); to add 5 comments to our database. Now if we query for the comments with Eloquent, we can see all of the comments we just created.

>>> \App\Comment::all();
=> Illuminate\Database\Eloquent\Collection {#808
     all: [
       App\Comment {#812
         id: 1,
         user_id: 1,
         body: "Iure ratione laborum provident necessitatibus dolore.",
         created_at: "2018-05-02 23:08:37",
         updated_at: "2018-05-02 23:08:37",
       },
       App\Comment {#813
         id: 2,
         user_id: 2,
         body: "Sed mollitia dolorum itaque reprehenderit tempora.",
         created_at: "2018-05-02 23:08:37",
         updated_at: "2018-05-02 23:08:37",
       },
       App\Comment {#814
         id: 3,
         user_id: 3,
         body: "Tenetur a et consequatur expedita eligendi voluptatem atque.",
         created_at: "2018-05-02 23:08:37",
         updated_at: "2018-05-02 23:08:37",
       },
       App\Comment {#815
         id: 4,
         user_id: 4,
         body: "Cum id perferendis corrupti et qui.",
         created_at: "2018-05-02 23:08:37",
         updated_at: "2018-05-02 23:08:37",
       },
       App\Comment {#816
         id: 5,
         user_id: 5,
         body: "Doloremque et a adipisci non officia.",
         created_at: "2018-05-02 23:08:37",
         updated_at: "2018-05-02 23:08:37",
       },
     ],
   }

Your comments might look a little different than mine, but we've got them in the database! Now let's head over to our model file and set that up.

The Model

Inside of our app directory, we'll find our Comment.php file. This is where we can define our relationships and what data we should be exposing in our queries.

This is critically important from a security standpoint, so we'll want to make sure we handle it correctly. We'll add a protected variable called $fillable where we can define the fields that we'd like to make editable to safeguard against a mass-assignment vulnerability.

protected $fillable = [
    'body',
    'user_id',
];

Next up, we can add a protected $casts variable to set make sure our user_id always comes through as an integer.

protected $casts = [
    'user_id' => 'integer',
];

This isn't strictly required, but I find it helps me quickly scan a model to and remind myself about some of the data types we're using.

Finally, we'll need to define our author relationship. As you may recall, as we were scaffolding our comments in our Vue components, we called the user who created the comment the author. So, to maintain that structure, we'll need to define our relationship a bit differently than usual.

public function author()
{
    return $this->belongsTo(User::class, 'user_id');
}

The name of the method takes care of name for use, but we need to reference the User::class and explicitly define the user_id as the foreign key so Laravel can figure out how to map the way the data is structured to our naming convention.

With our Comment model set up, we can jump into User.php and add the inverse of our author relationship to our user.

public function comments()
{
    return $this->hasMany(Comment::class);
}

With our models set up, we're ready to figure out our controller.

The Controller

We'll find our CommentController.php inside of app/Http/Controllers. Once again, Laravel has provided us with some useful boilerplate. But, we won't need all of these methods so we can go ahead and delete create(), edit(), and `show().

With our controller all cleaned up, let's start with index(). Following our "REST-ful" pattern, the index() method will fetch all of our comments for us. We'll also want to make sure to eager load our author relationship.

public function index()
{
    $comments = Comment::with('author')
        ->orderByDesc('id')
        ->get();

    return response($comments, 200);
}

Nice! Now we can wire up our store() method. We'll use this to add new comments to our application. First, we'll validate that the incoming data contains a body.

Then, we'll use the authenticated user's comments relationship to create the new comment. With this approach, we don't have to worry about manually setting the user_id because Laravel will take care of that for us!

Finally, once our comment is created, we'll make sure to load() the author relationship because we're expecting that data in our Vue component.

public function store(Request $request)
{
    $data = $request->validate([
        'body' => 'required|string'
    ]);

    $comment = auth()->user()
        ->comments()
        ->create($data);

    $comment->load('author');

    return response($comment, 200);
}

We're flying now! Now that we can create new comments let's work on updating existing ones. For that, we can move to the aptly named update() method.

For the update() method, we'll get a little fancy and use route-model binding to "automatically" retrieve the comment we'd like to edit. Once again, we'll validate that the incoming data has a body for us to update.

If that checks out, we'll update our comment and load() our author relationship again.

public function update(Request $request, Comment $comment)
{
    $data = $request->validate([
        'body' => 'required|string'
    ]);

    $comment->body = $data['body'];

    $comment->save();

    $comment->load('author');

    return response($comment, 200);
}

We're down to our last controller method: destroy(). We'll leverage route-model binding once again to retrieve the comment that needs to be deleted, and then we'll delete it.

public function destroy(Comment $comment)
{
    $comment->delete();

    return response( null,204);
}

With our controller functionality all wrapped up, we'll need to add some routes so our Vue components can use it.

Routing

Since this functionality lives in the same app, we can define our routes in the usual web.php file. We'll need to define a route for each of the controller methods we created. And, if the method uses route-model binding, we'll need to define our comment in the route.

Route::get('/comments', 'CommentController@index');
Route::post('/comments', 'CommentController@store');
Route::put('/comments/{comment}', 'CommentController@update');
Route::delete('/comments/{comment}', 'CommentController@destroy');

We continued our "REST-ful" pattern and used the HTTP request that corresponded to our controller method. With our routes defined, it's time for us to update our Vue components to interact with our backend data.

Sharing The Data With Vue

Hopping into CommentsManager.vue, let's start by fetching our dummy comments from the database. We'll go ahead and add a fetchComments() method to our component. In the method, we'll make an AJAX request using Axios and then set our comments data to the response returned from the request.

fetchComments() {
    const t = this;

    axios.get('/comments')
        .then(({data}) => {
            t.comments = data;
        })
},

Awesome! Now we need to fire this method when our component is created. So, we'll hook into Vue's created lifecycle method and call our new method then.

created() {
    this.fetchComments();
},

If we give our page a refresh, we can see that our comments are being pulled from the database! Now we can clear out the hard-coded comments that we used to build out our Vue component because we don't need them anymore.

data: function() {
    return {
        state: 'default',
        data: {
            body: ''
        },
        comments: []
    }
},

Let's work on adding new comments now. Currently, we're using the saveComment() method to add new comments locally. But, we want to make sure that they're getting saved in the database.

Once again we'll use Axios to make our POST request. Then we'll add our new comment to the beginning of our comments Array and toggle the editing state.

saveComment() {
    const t = this;

    axios.post('/comments', t.data)
        .then(({data}) => {
            t.comments.unshift(data);

            t.stopEditing();
        })
},

Now we're ready to work on the updateComment() and deleteComment() methods. I noticed that we had some duplicate functionality for finding the index of our comments, so I extracted that to its own method.

commentIndex(commentId) {
    return this.comments.findIndex((element) => {
        return element.id === commentId;
    });
}

For our updateComment() method, we'll make a PUT request using the comment.id we received from our $event. Then we can use the response data to update our existing comment's body.

updateComment($event) {
    const t = this;

    axios.put(`/comments/${$event.id}`, $event)
        .then(({data}) => {
            t.comments[t.commentIndex($event.id)].body = data.body;
        })
},

Finally, we can do a similar refactor to our deleteComment() method, but instead of updating the body we can .splice() it from the comments Array.

deleteComment($event) {
    const t = this;

    axios.delete(`/comments/${$event.id}`, $event)
        .then(() => {
            t.comments.splice(t.commentIndex($event.id), 1);
        })
},

Just like that our frontend is wired up and talking to our backend!

The Wrap-Up

We've certainly covered a lot of ground in this series. We started with a Vue component, styled it up with Tailwind, and now we're persisting our comments with Laravel. Pretty impressive work!

While we have the core functionality down, there's always room for improvement. We could add some loading indicators during our AJAX calls; implement error handling for when things go wrong, or write some tests so we can have the confidence we need to make changes.

What do you think we could improve? I'd love to hear/see what you come up with! As always, feel free to ask me any questions on Twitter. And until next time, happy coding!

Nick Basile

I craft experiences that help people reach their full potential.

Interested in getting my latest content? Join my mailing list.