DEV Community

Cover image for Build a Laravel Admin CRUD in Minutes with MrCatz DataTable (Livewire)
Ryan Nr
Ryan Nr

Posted on • Originally published at datatable.catzoid.tech

Build a Laravel Admin CRUD in Minutes with MrCatz DataTable (Livewire)

Every Laravel project eventually needs the same thing: an admin page with a table, filters, a search box, an add/edit modal, and CRUD buttons. You

rebuild it from scratch every time — or you reach for a heavyweight framework that replaces your whole UI stack.

I got tired of both options, so I built MrCatz DataTable — a Livewire-first Laravel DataTable package that gives

you Filament-level CRUD productivity without taking over your Tailwind + DaisyUI stack.

In this post I'll show you how to go from composer require to a production-ready admin CRUD page in under 5 minutes.

What you get out of the box

  • Pagination, sorting, column resize, column reorder, column visibility toggle
  • Multi-keyword search with per-column relevance scoring + typo tolerance
  • Filters (select, callback, date, date-range, dependent)
  • Inline cell editing with validation
  • Bulk select + bulk delete
  • Expandable rows
  • Excel + PDF export
  • Form Builder with 25+ field types (text, number, date, file, rich editor, repeater, toggle, etc.)
  • Modal form or full-page form view — switchable per page
  • URL persistence for filters, sort, page, hidden columns
  • Optional Meilisearch integration via Laravel Scout
  • Tailwind CSS v4 + DaisyUI v5 styling — theme it however you want

All of it is opt-in. Turn off what you don't need, extend what you do.


Install

  composer require mrcatz/datatable
Enter fullscreen mode Exit fullscreen mode

That's it — no config publish required. Livewire 3, Laravel 10+, Tailwind v4.

Add the package's Blade files to your Tailwind source paths so its classes get compiled:

  /* resources/css/app.css */                               
  @source "../../vendor/mrcatz/datatable/resources/**/*.blade.php";                                                                                        
Enter fullscreen mode Exit fullscreen mode

Your first CRUD page

An admin CRUD in MrCatz is always two Livewire components:

  1. A Page component (extends MrCatzComponent) — handles the form modal, add/edit/delete lifecycle, notifications.
  2. A Table component (extends MrCatzDataTablesComponent) — defines columns, filters, and the base query.

Generate both with one artisan command:

  php artisan make:mrcatz-table Product                     
Enter fullscreen mode Exit fullscreen mode

Or write them by hand. Here's a complete product admin page in ~80 lines.

1. The Page component

  namespace App\Livewire\Admin\Product;

  use Illuminate\Support\Facades\DB;                                                                                                                       
  use MrCatz\DataTable\MrCatzComponent;                                                                                                                    
  use MrCatz\DataTable\MrCatzFormField;                                                                                                                    

  class ProductPage extends MrCatzComponent
  {                                                                                                                                                        
      public $name, $sku, $price, $stock;                   
      public $active = true;                                                                                                                               

      public function mount()                                                                                                                              
      {                                                                                                                                                    
          $this->setTitle('Product');                       
      }                                                                                                                                                    

      public function render()                                                                                                                             
      {                                                     
          return view('livewire.admin.product.product-page');
      }

      public function setForm(): array                                                                                                                     
      {
          return [                                                                                                                                         
              MrCatzFormField::text('name',  label: 'Name',  rules: 'required|max:100',  icon: 'inventory_2'),
              MrCatzFormField::text('sku',   label: 'SKU',   rules: 'required|max:30',   icon: 'tag'),                                                     
              MrCatzFormField::number('price', label: 'Price', rules: 'required|numeric|min:0', icon: 'payments'),                                         
              MrCatzFormField::number('stock', label: 'Stock', rules: 'required|integer|min:0', icon: 'inventory'),                                        
              MrCatzFormField::toggle('active', label: 'Active'),                                                                                          
          ];                                                                                                                                               
      }                                                                                                                                                    

      public function prepareEditData($data)                                                                                                               
      {
          $this->id    = $data['id'];                                                                                                                      
          $this->name  = $data['name'];                                                                                                                    
          $this->sku   = $data['sku'];
          $this->price = $data['price'];                                                                                                                   
          $this->stock = $data['stock'];                    
      }                                                                                                                                                    

      public function saveData()                                                                                                                           
      {                                                     
          $this->validate($this->getFormValidationRules());

          $payload = compact('name', 'sku', 'price', 'stock') +                                                                                            
                     ['active' => $this->active ? 1 : 0];                                                                                                  

          $this->isEdit                                                                                                                                    
              ? DB::table('products')->where('id', $this->id)->update($payload)
              : DB::table('products')->insert($payload);                                                                                                   

          $this->dispatch_to_view(true, $this->isEdit ? 'update' : 'insert');                                                                              
      }

      public function dropData()                            
      {
          $ok = DB::table('products')->where('id', $this->id)->delete();
          $this->dispatch_to_view((bool) $ok, 'delete');                                                                                                   
      }                                                                                                                                                    
  }                                                                                                                                                        
Enter fullscreen mode Exit fullscreen mode

Zero boilerplate for the modal — it's already wired up by MrCatzComponent.

