Skip to the content.

10.1 JavaScript Framework — KMP Frontend Architecture

Overview

KMP uses a modern JavaScript architecture built on Stimulus.js and Turbo (from the Hotwired suite) for interactive frontend functionality. Controllers are bound to HTML elements through data attributes, creating a clean separation between markup and behavior.

Important: Turbo Drive is disabled — only Turbo Frames are used for partial page updates.

Core Technologies

Package Version Purpose
@hotwired/stimulus ^3.2.2 DOM interaction framework
@hotwired/turbo ^8.0.21 Turbo Frames for partial page updates
bootstrap ^5.3.6 UI component framework
popper.js ^1.16.1 Tooltip/popover positioning
easymde ^2.20.0 Markdown editor
qrcode ^1.5.4 QR code generation
pdfjs-dist ^5.4.530 PDF rendering
guifier ^1.0.32 JSON schema form generation
@fortawesome/fontawesome-free ^7.1.0 Icon fonts

Main Entry Point

app/assets/js/index.js

import { Application } from "@hotwired/stimulus";
import * as Turbo from "@hotwired/turbo";
import 'bootstrap';
import KMP_utils from './KMP_utils.js';
import './timezone-utils.js';

// Import controllers that need direct registration
import './controllers/qrcode-controller.js';
import './controllers/timezone-input-controller.js';
import './controllers/security-debug-controller.js';
import './controllers/popover-controller.js';

// Disable Turbo Drive — only Turbo Frames are used
Turbo.session.drive = false;

window.KMP_utils = KMP_utils;
const stimulusApp = Application.start();
window.Stimulus = stimulusApp;

// Register all controllers from the global registry
for (const controller in window.Controllers) {
    stimulusApp.register(controller, window.Controllers[controller]);
}

// Initialize Bootstrap tooltips (re-initialized after Turbo renders)
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(
    tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)
)

document.addEventListener('turbo:render', () => {
    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
        if (!bootstrap.Tooltip.getInstance(el)) {
            new bootstrap.Tooltip(el);
        }
    });
});

Key Initialization Steps

  1. Imports core frameworks (Stimulus, Turbo, Bootstrap)
  2. Imports timezone-utils.js (creates global KMP_Timezone object)
  3. Imports specific controllers that need early loading
  4. Disables Turbo DriveTurbo.session.drive = false
  5. Registers all controllers from window.Controllers global registry
  6. Initializes Bootstrap tooltips (and re-initializes after Turbo Frame renders)

KMP Utilities

app/assets/js/KMP_utils.js

Available globally as window.KMP_utils:

