View Components
View components are reusable UI building blocks inspired by modern frontend frameworks. This page explains how to create, use, and organize components with step-by-step examples.
Table of Contents
- What Are Components?
- Creating a Component (Step-by-Step)
- Understanding Layouts
- JavaScript Integration
- Form Components (Select, Datepicker)
- AJAX Components (Dynamic Loading)
- Standard UI Components
- AJAX Forms
- Modal Form Component
- Chart Component (ApexCharts)
- D3 Chart Component (D3.js)
- MapLibre Component (Maps)
- AppAlert (SweetAlert Wrapper)
- JavaScript Components Reference
- FileDownloader (Export/Download)
- Best Practices
- Docker Verification
1. What Are Components?
Technical Definition
A Component is a reusable PHP partial that receives data via parameters and renders HTML. Components:
- Accept data as parameters
- Return rendered HTML
- Can be used across multiple views
- Live in
Modules/*/Shared/Components/
Simple Analogy
Think of components like LEGO bricks:
| LEGO | Components |
|---|---|
| Same brick shape, different colors | Same component, different data |
| Combine bricks to build anything | Combine components to build pages |
| Reuse the same brick in many sets | Reuse same component on many pages |
Before/After Comparison
2. Creating a Component (Step-by-Step)
Scenario: Create a Stats Card Component
Step 1: Create the Component File
File: app/Modules/Admin/Shared/Components/stat_card.php
<?php
/**
* Stat Card Component
*
* Displays a statistics card with icon, value, and optional trend.
*
* @param string $title Card title (required)
* @param mixed $value Display value - number or string (required)
* @param string $icon Icon name (optional, default: 'chart')
* @param string $color Card accent color: blue, green, red, yellow (optional)
* @param string $trend Trend indicator: up, down, or null (optional)
* @param string $change Percentage change text (optional)
*/
// Set defaults for optional parameters
$icon = $icon ?? 'chart';
$color = $color ?? 'blue';
$trend = $trend ?? null;
$change = $change ?? null;
?>
<div class="stat-card stat-card--<?= esc($color) ?>">
<div class="stat-card__icon">
<i class="icon icon-<?= esc($icon) ?>"></i>
</div>
<div class="stat-card__content">
<h3 class="stat-card__title"><?= esc($title) ?></h3>
<p class="stat-card__value"><?= esc($value) ?></p>
<?php if ($trend !== null): ?>
<div class="stat-card__trend stat-card__trend--<?= $trend ?>">
<span class="trend-arrow">
<?= $trend === 'up' ? '↑' : '↓' ?>
</span>
<?php if ($change): ?>
<span class="trend-value"><?= esc($change) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>Step 2: Add CSS Styles
File: public/assets/css/components/stat-card.css
.stat-card {
display: flex;
align-items: center;
padding: 1.5rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-card__icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
.stat-card--blue .stat-card__icon { background: #e3f2fd; color: #1976d2; }
.stat-card--green .stat-card__icon { background: #e8f5e9; color: #388e3c; }
.stat-card--red .stat-card__icon { background: #ffebee; color: #d32f2f; }
.stat-card--yellow .stat-card__icon { background: #fff8e1; color: #f57c00; }
.stat-card__title {
font-size: 0.875rem;
color: #666;
margin: 0;
}
.stat-card__value {
font-size: 1.75rem;
font-weight: 700;
margin: 0.25rem 0 0;
}
.stat-card__trend {
display: flex;
align-items: center;
font-size: 0.75rem;
margin-top: 0.5rem;
}
.stat-card__trend--up { color: #388e3c; }
.stat-card__trend--down { color: #d32f2f; }Step 3: Use the Component in a View
File: app/Modules/Admin/Dashboard/Views/index.php
<?= $this->extend('Modules/Admin/Shared/Layouts/admin') ?>
<?= $this->section('content') ?>
<h1>Dashboard</h1>
<div class="stats-grid">
<?= component('stat_card', [
'title' => 'Total Users',
'value' => number_format($stats['users']),
'icon' => 'users',
'color' => 'blue',
'trend' => 'up',
'change' => '+12%',
]) ?>
<?= component('stat_card', [
'title' => 'Revenue',
'value' => '$' . number_format($stats['revenue'], 2),
'icon' => 'dollar',
'color' => 'green',
'trend' => 'up',
'change' => '+8.5%',
]) ?>
<?= component('stat_card', [
'title' => 'Orders',
'value' => number_format($stats['orders']),
'icon' => 'shopping-cart',
'color' => 'yellow',
]) ?>
<?= component('stat_card', [
'title' => 'Bounce Rate',
'value' => $stats['bounce_rate'] . '%',
'icon' => 'trending-down',
'color' => 'red',
'trend' => 'down',
'change' => '-3.2%',
]) ?>
</div>
<?= $this->endSection() ?>Result
The page now renders 4 stat cards, each with different data but consistent styling.
3. Understanding Layouts
What is a Layout?
A Layout is a base template that other views extend. It defines the common page structure (header, sidebar, footer) and provides sections for content.
Layout Structure
File: app/Modules/Admin/Shared/Layouts/admin.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= esc($title ?? 'Admin Panel') ?></title>
<!-- CSS -->
<link rel="stylesheet" href="/assets/css/app.css">
<link rel="stylesheet" href="/assets/css/admin.css">
<?= $this->renderSection('styles') ?>
</head>
<body class="admin-layout">
<!-- Sidebar Component -->
<?= component('sidebar') ?>
<main class="main-content">
<!-- Header Component -->
<?= component('header') ?>
<div class="page-content">
<!-- Page-specific content inserted here -->
<?= $this->renderSection('content') ?>
</div>
</main>
<!-- JS -->
<script src="/assets/js/app.js"></script>
<?= $this->renderSection('scripts') ?>
</body>
</html>Extending a Layout
<!-- View file: app/Modules/Admin/User/Views/index.php -->
<?= $this->extend('Modules/Admin/Shared/Layouts/admin') ?>
<!-- Required: Content section -->
<?= $this->section('content') ?>
<h1>Users</h1>
<table>...</table>
<?= $this->endSection() ?>
<!-- Optional: Additional CSS -->
<?= $this->section('styles') ?>
<link rel="stylesheet" href="/assets/css/pages/users.css">
<?= $this->endSection() ?>
<!-- Optional: Additional JS -->
<?= $this->section('scripts') ?>
<script src="/assets/js/pages/users.js"></script>
<?= $this->endSection() ?>Layout Hierarchy
4. JavaScript Integration
Using Data Attributes
Components use data-* attributes for JavaScript initialization:
<!-- Component: sidebar.php -->
<aside class="sidebar" data-sidebar data-persist="sidebar-state">
<button class="sidebar__toggle" data-sidebar-toggle>
☰
</button>
<nav class="sidebar__nav" data-sidebar-nav>
<a href="/admin/dashboard" data-tooltip="Dashboard">Dashboard</a>
<a href="/admin/users" data-tooltip="Users">Users</a>
</nav>
</aside>JavaScript Initialization
File: public/assets/js/components/sidebar.js
/**
* Sidebar Component
*
* Handles sidebar toggle, collapse state persistence, and tooltips.
*/
class Sidebar {
constructor(element) {
this.element = element;
this.toggleBtn = element.querySelector('[data-sidebar-toggle]');
this.persistKey = element.dataset.persist;
this.init();
}
init() {
// Restore saved state
if (this.persistKey) {
const isCollapsed = localStorage.getItem(this.persistKey) === 'true';
if (isCollapsed) {
this.element.classList.add('is-collapsed');
}
}
// Toggle button click
if (this.toggleBtn) {
this.toggleBtn.addEventListener('click', () => this.toggle());
}
}
toggle() {
this.element.classList.toggle('is-collapsed');
// Save state
if (this.persistKey) {
const isCollapsed = this.element.classList.contains('is-collapsed');
localStorage.setItem(this.persistKey, isCollapsed);
}
}
}
// Auto-initialize
document.querySelectorAll('[data-sidebar]').forEach(el => new Sidebar(el));Other Common Patterns
// Modal component
<div class="modal" data-modal="confirm-delete">
<button data-modal-close>×</button>
</div>
// Open modal via JS
document.querySelector('[data-modal="confirm-delete"]').classList.add('is-open');
// Dropdown component
<div class="dropdown" data-dropdown>
<button data-dropdown-trigger>Menu</button>
<div data-dropdown-content>...</div>
</div>
// Tooltip component
<button data-tooltip="Click to save">Save</button>4b. Form Components
Enhanced form controls for better UX.
TomSelect (Rich Select)
Replace standard <select> with TomSelect for searching and tagging.
<!-- Basic -->
<select data-tomselect>...</select>
<!-- AJAX Loading -->
<select data-tomselect
data-url="/api/users/search"
data-value-field="id"
data-label-field="name">
</select>Flatpickr (Date/Time Picker)
Lightweight and powerful datetime picker.
<!-- Date only -->
<input type="text" data-flatpickr placeholder="Select Date">
<!-- Date & Time -->
<input type="text" data-flatpickr data-enable-time="true">
<!-- Range -->
<input type="text" data-flatpickr data-mode="range">Cleave.js (Input Formatting)
Format input content while typing (credit cards, dates, phone numbers).
<!-- Credit Card -->
<input type="text" data-cleave="credit-card" placeholder="0000 0000 0000 0000">
<!-- Date (YYYY-MM-DD) -->
<input type="text" data-cleave="date" placeholder="YYYY-MM-DD">
<!-- Time (hh:mm) -->
<input type="text" data-cleave="time" placeholder="hh:mm">
<!-- Numeral (Thousands separator) -->
<input type="text" data-cleave="numeral" placeholder="10,000">5. AJAX Components (Dynamic Loading)
What are AJAX Components?
AJAX Components can be refreshed individually without reloading the entire page. They are useful for:
- Notification lists (refreshing every minute)
- Real-time counters (like unread messages)
- Dynamic widgets (charts, specialized feeds)
Step 1: Register the Component
You must allow the component to be loaded via AJAX by adding it to the map in app/Modules/Admin/Shared/Controllers/ComponentController.php:
protected function getComponentMap(): array
{
return [
// 'key' => [configuration]
'notification_list' => [
'view' => 'Modules/Admin/Shared/Components/notification_list',
'data' => function($params) {
// This generic function fetches fresh data on every AJAX call
$repo = new \App\Infrastructure\Database\MySQL\Notification\NotificationRepository();
return [
'notifications' => $repo->findForUser(session()->get('user_id'), [], 50)
];
}
],
// Example: Dynamic Order Chart
'order_chart' => [
'view' => 'Modules/Admin/Shared/Components/chart_widget',
'data' => function($params) {
// $params contains query string variables (e.g. ?period=7days)
return ['chartData' => ...];
}
]
];
}Step 2: Render with Wrapper
Use the ajax_component() helper instead of the standard component() helper. This creates the necessary HTML div wrapper and data- attributes.
<!-- Syntax: ajax_component(id, data, ajaxEnabled, domId, pollingInterval) -->
<!-- Example 1: Load immediately, allow manual refresh -->
<?= ajax_component('notification_list', ['notifications' => $notifications], true, 'notification_list') ?>
<!-- Example 2: Auto-refresh every 30 seconds with Lazy Loader -->
<?= ajax_component('notification_bell', ['count' => 5], true, 'bell-icon', 30000, [], 'Admin', 'lazy') ?>
<!-- Or using named arguments (PHP 8+) -->
<?= ajax_component('notification_bell', [], true, 'bell-icon', loader: 'lazy') ?>Step 3: Trigger Refresh via JavaScript
You can verify the component is working by refreshing it manually from the console or a button:
// Refresh by DOM ID
AjaxComponent.refresh('notification_list');
// With a button
<button onclick="AjaxComponent.refresh('notification_list')">
Refresh List
</button>6. Standard UI Components
We provide a set of standard components to ensure UI consistency.
Validation Error
Displays validation errors for a specific field using Bootstrap classes. Used with redirect()->with('errors', ...) or AJAX forms.
<!-- View -->
<?= component('validation_error', ['field' => 'email']) ?>Generic Button
A standardized button component that supports icons, loading states, and different types (submit, button, reset) or links.
<!-- Button -->
<?= component('button', [
'label' => 'Save Changes',
'type' => 'submit',
'class' => 'btn btn-primary',
'icon' => 'fas fa-save'
]) ?>
<!-- Link / Download -->
<?= component('button', [
'href' => '/path/to/file',
'label' => 'Download PDF',
'class' => 'btn btn-outline-primary',
'icon' => 'fas fa-download',
'target' => '_blank'
]) ?>File Downloader Utility
A global utility downloader.js is available for handling AJAX downloads with loading states and dynamic filename resolution.
<!-- Include Script -->
<script src="<?= base_url('assets/js/components/downloader.js') ?>"></script>
<!-- Usage -->
downloadExport('/path/to/export/endpoint');
// Features:
// - Shows SweetAlert loading spinner
// - Automatic filename extraction from 'Content-Disposition' header
// - Handles Blob conversion client-side7. AJAX Forms
You can convert any standard HTML form into an AJAX-submitted form simply by adding the data-ajax-form attribute. The system handles sending the request, displaying validation errors inline, and showing success alerts.
Supported Attributes
| Attribute | Description |
|---|---|
data-ajax-form | Enables AJAX submission (required) |
data-redirect="/path" | Redirect to URL after success |
data-reload="true" | Reload current page after success |
data-confirm="message" | Show SweetAlert confirmation before submit |
data-loading-target=".card" | Show loading overlay on target element during submission |
Usage Examples
<!-- Basic AJAX form with redirect -->
<form action="/admin/save" method="post"
data-ajax-form
data-redirect="/admin/list"
data-loading-target=".card">
<?= csrf_field() ?>
<input name="title" ...>
<?= component('validation_error', ['field' => 'title']) ?>
<?= component('button', ['type' => 'submit', 'label' => 'Save']) ?>
</form>
<!-- Delete form with confirmation -->
<form action="/admin/delete/123" method="post" class="d-inline"
data-ajax-form
data-confirm="Are you sure you want to delete this?"
data-reload="true">
<?= csrf_field() ?>
<button type="submit" class="btn btn-danger">Delete</button>
</form>
<!-- Required JS -->
<script src="<?= base_url('assets/js/components/ajax-form.js') ?>"></script>Controller Requirements
The controller method must return JSON when called via AJAX:
public function save() {
if (!$this->validate(...)) {
if ($this->request->isAJAX()) {
return $this->response->setJSON([
'success' => false,
'message' => 'Validation Failed',
'errors' => $this->validator->getErrors()
]);
}
return redirect()->back()->withInput()->with('errors', ...);
}
// ... Success processing ...
if ($this->request->isAJAX()) {
return $this->response->setJSON(['success' => true, 'message' => 'Saved!']);
}
return redirect()->back()->with('success', 'Saved!');
}Loading Overlay CSS
The data-loading-target attribute applies the .ajax-loading class to the target element. The styles are defined in app.css:
.ajax-loading {
position: relative;
pointer-events: none;
}
.ajax-loading::before {
content: '';
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.7);
z-index: 10;
}
.ajax-loading::after {
/* Spinner centered on element */
animation: ajax-spin 0.8s linear infinite;
}You can customize the overlay or spinner by overriding these styles in your module's CSS.
8. Modal Form Component
A reusable modal dialog with an AJAX-enabled form for quick CRUD operations.
Usage
<?= component('modal_form', [
'id' => 'createItem',
'title' => 'Create Item',
'action' => '/admin/items',
'size' => 'md', // sm, md, lg, xl
'animation' => 'slide-up', // fade, slide-up, slide-down, zoom, slide-right
'submitLabel' => 'Create',
'reload' => true, // Reload page on success
'fields' => [
['name' => 'title', 'label' => 'Title', 'required' => true],
['name' => 'description', 'label' => 'Description', 'type' => 'textarea'],
['name' => 'category', 'label' => 'Category', 'type' => 'select', 'options' => [1 => 'A', 2 => 'B']],
['name' => 'active', 'label' => 'Active', 'type' => 'checkbox'],
],
]) ?>
<!-- Trigger -->
<button data-modal-open="createItem">Create Item</button>Field Types
| Type | Description |
|---|---|
text (default) | Text input |
email, number | Specialized inputs |
textarea | Multi-line text |
select | Dropdown (requires options) |
checkbox | Boolean checkbox |
8b. Chart Component (ApexCharts)
A reusable charting component using ApexCharts library.
Basic Usage
<!-- Include ApexCharts library first -->
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script src="/assets/js/components/chart.js"></script>
<link rel="stylesheet" href="/assets/css/components/chart.css">
<!-- With AJAX data -->
<?= component('chart', [
'id' => 'revenueChart',
'type' => 'area',
'title' => 'Revenue Overview',
'dataUrl' => '/api/charts/revenue',
'height' => 350,
]) ?>
<!-- With static data -->
<?= component('chart', [
'id' => 'salesChart',
'type' => 'bar',
'title' => 'Monthly Sales',
'series' => [
['name' => 'Sales', 'data' => [30, 40, 35, 50, 49, 60]],
],
'categories' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
]) ?>Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
id | string | auto | Unique chart ID (required) |
type | string | 'line' | Chart type: line, area, bar, pie, donut, radialBar |
title | string | '' | Chart header title |
dataUrl | string | '' | URL to fetch chart data via AJAX |
series | array | [] | Static series data |
categories | array | [] | X-axis categories |
height | int | 350 | Chart height in pixels |
colors | array | [] | Custom color palette |
toolbar | bool | true | Show chart toolbar |
sparkline | bool | false | Enable sparkline mode (minimal) |
options | array | [] | Additional ApexCharts options |
Chart Types
<!-- Line Chart -->
<?= component('chart', ['id' => 'lineChart', 'type' => 'line', ...]) ?>
<!-- Area Chart (gradient fill) -->
<?= component('chart', ['id' => 'areaChart', 'type' => 'area', ...]) ?>
<!-- Bar Chart -->
<?= component('chart', ['id' => 'barChart', 'type' => 'bar', ...]) ?>
<!-- Pie Chart -->
<?= component('chart', [
'id' => 'pieChart',
'type' => 'pie',
'series' => [44, 55, 13, 43], // Single array for pie
'options' => ['labels' => ['A', 'B', 'C', 'D']],
]) ?>
<!-- Donut Chart -->
<?= component('chart', ['id' => 'donutChart', 'type' => 'donut', ...]) ?>AJAX Data Format
The API endpoint should return JSON in this format:
// For line/area/bar charts
{
"series": [
{"name": "Revenue", "data": [30, 40, 35, 50, 49, 60]},
{"name": "Expenses", "data": [20, 30, 25, 40, 39, 50]}
],
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
}
// For pie/donut charts
{
"series": [44, 55, 13, 43],
"labels": ["Product A", "Product B", "Product C", "Product D"]
}JavaScript API
// Refresh chart with new data
ChartComponent.refresh('chartId');
ChartComponent.refresh('chartId', '/api/new-data-url');
// Update series data
ChartComponent.updateSeries('chartId', [
{ name: 'Updated', data: [10, 20, 30] }
]);
// Update options
ChartComponent.updateOptions('chartId', {
colors: ['#ff0000', '#00ff00']
});
// Get chart instance (for advanced usage)
const chart = ChartComponent.getChart('chartId');
// Destroy chart
ChartComponent.destroy('chartId');8c. D3 Chart Component (D3.js)
A powerful charting component using D3.js for advanced, customizable visualizations.
Basic Usage
<!-- Already included in admin layout -->
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="/assets/js/components/d3-chart.js"></script>
<link rel="stylesheet" href="/assets/css/components/d3-chart.css">
<!-- Line Chart -->
<?= component('d3_chart', [
'id' => 'salesTrend',
'type' => 'line',
'title' => 'Sales Trend',
'data' => [
['label' => 'Jan', 'value' => 100],
['label' => 'Feb', 'value' => 150],
['label' => 'Mar', 'value' => 120],
],
'height' => 300,
]) ?>
<!-- Area Chart -->
<?= component('d3_chart', [
'id' => 'visitorChart',
'type' => 'area',
'title' => 'Visitors',
'dataUrl' => '/api/charts/visitors',
]) ?>Supported Chart Types
| Type | Description | Data Format |
|---|---|---|
line | Line chart with dots | [{label, value}] |
area | Filled area chart | [{label, value}] |
bar | Vertical bar chart | [{label, value}] |
pie | Pie chart | [{label, value}] |
donut | Donut chart | [{label, value}] |
scatter | Scatter plot | [{x, y, size?}] |
treemap | Treemap layout | [{label, value}] |
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
id | string | auto | Unique chart ID |
type | string | 'line' | Chart type (see above) |
title | string | '' | Chart header title |
data | array | [] | Static data array |
dataUrl | string | '' | URL to fetch data via AJAX |
height | int | 350 | Chart height in pixels |
colors | array | [] | Custom color palette |
margin | array | [20,30,40,50] | [top, right, bottom, left] |
responsive | bool | true | Auto-resize on window resize |
animate | bool | true | Enable animations |
JavaScript API
// Refresh chart from URL
D3ChartComponent.refresh('chartId');
D3ChartComponent.refresh('chartId', '/api/new-data');
// Update with new data
D3ChartComponent.updateData('chartId', [
{ label: 'A', value: 10 },
{ label: 'B', value: 20 }
]);
// Get/destroy chart
const chartData = D3ChartComponent.getChart('chartId');
D3ChartComponent.destroy('chartId');When to Use D3 vs ApexCharts
| Use Case | Recommended |
|---|---|
| Quick dashboard charts | ApexCharts - easier config |
| Highly custom visualizations | D3.js - full control |
| Treemaps, force diagrams | D3.js - specialized types |
| Standard bar/line/pie | ApexCharts - built-in features |
8d. MapLibre Component (Maps)
A high-performance map component using MapLibre GL JS with OpenStreetMap tiles. Supports markers, location search, GPS navigation, and route simulation.
Basic Usage
<!-- Simple Map with Search -->
<?= component('maplibre', [
'id' => 'cityMap',
'height' => 450,
'center' => [106.8456, -6.2088], // [lng, lat]
'zoom' => 12,
'search' => true,
'markers' => [
['lng' => 106.8456, 'lat' => -6.2088, 'popup' => '<b>Jakarta</b>']
]
]) ?>GPS Navigation Mode
<?= component('maplibre', [
'id' => 'navMap',
'height' => 600,
'center' => [106.8272, -6.1754],
'zoom' => 13,
'navigation' => true, // Enables routing panel
'search' => true, // Enables location search
]) ?>When navigation => true, users can:
- Click to Set Route: Click map twice for start/end points
- Use My Location: Use browser GPS as starting point
- Simulate Drive: Animated car follows the route
- Follow GPS: Track real GPS position along route (Uber-style)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
id | string | auto | Unique map ID (required) |
center | array | [0, 0] | Initial center [lng, lat] |
zoom | int | 1 | Initial zoom level (0-24) |
height | int | 400 | Map height in pixels |
style | string|array | (OSM Raster) | Map style JSON URL or inline style object |
markers | array | [] | Array of markers `[['lng'=>, 'lat'=>, 'popup'=>]]` |
controls | bool | true | Show zoom/nav controls |
search | bool | false | NEW: Enable location search box with autocomplete |
navigation | bool | false | NEW: Enable GPS navigation panel with routing |
JavaScript API
// Get Map Instance
const map = MapLibreComponent.getMap('myMap');
// Fly to location
MapLibreComponent.flyTo('myMap', 106.8456, -6.2088, 14);
// Add Marker
MapLibreComponent.addMarker('myMap', 106.8456, -6.2088, '<b>Hello</b>');
// Calculate Route (OSRM)
MapLibreComponent.calculateRoute('myMap', [lng1, lat1], [lng2, lat2]);
// Clear Route
MapLibreComponent.clearRoute('myMap');
// Enable click-to-set waypoints mode
MapLibreComponent.enableClickToRoute('myMap');
// Use current GPS location as start point
MapLibreComponent.useMyLocationAsStart('myMap');
// Start route simulation (animated car)
MapLibreComponent.startSimulation('myMap');
// Start real GPS tracking along route
MapLibreComponent.startGPSTracking('myMap');
// Stop simulation or GPS tracking
MapLibreComponent.stopSimulation('myMap');
MapLibreComponent.stopGPSTracking('myMap');Search Feature
When search => true, a Google Maps-style search box appears in the top-right corner:
- Type at least 3 characters to trigger search
- Uses Nominatim (OpenStreetMap geocoder) - free, no API key needed
- Autocomplete dropdown with up to 5 results
- Click a result to fly to that location with a marker
GPS Navigation Flow
- Click "Click to Set Route" or "Use My Location"
- Click on map to set start point (or GPS provides it)
- Click on map to set destination
- Route is calculated automatically via OSRM
- Choose Simulate (demo) or Follow GPS (real tracking)
9. AppAlert (SweetAlert Wrapper)
A wrapper for SweetAlert2 with consistent styling and custom animations.
Methods
| Method | Description |
|---|---|
AppAlert.success(msg) | Success notification (auto-close) |
AppAlert.error(msg) | Error notification |
AppAlert.warning(msg) | Warning notification |
AppAlert.info(msg) | Info notification |
AppAlert.confirm(msg) | Confirmation dialog |
AppAlert.confirmDelete(msg) | Delete confirmation (bounce) |
AppAlert.loading(msg) | Loading indicator |
AppAlert.toast(msg, type) | Toast notification |
AppAlert.close() | Close any open alert |
Confirmation Animations (AJAX Forms)
Use data-confirm-animation to specify the animation:
<form data-ajax-form
data-confirm="Delete this item?"
data-confirm-animation="bounce">
<!-- Available animations: bounce, slide, zoom, fade, shake -->| Animation | Effect |
|---|---|
bounce | Bouncy scale (default for delete) |
slide | Slide up from bottom |
zoom | Zoom in from center |
fade | Simple fade in |
shake | Shake effect (for warnings) |
Required Scripts
<script src="/assets/js/components/swal-wrapper.js"></script>
<script src="/assets/js/components/modal.js"></script>
<script src="/assets/js/components/ajax-form.js"></script>10. JavaScript Components Reference
All JS components follow the IIFE + window pattern for consistency. Each component exposes a global object with public methods.
Component List
| File | Window Object | Key Methods |
|---|---|---|
ajax-component.js | AjaxComponent | init(el), refresh(id), refreshAll() |
ajax-form.js | AjaxForm | init(), submit(form) |
datatable.js | DataTableComponent | init(selector, options), initAll(), getInstance(selector), refresh(table), getSelected(table) |
downloader.js | FileDownloader | download(url, filename) |
dropdown.js | DropdownComponent | initAll(), open(el, menu), close(el, menu) |
filepond.js | FilePondComponent | init(el), destroy(el), getFiles(el) |
lightbox.js | LightboxComponent | init(), refresh(), getInstance() |
tomselect.js | TomSelectComponent | init(el), initAll(), getValue(el) |
flatpickr.js | FlatpickrComponent | init(el), initAll() |
select2.js | Select2Component | init(el), initAll() |
cleave.js | CleaveComponent | init(el), initAll() |
modal.js | ModalComponent | initAll(), open(id), close(id) |
notification-polling.js | NotificationPolling | start(), stop(), refresh(), markRead(id) |
prefetch.js | Prefetch | prefetch(url), enable(), disable(), clearCache() |
sidebar.js | SidebarComponent | initAll(), toggle(), collapse(), expand() |
swal-wrapper.js | AppAlert | success(msg), error(msg), confirm(msg), toast(msg) |
toast.js | ToastComponent | show(msg, type), success(msg), error(msg) |
tooltip.js | TooltipComponent | initAll(), show(el, config), hide() |
Standard Pattern
All components follow this structure:
window.ComponentName = (function() {
'use strict';
function init(context) {
// Initialize on context (default: document)
}
// Auto-init on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
return { init, /* other methods */ };
})();// Modal
ModalComponent.open('myModal');
ModalComponent.close('myModal');
// Toast
ToastComponent.success('Item saved!');
ToastComponent.error('Something went wrong');
// File Download
FileDownloader.download('/export/report', 'report.xlsx');
// DataTable - Basic
DataTableComponent.initAll();
const selected = DataTableComponent.getSelected(table);
// DataTable - Programmatic with AJAX and Row Click
const dt = DataTableComponent.init('#my-table', {
serverSide: true,
ajax: {
url: '/api/data',
data: function(d) {
d.filter = document.getElementById('myFilter').value;
}
},
columns: [
{ data: 'id' },
{ data: 'name' },
{ data: 'status' }
],
onRowClick: function(rowData) {
console.log('Clicked:', rowData.id);
}
});
// DataTable - Grouped with Subtotals
// HTML: data-grouped="true" data-group-column="1" data-subtotal-columns="[3,4]"
DataTableComponent.initGrouped(table, {
groupColumn: 1, // Column index to group by (NAMA PELANGGAN)
showSubtotals: true, // Show subtotal rows
subtotalColumns: [3,4], // Columns to sum (KUANTITAS, PENJUALAN)
collapsible: true // Allow group collapse
});
// Prefetch
Prefetch.disable(); // Disable prefetching
Prefetch.clearCache();Grouped DataTable
Create tables with row grouping, subtotals, and collapsible sections:
<table data-datatable
data-grouped="true"
data-group-column="1"
data-subtotal-columns="[3,4]"
data-collapsible="true">
<thead>
<tr>
<th>NO</th>
<th>NAMA PELANGGAN</th>
<th>NAMA BARANG</th>
<th>KUANTITAS</th>
<th>PENJUALAN</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>AFCAR CELL (WKB)</td>
<td>GAJAH BARU KRETEK (12)</td>
<td>800</td>
<td>7.200.000</td>
</tr>
...
</tbody>
</table>Data Attributes:
data-grouped="true"- Enable row groupingdata-group-column="1"- Column index to group by (0-based)data-subtotal-columns="[3,4]"- JSON array of column indices to sumdata-collapsible="true"- Click group headers to collapse/expanddata-show-subtotals="true"- Show subtotal row per group (default: true)data-show-grand-total="true"- Show grand total row at bottomdata-grand-total-label="..."- Custom grand total label
12. FileDownloader (Export/Download)
The FileDownloader component handles file downloads from URLs via AJAX/Fetch with automatic filename extraction from headers.
Basic Usage
<script src="/assets/js/components/downloader.js"></script>
<script src="/assets/js/components/swal-wrapper.js"></script> <!-- Optional: for loading indicator -->
<script>
// Basic download
FileDownloader.download('/export/report', 'report.xlsx');
// Download with POST data
FileDownloader.download('/export/data', 'data.xlsx', {
method: 'POST',
body: JSON.stringify({ filters: { status: 'active' } }),
headers: { 'Content-Type': 'application/json' }
});
</script>Button Example
<button onclick="exportData()" class="btn btn-primary">
<i class="fas fa-download"></i> Export Excel
</button>
<script>
function exportData() {
FileDownloader.download('/admin/reports/export', 'report.xlsx');
}
</script>API Reference
| Method | Parameters | Description |
|---|---|---|
download(url, filename, options) | url - Download endpointfilename - Fallback filename (optional)options - Request options (optional)
| Downloads file and triggers browser save dialog |
Options Object
FileDownloader.download(url, filename, {
method: 'POST', // HTTP method (default: POST)
body: formData, // Request body (FormData or JSON string)
headers: { // Additional headers
'Content-Type': 'application/json'
}
});Features
- Automatic filename detection - Extracts filename from
Content-Dispositionheader - Loading indicator - Shows "Exporting..." overlay via AppAlert/Swal
- Success toast - Shows "Download started" toast on completion
- Error handling - Shows error alert if download fails
- CSRF support - Automatically includes CSRF token from meta tag
Backend Integration (PHP)
// Controller method
public function export()
{
$data = $this->model->findAll();
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// ... build spreadsheet ...
$writer = new Xlsx($spreadsheet);
$filename = 'report_' . date('Y-m-d') . '.xlsx';
return $this->response
->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->setBody($writer->save('php://output'));
}Legacy Support
The old function window.downloadExport() still works for backward compatibility:
// Old way (still supported)
downloadExport('/export/data', 'report.xlsx');
// New way (recommended)
FileDownloader.download('/export/data', 'report.xlsx');13. Best Practices
Best Practices
- Document component parameters:
<?php
/**
* @param string $title Required - Card title
* @param mixed $value Required - Display value
* @param string $icon Optional - Icon name (default: 'chart')
*/
?>- Set sensible defaults:
$color = $color ?? 'blue';
$icon = $icon ?? 'default-icon';- Always escape output:
<!-- GOOD: Use esc() -->
<h3><?= esc($title) ?></h3>
<!-- BAD: Direct echo -->
<h3><?= $title ?></h3>- Keep components focused: One component = one purpose
Anti-Patterns
- Don't put business logic in components:
<!-- BAD: Database query in component -->
<?php $users = db_connect()->table('users')->get()->getResult(); ?>
<!-- GOOD: Receive data as parameter -->
<?php foreach ($users as $user): ?>- Don't hardcode styles: Use CSS classes instead
- Don't use inline JavaScript: Use data attributes
9. Docker Verification
List Components
# List Admin components
docker-compose exec php ls -la /var/www/html/app/Modules/Admin/Shared/Components/
# Output:
# header.php
# sidebar.php
# stat_card.php
# user_menu.php
# List Web components
docker-compose exec php ls -la /var/www/html/app/Modules/Web/Shared/Components/View Component Content
# View a component
docker-compose exec php cat /var/www/html/app/Modules/Admin/Shared/Components/stat_card.phpTest Component Rendering
# Make a request to a page that uses the component
curl http://localhost:81/admin/dashboard
# Check if the HTML contains component output
curl http://localhost:81/admin/dashboard | grep "stat-card"Check CSS Loading
# Verify CSS file exists
docker-compose exec php ls -la /var/www/html/public/assets/css/
# Check browser for CSS
curl -I http://localhost:81/assets/css/app.css
# Should return: HTTP/1.1 200 OK