v1.0
Docs / View Components

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

  1. What Are Components?
  2. Creating a Component (Step-by-Step)
  3. Understanding Layouts
  4. JavaScript Integration
  5. Form Components (Select, Datepicker)
  6. AJAX Components (Dynamic Loading)
  7. Standard UI Components
  8. AJAX Forms
  9. Modal Form Component
  10. Chart Component (ApexCharts)
  11. D3 Chart Component (D3.js)
  12. MapLibre Component (Maps)
  13. AppAlert (SweetAlert Wrapper)
  14. JavaScript Components Reference
  15. FileDownloader (Export/Download)
  16. Best Practices
  17. Docker Verification

1. What Are Components?

Goal: Learn how to reuse UI elements to keep your views clean and consistent.

Technical Definition

A Component is a reusable PHP partial that receives data via parameters and renders HTML. Components:

Simple Analogy

Think of components like LEGO bricks:

LEGOComponents
Same brick shape, different colorsSame component, different data
Combine bricks to build anythingCombine components to build pages
Reuse the same brick in many setsReuse same component on many pages

Before/After Comparison

WITHOUT COMPONENTS: WITH COMPONENTS: Page 1: Page 1: ├── Same HTML for stat card <?= component('stat_card', [...]) ?> │ (copied 50 lines) <?= component('stat_card', [...]) ?> ├── Same HTML again │ (copied 50 lines) Page 2: <?= component('stat_card', [...]) ?> Page 2: ├── Same HTML again Component defined ONCE: │ (copied 50 lines) Shared/Components/stat_card.php (50 lines, single source of truth) Total: 150+ lines duplicated

2. Creating a Component (Step-by-Step)

Task: Create a new `Alert` component and use it in a view.

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

Layout (admin.php) provides: ├── <html>, <head>, <body> ├── Sidebar component ├── Header component ├── Section: 'styles' (optional CSS) ├── Section: 'content' (required) └── Section: 'scripts' (optional JS) View (index.php) provides: ├── Section: 'content' → rendered in page-content div ├── Section: 'styles' → rendered in <head> └── Section: 'scripts' → rendered before </body>

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)

Important: To use AJAX loading, you must register your component in the `ComponentController`.

What are AJAX Components?

AJAX Components can be refreshed individually without reloading the entire page. They are useful for:

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-side

7. 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

AttributeDescription
data-ajax-formEnables 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.


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

TypeDescription
text (default)Text input
email, numberSpecialized inputs
textareaMulti-line text
selectDropdown (requires options)
checkboxBoolean 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

ParameterTypeDefaultDescription
idstringautoUnique chart ID (required)
typestring'line'Chart type: line, area, bar, pie, donut, radialBar
titlestring''Chart header title
dataUrlstring''URL to fetch chart data via AJAX
seriesarray[]Static series data
categoriesarray[]X-axis categories
heightint350Chart height in pixels
colorsarray[]Custom color palette
toolbarbooltrueShow chart toolbar
sparklineboolfalseEnable sparkline mode (minimal)
optionsarray[]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

TypeDescriptionData Format
lineLine chart with dots[{label, value}]
areaFilled area chart[{label, value}]
barVertical bar chart[{label, value}]
piePie chart[{label, value}]
donutDonut chart[{label, value}]
scatterScatter plot[{x, y, size?}]
treemapTreemap layout[{label, value}]

Parameters

ParameterTypeDefaultDescription
idstringautoUnique chart ID
typestring'line'Chart type (see above)
titlestring''Chart header title
dataarray[]Static data array
dataUrlstring''URL to fetch data via AJAX
heightint350Chart height in pixels
colorsarray[]Custom color palette
marginarray[20,30,40,50][top, right, bottom, left]
responsivebooltrueAuto-resize on window resize
animatebooltrueEnable 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 CaseRecommended
Quick dashboard chartsApexCharts - easier config
Highly custom visualizationsD3.js - full control
Treemaps, force diagramsD3.js - specialized types
Standard bar/line/pieApexCharts - 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:

Parameters

ParameterTypeDefaultDescription
idstringautoUnique map ID (required)
centerarray[0, 0]Initial center [lng, lat]
zoomint1Initial zoom level (0-24)
heightint400Map height in pixels
stylestring|array(OSM Raster)Map style JSON URL or inline style object
markersarray[]Array of markers `[['lng'=>, 'lat'=>, 'popup'=>]]`
controlsbooltrueShow zoom/nav controls
searchboolfalseNEW: Enable location search box with autocomplete
navigationboolfalseNEW: 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:

GPS Navigation Flow

  1. Click "Click to Set Route" or "Use My Location"
  2. Click on map to set start point (or GPS provides it)
  3. Click on map to set destination
  4. Route is calculated automatically via OSRM
  5. Choose Simulate (demo) or Follow GPS (real tracking)

9. AppAlert (SweetAlert Wrapper)

A wrapper for SweetAlert2 with consistent styling and custom animations.

Methods

MethodDescription
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 -->
AnimationEffect
bounceBouncy scale (default for delete)
slideSlide up from bottom
zoomZoom in from center
fadeSimple fade in
shakeShake 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

FileWindow ObjectKey Methods
ajax-component.jsAjaxComponentinit(el), refresh(id), refreshAll()
ajax-form.jsAjaxForminit(), submit(form)
datatable.jsDataTableComponentinit(selector, options), initAll(), getInstance(selector), refresh(table), getSelected(table)
downloader.jsFileDownloaderdownload(url, filename)
dropdown.jsDropdownComponentinitAll(), open(el, menu), close(el, menu)
filepond.jsFilePondComponentinit(el), destroy(el), getFiles(el)
lightbox.jsLightboxComponentinit(), refresh(), getInstance()
tomselect.jsTomSelectComponentinit(el), initAll(), getValue(el)
flatpickr.jsFlatpickrComponentinit(el), initAll()
select2.jsSelect2Componentinit(el), initAll()
cleave.jsCleaveComponentinit(el), initAll()
modal.jsModalComponentinitAll(), open(id), close(id)
notification-polling.jsNotificationPollingstart(), stop(), refresh(), markRead(id)
prefetch.jsPrefetchprefetch(url), enable(), disable(), clearCache()
sidebar.jsSidebarComponentinitAll(), toggle(), collapse(), expand()
swal-wrapper.jsAppAlertsuccess(msg), error(msg), confirm(msg), toast(msg)
toast.jsToastComponentshow(msg, type), success(msg), error(msg)
tooltip.jsTooltipComponentinitAll(), 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:


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

MethodParametersDescription
download(url, filename, options)url - Download endpoint
filename - 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

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

  1. Document component parameters:
<?php
/**
 * @param string $title   Required - Card title
 * @param mixed  $value   Required - Display value
 * @param string $icon    Optional - Icon name (default: 'chart')
 */
?>
  1. Set sensible defaults:
$color = $color ?? 'blue';
$icon = $icon ?? 'default-icon';
  1. Always escape output:
<!-- GOOD: Use esc() -->
<h3><?= esc($title) ?></h3>

<!-- BAD: Direct echo -->
<h3><?= $title ?></h3>
  1. Keep components focused: One component = one purpose

Anti-Patterns

  1. 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): ?>
  1. Don't hardcode styles: Use CSS classes instead
  2. 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.php

Test 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
ESC

Start typing to search the documentation