export default {
    urlParam(name) {
        var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
        return results ? decodeURIComponent(results[1]) : null;
    },

    sanitizeString(str) {
        const map = {
            '&': '&amp;', '<': '&lt;', '>': '&gt;',
            '"': '&quot;', "'": '&#x27;', "/": '&#x2F;',
        };
        return str.replace(/[&<>"'/]/ig, (match) => (map[match]));
    },

    sanitizeUrl(str) {
        const map = { '<': '%3C', '>': '%3E', '"': '%22', "'": '%27', ' ': '%20' };
        return str.replace(/[<>"' ]/ig, (match) => (map[match]));
    }
};

Controller Registration Pattern

KMP uses a manual global registry pattern — NOT the Stimulus webpack auto-loader. Every controller registers itself on window.Controllers:

import { Controller } from "@hotwired/stimulus"

class MyFeatureController extends Controller {
    static targets = ["input", "output"]
    static values = {
        url: String,
        delay: { type: Number, default: 300 }
    }
    static outlets = ["other-controller"]

    connect() { /* setup */ }
    handleEvent(event) { /* action */ }
    disconnect() { /* cleanup */ }
}

// Register in global registry
if (!window.Controllers) {
    window.Controllers = {};
}
window.Controllers["my-feature"] = MyFeatureController;

Registration Key Naming

The registration key usually matches the kebab-case filename, with one notable exception:

HTML Integration

<div data-controller="my-feature"
     data-my-feature-url-value="/api/endpoint"
     data-my-feature-delay-value="500">
  <input data-my-feature-target="input" type="text">
  <div data-my-feature-target="output"></div>
  <button data-action="click->my-feature#handleEvent">Submit</button>
</div>

Asset Compilation

app/webpack.mix.js

The build process uses dynamic file discovery:

const mix = require('laravel-mix');
const webpack = require('webpack');

// 1. Recursively discover all *-controller.js files
//    from assets/js/ and plugins/
const files = [];
getJsFilesFromDir('./assets/js', skipList, '-controller.js', (f) => files.push(f));
getJsFilesFromDir('./plugins', skipList, '-controller.js', (f) => files.push(f));

// 2. Discover service files
const serviceFiles = [];
getJsFilesFromDir('./assets/js/services', skipList, '-service.js', (f) => serviceFiles.push(f));

const allJsFiles = [...files, ...serviceFiles];

// 3. Configure Mix
mix.setPublicPath('./webroot')
    .js(allJsFiles, 'webroot/js/controllers.js')           // All controllers → controllers.js
    .js('assets/js/index.js', 'webroot/js')                // Entry point → index.js
    .extract([                                              // Vendor libs → core.js
        'bootstrap', 'popper.js',
        '@hotwired/turbo', '@hotwired/stimulus',
        '@hotwired/stimulus-webpack-helpers'
    ], 'webroot/js/core.js')
    .webpackConfig({
        devtool: "source-map",
        optimization: { runtimeChunk: true },               // → manifest.js
        plugins: [new webpack.ProvidePlugin({ 'bootstrap': 'bootstrap' })],
        module: { rules: [{ test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
            type: 'asset/resource',
            generator: { filename: 'fonts/[name][ext]' }
        }]}
    })
    .css('assets/css/app.css', 'webroot/css')
    .css('assets/css/signin.css', 'webroot/css')
    .css('assets/css/cover.css', 'webroot/css')
    .css('assets/css/dashboard.css', 'webroot/css')
    .css('plugins/Waivers/assets/css/waivers.css', 'webroot/css/waivers.css')
    .css('plugins/Waivers/assets/css/waiver-upload.css', 'webroot/css/waiver-upload.css')
    .copyDirectory('node_modules/@fortawesome/fontawesome-free/webfonts', 'webroot/fonts')
    .version()
    .sourceMaps();

Compiled Output

File Contents
webroot/js/index.js Main entry point
webroot/js/controllers.js All controllers + service files
webroot/js/core.js Vendor libraries
webroot/js/manifest.js Webpack runtime

Build Commands

cd app
npm run dev        # Development build
npm run watch      # Watch + auto-recompile
npm run prod       # Production (minified + versioned)

Inter-Controller Communication

KMP uses the outlet-btn controller as a hub for cross-controller events:

// Controllers define outlet connections
static outlets = ["outlet-btn"]

outletBtnOutletConnected(outlet, element) {
    // Handle connection
}

outletBtnOutletDisconnected(outlet) {
    // Handle disconnection
}

Integration with CakePHP

View Integration

// CakePHP template with Stimulus controller
echo $this->Html->div('', $content, [
    'data-controller' => 'ac',
    'data-ac-url-value' => $this->Url->build([
        'controller' => 'Members', 'action' => 'search'
    ])
]);

CSRF Protection

const csrfToken = document.querySelector('meta[name="csrfToken"]').content;

fetch(url, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify(data)
});

Creating New Controllers

  1. Create app/assets/js/controllers/your-feature-controller.js (or plugins/YourPlugin/assets/js/controllers/)
  2. Use kebab-case with -controller.js suffix
  3. Register on window.Controllers at the bottom of the file
  4. Rebuild: npm run dev — the file is auto-discovered by webpack.mix.js

Troubleshooting

  1. Controller Not Loading — Check window.Controllers registration and filename suffix
  2. Targets Not Found — Verify data-*-target attribute matches static targets array
  3. Actions Not Firing — Confirm data-action syntax: event->controller#method
  4. Turbo Frame issues — Remember Turbo Drive is disabled; only Frames work

See Also