Laravel + Vue.js AdminPanel Generator

Last updated on by

Laravel + Vue.js AdminPanel Generator image

Laravel and Vue.js are often used together. With more tools on these technologies are released, here’s one of them – presenting to you Vue+Laravel Admin Panel Generator.

Disclaimer: I’m the founder and one of the developers of this tool, and also Laravel-only generator QuickAdminPanel, but the goal in this article is not only to present you the product, but explain what it generates, and how Vue + Laravel work together. Also, you will find an example project with source available on Github.

How does the generator work?

For those who prefer video, here’s a quick demo:

Now, let’s look at it with more details.

Step 1. You create your panel without coding, just adding menu items and fields.

Step 2. At any point, you can view the generated code, file by file.

Step 3. Then you download the code and install it – locally or on your remote server, with these commands:

composer install
php artisan key:generate
php artisan migrate --seed
php artisan passport:install

Of course, your .env file should be configured at that point.

And then, on the front-end:

npm install
npm run dev

Step 4. That’s it; you have your panel.

Step 5. The most important thing: you can change the code however you want, it’s pure Laravel+Vue, without our generator’s package as a dependency. That’s the main difference from packages like Voyager or Laravel Backpack (which are both excellent, by the way!).


What are we generating – structure of the project

After you download the project, you see something like this:

Generated Code: Back-end Laravel

Let’s first analyze the back-end Laravel part, which serves as API:

Here’s routes/api.php file:

Route::group(['prefix' => '/v1', 'middleware' => ['auth:api'], 'namespace' => 'Api\V1', 'as' => 'api.'], function () {
Route::post('change-password', 'ChangePasswordController@changePassword')->name('auth.change_password');
Route::apiResource('roles', 'RolesController');
Route::apiResource('users', 'UsersController');
Route::apiResource('companies', 'CompaniesController');
Route::apiResource('employees', 'EmployeesController');
});

You can see apiResource for every CRUD, and also one separate POST for changing the password.

Controllers are namespaces under Api/V1, so here’s our app/Http/Controllers/Api/V1/CompaniesController.php:

namespace App\Http\Controllers\Api\V1;
 
use App\Company;
use App\Http\Controllers\Controller;
use App\Http\Resources\Company as CompanyResource;
use App\Http\Requests\Admin\StoreCompaniesRequest;
use App\Http\Requests\Admin\UpdateCompaniesRequest;
use Illuminate\Http\Request;
 
class CompaniesController extends Controller
{
public function index()
{
return new CompanyResource(Company::with([])->get());
}
 
public function show($id)
{
$company = Company::with([])->findOrFail($id);
 
return new CompanyResource($company);
}
 
public function store(StoreCompaniesRequest $request)
{
$company = Company::create($request->all());
 
 
return (new CompanyResource($company))
->response()
->setStatusCode(201);
}
 
public function update(UpdateCompaniesRequest $request, $id)
{
$company = Company::findOrFail($id);
$company->update($request->all());
 
 
return (new CompanyResource($company))
->response()
->setStatusCode(202);
}
 
public function destroy($id)
{
$company = Company::findOrFail($id);
$company->delete();
 
return response(null, 204);
}
}

We have a typical resourceful Controller, with one exception – Resources classes, which have been available since Laravel 5.5.

In our case, every resource is a simple conversion to an array, here’s a file app/Http/Resources/Company.php

namespace App\Http\Resources;
 
use Illuminate\Http\Resources\Json\JsonResource;
 
class Company extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}

But you can extend it, adding your logic on top – see more examples here and here.

Finally, Laravel Passport protects all the routes – when installing the project, you need to run this:

php artisan passport:install

As an overall back-end result, every Controller is responsible for that specific CRUD operations called to the API, from Vue.js front-end.

Generated Code: Front-end Vue.js

Now, let’s take a look at front-end part. The main file for this is resources/client/assets/js/app.js, where we initiate the Vue and some libraries:

