bonsai/crud

A reusable CRUD package for Laravel with Inertia.js and Vue 3

Installs: 16

Dependents: 1

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

Language:Vue

pkg:composer/bonsai/crud

v1.0.0 2025-10-16 19:21 UTC

This package is auto-updated.

Last update: 2025-10-16 19:30:24 UTC


README

A powerful, reusable CRUD (Create, Read, Update, Delete) package for Laravel applications using Inertia.js and Vue 3. Build complete admin panels and data management interfaces in minutes without repeating yourself.

✨ Features

  • πŸš€ Zero Configuration CRUD - Define your fields and you're done
  • 🎨 Bootstrap 5 Ready - Beautiful UI out of the box
  • πŸ” Built-in Search - Real-time search across all visible columns
  • πŸ“„ Pagination - Automatic pagination with customizable items per page
  • ✏️ Multiple Field Types - Text, textarea, number, currency, date, datetime, checkbox, combo/select, and more
  • πŸ”— Relationship Support - Handle Eloquent relationships with ease
  • 🎯 Validation - Full Laravel validation support
  • πŸ”’ Flexible Actions - Enable/disable create, edit, delete, and search actions
  • 🎨 Customizable Buttons - Add custom action buttons per row
  • 🌐 i18n Ready - Easy to translate
  • πŸ“¦ No Publishing Required - Components stay in the package for automatic updates

πŸ› οΈ Technology Stack

This package is built with:

  • Backend:

    • Laravel 10.x / 11.x / 12.x
    • PHP 8.1+
    • Inertia.js (Server-side adapter)
  • Frontend:

    • Vue 3 (Composition API)
    • Inertia.js (Client-side adapter)
    • Bootstrap 5
    • FontAwesome 6
    • Axios
    • SweetAlert2
  • Build Tools:

    • Vite
    • Laravel Vite Plugin

πŸ“‹ Requirements

  • PHP >= 8.1
  • Laravel >= 10.0
  • Node.js >= 16.x
  • NPM >= 8.x
  • Inertia.js
  • Vue 3

πŸ“¦ Installation

Step 1: Install the Package

Install via Composer:

composer require bonsai/crud

Step 2: Install Frontend Dependencies

Install the required JavaScript dependencies:

npm install axios bootstrap @fortawesome/fontawesome-free sweetalert2

Step 3: Configure Vite

Add the package alias to your vite.config.js:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
    resolve: {
        alias: {
            '@': '/resources/js',
            '@crud': '/vendor/bonsai/crud/resources/js',
        },
    },
});

Step 4: Configure Your App Entry Point

Update your resources/js/app.js (or app.ts):

import './bootstrap';
import '../css/app.css';

// Import Bootstrap and FontAwesome
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap';
import '@fortawesome/fontawesome-free/css/all.min.css';

// Import the CRUD helpers plugin
import CrudHelpers from '../../vendor/bonsai/crud/resources/js/plugin.js';
import axios from 'axios';

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';

// Make axios globally available
window.axios = axios;

const appName = import.meta.env.VITE_APP_NAME || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        return createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(CrudHelpers)  // Register the CRUD helpers
            .mount(el);
    },
    progress: {
        color: '#4B5563',
    },
});

For TypeScript users, create resources/js/types/global.d.ts:

import type { AxiosInstance } from 'axios';

declare global {
    interface Window {
        axios: AxiosInstance;
    }
}

export {};

Step 5: Create Inertia Page Wrappers

Create the directory structure:

mkdir -p resources/js/Pages/Crud

Create resources/js/Pages/Crud/Index.vue:

<script setup>
import IndexComponent from '../../../../vendor/bonsai/crud/resources/js/Pages/Index.vue';

defineProps(['title', 'breadcrumb', 'url', 'actions', 'extra_buttons', 'alert']);
</script>

<template>
    <IndexComponent v-bind="$attrs" />
</template>

Create resources/js/Pages/Crud/Edit.vue:

<script setup>
import EditComponent from '../../../../vendor/bonsai/crud/resources/js/Pages/Edit.vue';

defineProps(['title', 'breadcrumb', 'id', 'url', 'static_url']);
</script>

<template>
    <EditComponent v-bind="$attrs" />
</template>

Step 6: Build Assets

npm run dev

Or for production:

npm run build

πŸš€ Usage

Basic Example

  1. Create a Model and Migration
php artisan make:model Product -m
  1. Define Your Migration
// database/migrations/xxxx_create_products_table.php
public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('description')->nullable();
        $table->decimal('price', 10, 2)->default(0);
        $table->boolean('active')->default(true);
        $table->timestamps();
    });
}
  1. Configure Your Model
// app/Models/Product.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = [
        'name',
        'description',
        'price',
        'active'
    ];

    protected $casts = [
        'active' => 'boolean',
        'price' => 'decimal:2'
    ];
}
  1. Create a Controller
