Introduction
WakaPAC is a lightweight JavaScript toolkit for building interactive applications in plain HTML. It combines a Win32-style message system, reactive data binding, and declarative HTML templates in a single script with no build step. The name combines Waka (inspired by Pac-Man) with PAC, the Presentation–Abstraction–Control architecture pattern it implements.
Core Concept
WakaPAC applications are driven by messages. Browser input events, component communication, and system
events all flow through a single handler called msgProc. This creates a predictable event pipeline
similar to Win32 applications, where input handling, messaging, and state updates follow a consistent model.
Instead of scattering listeners across components, you reason about all interaction in one place.
Quick Example
A complete interactive to-do application:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/wakapac.min.js"></script>
</head>
<body>
<div id="app">
<input data-pac-bind="value: task" placeholder="New task">
<button data-pac-bind="click: add, enable: task.length > 0">Add</button>
<ul data-pac-bind="foreach: tasks" data-pac-item="task">
<li>{{ task.name }}</li>
</ul>
</div>
<script>
wakaPAC('#app', {
task: '',
tasks: [],
add() {
this.tasks.push({ name: this.task });
this.task = '';
},
msgProc(event) {
if (event.message === wakaPAC.MSG_KEYDOWN &&
event.wParam === wakaPAC.VK_RETURN
) {
this.add();
}
}
});
</script>
</body>
</html>
The button stays disabled until there is input, the list updates automatically when an item is added, and
msgProc handles Enter via VK_RETURN — the same mental model as a Win32 window
procedure. No compilation, no virtual DOM, no JSX.
How WakaPAC Works
- Create a component with
wakaPAC(selector, abstraction, options?) - The abstraction object is exposed to the template via reactive bindings
- DOM bindings update automatically when state changes
- All input — keyboard, mouse, scroll, etc. — flows through
msgProc - Components communicate using
sendMessage()andpostMessage()
Comparison with Component Frameworks
| Scenario | Vue / React / Alpine | WakaPAC |
|---|---|---|
| Components talking to each other | Props, event emitters, or external state libraries (Pinia, Redux) | sendMessage() and postMessage() — the same model as Win32 |
| Handling keyboard and mouse input | Per-component event listeners wired manually | Central msgProc handler — one place for all input, like a window procedure |
| Reacting to state changes | Framework-specific reactivity APIs with varying edge cases | Declare your data, bind it in HTML — changes propagate automatically |
| Mental model | Component tree with lifecycle hooks and framework conventions | Message loop, centralized input handling, reactive state — a coherent application model |
| Getting started | Build tooling, a CLI, or at minimum a module bundler | One script tag |
Key Features
Desktop Subsystem
All browser input flows through a unified message-driven interface. Mouse coordinates, modifier keys, and virtual key codes follow Win32 conventions.
wakaPAC('#app', {
msgProc(event) {
switch (event.message) {
case wakaPAC.MSG_LBUTTONDOWN:
// wParam contains modifier flags: MK_SHIFT, MK_CONTROL, etc.
// lParam contains mouse coordinates
const x = wakaPAC.LOWORD(event.lParam);
const y = wakaPAC.HIWORD(event.lParam);
console.log(`Left click at ${x},${y}`);
break;
case wakaPAC.MSG_KEYDOWN:
// wParam is the virtual key code
if (event.wParam === wakaPAC.VK_RETURN) {
console.log('Enter pressed');
}
// Check modifiers in lParam
if (event.lParam & wakaPAC.KM_CONTROL) {
console.log('Ctrl was held');
}
break;
}
}
});
Inter-Component Messaging
Any component can send messages to any other using synchronous sendMessage() or queued
postMessage(). The receiving component handles them in msgProc alongside all
other events.
// Sender
wakaPAC('#toolbar', {
notifyContent() {
wakaPAC.sendMessage('content', wakaPAC.MSG_USER + 1, 0, 0, { action: 'refresh' });
}
});
// Receiver
wakaPAC('#content', {
msgProc(event) {
switch (event.message) {
case wakaPAC.MSG_USER + 1:
console.log('Refresh requested:', event.detail.action);
break;
}
}
});
Reactive Data Binding
Updating state automatically updates the DOM. User interaction updates state without manual event wiring.
<input data-pac-bind="value: email">
<p>You entered: {{ email }}</p>
Declarative Templates
Describe how data maps to the DOM directly in HTML, without imperative DOM manipulation.
<button data-pac-bind="click: save, enable: !saving">
{{ saving ? 'Saving...' : 'Save' }}
</button>
<ul data-pac-bind="foreach: items" data-pac-item="item">
<li>{{ item.name }}: {{ item.price }}</li>
</ul>
Computed Properties
Derived values that recalculate automatically when their dependencies change.
wakaPAC('#app', {
firstName: 'John',
lastName: 'Doe',
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
});
Multiple Element Binding
A single wakaPAC() call on a class selector creates independent reactive instances for each matched element.
<div class="counter">Count: {{count}} <button data-pac-bind="click: increment">+</button></div>
<div class="counter">Count: {{count}} <button data-pac-bind="click: increment">+</button></div>
<script>
wakaPAC('.counter', { count: 0, increment() { this.count++; } });
</script>
Browser & Container Properties
Scroll position, visibility, dimensions, and connectivity are exposed as reactive properties — no manual listeners required.
<button data-pac-bind="visible: browserScrollY > 300">
↑ Back to Top
</button>
<div data-pac-bind="visible: !browserOnline" class="offline-banner">
You're offline - changes will sync when reconnected
</div>
Expression Language
Template bindings support conditional logic, arithmetic, method calls, and common data operations.
Expressions are evaluated through a dedicated parser — not JavaScript's eval — for
sandboxed, predictable execution.
<p>Total: ${{ total }}</p>
<span>{{ count > 0 ? count + ' items' : 'Empty' }}</span>
<div data-pac-bind="class: { active: isSelected, disabled: !enabled }"></div>
<p>{{ formatDate(createdAt) }}</p>
<span>{{ tags.join(', ') }}</span>
filter and map,
are not supported in expressions. That logic belongs in a computed property, where it is cached and only
recalculates when dependencies change.
Size
WakaPAC is a single JavaScript file with no dependencies.
- Source: ~460KB (with extensive comments)
- Minified: ~95KB
- Minified + gzipped: ~28KB