// ...
window.Vue = require('vue')
Vue.prototype.$eventHub = new Vue()
 
import router from './routes'
import store from './store'
import Datatable from 'vue2-datatable-component'
import VueAWN from 'vue-awesome-notifications'
import vSelect from 'vue-select'
import datePicker from 'vue-bootstrap-datetimepicker'
import VueSweetalert2 from 'vue-sweetalert2'
import 'eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.css'
 
Vue.use(Datatable)
Vue.use(VueAWN, {
position: 'top-right'
})
Vue.use(datePicker)
Vue.use(VueSweetalert2)
 
Vue.component('back-buttton', require('./components/BackButton.vue'))
Vue.component('bootstrap-alert', require('./components/Alert.vue'))
Vue.component('event-hub', require('./components/EventHub.vue'))
Vue.component('vue-button-spinner', require('./components/VueButtonSpinner.vue'))
Vue.component('v-select', vSelect)
 
moment.updateLocale(window.app_locale, {
week: {
dow: 1
}
})
 
const app = new Vue({
data: {
relationships: {},
dpconfigDate: {
format: window.date_format_moment
},
dpconfigTime: {
format: window.time_format_moment
},
dpconfigDatetime: {
format: window.datetime_format_moment,
sideBySide: true
}
},
router,
store
}).$mount('#app')

Related: Laravel Sweetalert

Next, every CRUD has its own set of components:

For showing the data table, we’re using vue2-datatable-component – here’s full code of resources/clients/assets/components/cruds/Companies/Index.vue:

<template>
<section class="content-wrapper" style="min-height: 960px;">
<section class="content-header">
<h1>Companies</h1>
</section>
 
<section class="content">
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">List</h3>
</div>
 
<div class="box-body">
<div class="btn-group">
<router-link :to="{ name: xprops.route + '.create' }" class="btn btn-success btn-sm">
<i class="fa fa-plus"></i> Add new
</router-link>
<button type="button" class="btn btn-default btn-sm" @click="fetchData">
<i class="fa fa-refresh" :class="{'fa-spin': loading}"></i> Refresh
</button>
</div>
</div>
 
<div class="box-body">
<div class="row" v-if="loading">
<div class="col-xs-4 col-xs-offset-4">
<div class="alert text-center">
<i class="fa fa-spin fa-refresh"></i> Loading
</div>
</div>
</div>
 
<datatable
v-if="!loading"
:columns="columns"
:data="data"
:total="total"
:query="query"
:xprops="xprops"
/>
</div>
</div>
</div>
</div>
</section>
</section>
</template>
 
 
<script>
import { mapGetters, mapActions } from 'vuex'
import DatatableActions from '../../dtmodules/DatatableActions'
import DatatableSingle from '../../dtmodules/DatatableSingle'
import DatatableList from '../../dtmodules/DatatableList'
import DatatableCheckbox from '../../dtmodules/DatatableCheckbox'
 
export default {
data() {
return {
columns: [
{ title: '#', field: 'id', sortable: true, colStyle: 'width: 50px;' },
{ title: 'Name', field: 'name', sortable: true },
{ title: 'Description', field: 'description', sortable: true },
{ title: 'Actions', tdComp: DatatableActions, visible: true, thClass: 'text-right', tdClass: 'text-right', colStyle: 'width: 130px;' }
],
query: { sort: 'id', order: 'desc' },
xprops: {
module: 'CompaniesIndex',
route: 'companies'
}
}
},
created() {
this.$root.relationships = this.relationships
this.fetchData()
},
destroyed() {
this.resetState()
},
computed: {
...mapGetters('CompaniesIndex', ['data', 'total', 'loading', 'relationships']),
},
watch: {
query: {
handler(query) {
this.setQuery(query)
},
deep: true
}
},
methods: {
...mapActions('CompaniesIndex', ['fetchData', 'setQuery', 'resetState']),
}
}
</script>
 
 
<style scoped>
 