2. The Table component

  namespace App\Livewire\Admin\Product;

  use Illuminate\Support\Facades\DB;                        
  use MrCatz\DataTable\MrCatzDataTablesComponent;
  use MrCatz\DataTable\MrCatzDataTableFilter;

  class ProductTable extends MrCatzDataTablesComponent                                                                                                     
  {                                                                                                                                                        
      public $showAddButton = true;                                                                                                                        
      public $showSearch    = true;                         
      public $exportTitle   = 'Products';

      public function baseQuery()
      {                                                                                                                                                    
          return DB::table('products');                     
      }                                                                                                                                                    

      public function setTable()                                                                                                                           
      {                                                     
          return $this->CreateMrCatzTable()
              ->withColumnIndex('#')                                                                                                                       
              ->withColumn('Name',  'name',  editable: true, sort: true)                                                                                   
              ->withColumn('SKU',   'sku',   uppercase: true, sort: true)                                                                                  
              ->withColumn('Price', 'price', gravity: 'right', sort: true)                                                                                 
              ->withColumn('Stock', 'stock', gravity: 'right', editable: true)                                                                             
              ->withActionColumn('Actions', editable: true, deletable: true)                                                                               
              ->setDefaultOrder('name', 'asc');             
      }                                                                                                                                                    

      public function setFilter()                                                                                                                          
      {                                                     
          return [
              MrCatzDataTableFilter::create(                                                                                                               
                  id: 'active',
                  label: 'Status',                                                                                                                         
                  data: [1 => 'Active', 0 => 'Inactive'],   
                  key: 'active',                                                                                                                           
              )->get(),
          ];                                                                                                                                               
      }                                                     
  }                                                                                                                                                        
Enter fullscreen mode Exit fullscreen mode

3. Wire them up

The Page view just mounts the Table + includes the form/scripts partials:

  <div>                                                     
      <livewire:admin.product.product-table />

      @include('mrcatz::components.ui.datatable-form')                                                                                                     
      @include('mrcatz::components.ui.datatable-scripts')                                                                                                  
  </div>                                                                                                                                                   
Enter fullscreen mode Exit fullscreen mode

Register a route:

  Route::get('/admin/products', ProductPage::class)->name('admin.products');
Enter fullscreen mode Exit fullscreen mode

Reload — you have a fully functional admin CRUD with search, filter, sort, pagination, inline edit, Excel/PDF export, and a modal form. All in ~100

lines.


Features you'll appreciate later

Inline editing with validation

  ->withColumn('Price', 'price', editable: true, rules: 'numeric|min:0')
Enter fullscreen mode Exit fullscreen mode

User clicks a cell → input appears → Enter saves via a onInlineUpdate() hook on your Page component. Validation rules ship to the client automatically.

Full-page form mode for long forms

Some forms don't fit in a modal. One flag switches to a full-page view:

  public $modalFullScreen = true;                           
Enter fullscreen mode Exit fullscreen mode

The table hides, the form takes its place, and all table state is preserved (search, filters, sort, page, scroll) when the user returns.

Per-column search scoring

Boost matches in important columns:

  public function configTable(): array
  {                                                                                                                                                        
      return [
          'searchScoring' => [                                                                                                                             
              'name' => 3,   // 3x weight                                                                                                                  
              'sku'  => 2,                                                                                                                                 
              'tags' => 1,                                                                                                                                 
          ],                                                                                                                                               
          'typoTolerance' => true,                          
      ];                                                                                                                                                   
  }
Enter fullscreen mode Exit fullscreen mode

Form Builder — 25+ field types

  return [
      MrCatzFormField::text('name'),
      MrCatzFormField::select('category_id', data: $categories),                                                                                           
      MrCatzFormField::date('published_at'),
      MrCatzFormField::editor('description'),       // Quill rich text                                                                                     
      MrCatzFormField::repeater('variants', fields: [                                                                                                      
          MrCatzFormField::text('sku'),                                                                                                                    
          MrCatzFormField::number('stock'),                                                                                                                
      ]),                                                                                                                                                  
      MrCatzFormField::file('thumbnail', accept: 'image/*'),
  ];                                                                                                                                                       
Enter fullscreen mode Exit fullscreen mode

Meilisearch (beta)

  public function configTable(): array
  {                                   
      return ['useScout' => true];
  }                               
Enter fullscreen mode Exit fullscreen mode

Search offloads to Meilisearch; filters/sort stay on the database side. No other changes needed.


Live docs with interactive playgrounds

Every feature in the docs is live — not just code snippets. You can type in a real search box, flip filters, edit cells, open the form modal. Try it:


Why another DataTable package?

  • Yajra DataTables — jQuery-based, predates Livewire. Great if you're on Blade + jQuery.
  • Filament — amazing, but it's a full admin framework. You adopt its UI, its panels, its conventions.
  • Livewire PowerGrid — closest alternative. MrCatz differs by shipping a Form Builder in the same package, a native full-page form mode, and stricter adherence to DaisyUI (so theming is just data-theme=).

MrCatz is for developers who want CRUD productivity without UI lock-in. Your Tailwind config, your DaisyUI theme, your Livewire flavor — MrCatz just
plugs into it.


If you try it out, I'd love feedback — GitHub issues, Reddit DMs, whatever. Star the repo if it saves you a few hours, and if you find a bug, the issue
tracker is open.

Happy CRUDing 🛠️


Top comments (0)