Laravel + Datatables
The following package provides a simple & first party integration between Laravel and the Datatable Component. Never again will you have to write a single line of Javascript to get your tables up and running. Please keep in mind the current implementation is intended to be simple and easy to use, if you are looking for a more advanced package please checkout AG-Grid or similar, since we WON'T over-complicate this package.
Current Features
- Server Side Integration for Server Side Request
- Ability to fully control & generate the datatable configuration from Fluent methods on your Laravel Application
- Ability to generate/include Filters,Actions Search, Pagination, etc... from the same Fluent methods
- Actions are dispatched to Server side, converted to their corresponding Models and/or Eloquent Queries
- Fluent Methods inspired by Filament & Laravel Nova, so you always feel at home!
- Many more to come!
Installation
Get started by installing our official Laravel Package:
composer require flavorly/laravel-vanilla-components
# Optional for Vendor Files & Translations
php artisan vendor:publish --tag="vanilla-components-config"
php artisan vendor:publish --tag="vanilla-components-translations
Search - Laravel Scout
Optionally but recommended, install Laravel Scout, Scout is used to perform the search on the datatable ONLY if your model uses the Searchable
trait, we did not feel like we should implement our own method to search, since scout provide a nice way already to search through your models, and the big plus it also supports Database, Algolia, Meilisearch drivers out of the box.
# Install Scout for Search
# For more information please visit https://laravel.com/docs/scout
composer require laravel/scout
On your model add the Laravel Scout Searchable
trait, and define the toSearchableArray
method, this method is used to search your model.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Payment extends Model
{
use Searchable;
use HasFactory;
public function searchableUsing()
{
// If can use any engines of your choice here
return app(EngineManager::class)->engine('database');
}
#[SearchUsingPrefix(['id', 'gateway'])]
public function toSearchableArray()
{
return [
'id' => $this->id,
'gateway' => $this->gateway,
'status' => $this->status,
];
}
}
Search - Without Scout
If scout is not your thing, you can still use the search feature, a really basic query using like
will be used to search your models. You are still free to modify this behavior by overriding the applySearch
method on your table. We will discuss this later on.
Create your first table
Now that the package is installed, you may move on to create your first table. The following example will show a simple skeleton on how your Datable file class should look like ( Yeah, stubs coming soon! ). We will get more into details on how to configure your table later on.
We usually recommend to set your Datatables in a app/Datatables
folder, but you can place them wherever you want.
├─ app
├─ Datatables
├─ UserDatatable.php
Here is the skeleton:
namespace App\Datatables\Users;
use Flavorly\VanillaComponents\Datatables\Datatable;
use Flavorly\VanillaComponents\Datatables\Options\General\Options;
class User extends Datatable
{
public function query(): mixed
{
return User::query()->where('user_id', auth()->id());
// You can also return a closure or a relationship here
// return fn() => User::query()->where('user_id', auth()->id());
// Auth::user()->payments();
}
public function fetchEndpoint(): ?string
{
// Endpoint where the datatable will fetch the data from
return route('datatables.demo');
}
public function columns(): array
{
return [
// Columns go here
];
}
public function filters(): array
{
return [
// Filters go here
];
}
public function actions(): array
{
return [
// Actions go here
];
}
public function options(): array| Options
{
return Options::make()->refreshable()->hideSearchBar();
}
}
Sharing your table to Vue & Setup the Endpoint
After you create your table, you should be able to share it to Vue, so you can use it in your frontend. The following example will show you how to share your table to Vue using inertia, you are not limited to use Inertia You could also have a dedicated endpoint that returns the datatable configuration, as long as the array arrives on the frontend you should be good to go!
From your Laravel Controller send the table to your view as an array:
public function index(){
return inertia('index', [
'usersTable' => (new UserTable())->toArray(),
]);
}
// Or with Hybridly
public function index(){
return hybridly('index', [
'usersTable' => (new UserTable())->toArray(),
]);
}
On the frontend you can use the Datatable
component to render your table:
<script setup lang="ts">
import { Datatable } from "@flavorly/vanilla-components";
const props = defineProps({
usersTable: [Object, Array],
});
</script>
<template>
<div class="p-5 sm:p-20 bg-gray-200 dark:bg-gray-900">
<Datatable
:config="props.usersTable"
/>
</div>
</template>
You will need another endpoint or controller, this control will handle the incoming ajax requests from the table. The controller will simply take the request and send a normal json response back to the client.
Personal recommendation is to create a dedicated controller for your tables, and place everything there :p
<?php
namespace App\Http\Controllers;
use App\Datatables\Users\User as UserTable;
class DatatableController extends Controller
{
public function usersTable()
{
return (new UserTable())->response();
}
}
Columns
You may create columns with the Flavorly\VanillaComponents\Datatables\Columns\Column
class that is provided by the package. For the time being we will keep it short, feel free to explore the fluent methods.
Make sure to return an array of columns from the columns()
method.
- The name define for your column is the actual name that it will try to search for in the database, so make sure to use the correct name.
- The label is the actual label that will show on the table head
// Import at the top level
use Flavorly\VanillaComponents\Datatables\Columns\Column;
public function columns(): array
{
return [
Column::make()->name('id')->label('ID')->sortable(),
Column::make()->name('name')->label('Name')->raw()->sortable(),
Column::make()->name('email')->label('Email')->sortable(applySortUsing: fn($query, $direction) => $query->orderBy('email', $direction)),
];
}
Actions
Actions are shown when you select one or multiple rows on your table, a list of actions will be shown in the top right corner of the table. Use the Flavorly\VanillaComponents\Datatables\Actions\Action
Make sure to return an array of columns from the actions()
method.
The actions can be created inline, or if you prefer you are also able to create them as a separate class, and then just reference them in the array, this will work as long as your class extends the base action class and implements both a: handle()
, setup()
method, the setup method is used to boostrap your action, so you want to define anything related to it, while the handle method is used to actually perform the action, more on this will be explained bellow.
// Import at the top level
use Flavorly\VanillaComponents\Datatables\Actions\Action;
use Flavorly\VanillaComponents\Core\Confirmation\Confirmation;
public function actions(): array
{
return [
Action::make()
->name('copy')
->label('Copy Orders')
->confirmation(
Confirmation::make()
->buttons('Ok', 'No, lets forget it')
->title('Are you sure you really really really sure?')
->text('Are you sure you want to copy this orders?')
->danger()
//->info()
//->warning()
)
->onBefore(function (RequestPayload $data) {
ray('Im running before the action',$data);
})
->onFinished(function (RequestPayload $data) {
ray('Im running After',$data);
})
//->using(MyAction::class)
->using(function(RequestPayload $data, Collection $models){
ray('Im running the actual action',$data, $models);
}),
// Other actions may go here
];
}
Actions - Hooks & Events 🚇
Actions provide a set of hooks & events that are dispatched before, after, failed, & on finished. The action events and hooks are dispatched with a RequestPayload
containing relevant information & data about the action. The hooks accept a callback that will be executed when the action is about to run/finish.
onBefore - Dispatched before the action is executed and also emits a
Flavorly\VanillaComponents\Events\DatatableActionStarted
onAfter - Dispatched after the action is executed with
success
and also emits aFlavorly\VanillaComponents\Events\DatatableActionExecuted
onFailed - Dispatched when the action failed to execute with
exception
and also emits aFlavorly\VanillaComponents\Events\DatatableActionFailed
onSuccess - Dispatched when the action failed to execute with
success
and also emits aFlavorly\VanillaComponents\Events\DatatableActionSuccess
onFinished - Dispatched
always
on finish no matter if success or not and also emits aFlavorly\VanillaComponents\Events\DatatableActionFinished
Actions - Confirmation ✅
The confirmation()
action fluent method accepts an instance of Flavorly\VanillaComponents\Core\Confirmation\Confirmation
, this will help you create a confirmation dialog before the action is executed for destructive actions. The method allows you to choose the button labels, titles, text, style of the dialog ( warning, info, etc ) and in the future also include additional fields/data to be sent along-side with the action.
Actions - Using & Callback 🚩
Actions are only useful if you can actually do something with them, so the using()
method is used to define the actual action that will be executed when the action is triggered. The using method takes a closure
or a class
The closure will receive one argument, RequestPayload
that is injected and contains the relevant data about the action, we will explain the request payload in full later on. You can also type-hint
the Illuminate\Database\Eloquent\Collection
if you do this, we will convert all the selected ids into their corresponding models and pass them to the closure or class that you defined.
When using a class to handle the action, you must ensure that the class implement one of the methods: __invoke
or handle
you may also define your own method name as a second argument of the using()
fluent method, we will attempt to find those methods and return the resolve back.
Actions - Request Payload 📨
The request payload RequestPayload
contains all the important information when a action is dispatched and its inject on most of the closures that are related to the action itself, the payload contains the following information, keep in mind that some information might not be present at all times, example the before hook contains the RequestPayload, but doesn't contain any models, since the resolving did not happen yet.
Here are the following properties that are available on the payload:
getFilters() - Return a collection of the filter applied, each item on the collection is instance of a Vanilla Components
Filter
ObjectgetSorting() - Return a collection of the sorting applied, each item on the collection is instance of a Vanilla Components
Column
ObjectisAllSelected() - Returns a boolean if the user selected all the rows on the table ( not just the current page ) but the whole datatset for the applied query.
getSelectedRowsIds() - Returns a collection of the selected ids, this will be empty if the user selected all the rows on the table.
getAction() - Return the current action being executed as a Vanilla Components
Action
Object that also contains the Table instance inside it.getQuery() - Return the current Query being executed in case you want to modify or clone. This is a Laravel Query Builder or a Scout Builder instance
getModels() - Return the collection of Eloquent Database Models in case you type-hinted the models or used the function to load the models
getPerPage() - Returns the number of items being paginated per page
getSearch() - Returns the search query if any, if no search query it will return a empty string
Actions - Other Methods
Actions also provide a set of other methods that you may use to customize the action, since those are pretty self-explanatory, we will not go into detail about them, but you can check the source code for more information.
canSee() / canExecute() - Defines if a user is allowed to see or execute the action
clearFilters() - Clears the filters after the action is executed defaults to
true
clearSelectionAfterAction() - Clears the selected items after the action is executed and finished, defaults to
true
polling() - Accepts a
Flavorly\VanillaComponents\Core\Polling\Polling
instance and can be used to start polling the server after the action is executed, useful if you expect something to change within a timeframe after the action is executed.
Actions - Redirect
If you want to redirect to a certain page after an action you are allowed to do so using the redirect()
fluent method, the redirect will take 2 parameters one, for the url and another if you want to open in a new tab or not, the default is false
which means it will open in the same tab.
Action::make()
->name('visit_google')
->label('Visit Google')
->redirect('https://google.com', openInNewTab: true)
Actions - Inertia & Hybridly
You may want to dispatch a inertia call after the action is executed, you can do that by using the inertia()
or hybridly()
fluent method, this method accepts a closure or a object that receives a Flavorly\VanillaComponents\Core\Integrations\VanillaInertia
or Flavorly\VanillaComponents\Core\Integrations\VanillaHybridly
instance that can be used to define the inertia call that will be dispatched after the action is executed.
First install the adapter package with one of the following commands:
For Inertia:
pnpm add @flavorly/vanilla-components-inertia
yarn add @flavorly/vanilla-components-inertia
npm install @flavorly/vanilla-components-inertia
For Hybridly:
pnpm add @flavorly/vanilla-components-hybridly
yarn add @flavorly/vanilla-components-hybridly
npm install @flavorly/vanilla-components-hybridly
On your table actions you should be able to do the following:
Action::make()
->name('delete')
->label('Delete')
->inertia(function(VanillaInertia $inertia) {
// Reload can only be used standalone, for other calls you must use the visit method
$inertia
->route('index') // Shortcut for Laravel route
->visit(route('index'))
->only(['foo', 'bar'])
->method('put')
->data(['foo' => 'bar'])
->post(['foo' => 'bar'])
->options([
'headers' => [
'some-weirdo-header' => csrf_token()
]
]);
})
Action::make()
->name('delete')
->label('Delete')
->inertia(function(VanillaHybridly $hybridly) {
// Expect same methods as Inertia + Hybridly goodies
$hybridly
->route('index')
->except(['foo', 'bar']);
})
On your frontend, you should then pass your configuration into the proper adapter, so it can transform and register all the necessary callbacks, here is a small example on how to do it:
<script setup>
import { Datatable } from "@flavorly/vanilla-components";
import { useInertiaDatatable } from "@flavorly/vanilla-components-inertia";
import { useHybridlyDatatable } from "@flavorly/vanilla-components-hybridly";
const props = defineProps({
usersTable: [Object, Array],
paymentsTable: [Object, Array],
});
// Choose what you love! :p
const usersTable = useInertiaDatatable(props.usersTable);
const usersTable = useHybridlyDatatable(props.usersTable);
</script>
<template>
<Datatable :config="usersTable" />
</template>
Actions - Final Note
That's it! You should be covered using actions in full! The rest of UI and customization is up to you using the Vue component and slots and what not.
Filters
Filters are a way to filter the data on the table, they are very similar to the actions, but instead of executing a action, they will modify the query that is being executed to filter the data.
Filters are generic enough to take any component of your choice, they will simply forward value/default value and tell vue what component should render.
By Default the Package ships with the default Vanilla Components that can be used as filters:
- Text - A simple text input that will filter the data based on the column name
- Select - A simple select supporting options
- Rich Select - An extended select that supports search, multiple, and more
- Date - A date picker that supports date, date range, and time
- Toggle - A simple toggle
- Switch - A simple Switch
Make sure to return an array of filters from the filters()
method.
// Import at the top level
use Flavorly\VanillaComponents\Datatables\Filters\Filter;
use Flavorly\VanillaComponents\Core\Components\Input;
use Flavorly\VanillaComponents\Core\Components\RichSelect;
use Flavorly\VanillaComponents\Core\Option\Option;
public function filters(): array
{
return [
// Note that here we are using the Component Select to create a filter
Select::make()
// Name is the actual Datatable column if no apply closure is provided
->name('verified')
->label('Verified')
->placeholder('Choose an option')
->options([
['value' => false, 'text' => 'Not Verified'],
['value' => true, 'text' => 'Verified'],
]),
// Here we are using a generic implementation of a filter
Filter::make()
->name('email')
->placeholder('Email')
->label('Email')
// Use the attributes to forward any attributes to the component
->attributes([
'type' => 'email',
])
// Here we instruct what compomnent to use
->input(),
// Another filter
RichSelect::make()
->name('banned')
->label('Banned')
// NEW: This will make the select multiple and allow to toggle multiple options
->multiple()
// NEW: Pick a endpoint to fetch options from
->fetchOptionsFrom(route('api.fetch-users'),'name', 'id')
// Other options
->options([
// Note here we are using the Options class to generate actual options instead of a plain array
Option::make()
->label('foooo')
->value('im_some_value')
->children([
Option::make()
->label('foooo_back')
->value('im_some_value_1'),
Option::make()
->label('foooo_back2')
->value('im_some_value_2'),
]),
]),
// Applying using your own closure
Input::make()
->name('username')
->placeholder('Write your username')
->label('Username')
// You can use apply using to define your query.
->applyUsing(function (Builder $query, $column, $value) {
if ($value === 'true') {
$query->whereNotNull('verified_at');
} elseif ($value === 'false') {
$query->whereNull('verified_at');
}
})
];
}
Filters - Modify the query
Filters can modify the query by using the applyUsing()
method, this method accepts a closure that will be executed when the filter is applied, the closure will receive the following arguments:
- query - The current query that is being executed, this is a Laravel Query Builder or a Scout Builder instance
- column - The column name that is being filtered, this is the name that you provided on the filter
- value - The value that the user provided on the filter
Filters - Adding your own Components
Filters are generic enough to take any component of your choice, they will simply forward value/default value and tell vue what component should render.
You may define the ->component()
method that accepts any string, all you need to make sure is that the component is registered on the Vue instance.
You may also use the ->attributes()
method to forward any attributes to the component. Please keep in mind that we will ignore certain attributes like v-model
, v-bind
and so on.
Filters - Copy URL
You will notice that when opening the filters modal/dialog, you will are able to copy the direct link for this specific filter, this is useful if you want to share a specific filter with someone else. The filters key is based on the ->name()
method, so make sure to use a unique name for table.
Options
Options are a way to customize the table with certain features, you may use the options()
method to return a Options
instace
Here is a list of the options that are available:
- refreshable() - Turn on/off the refresh button. Default:
true
- searchable() - Turn on/off the search input. Default:
true
- hideSearchBar() - Hides the search bar by default and places a icon to toggle it. Default:
false
- selectable() - Turn on/off the selection. Default:
true
- canSelectAllMatching() - Enable or disable select all records matching feature. Default:
true
- compact() - Makes the table compact. Default:
false
- striped() - Makes the table striped. Default:
false
- manageSettings() - Enable or disable settings management. Default:
true
- showTotalNumberOfItems() - Show the total number of items. Default:
true
- showCurrentPage() - Show the current page. Default:
true
- showNextPages() - Show the next pages. Default:
true
- showPages() - Show/hide pages. Default :
false
Here is a small example of how to use the options:
public function options(): array| Options
{
return Options::make()
->refreshable()
->hideSearchBar();
}
Advanced Options
While the most basic options are covered in the sections above, datatable provides way more options that can be used to customize the table.
Modifying the query
Sometimes and most likely you will want to define a query for the datatable You may do this using the query()
method inside the datatable, that must return a Eloquent Query builder If you do prefer you may also inject the query directly to the response
as the following example:
<?php
namespace App\Http\Controllers;
use App\Datatables\Users\User as UserTable;
class DatatableController extends Controller
{
public function usersTable()
{
$query = User::query()->where('is_admin', true);
return (new UserTable())->response($query);
}
public function usersTableExample()
{
// Passing a eloquent model class is also possible!
return (new UserTable())->response(User::class);
}
// Or if you prefer a simplified version
public function usersTable(Request $request)
{
$datatable = (new UserTable());
if($request->wantsJson() && $request->method() === 'POST'){
return $datatable->response();
}
// Or Hybridly! No bad feelings!
return inertia('backend', [
'datatable' => $datatable->toArray(),
]);
}
}
Model Primary Key
By default, the datatable will attempt to resolve the Model based on the query that you pass for it, it will also attempt to discover the primary key to be used for the datatable if for any reason it's unable to resolve, it fall back to id
as the primary key.
If for some reason you want to modify this you are able to use another key by defining a primaryKey()
method on the datatable, this method must return a string.
public function primaryKey(): ?string
{
return 'uuid';
}
Origin URL
Datatables always live on a page of your application, with features like reset filters we need to know where the datatable are coming from, this is done by using the originUrl()
method, this method must return a string. This way, when reseting filters, we will know the exact url to redirect to and to transform, if no URL is provided the current url
will be used.
public function originUrl(): string
{
return route('users.index');
}
Polling
Sometimes its really nice if we could have our data to be pooled every X seconds, this is possible on Vanilla Datatables! :p
To enable polling you must use the polling()
method, this method must return Polling
instance with your configuration. Polling is disabled by default, and the times are always in seconds
, accepts also a closure to define time. Its possible also to stop polling when the data changes.
use Flavorly\VanillaComponents\Core\Polling\Polling;
public function polling(): Polling
{
return Polling::make()
->every(10)
->during(60)
->stopOnDataChange();
}
Name & Unique Table ID
Each datatable must have a unique name, the name is used to store the settings for the datatable on the user browser, and also to identify the datatable when using the Copy Filter Link
feature on the frontend.
By default, we will simply hash the Datatable namespace and use it as a unique identifier, but you may also define your own name by using the name()
method.
public function name(): string
{
return 'users-table';
}
// Or even
public function name(): string
{
return 'users-payments';
}
Translations
There a few translations that are used on the datatable, you may define your own translations by using the translations()
method, this method must return an array of translations. You may also override the default translations by overriding the defaultTranslations()
method.
public function translations(): array
{
return [
'title' => 'Items',
'subtitle' => 'Here you can check your latest items',
'resource' => 'Item',
'resources' => 'Items',
'actionsButton' => 'Actions',
'actionsSelectedRows' => 'With =>rows selected',
// ... and more
];
}
// or if you prefer you may also merge with the defaults
public function translations(): array
{
return $this->mergeTranslations(['title' => 'Users']);
}
Fetch Endpoint & Actions Endpoint
Datatable requires an endpoint to pull the json data to feed the table, in order to configure that please use the fetchEndpoint()
method, this method must return a string. Keep in mind this needs to be a full qualified URL, using the route()
helper is recommended
Vanilla components uses fetch to perform the requests by default, so if you are using Laravel you should probably be fine!
public function fetchEndpoint(): ?string
{
return route('datatables.demo');
}
Actions can be sent to a separate endpoint if you would like, if thats the case you pay use the actionsEndpoint()
method, this method must return a string. If nothing is provided for the actions endpoint, the fetch endpoint will be used instead.
public function actionsEndpoint(): ?string
{
return route('datatables.demo.some.action');
}
If you readed till here your the real MPV, otherwise thank you for reading this far, I hope you enjoy using Vanilla Components as much as I do!
Transforming Data
Sometimes you may want to transform the data before it gets to the frontend, this is possible by using the transform()
method, this method must return an array and accepts the current row as the first argument.
public function transform(Payment $payment): array
{
return [
'id' => $payment->id,
'gateway' => $payment->gateway,
'amount' => $payment->amount,
'user.name' => fn() => $payment->user->full_name,
'created_at' => $payment->created_at->format('Y-m-d H:i:s'),
];
}
Overriding Search Query, Sorting Query & Filtering Query
Sometimes you may want to override the default search, sorting or filtering query, this is possible by using the applySearch()
, applyQuerySorting()
or applyQueryFilters()
methods, these methods must return a Eloquent Query Builder. All of the methods get the current query as the first argument, and the Payload instance ( with your frontend data ) as the second argument.
There you are free to modify the query as you wish, and return it back to the datatable.
Here is an example of the standard implementation of the applyQueryFilters()
method:
protected function applyQueryFilters(Builder $query, RequestPayload $payload): Builder
{
return $query->when($payload->hasFilters(), function (Builder $subQuery) use($payload){
// Each column that needs to be sorted
$payload
->getFilters()
// Apply Sorting
->each(fn (Filter $filter) => $filter->apply($subQuery, $filter->getName(), $filter->getValue()));
return $subQuery;
});
}