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
- Imports core frameworks (Stimulus, Turbo, Bootstrap)
- Imports
timezone-utils.js(creates globalKMP_Timezoneobject) - Imports specific controllers that need early loading
- Disables Turbo Drive —
Turbo.session.drive = false - Registers all controllers from
window.Controllersglobal registry - 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 = {
'&': '&', '<': '<', '>': '>',
'"': '"', "'": ''', "/": '/',
};
return str.replace(/[&<>"'/]/ig, (match) => (map[match]));
},
sanitizeUrl(str) {
const map = { '<': '%3C', '>': '%3E', '"': '%22', "'": '%27', ' ': '%20' };
return str.replace(/[<>"' ]/ig, (match) => (map[match]));
}
};
urlParam(name)— Extracts URL query parameters via regexsanitizeString(str)— HTML entity encoding to prevent XSSsanitizeUrl(str)— URL-encodes special characters
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:
auto-complete-controller.js→ registered as"ac"(abbreviated)modal-opener-controller.js→ registered as"modal-opener"grid-view-controller.js→ registered as"grid-view"detail-tabs-controller.js→ registered as"detail-tabs"
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
- Create
app/assets/js/controllers/your-feature-controller.js(orplugins/YourPlugin/assets/js/controllers/) - Use kebab-case with
-controller.jssuffix - Register on
window.Controllersat the bottom of the file - Rebuild:
npm run dev— the file is auto-discovered by webpack.mix.js
Troubleshooting
- Controller Not Loading — Check
window.Controllersregistration and filename suffix - Targets Not Found — Verify
data-*-targetattribute matches static targets array - Actions Not Firing — Confirm
data-actionsyntax:event->controller#method - Turbo Frame issues — Remember Turbo Drive is disabled; only Frames work
See Also
- Asset Management — Build configuration details
- JavaScript Development — Controller reference
- QR Code Controller — Example controller deep-dive
- Dataverse Grid — Grid system controller