Community Post

Build nested commenting system using Laravel and VueJs - Part 3

jagadeshanh

# Creating vue components for our post and comments

Before we start creating our vue components, make sure node and npm are installed. Inside the project directory type npm install this will install all the required dependencies.

Vuex is a good option for managing state of the application and hence, will make use of vuex to manage state of our application. Let's start by installing vuex inside your project directory open terminal and type

npm install vuex --save

this will install the latest version of vuex. Go to your resources/assets/js/ and create a file called store.js like the following.

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
    state: {
        posts: [],
    },
    mutations: {
        pushPost(state, payLoad) {
            state.posts.unshift(payLoad);
        }
    }
});

Go to app.js file import this store.js file

import { store } from './store';
const app = new Vue({
    el: '#app',
    store: store
});

Since, we would be working with api's I kind of like the following approach, where i can return whether or not an error exists and then the data and a msg for it. Lets start by creating such a trait and put it inside App/Traits

<?php
namespace App\Traits;
trait ApiJsonResponse
{
    private $res = [
        'error' => null,
        'data'  => null,
        'msg'   => null
    ];

    public function errorResponse($data,$msg){
        $this->res['error'] = true;
        $this->res['data'] = $data;
        $this->res['msg'] = $msg;
        return $this->res;
    }

    public function successResponse($data,$msg){
        $this->res['error'] = false;
        $this->res['data'] = $data;
        $this->res['msg'] = $msg;
        return $this->res;
    }
}

Now, will start by creating a vue component which allows user to post some content, and then will save it in database.

Go to resources/assets/js/components and create a new file and name it Post.vue

<template>
    <div class="panel panel-default">
        <div class="panel-heading">What's there in your mind?</div>
        <div class="panel-body">
            <div class="form-group" v-bind:class='[ errors.hasOwnProperty("content") ? "has-error" : "" ]'>
                <textarea v-model="content" rows="3" class="form-control"></textarea>
                <span v-if='[errors.hasOwnProperty("content")]' class="help-block" v-for="error in errors.content">{{ error }}</span>
            </div>
        </div>
        <div class="panel-footer">
            <button class="btn btn-info pull-right" @click="post"><span class="glyphicon glyphicon-share"></span> Share</button>
            <div class="clearfix"></div>
        </div>
    </div>
</template>
<style scoped>
    textarea{
        resize: vertical;
    }
</style>
<script>
    export default{
        data(){
            return {
                content: '',
                errors: []
            }
        },
        methods:{
            post(){
                axios.post('/api/post',{content: this.content}).then(response => {
                    if(!response.data.error){
                        this.$store.commit('pushPost',response.data.data);
                    }else{
                        this.errors = response.data.data;
                    }
                });
            }
        }
    }
</script>

Go to your PostController and edit the store method, use ApiJsonResponse trait

        try{
            $validator = Validator::make($request->all(), [
                'content' => 'required|max:65535'
                ]);
            if($validator->fails()){
                return response()->json($this->errorResponse($validator->errors(),''));
            }else{
                $post = Post::create(['user_id' => $request->user()->id,'content' => $request->get('content')]);
                return response()->json($this->successResponse($post,''));
            }
        }catch(\Exception $exception){
            return response()->json($exception->getMessage());
        }

In your app.js register Post component below the example component like Vue.component('post', require('./components/Post.vue')); go to home.blade.php and update it

@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <post></post>
        </div>
    </div>
</div>
@endsection

Finally go to routes/api.php and register routes

Route::middleware('auth:api')->namespace('Api')->group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
    Route::resource('post','PostController');
    Route::resource('comment','CommentPostController');
});

Now go to terminal and type npm run dev or npm run watch and visit browser and you should see an output similar to this, I am using chrome vue debugger.

Now that we are able to store the posts in database in the next step will display these posts and allow users to comment

Laravel makes it easy to work with events and broadcasting, let's use pusher for listening to events and whenever someone posts a status will trigger and event and listen to it. Go to pusher.com and create a free account and try it. By default laravel comments out App\Providers\BroadcastServiceProvider::class inside config/app.php go to config/app.pp and uncomment BroadcastServiceProvider

If you want to use pusher you need to install pusher php sdk, in your project directory type

composer require pusher/pusher-php-server

in your .env file update BROADCAST_DRIVER=pusher and update key,app_id and secret, you will get these from pusher dashboard.

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=

let's start by making an event

php artisan make:event StatusWasUpdated
<?php
namespace App\Events;

use App\Post;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class StatusWasUpdated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $post;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return Channel|array
     */
    public function broadcastOn()
    {
        return ['status'];
    }
}

Let's make Feed.vue component which will get all the posts and displays them.

<template>
    <div>
        <div class="panel panel-default" v-for="post in posts">
            <div class="panel-heading">
                {{ post.user.name }} updated status
            </div>
            <div class="panel-body">
                <h4>
                    {{ post.content }}
                </h4>
            </div>
            <div class="panel-footer">
            </div>
        </div>
    </div>
</template>
<script>
    export default {
        computed: {
            posts(){
                return this.$store.state.posts;
            }
        },
        mounted(){
            this.getPosts();
        },
        methods: {
            getPosts(){
                axios.get('/api/post').then(response => {
                    if(!response.data.error){
                        response.data.data.forEach((post) => {
                            this.$store.commit('pushPost',post);
                        });
                    }
                });
            }
        }
    }
</script>

update home.blade.php and add <feed></feed> below the post

@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <post></post>
        </div>
        <div class="col-md-8 col-md-offset-2">
            <feed></feed>
        </div>
    </div>
</div>
@endsection

Now that we are able to retrive posts, but when you post a new status this won't be updated, because we are not yet listening to events.

Lets make Notification.vue component which will listen to all our events.

<template>
    <div></div>
</template>
<script>
    export default{
        mounted(){
            this.listen();
        },
        methods: {
            listen(){
                Echo.channel('status')
                .listen('StatusWasUpdated', (e) => {
                    this.$store.commit('pushPost',e.post);
                });
            }
        }
    }
</script>

place it inside app.blade.php and don't forget to register any new components that we make.

@if (Auth::guest())
     <li><a href="{{ route('login') }}">Login</a></li>
        <li><a href="{{ route('register') }}">Register</a></li>
@else
         <notification></notification>

Now run npm run dev refresh browser and you should see posts getting updated in real time.

update PostController index function

    public function index()
    {
        try{
            $posts = Post::all();
            return response()->json($this->successResponse($posts,''));
        }catch(\Exception $exception){
            return response()->json($exception->getMessage());
        }
    }

updated PostController store function

        try{
            $validator = Validator::make($request->all(), [
                'content' => 'required|max:65535'
                ]);
            if($validator->fails()){
                return response()->json($this->errorResponse($validator->errors(),''));
            }else{
                $post = Post::create(['user_id' => $request->user()->id,'content' => $request->get('content')]);
                $post['comments'] = $post->getThreadedComments();
                event(new StatusWasUpdated($post));
                return response()->json($this->successResponse($post,''));
            }
        }catch(\Exception $exception){
            return response()->json($exception->getMessage());
        }

Now that we have our posts ready will work on comments in the next part (part-3) Part 3