</style>

Quite a lot of code, isn’t it? Of course, it could be more straightforward, but we tried to follow the official documentation and best practices, generating code for the cases that could be extended for bigger projects.

Next, we can take a look at Create.vue:

<template>
<section class="content-wrapper" style="min-height: 960px;">
<section class="content-header">
<h1>Companies</h1>
</section>
 
<section class="content">
<div class="row">
<div class="col-xs-12">
<form @submit.prevent="submitForm">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Create</h3>
</div>
 
<div class="box-body">
<back-buttton></back-buttton>
</div>
 
<bootstrap-alert />
 
<div class="box-body">
<div class="form-group">
<label for="name">Name</label>
<input
type="text"
class="form-control"
name="name"
placeholder="Enter Name"
:value="item.name"
@input="updateName"
>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
rows="3"
class="form-control"
name="description"
placeholder="Enter Description"
:value="item.description"
@input="updateDescription"
>
</textarea>
</div>
</div>
 
<div class="box-footer">
<vue-button-spinner
class="btn btn-primary btn-sm"
:isLoading="loading"
:disabled="loading"
>
Save
</vue-button-spinner>
</div>
</div>
</form>
</div>
</div>
</section>
</section>
</template>
 
 
<script>
import { mapGetters, mapActions } from 'vuex'
 
export default {
data() {
return {
// Code...
}
},
computed: {
...mapGetters('CompaniesSingle', ['item', 'loading'])
},
created() {
// Code ...
},
destroyed() {
this.resetState()
},
methods: {
...mapActions('CompaniesSingle', ['storeData', 'resetState', 'setName', 'setDescription']),
updateName(e) {
this.setName(e.target.value)
},
updateDescription(e) {
this.setDescription(e.target.value)
},
submitForm() {
this.storeData()
.then(() => {
this.$router.push({ name: 'companies.index' })
this.$eventHub.$emit('create-success')
})
.catch((error) => {
console.error(error)
})
}
}
}
</script>
 
 
<style scoped>
 
</style>

Edit and Show components for the CRUD are pretty similar, so won’t discuss them here.

In addition to that Vue code, there are many small details and helpers like Sweet Alert, Notifications, Datepickers, and setting/getting relationships data for the forms. I guess I will leave it for you to analyze.

Notice: The choice of Vue.js libraries is pretty subjective, and it was the most challenging part of the project – to choose the Vue libraries to trust. Ecosystem still lacks standards, or 100% trusted open-source – a lot of movement in the market, some libraries are better supported than others. So it’s always hard to guess, and the best libraries will probably change with time, or new ones will appear.


That’s the end of a quick overview of Vue+Laravel QuickAdminPanel, try it out here: https://vue.quickadminpanel.com

Finally, here’s the source of a demo-project with two CRUDs: Companies and Customers.

I hope our generator will not only save you time on writing code but also show you how Vue can work with Laravel. Our way of structuring this code is not the only way, and you can structure your code differently, but we tried our best to stick to standards.

PovilasKorop photo

Creator of Courses and Tutorials at Laravel Daily

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project.

Visit No Compromises
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
Apply Dynamic Filters to Eloquent Models with the Filterable Package image

Apply Dynamic Filters to Eloquent Models with the Filterable Package

Read article
Property Hooks Get Closer to Becoming a Reality in PHP 8.4 image

Property Hooks Get Closer to Becoming a Reality in PHP 8.4

Read article
Asserting Exceptions in Laravel Tests image

Asserting Exceptions in Laravel Tests

Read article
Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4 image

Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4

Read article
Basset is an alternative way to load CSS & JS assets image

Basset is an alternative way to load CSS & JS assets

Read article
Integrate Laravel with Stripe Connect Using This Package image

Integrate Laravel with Stripe Connect Using This Package

Read article