php artisan make:controller ProductController
<?php

namespace App\Http\Controllers;

use bonsai\Crud\Http\Controllers\CrudController;
use App\Models\Product;

class ProductController extends CrudController
{
    public function __construct()
    {
        $this->render([
            'model' => new Product(),
            'title' => '<i class="fas fa-box"></i> Products',
            'breadcrumb' => [
                ['title' => 'Home', 'icon' => '<i class="fas fa-home"></i>', 'url' => '/', 'active' => false],
                ['title' => 'Products', 'icon' => '<i class="fas fa-box"></i>', 'url' => '/products', 'active' => true],
            ],
            'url' => '/products',
            'fields' => [
                ['title' => 'ID', 'column' => 'id', 'editable' => false],
                ['title' => 'Name', 'column' => 'name', 'type' => 'texto'],
                ['title' => 'Description', 'column' => 'description', 'type' => 'textarea'],
                ['title' => 'Price', 'column' => 'price', 'type' => 'currency'],
                ['title' => 'Active', 'column' => 'active', 'type' => 'checkbox'],
            ],
            'rules' => [
                'data.name' => 'required|string|max:255',
                'data.price' => 'required|numeric|min:0',
            ],
        ]);
    }
}
  1. Add Routes
// routes/web.php
use App\Http\Controllers\ProductController;

Route::resource('products', ProductController::class);
  1. Visit Your CRUD

Navigate to http://your-app.test/products and start managing your data!

πŸ“– Configuration Options

Field Types

The package supports various field types:

Type Description Example
texto Text input (default) ['title' => 'Name', 'column' => 'name']
textarea Multi-line text ['title' => 'Description', 'column' => 'description', 'type' => 'textarea']
number Number input ['title' => 'Quantity', 'column' => 'quantity', 'type' => 'number']
currency Formatted currency display ['title' => 'Price', 'column' => 'price', 'type' => 'currency']
date Date picker ['title' => 'Birth Date', 'column' => 'birth_date', 'type' => 'date']
datetime DateTime picker ['title' => 'Created', 'column' => 'created_at', 'type' => 'datetime']
checkbox Boolean checkbox ['title' => 'Active', 'column' => 'active', 'type' => 'checkbox']
combo Select dropdown ['title' => 'Category', 'column' => 'category_id', 'type' => 'combo', 'relation' => 'category', 'options' => $categories]
password Password input ['title' => 'Password', 'column' => 'password', 'type' => 'password']
url Clickable link ['title' => 'Website', 'column' => 'url', 'type' => 'url']

Field Options

[
    'title' => 'Field Label',           // Required: Display name
    'column' => 'database_column',      // Required: Database column name
    'type' => 'texto',                  // Optional: Field type (default: 'texto')
    'editable' => true,                 // Optional: Can be edited (default: true)
    'visible' => true,                  // Optional: Show in list (default: true)
    'relation' => 'relationName',       // Optional: For combo fields
    'options' => $collection,           // Optional: For combo fields
]

Render Options

$this->render([
    'model' => new YourModel(),              // Required: Eloquent model instance
    'title' => 'Page Title',                 // Required: Page title
    'url' => '/your-route',                  // Required: Base URL for CRUD
    'breadcrumb' => [],                      // Optional: Breadcrumb array
    'fields' => [],                          // Required: Field definitions
    'rules' => [],                           // Optional: Validation rules
    'perpage' => 25,                         // Optional: Items per page (default: 25)
    'buttons' => [],                         // Optional: Custom row buttons
    'extra_buttons' => [],                   // Optional: Extra header buttons
    'actions' => [                           // Optional: Enable/disable actions
        'create' => true,
        'edit' => true,
        'delete' => true,
        'search' => true,
    ],
    'where' => [],                           // Optional: Query filters
    'order' => [],                           // Optional: Default ordering
    'relations' => [],                       // Optional: Eager load relations
    'hiddens' => [],                         // Optional: Hidden field values
    'filesystem' => 'public',                // Optional: File storage disk
    'alert' => null,                         // Optional: Alert message
]);

Custom Buttons

Add custom action buttons to each row:

'buttons' => [
    [
        'type' => 'info',                    // Bootstrap button type
        'icon' => 'fas fa-eye',              // FontAwesome icon
        'url' => '/products/{id}/view',      // URL with {id} placeholder
        'tooltip' => 'View Details',         // Tooltip text
        'target' => '_blank',                // Optional: Link target
        'confirm' => false,                  // Optional: Show confirmation
        'confirmsg' => 'Are you sure?',     // Optional: Confirmation message
    ]
],

Extra Header Buttons

Add buttons to the header area:

'extra_buttons' => [
    [
        'title' => 'Export',
        'url' => '/products/export',
        'class' => 'success',               // Bootstrap color class
        'icon' => 'fa-download',            // FontAwesome icon
    ]
],

