This is a little bit of a challenging tutorial. What we want to do is implement inline-templates using blade into pure JavaScript or VueJS. Here is the challenge. As you know, blade has many convenience functions built in that allow you to quickly handle authorization, authentication, and helpful constructs like the @forelse loop. If we want an entirely Vue based solution, we’ll need to redo much of this functionality again on the client side. It’s not the most fun thing in the world, but if you want that entirely slick SPA like feel – it’s something we need to do. So with that, let’s see if we can figure out how to move blade functionality over into Vue components.
The Current Code
At the moment, in threads/show.blade.php, we loop over all of the replies and for each one a Reply.vue single file component is rendered. The goal is to move the replies into a collection of replies that can be accessed and modified in Vue itself.
Essentially, instead of this:
@foreach($replies as $reply)
@include('threads.reply')
@endforeach
We want to do this:
<replies :data="{{ $thread->replies }}"></replies>
Replies.vue
Ok so the first step to make this work is to create that new Replies.vue component in our project.
Our first bit of code in the Replies.vue component will start with this.
<template>
<div>
<div v-for="reply in items">
<reply :data="reply"></reply>
</div>
</div>
</template>
<script>
import Reply from './Reply.vue';
export default {
props: ['data'], // accepts data for replies
components: {Reply},
data() {
return {
items: this.data
}
}
}
</script>
Since we have used an import statement for the Reply component above, it no longer needs to be declared globally in app.js. So go ahead and remove the line:
Vue.component('reply', require('./components/Reply.vue'));
from the app.js file.
While we are at it, we’ll make use of Ctrl+Alt+Shift+J
to select all occurrences of ‘attributes’ in Reply.vue and refactor to ‘data’ to keep things more consistent.
pages/Thread.vue
We also want control over other areas of the thread page. For example, the sidebar that displays how many replies there are needs to update in real time. We’ll add another page specific component for that with Thread.vue.
<script>
export default {
}
</script>
And we will register this in app.js like so.
require('./bootstrap');
window.Vue = require('vue');
Vue.component('flash', require('./components/Flash.vue'));
Vue.component('thread-view', require('./pages/Thread.vue'));
const app = new Vue({
el: '#app'
});
Calling <thread-view> in threads/show.blade.php
With this new thread view component defined, we can reference it in threads/show.blade.php like so.
@extends('layouts.app')
@section('content')
<thread-view inline-template>
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<div class="level">
<span class="flex">
<a href="{{ route('profile', $thread->creator) }}">{{ $thread->creator->name }}</a> posted:
{{ $thread->title }}
</span>
@can('update', $thread)
<form action="{{$thread->path()}}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button class="btn btn-link">Delete Thread</button>
</form>
@endcan
</div>
</div>
<div class="panel-body">
{{ $thread->body }}
</div>
</div>
<replies :data="{{ $thread->replies }}"></replies>
{{--@foreach($replies as $reply)--}}
{{--@include('threads.reply')--}}
{{--@endforeach--}}
{{ $replies->links() }}
@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
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-body">
This thread was created {{ $thread->created_at->diffForHumans() }} by
<a href="{{ route('profile', $thread->creator) }}">{{ $thread->creator->name }}</a> and currently
has {{ $thread->replies_count }} {{ str_plural('reply', $thread->replies_count) }}.
</div>
</div>
</div>
</div>
</div>
</thread-view>
@endsection
Thread Must import Replies
In this snippet above, we are rendering replies using <replies :data=”{{ $thread->replies }}”></replies> so that must be explicitly defined in Thread.vue like so.
<script>
import Replies from '../components/Replies.vue';
export default {
components: { Replies }
}
</script>
Refactoring From Blade To Vue
With the work we have done so far, if we try to load up a threads page and look at Vue Developer tools, we do get an error. [Vue warn]: Failed to mount component: template or render function not defined. found in —> <Reply>
This is because as of now, that is referencing the inline template defined in the reply.blade.php file. But wait. We are using Vue components all the way now, so what do we do? Well, this part is a little challenging, but it’s the only solution at this time. We need to basically reproduce everything blade does in the <template> section of the Reply.vue component. Here’s the rub. You lose all those nice little helper and convenience functions built into blade. In addition, all your variables need to be changed to work on the VueJS / JavaScript side. Fun. In any event, what happens is that the <template> portion of Reply.vue would translate roughly to something like this:
<template>
<div :id="'reply-'+id" class="panel panel-default">
<div class="panel-heading">
<div class="level">
<h5 class="flex">
<a :href="'/profiles/'+data.owner.name"
v-text="data.owner.name">
</a> said {{ data.created_at }}...
</h5>
<div v-if="signedIn">
<favorite :reply="data"></favorite>
</div>
</div>
</div>
<div class="panel-body">
<div v-if="editing">
<div class="form-group">
<textarea class="form-control" v-model="body"></textarea>
</div>
<button class="btn btn-xs btn-primary" @click="update">Update</button>
<button class="btn btn-xs btn-link" @click="editing = false">Cancel</button>
</div>
<div v-else v-text="body"></div>
</div>
<div class="panel-footer level" v-if="canUpdate">
<button class="btn btn-xs mr-1" @click="editing = true">Edit</button>
<button class="btn btn-xs btn-danger mr-1" @click="destroy">Delete</button>
</div>
</div>
</template>
So what’s happening here? Well, the highlighted lines show where edits were needed to reproduce what we can do in blade, but now in JavaScript.
Removing jQuery and Using Vue
Prior to this tutorial, we were making use of a quick line of jQuery to fade out the reply element once it was deleted from the database. What is different now however is that each reply is part of a collection of replies in Vue. So we can just remove the element from the array in Vue, and the page will update automatically for us. No need to use jQuery in this case.
Children Fire Events, Parents Listen To Events
In order for a child component to communicate with a parent component, that child must emit an event. So what we are going to do is add the highlighted line below in Reply.vue to make this announcement that the element was deleted while simultaneously removing the call to jQuery.
<template>
<div :id="'reply-'+id" class="panel panel-default">
<div class="panel-heading">
<div class="level">
<h5 class="flex">
<a :href="'/profiles/'+data.owner.name"
v-text="data.owner.name">
</a> said {{ data.created_at }}...
</h5>
<div v-if="signedIn">
<favorite :reply="data"></favorite>
</div>
</div>
</div>
<div class="panel-body">
<div v-if="editing">
<div class="form-group">
<textarea class="form-control" v-model="body"></textarea>
</div>
<button class="btn btn-xs btn-primary" @click="update">Update</button>
<button class="btn btn-xs btn-link" @click="editing = false">Cancel</button>
</div>
<div v-else v-text="body"></div>
</div>
<div class="panel-footer level" v-if="canUpdate">
<button class="btn btn-xs mr-1" @click="editing = true">Edit</button>
<button class="btn btn-xs btn-danger mr-1" @click="destroy">Delete</button>
</div>
</div>
</template>
<script>
import Favorite from './Favorite.vue';
export default {
props: ['data'],
components: {Favorite},
data() {
return {
editing: false,
id: this.data.id,
body: this.data.body
};
},
methods: {
update() {
axios.patch('/replies/' + this.data.id, {
body: this.body
});
this.editing = false;
flash('Updated!');
},
destroy() {
axios.delete('/replies/' + this.data.id);
this.$emit('deleted', this.data.id);
// $(this.$el).fadeOut(1000, () => {
// flash('Your reply is now deleted!');
// });
}
}
};
</script>
Who is the parent of Reply? Well that would be Replies.vue of course. So since we are setting up the firing of an event on the child, we must now set up a listener on the parent. So in the <template> area of the parent Replies, we will modify the code like so:
<template>
<div>
<div v-for="(reply, index) in items">
<reply :data="reply" @deleted="remove(index)"></reply>
</div>
</div>
</template>
<script>
import Reply from './Reply.vue';
export default {
props: ['data'], // accepts data for replies
components: {Reply},
data() {
return {
items: this.data
}
},
methods: {
remove(index) {
this.items.splice(index, 1)
}
}
}
</script>
What happens now is that the parent picks up on that ‘deleted’ event and we can trigger a remove() method that we also defined in the methods area of the Vue instance. In that method, we just use the JavaScript Splice Function to remove the exact element we want gone. Once it is removed, Vue automatically re renders the page, and Poof! The element is gone.
Updating The Reply Count In Real Time
Another feature we can now add is the ability to update the reply count on the side bar instantly with no need for a page refresh. We’ll set up Thread.vue like so:
<script>
import Replies from '../components/Replies.vue';
export default {
props: ['initialRepliesCount'],
components: {Replies},
data() {
return {
repliesCount: this.initialRepliesCount
};
}
}
</script>
Then bind repliesCount in threads/show.blade.php with something like <span v-text=”repliesCount”></span> Once again, we remove the blade syntax of {{ $thread->replies_count }} and replace it with Vue. You would also need to update the opening <thread-view> tag like so <thread-view :initial-replies-count=”{{ $thread->replies_count }}” inline-template> in threads/show.blade.php.
Replies fires an event, Thread listens for an event
The same thing is now occurring where we need to notify the parent that something has changed. We’ll need to update Replies.vue to emit an event anytime a reply is added or removed. We’ll also add in the add() method which makes use of the JavaScript Push Function to add an item to the collection.
<template>
<div>
<div v-for="(reply, index) in items">
<reply :data="reply" @deleted="remove(index)"></reply>
</div>
</div>
</template>
<script>
import Reply from './Reply.vue';
export default {
props: ['data'], // accepts data for replies
components: {Reply},
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>
This means that we now have a custom event of ‘added’ being emitted when a reply is added, and a custom event of ‘removed’ when a reply is deleted. Therefore, in threads/show.blade.php we can update the
<replies :data="{{ $thread->replies }}" @added="repliesCount++" @removed="repliesCount--"></replies>
Handling Blade Authorization In Vue Components
Here is yet another tricky thing we must tackle when trying to extract components with blade functionality. In blade, we were using authorization to selectively display the Favorite button like so.
@if (Auth::check())
<div>
<favorite :reply="{{ $reply }}"></favorite>
</div>
@endif
This can not be used in Vue, so we need an alternate solution. What do we do? Well ultimately in Vue, it is going to look a bit like this:
<div v-if="signedIn">
<favorite :reply="data"></favorite>
</div>
We’ll need to make use of a new JavaScript variable, and use the v-if directive to decide whether to show the favorite button or not. We can set up this variable in addition to a user variable in the app.blade.php layout file like so.
<script>
window.App = {!! json_encode([
'user' => Auth::user(),
'signedIn' => Auth::check()
]) !!};
</script>
With the global set above, we can use a computed property in Reply.vue like this:
computed: {
signedIn() {
return window.App.signedIn;
},
With that in place, the authorization for the favorite button should now work.
The other blade markup we have to re create in JavaScript is this.
@can('update', $thread)
<form action="{{$thread->path()}}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button class="btn btn-link">Delete Thread</button>
</form>
@endcan
In Vue, we are going to translate that to this.
<div class="panel-footer level" v-if="canUpdate">
<button class="btn btn-xs mr-1" @click="editing = true">Edit</button>
<button class="btn btn-xs btn-danger mr-1" @click="destroy">Delete</button>
</div>
To make this work, we’ll need to define a computed property like so in Reply.vue.
canUpdate() {
return this.authorize(user => this.data.user_id == user.id);
}
as well as add an authorize() function in bootstrap.js like this.
window.Vue.prototype.authorize = function (handler) {
let user = window.App.user;
return user ? handler(user) : false;
};
With these updates, authorization will now work properly on the client side, even without the familiar blade shortcuts we are used to. We’ll take just a quick look at how things are working now. Notice when the user deletes a reply, it is removed from the list of replies instantly. There was no page refresh. In addition, the reply count also updated instantaneously thanks to the communication happenning between Vue components. It’s a lot to keep track of when you’re building it, but the end result is very nice.
A Visual Guide To Your Components
Finally, we can just take a quick look in Vue Developer tools and see how these various components are working together. We now have a parent <ThreadView> component, with <Replies> existing inside of that. The <Replies> component, is a a collection of <Reply> components. This is what makes it so easy to update the number of actual <Reply> elements on the page now. We also see that each <Reply> has a <Favorite> component as well as the <Flash> component we had created earlier. Clicking on each component shows you the reactive data associated with it in the right pane of the developer tools. Pretty slick!
How to use VueJS with Laravel Blade Summary
Recreating all of what blade view files offer into VueJS components is somewhat challenging. There are definitely tradeoffs for the amount of refactoring it takes to get a fully component based solution working well. Yes, the slick single page application like feel is very nice, but it takes time to re do the things you are used to completing very quickly in blade such as handling user authentication and authorization if else type UI display issues. Over time, this will likely get much easier as some best practices will evolve that will make components second nature. Until then, be ready to spend a lot of time refactoring your blade views to a collection of Vue components that talk to each other!