Vue.js is transforming how we build Laravel frontends. Unlike heavy frameworks, Vue integrates incrementally, letting you add reactivity where needed without rebuilding your entire application. At ZIRA Software, we've built dozens of Laravel + Vue applications, and this combination delivers the best developer experience.
Why Vue.js with Laravel?
Vue advantages:
- Gentle learning curve
- Reactive data binding
- Component-based architecture
- Lightweight (20KB gzipped)
- Progressive framework
- Official Laravel support
- Excellent documentation
Perfect for:
- Dashboard interfaces
- Real-time data displays
- Interactive forms
- Single-page application sections
- Progressive enhancement
Setup Vue.js in Laravel
Install Vue
Laravel 5.1+ includes Vue out of the box via Elixir:
npm install
package.json includes:
{
"dependencies": {
"vue": "^1.0.0",
"vue-resource": "^0.7.0"
}
}
Compile Assets
gulpfile.js:
var elixir = require('laravel-elixir');
elixir(function(mix) {
mix.browserify('app.js');
mix.sass('app.scss');
});
resources/assets/js/app.js:
var Vue = require('vue');
var VueResource = require('vue-resource');
Vue.use(VueResource);
// Enable CSRF token
Vue.http.headers.common['X-CSRF-TOKEN'] = document.querySelector('#token').getAttribute('value');
// Boot Vue
new Vue({
el: 'body'
});
Run build:
gulp watch
Your First Vue Component
Create Component
resources/assets/js/components/TaskList.vue:
<template>
<div class="task-list">
<h2>{{ title }}</h2>
<ul>
<li v-for="task in tasks" :class="{ completed: task.completed }">
<input type="checkbox" v-model="task.completed" @change="toggle(task)">
{{ task.name }}
<button @click="remove(task)" class="delete">×</button>
</li>
</ul>
<form @submit.prevent="addTask">
<input v-model="newTask" placeholder="Add new task...">
<button type="submit">Add</button>
</form>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
title: 'My Tasks',
tasks: [],
newTask: ''
};
},
ready: function() {
this.fetchTasks();
},
methods: {
fetchTasks: function() {
this.$http.get('/api/tasks').then(function(response) {
this.tasks = response.data;
});
},
addTask: function() {
if (!this.newTask) return;
this.$http.post('/api/tasks', { name: this.newTask }).then(function(response) {
this.tasks.push(response.data);
this.newTask = '';
});
},
toggle: function(task) {
this.$http.put('/api/tasks/' + task.id, task);
},
remove: function(task) {
this.$http.delete('/api/tasks/' + task.id).then(function() {
this.tasks.$remove(task);
});
}
}
};
</script>
<style scoped>
.task-list {
max-width: 600px;
margin: 0 auto;
}
.completed {
text-decoration: line-through;
opacity: 0.6;
}
.delete {
float: right;
color: red;
cursor: pointer;
}
</style>
Register Component
app.js:
Vue.component('task-list', require('./components/TaskList.vue'));
new Vue({
el: 'body'
});
Use in Blade
resources/views/tasks/index.blade.php:
@extends('layouts.app')
@section('content')
<task-list></task-list>
@endsection
@section('scripts')
<input type="hidden" id="token" value="{{ csrf_token() }}">
<script src="{{ elixir('js/app.js') }}"></script>
@endsection
Laravel API Routes
app/Http/routes.php:
Route::group(['prefix' => 'api', 'middleware' => 'auth'], function () {
Route::get('tasks', 'TaskController@index');
Route::post('tasks', 'TaskController@store');
Route::put('tasks/{task}', 'TaskController@update');
Route::delete('tasks/{task}', 'TaskController@destroy');
});
TaskController:
<?php namespace App\Http\Controllers;
use App\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index()
{
return response()->json(
auth()->user()->tasks()->get()
);
}
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|max:255'
]);
$task = auth()->user()->tasks()->create([
'name' => $request->name,
'completed' => false
]);
return response()->json($task, 201);
}
public function update(Request $request, Task $task)
{
$this->authorize('update', $task);
$task->update($request->all());
return response()->json($task);
}
public function destroy(Task $task)
{
$this->authorize('delete', $task);
$task->delete();
return response()->json(null, 204);
}
}
Advanced Components
Parent-Child Communication
ParentComponent.vue:
<template>
<div>
<user-profile :user="currentUser" @updated="onUserUpdated"></user-profile>
</div>
</template>
<script>
module.exports = {
data: function() {
return {
currentUser: {}
};
},
ready: function() {
this.$http.get('/api/user').then(function(response) {
this.currentUser = response.data;
});
},
methods: {
onUserUpdated: function(user) {
this.currentUser = user;
alert('User updated!');
}
}
};
</script>
UserProfile.vue:
<template>
<form @submit.prevent="save">
<input v-model="user.name">
<input v-model="user.email">
<button type="submit">Save</button>
</form>
</template>
<script>
module.exports = {
props: ['user'],
methods: {
save: function() {
this.$http.put('/api/user', this.user).then(function(response) {
this.$emit('updated', response.data);
});
}
}
};
</script>
Custom Events
// Emit event
this.$emit('task-completed', task);
// Listen to event
<task-item @task-completed="onTaskCompleted"></task-item>
// Event bus for global events
var bus = new Vue();
// Emit
bus.$emit('notification', { message: 'Task completed!' });
// Listen
bus.$on('notification', function(data) {
alert(data.message);
});
Computed Properties
<script>
module.exports = {
data: function() {
return {
tasks: []
};
},
computed: {
completedTasks: function() {
return this.tasks.filter(function(task) {
return task.completed;
});
},
pendingTasks: function() {
return this.tasks.filter(function(task) {
return !task.completed;
});
},
progress: function() {
if (this.tasks.length === 0) return 0;
return (this.completedTasks.length / this.tasks.length) * 100;
}
}
};
</script>
<template>
<div>
<div class="progress">
<div class="bar" :style="{ width: progress + '%' }"></div>
</div>
<p>{{ completedTasks.length }} / {{ tasks.length }} completed</p>
</div>
</template>
Real-Time Updates with Broadcasting
Install Pusher:
composer require pusher/pusher-php-server
config/broadcasting.php:
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_KEY'),
'secret' => env('PUSHER_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
],
],
Create Event:
<?php namespace App\Events;
use App\Task;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class TaskCreated implements ShouldBroadcast
{
public $task;
public function __construct(Task $task)
{
$this->task = $task;
}
public function broadcastOn()
{
return ['tasks'];
}
}
Fire Event:
event(new TaskCreated($task));
Listen in Vue:
<script>
module.exports = {
ready: function() {
var pusher = new Pusher('your-pusher-key');
var channel = pusher.subscribe('tasks');
channel.bind('App\\Events\\TaskCreated', function(data) {
this.tasks.push(data.task);
}.bind(this));
}
};
</script>
Form Validation
ValidatedForm.vue:
<template>
<form @submit.prevent="submit">
<div :class="{ 'has-error': errors.name }">
<input v-model="user.name" placeholder="Name">
<span class="error" v-if="errors.name">{{ errors.name }}</span>
</div>
<div :class="{ 'has-error': errors.email }">
<input v-model="user.email" placeholder="Email">
<span class="error" v-if="errors.email">{{ errors.email }}</span>
</div>
<button type="submit">Save</button>
</form>
</template>
<script>
module.exports = {
data: function() {
return {
user: {
name: '',
email: ''
},
errors: {}
};
},
methods: {
submit: function() {
this.errors = {};
this.$http.post('/api/users', this.user).then(
function(response) {
alert('User created!');
},
function(response) {
// Laravel validation errors
this.errors = response.data;
}
);
}
}
};
</script>
Vuex for State Management
Install Vuex:
npm install vuex --save
store.js:
var Vue = require('vue');
var Vuex = require('vuex');
Vue.use(Vuex);
module.exports = new Vuex.Store({
state: {
tasks: [],
loading: false
},
mutations: {
SET_TASKS: function(state, tasks) {
state.tasks = tasks;
},
ADD_TASK: function(state, task) {
state.tasks.push(task);
},
REMOVE_TASK: function(state, task) {
state.tasks.$remove(task);
},
SET_LOADING: function(state, loading) {
state.loading = loading;
}
},
actions: {
fetchTasks: function(context) {
context.commit('SET_LOADING', true);
Vue.http.get('/api/tasks').then(function(response) {
context.commit('SET_TASKS', response.data);
context.commit('SET_LOADING', false);
});
},
createTask: function(context, name) {
return Vue.http.post('/api/tasks', { name: name }).then(function(response) {
context.commit('ADD_TASK', response.data);
return response.data;
});
}
}
});
Use in component:
<script>
module.exports = {
computed: {
tasks: function() {
return this.$store.state.tasks;
},
loading: function() {
return this.$store.state.loading;
}
},
ready: function() {
this.$store.dispatch('fetchTasks');
},
methods: {
addTask: function() {
this.$store.dispatch('createTask', this.newTask).then(function() {
this.newTask = '';
}.bind(this));
}
}
};
</script>
Testing Vue Components
Install Vue Test Utils:
npm install vue-test-utils --save-dev
npm install mocha chai --save-dev
test/TaskList.spec.js:
var Vue = require('vue');
var TaskList = require('../components/TaskList.vue');
describe('TaskList', function() {
it('renders tasks', function() {
var vm = new Vue({
template: '<div><task-list :tasks="tasks"></task-list></div>',
components: { 'task-list': TaskList },
data: {
tasks: [
{ id: 1, name: 'Task 1', completed: false },
{ id: 2, name: 'Task 2', completed: true }
]
}
}).$mount();
expect(vm.$el.querySelectorAll('li').length).to.equal(2);
});
it('adds task', function(done) {
var vm = new Vue({
template: '<div><task-list></task-list></div>',
components: { 'task-list': TaskList }
}).$mount();
var component = vm.$children[0];
component.newTask = 'New Task';
component.addTask();
Vue.nextTick(function() {
expect(component.tasks.length).to.equal(1);
expect(component.tasks[0].name).to.equal('New Task');
done();
});
});
});
Production Build
Optimize for production:
gulp --production
This will:
- Minify JavaScript
- Remove Vue warnings
- Version assets
- Generate source maps
Enable production mode:
if (process.env.NODE_ENV === 'production') {
Vue.config.debug = false;
Vue.config.silent = true;
}
Best Practices
1. Keep components small
- Single responsibility
- Reusable pieces
- Maximum 200 lines
2. Use props for data down
<user-profile :user="currentUser"></user-profile>
3. Use events for actions up
this.$emit('user-updated', user);
4. Leverage computed properties
- Cached until dependencies change
- Better than methods for derived data
5. Use Vuex for shared state
- Don't pass data through many levels
- Centralized state management
6. Validate API responses
this.$http.get('/api/tasks').then(function(response) {
if (Array.isArray(response.data)) {
this.tasks = response.data;
}
});
Common Patterns
Pagination
<script>
module.exports = {
data: function() {
return {
tasks: [],
currentPage: 1,
lastPage: 1
};
},
methods: {
loadPage: function(page) {
this.$http.get('/api/tasks?page=' + page).then(function(response) {
this.tasks = response.data.data;
this.currentPage = response.data.current_page;
this.lastPage = response.data.last_page;
});
},
nextPage: function() {
if (this.currentPage < this.lastPage) {
this.loadPage(this.currentPage + 1);
}
},
prevPage: function() {
if (this.currentPage > 1) {
this.loadPage(this.currentPage - 1);
}
}
}
};
</script>
Infinite Scroll
<script>
module.exports = {
data: function() {
return {
tasks: [],
page: 1,
loading: false,
hasMore: true
};
},
ready: function() {
window.addEventListener('scroll', this.onScroll.bind(this));
this.loadMore();
},
methods: {
onScroll: function() {
var bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
if (bottomOfWindow && !this.loading && this.hasMore) {
this.loadMore();
}
},
loadMore: function() {
this.loading = true;
this.$http.get('/api/tasks?page=' + this.page).then(function(response) {
this.tasks = this.tasks.concat(response.data.data);
this.hasMore = response.data.current_page < response.data.last_page;
this.page++;
this.loading= false;
});
}
}
};
</script>
Conclusion
Vue.js with Laravel creates a powerful, productive development experience. Start with small components, progressively enhance your application, and scale to full single-page applications when needed. At ZIRA Software, this stack powers our most successful client projects.
Vue's simplicity combined with Laravel's elegance means you spend less time fighting frameworks and more time building features users love.
Ready to build modern Laravel applications with Vue.js? Contact ZIRA Software to discuss how we can help you create reactive, performant web applications.