# ApplicationPrototype
A lightweight event-driven object builder that adds observable lifecycle hooks to every method you create.
## Overview
`ApplicationPrototype` is the foundation of the framework. It takes a plain JavaScript object and transforms it into an event-driven system where every method automatically gets `before`, `on`, and `after` lifecycle hooks. You can intercept method calls, validate inputs, log activity, trigger side effects -- all without modifying the original method.
## Advantages
- **Automatic lifecycle hooks** -- every bound method gets `before`/`on`/`after` events for free
- **Method interruption** -- guard patterns via `beforeX` returning `false`
- **No class syntax** -- works in ES3+, no transpilation needed
- **Tiny footprint** -- single file, zero dependencies
- **Browser + Node.js** -- identical API in both environments
- **Reactive properties** -- define getters/setters with change events
- **Multi-event listeners** -- subscribe to multiple events in one call
- **Listener IDs** -- precise control over event subscription/unsubscription
## Getting Started
```js
// @run
var app = new ApplicationPrototype();
// Bind a method
app.bind('multiply', function (a, b) {
return a * b;
});
// Use it
var result = app.multiply(3, 7); // 21
// Listen to its lifecycle
app.on('beforeMultiply', function (a, b) {
console.log('Calculating:', a, '*', b);
});
console.log(app.multiply(2, 5)); // logs "Calculating: 2 * 5", then 10
```
## Constructor
```js
var app = new ApplicationPrototype([builder]);
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `builder` | function | Optional. Receives `(config, vars, methods, public_methods, private_methods)`. If it returns `false`, the event system is not initialized and the raw `methods` object is returned |
The `builder` function gives low-level access to the internals:
| Argument | Type | Description |
|----------|------|-------------|
| `config` | object | Shared configuration object (empty by default) |
| `vars` | object | Shared variables object (empty by default) |
| `methods` | object | Internal methods object (listeners' `this` when `track` is false) |
| `public_methods` | object | Public-facing object returned by the constructor |
| `private_methods` | array | Method names to exclude from automatic hook binding |
```js
// @run
var app = new ApplicationPrototype(function (config, vars, methods, pub, priv) {
config.version = '1.0';
priv.push('_internal'); // _internal won't get lifecycle hooks
methods._internal = function () { return config; };
methods.version = function () { return config.version; };
});
console.log(app.version()); // '1.0' -- has before/on/after hooks
console.log(app._internal()); // returns config -- no hooks (private)
```
## API Reference
### `bind(methodName, callback, [config])`
Attach a method to the application with optional lifecycle hooks. Returns the public methods object (chainable).
| Parameter | Type | Description |
|-----------|------|-------------|
| `methodName` | string or function | Method name. If a named function is passed, its `.name` is used |
| `callback` | function | The method implementation |
| `config` | string or object | Hook configuration (see below) |
**Config as string** (uses substring matching -- values can be combined):
| Value | Effect |
|-------|--------|
| `''` (empty) | No hooks -- pure method |
| `'all'` or `'default'` | All hooks + interruption enabled (default) |
| `'light'` | `onMethod` + interruption |
| `'on'` | `onMethod` only |
| `'af'` | `afterMethod` only |
| `'bf'` or `'before'` | `beforeMethod` only |
| `'st'` | Allow interruption |
Combinations work: `'on af st'` enables `onMethod` + `afterMethod` + interruption.
**Config as object:**
```js
{
listenedBefore: true, // enable beforeMethod event
listenedOn: true, // enable onMethod event
listenedAfter: true, // enable afterMethod event
allowInterruption: true // allow beforeMethod to block execution
}
```
**Hook naming:** For a method named `foo`, hooks are named `beforeFoo`, `onFoo`, `afterFoo` (first letter capitalized).
**Example:**
```js
// Named function syntax
app.bind(function render(data) {
document.body.innerHTML = data;
});
app.render('
Hello
');
// No hooks -- lightweight utility
app.bind('utils', function () { return utilsObject; }, '');
// Only before hook (for validation)
app.bind('save', saveFunction, 'before');
```
> **Important:** `afterX` hooks run **asynchronously** (via `nextTick`/`setImmediate`/`setTimeout`). They execute after the method returns, so they cannot affect the method's return value.
---
### `on(eventName, handler, [handlerId])`
Register an event listener.
**Returns:** the listener ID (string) for single events, or `undefined` for comma-separated events.
| Parameter | Type | Description |
|-----------|------|-------------|
| `eventName` | string | Event name, or comma-separated list of event names |
| `handler` | function | The listener function |
| `handlerId` | string | Optional. Custom ID for this listener. If an ID already exists, it is replaced. If omitted, auto-generated as `"s-N"` |
```js
// Basic usage
app.on('onSave', function (data) {
console.log('Saved:', data);
});
// With custom ID (replaces previous listener with same ID)
app.on('onSave', handler, 'my-save-logger');
// Multiple events at once (returns undefined, not the ID)
app.on('onSave, onDelete, onUpdate', function () {
console.log('Data changed');
});
```
---
### `once(eventName, handler, [handlerId])`
Register a listener that fires only once, then auto-removes itself. Returns the listener ID.
| Parameter | Type | Description |
|-----------|------|-------------|
| `eventName` | string | Event name, or comma-separated list |
| `handler` | function | The listener function |
| `handlerId` | string | Optional custom listener ID |
```js
app.once('onInit', function () {
console.log('This runs exactly once');
});
```
---
### `off(eventName, [handlerId])`
Remove event listeners.
**Returns:** `true` if any were removed, `false` if nothing matched, or `undefined` for comma-separated events.
```js
// Remove specific listener
app.off('onSave', 'my-save-logger');
// Remove ALL listeners for an event
app.off('onSave');
// Remove from multiple events
app.off('onSave, onDelete', 'my-listener-id');
```
---
### `emit(eventName, [args], [track], [noSkipStopReturn])`
Emit an event to all registered listeners.
**Returns:** `false` if any listener returned `false` and `noSkipStopReturn` is falsy (emission stops early). Otherwise `undefined`.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `eventName` | string | | Event name |
| `args` | array | `[]` | Arguments passed to listeners |
| `track` | boolean | `false` | If true, listeners' `this` is the public methods object; if false, `this` is the internal methods object |
| `noSkipStopReturn` | boolean | `false` | If true, listeners returning `false` won't stop emission |
> **Note:** Errors thrown inside listeners are caught and logged to `console.error`. Execution continues with the next listener.
```js
// Basic emit
app.emit('dataLoaded', [data, timestamp]);
// Tracked -- listeners can call other app methods via `this`
app.emit('dataLoaded', [data], true);
// Unstoppable -- all listeners run regardless of return values
app.emit('notify', [message], false, true);
```
---
### `property(propName, getter, [setter], [config])`
Define a reactive property with get/set events.
**Signature 1: `property(propName, getter, [setter], [config])`**
| Parameter | Type | Description |
|-----------|------|-------------|
| `propName` | string | Property name |
| `getter` | function | Called on read: `getter(undefined, lastValue, false)`. Return value is the property value |
| `setter` | function | Called on write: `setter(newValue, lastValue, true)`. Return value is stored. If omitted, `getter` is used for both |
| `config` | object | Property descriptor options (see below) |
If neither `getter` nor `setter` is provided, defaults are used: getter returns the stored value, setter stores and returns the new value.
**Signature 2: `property(fn, [config])`** -- when the first argument is a named function, its `.name` is used as the property name and the function serves as both getter and setter:
| Parameter | Type | Description |
|-----------|------|-------------|
| `fn` | function | Named function used as both getter and setter. `fn.name` becomes the property name |
| `config` | object | Property descriptor options (see below) |
**Config options:**
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `configurable` | boolean | `true` | Whether the property can be redefined or deleted |
| `enumerable` | boolean | `true` | Whether the property shows up in `for...in` and `Object.keys()` |
```js
// Simple reactive property
app.property('theme',
function (newValue, lastValue, isSetter) {
if (isSetter) return newValue;
return lastValue;
}
);
// Named function shorthand -- fn.name becomes the property name
app.property(function count(newValue, lastValue, isSetter) {
if (isSetter) return newValue;
return lastValue || 0;
});
app.count = 5; // setter
console.log(app.count); // 5 (getter)
// Non-enumerable, non-configurable property
app.property('_internal', getter, setter, {
configurable: false,
enumerable: false
});
// Listen to changes
app.on('__onSet::theme', function (newValue, oldValue) {
document.body.className = newValue;
});
app.theme = 'dark'; // triggers __onSet::theme
console.log(app.theme); // 'dark', triggers __onGet::theme
```
**Property events:**
Property events are notifications. Transform or validate values in the getter / setter function you pass to `property()` -- that is where the stored value is decided. A handler that returns `false` stops the remaining handlers of that event **and** makes the resulting value `false` (because `emit()` returns `false` in that case); any other return value is ignored, since `emit()` does not propagate handler results.
| Event | Arguments | Description |
|-------|-----------|-------------|
| `__onGet` | `[propName, value, lastValue]` | Any property read (returning `false` makes the read result `false`) |
| `__onGet::propName` | `[value, lastValue]` | Specific property read |
| `__onSet` | `[propName, value, lastValue]` | Any property write (returning `false` stores `false`) |
| `__onSet::propName` | `[value, lastValue]` | Specific property write |
| `__afterGet` | `[propName, value, lastValue]` | After any property read |
| `__afterGet::propName` | `[value, lastValue]` | After specific property read |
| `__afterSet` | `[propName, value, lastValue]` | After any property write |
| `__afterSet::propName` | `[value, lastValue]` | After specific property write |
---
### `crudEvents(methods, public_methods, private_methods)`
Initialize the event system on an object. This is called automatically during construction unless the `builder` returns `false`. You can call it manually to add event capabilities to objects created outside the constructor.
```js
// Rarely needed -- mainly for advanced use cases
var obj = {};
app.crudEvents(obj, obj, []);
// obj now has: bind, on, once, off, emit, property
```
---
## Code Examples
### Example 1: Form Validation with Guard Pattern
```js
var form = new ApplicationPrototype();
form.bind('submit', function (data) {
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
});
console.log('Form submitted!');
});
// Guard: validate before submit
form.on('beforeSubmit', function (data) {
if (!data.email || !data.email.match(/@/)) {
alert('Please enter a valid email');
return false; // prevents submit() from running
}
if (!data.name || data.name.length < 2) {
alert('Name must be at least 2 characters');
return false;
}
});
// Log after submit
form.on('afterSubmit', function (data) {
console.log('Submitted at:', new Date().toISOString());
});
// Usage
form.submit({ name: 'Alice', email: 'alice@example.com' }); // submits
form.submit({ name: 'A', email: '' }); // blocked by validation
```
### Example 2: Logging Middleware
```js
var api = new ApplicationPrototype();
api.bind('getUsers', function () { return fetch('/api/users'); });
api.bind('getUser', function (id) { return fetch('/api/users/' + id); });
api.bind('deleteUser', function (id) { return fetch('/api/users/' + id, { method: 'DELETE' }); });
// One listener logs ALL method calls
['getUsers', 'getUser', 'deleteUser'].forEach(function (method) {
var eventName = 'before' + method.charAt(0).toUpperCase() + method.slice(1);
api.on(eventName, function () {
console.log('[API]', method, Array.prototype.slice.call(arguments));
});
});
api.getUser(42); // logs: [API] getUser [42]
api.deleteUser(7); // logs: [API] deleteUser [7]
```
### Example 3: Undo/Redo Pattern
```js
var editor = new ApplicationPrototype();
var history = [];
var historyIndex = -1;
editor.bind('setText', function (text) {
document.getElementById('editor').value = text;
});
editor.on('afterSetText', function (text) {
// Record state after each change
history = history.slice(0, historyIndex + 1);
history.push(text);
historyIndex = history.length - 1;
});
editor.bind('undo', function () {
if (historyIndex > 0) {
historyIndex--;
document.getElementById('editor').value = history[historyIndex];
}
}, ''); // no hooks needed for undo itself
editor.bind('redo', function () {
if (historyIndex < history.length - 1) {
historyIndex++;
document.getElementById('editor').value = history[historyIndex];
}
}, '');
```
### Example 4: Plugin System
```js
var app = new ApplicationPrototype();
app.bind('render', function (template, data) {
return template.replace(/\{(\w+)\}/g, function (m, key) {
return data[key] || '';
});
});
// Plugin: add caching
app.on('beforeRender', function (template, data) {
var key = template + JSON.stringify(data);
if (app._cache && app._cache[key]) {
console.log('Cache hit');
return app._cache[key]; // return cached result
}
});
app.on('afterRender', function (template, data) {
app._cache = app._cache || {};
var key = template + JSON.stringify(data);
app._cache[key] = app.render(template, data);
});
// Plugin: add analytics
app.on('afterRender', function () {
console.log('Render count:', (app._renderCount = (app._renderCount || 0) + 1));
});
```
### Example 5: Node.js Event-Driven Controller
```js
var ApplicationPrototype = require('application-prototype').application;
var controller = new ApplicationPrototype();
controller.bind('handleRequest', function (req, res) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', path: req.url }));
});
// Middleware: authentication
controller.on('beforeHandleRequest', function (req, res) {
if (!req.headers.authorization) {
res.writeHead(401);
res.end('Unauthorized');
return false; // block the handler
}
});
// Middleware: logging
controller.on('afterHandleRequest', function (req) {
console.log(new Date().toISOString(), req.method, req.url);
});
// Use with Node.js HTTP server
var http = require('http');
http.createServer(function (req, res) {
controller.handleRequest(req, res);
}).listen(3000);
```
## Common Patterns
| Pattern | How | When |
|---------|-----|------|
| **Guard** | `beforeX` returns `false` | Input validation, authorization |
| **Observer** | `onX` for side effects | Logging, analytics, notifications |
| **Cleanup** | `afterX` for async tasks | Caching, state recording, notifications |
| **Plugin** | Add listeners to extend behavior | Third-party extensions without modifying source |
| **Middleware** | Chain multiple `beforeX` listeners | Request pipelines, data transformation |
## Live Demo
Bind a method, watch the `before` / `on` / `after` hooks fire, block a call from a guard, try `once()` / `off()`, `emit()` a custom event and play with a reactive `property()`. [Open in a new tab](examples/application-prototype/index.html ':ignore :target=_blank') | [All live examples](../examples/index.md)
## Related Modules
- [ApplicationBuilder](application-builder.md) -- extends this with module loading
- [Architecture Overview](../architecture.md) -- how it all fits together