Documentation Index
Fetch the complete documentation index at: https://docs.mejik.web.id/llms.txt
Use this file to discover all available pages before exploring further.
Documentation: Advanced Datatable with Tree Structures
This document provides a comprehensive guide to implementing hierarchical datatables using the AdminController backend and the TreeDatatable.vue frontend component.
The backend generates a standardized JSON object for each row (or “node”) in the datatable. Understanding this structure is key to leveraging all the component’s features.
Anatomy of a Row Object
{
// --- Core Data from your Model ---
"id": "1101",
"accountCode": "1101",
"accountName": "Cash & Bank",
"parentId": null,
// --- Hierarchy Control Fields (from Backend) ---
"children": [
{
"id": "110101",
"accountCode": "110101",
"accountName": "Petty Cash",
"parentId": "1101",
"children": [],
"_expanded": false,
"_selectable": true,
"_selected": false
}
],
// --- UI State & Behavior Fields (from Backend) ---
"_expanded": true,
"_selectable": false,
"_selected": true,
"_hasChildren": true,
"_type": "account-group"
}
Property Breakdown
Properties starting with an underscore _ are metadata used to control the UI.
| Property | Type | Source | Description |
| Core Data | mixed | Model | All the original fields from your Eloquent model (e.g., id, accountCode, accountName). |
children | Array | Backend | An array containing child row objects. In backend lazy-load mode, this is populated by the server. In frontend lazy-load mode, it’s initially empty. The component renames this to _children internally. |
_expanded | Boolean | Backend | Server’s Suggestion. If true, the frontend will render this node as expanded on initial load or when table_tree_expand_all is enabled. The user’s interaction can override this. |
_selectable | Boolean | Backend | If false, the checkbox or radio button for this row will be disabled, and the row will be styled as non-selectable. Controlled by table_tree_leaf_only_select or table_tree_selectable_checker. |
_selected | Boolean | Backend | If true, the frontend component will automatically add this row to its selection state upon loading data. Controlled by table_pre_selected_ids. |
_hasChildren | Boolean | Backend | Crucial for Frontend Lazy Loading. Indicates if a node has children, even if the children array is empty. The UI uses this to show/hide the expander icon. |
_type | String | Backend | (Optional) A type identifier (e.g., 'group', 'employee') used for custom styling or logic on the frontend. |
Part 2: Backend - AdminController Setup
To serve tree data, configure these protected properties in your controller that extends AdminController.
Configuration Properties
| Property | Type | Description |
table_structure_mode | string | (Required) Set to 'simple_tree' or 'compound_tree'. |
table_lazy_load | string | Set to 'frontend' for the most scalable performance. The frontend will fetch children on demand. Set to 'backend' for the API to build the tree (simpler for frontend). |
table_tree_parent_field | string | For 'simple_tree' mode. The column in your model that holds the parent’s ID. |
table_pre_expanded_ids | array | Populated automatically from the frontend request (expandedIds payload). Contains IDs of rows the user has open. This takes priority over table_tree_expand_all. |
table_tree_expand_all | bool | If true and expandedIds is not sent from the frontend, all nodes will be sent as expanded. Useful for initial view. |
table_tree_leaf_only_select | bool | If true, only rows with no children will be selectable. |
table_tree_selectable_checker | Closure | A function that receives the row data (as an array) and returns true or false to determine selectability. e.g., fn($row) => $row['status'] === 'active' |
table_pre_selected_ids | array | An array of IDs you provide on the backend to force certain rows to be pre-selected on load. |
table_compound_tree_config | array | For 'compound_tree' mode. A detailed configuration array defining the multi-model hierarchy. |
Example Controller (ChartOfAccountController.php)
This example sets up a lazy-loaded, single-model tree where only “active” accounts can be selected.
<?php
namespace App\Http\Controllers\Finance;
use App\Http\Controllers\Core\AdminController;
use App\Models\Finance\ChartOfAccount;
class ChartOfAccountController extends AdminController
{
public function __construct()
{
parent::__construct();
$this->model = new ChartOfAccount();
$this->controller_base = 'finance/coa';
// --- Datatable Tree Configuration ---
$this->table_structure_mode = 'simple_tree';
$this->table_lazy_load = 'frontend'; // Recommended for performance
$this->table_tree_parent_field = 'parentAccountCode';
// Example: Only allow accounts with status 'active' to be selected
$this->table_tree_selectable_checker = fn($row) => $row['status'] === 'active';
// Pre-select a specific account for the user, perhaps from their settings
$this->table_pre_selected_ids = ['110101']; // e.g., Petty Cash
}
}
Part 3: Frontend - TreeDatatable.vue Usage
The TreeDatatable component is highly configurable via its props.
Key Props
| Prop | Type | Description |
rows | Array | The array of data from the backend. |
columns | Array | The column definitions. One column should have { isTreeColumn: true } to render the indented tree view. |
selectOptions | Object | Configures selection behavior: { enabled: Boolean, mode: String }. mode can be 'single' or 'multi'. |
selectedRows | Array | An array of the currently selected row objects. Use with .sync modifier or manage with events. |
expandedRows | Array | (For State Preservation) An array of the IDs of currently expanded rows. Use with .sync modifier to enable state preservation across data refreshes. |
Key Events
| Event | Payload | Description |
@update:sortBy | Array | Emitted when the user changes the sort order. The parent should re-fetch data with the new sort parameters. |
@update:expandedRows | Array | Emitted whenever the user expands or collapses a row. The parent should store this array of IDs and send it back on the next data fetch. |
@select-toggle | Object (the row) | Emitted when a single row’s selection state is toggled. |
@select-bulk | Array (of rows) | Emitted on initial load if the server sent _selected: true flags, allowing the parent to add these to its selection without removing existing ones. |
@load-children | Object (the parent row) | Emitted in frontend lazy-load mode when an un-loaded node is expanded. The parent must fetch its children and update the rows prop. |
Example Parent Component Implementation
This shows how a page would use TreeDatatable to manage all its state.
<template>
<div class="my-page-container">
<!-- The component is bound to the parent's data using props and .sync -->
<TreeDatatable
:columns="columns"
:rows="tableRows"
:select-options="{ enabled: true, mode: 'multi' }"
:selected-rows.sync="selectedItems"
:expanded-rows.sync="expandedItemIds"
@update:sortBy="handleSortChange"
@load-children="handleLoadChildren"
/>
</div>
</template>
<script>
import TreeDatatable from '@/components/TreeDatatable.vue';
import axios from 'axios';
export default {
components: { TreeDatatable },
data() {
return {
columns: [
{ label: 'Account', field: 'accountName', isTreeColumn: true, sortable: true },
{ label: 'Code', field: 'accountCode', sortable: true },
{ label: 'Status', field: 'status' }
],
tableRows: [],
selectedItems: [],
expandedItemIds: [], // State for preserving expanded rows
sortBy: [{ field: 'accountCode', type: 'asc' }],
// ... other pagination data
};
},
methods: {
async fetchData() {
// Include sort, pagination, and expanded state in the payload
const payload = {
sort: this.sortBy,
expandedIds: this.expandedItemIds, // Send the current expanded state to the server
// ... pagination params
};
const response = await axios.post('/api/finance/coa/index', payload);
this.tableRows = response.data.data;
},
handleSortChange(newSort) {
this.sortBy = newSort;
this.fetchData(); // Re-fetch data with new sort, preserving expansion
},
async handleLoadChildren(parentRow) {
// This is for frontend lazy loading
const payload = { parentId: parentRow.id };
const response = await axios.post('/api/finance/coa/index', payload);
// Find the parent in the existing data and assign its children
// (A helper function is recommended for deep searching)
const parentInTree = this.findNodeById(this.tableRows, parentRow.id);
if (parentInTree) {
parentInTree.children = response.data.data;
}
},
// Helper to find a node in a tree structure
findNodeById(nodes, id) {
for (const node of nodes) {
if (node.id === id) return node;
if (node.children) {
const found = this.findNodeById(node.children, id);
if (found) return found;
}
}
return null;
}
},
mounted() {
this.fetchData();
}
}
</script>