Relationships

For select/combo fields with relationships:

// In your controller
public function __construct()
{
    $categories = Category::all()->map(function($cat) {
        return [
            'id' => $cat->id,
            'name' => $cat->name
        ];
    });

    $this->render([
        // ... other options
        'fields' => [
            [
                'title' => 'Category',
                'column' => 'category_id',
                'type' => 'combo',
                'relation' => 'category',
                'options' => $categories
            ]
        ]
    ]);
}

Filters and Ordering

Apply default filters and sorting:

'where' => [
    ['column' => 'status', 'operator' => '=', 'value' => 'active'],
    ['column' => 'created_at', 'operator' => '>', 'value' => '2024-01-01'],
],
'order' => [
    ['column' => 'created_at', 'direction' => 'desc'],
    ['column' => 'name', 'direction' => 'asc'],
],

🎨 Customization

Using a Layout

Wrap your CRUD pages with a layout:

<!-- resources/js/Pages/Crud/Index.vue -->
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import IndexComponent from '../../../../vendor/bonsai/crud/resources/js/Pages/Index.vue';

defineProps(['title', 'breadcrumb', 'url', 'actions', 'extra_buttons', 'alert']);
</script>

<template>
    <AppLayout>
        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <IndexComponent v-bind="$attrs" />
            </div>
        </div>
    </AppLayout>
</template>

Overriding Styles

Since Bootstrap is used, you can override styles in your resources/css/app.css:

/* Customize CRUD table */
.table-striped tbody tr:nth-of-type(odd) {
    background-color: rgba(0, 0, 0, 0.02);
}

/* Customize buttons */
.btn-primary {
    background-color: #your-color;
}

πŸ”§ Troubleshooting

Components Not Found

Error: Failed to resolve import "../../../../vendor/bonsai/crud/resources/js/Pages/Index.vue"

Solution:

  1. Check that the package is installed: composer show bonsai/crud
  2. Verify the symlink exists: ls -la vendor/bonsai/crud
  3. Make sure Vite alias is configured in vite.config.js
  4. Restart Vite: Stop npm run dev and run it again

Axios is Not Defined

Error: Uncaught ReferenceError: axios is not defined

Solution:

  1. Install axios: npm install axios
  2. Add to your app.js: import axios from 'axios' and window.axios = axios
  3. Rebuild: npm run dev

Styles Not Loading

Error: Buttons don't appear, inputs look unstyled

Solution:

  1. Install Bootstrap: npm install bootstrap
  2. Import in app.js: import 'bootstrap/dist/css/bootstrap.min.css' and import 'bootstrap'
  3. Install FontAwesome: npm install @fortawesome/fontawesome-free
  4. Import in app.js: import '@fortawesome/fontawesome-free/css/all.min.css'
  5. Rebuild: npm run dev

SweetAlert2 Not Found

Error: Failed to resolve import "sweetalert2"

Solution:

npm install sweetalert2
npm run dev

Class CrudController Not Found

Error: Class "bonsai\Crud\Http\Controllers\CrudController" not found

Solution:

composer dump-autoload

If that doesn't work:

composer remove bonsai/crud
composer require bonsai/crud

Validation Not Working

Issue: Form submits without validation

Solution: Make sure you've defined validation rules in the render() method:

'rules' => [
    'data.name' => 'required|string|max:255',
    'data.email' => 'required|email',
]

Search Not Working

Issue: Search input doesn't filter results

Solution:

  1. Make sure 'search' => true in actions
  2. Ensure visible fields have searchable data
  3. Check browser console for JavaScript errors

Pagination Not Appearing

Issue: No pagination controls show up

Solution: Pagination only appears when you have more than 25 records (or your custom perpage value). Add more test data or reduce perpage:

'perpage' => 5,  // Show pagination with 5+ records

πŸ“š Advanced Usage

Working with File Uploads

// In your controller
'fields' => [
    ['title' => 'Image', 'column' => 'image_path', 'type' => 'file'],
],
'filesystem' => 'public',  // or 's3', etc.

Hidden Fields (Auto-fill)

Automatically set field values (like user_id):

'hiddens' => [
    ['field' => 'user_id', 'value' => auth()->id()],
    ['field' => 'company_id', 'value' => auth()->user()->company_id],
],

Nested Relationships

Display nested relationship data:

'fields' => [
    ['title' => 'Category Name', 'column' => 'category.name', 'editable' => false],
],
'relations' => ['category'],

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

πŸ“ License

This package is open-sourced software licensed under the MIT license.

πŸ‘€ Author

Erick MarroquΓ­n

πŸ™ Acknowledgments

πŸ“ž Support

If you discover any issues or have questions, please open an issue on GitHub.

Made with ❀️ for the Laravel community in πŸ‡¬πŸ‡Ή