Pool API LITE+
Complete reference for entity pool attributes, methods, and configuration options.
HTML Attributes
data-pool
Declares an element as a pool container. The attribute value is the pool name, used to access the pool via this.pool(name) in component methods.
<div data-pool="enemies" data-key="id">
<template>
<!-- Entity template -->
</template>
</div>
The container element must include:
- A
data-keyattribute specifying which entity property is the unique identifier (defaults to"id"if omitted) - A
<template>child element defining the DOM structure for each entity
data-key
Specifies the entity property used as a unique identifier. Every entity added to the pool must have this property. Defaults to "id".
<!-- Uses 'id' property (default) -->
<div data-pool="enemies" data-key="id">...</div>
<!-- Uses 'index' property as key -->
<div data-pool="towers" data-key="index">...</div>
<!-- Uses 'uuid' property as key -->
<div data-pool="cursors" data-key="uuid">...</div>
add() with an object whose key already exists in the pool, the add is silently ignored (with a console warning in dev builds). Remove the existing entity first if you need to replace it.
data-pool-fps
Throttles the pool's flush rate to a target frames-per-second. When set, the pool skips rAF frames until enough time has elapsed since the last flush. Useful for reducing CPU usage on pools that do not need 60fps updates.
<!-- Update this pool at most 30 times per second -->
<div data-pool="dashboard-items" data-key="id" data-pool-fps="30">
<template>
<div class="metric">
<span data-bind="label"></span>:
<span data-bind="value"></span>
</div>
</template>
</div>
| Value | Behavior |
|---|---|
0 or omitted | Flush every rAF frame (~60fps on most displays) |
30 | Flush at most 30 times per second (~33ms between flushes) |
10 | Flush at most 10 times per second (~100ms between flushes) |
data-pool-cull
Enables spatial culling with the specified padding in pixels. When enabled, the pool checks each entity's bounding rectangle against the visible viewport on every flush. Entities outside the viewport (plus padding) have their visibility set to hidden, which allows the browser to skip paint and compositing for those elements.
<!-- Cull entities more than 100px outside the viewport -->
<div data-pool="world-objects" data-key="id" data-pool-cull="100">
<template>
<div class="object" data-bind-style="{ left: x + 'px', top: y + 'px' }">
<img data-bind-attr="{ src: sprite }">
</div>
</template>
</div>
The padding value adds a buffer zone around the viewport. Entities within this buffer are kept visible so they do not pop in as the user scrolls or the camera pans:
data-pool-cull="0": cull exactly at the viewport edgedata-pool-cull="100": keep entities visible within 100px outside the viewportdata-pool-cull="200": larger buffer for fast-scrolling scenarios- Omitted: no culling (all entities always rendered)
The cull boundary is computed from the pool container's parent element (clamped to the actual viewport). This handles scrollable or pannable game worlds where the container is larger than the screen.
data-pool-cull-props
Enables data-based culling, reading entity position (and optionally size) from data properties instead of calling getBoundingClientRect() per entity per frame. This eliminates layout-forcing DOM reads entirely and enables the pool to skip both culling math AND binding evaluation for offscreen entities.
<!-- Read position from x,y properties, use template-measured default size -->
<div data-pool="sprites" data-key="id" data-pool-cull="100" data-pool-cull-props="x,y">
<template>...</template>
</div>
<!-- Read position AND size from entity properties -->
<div data-pool="sprites" data-key="id" data-pool-cull="100" data-pool-cull-props="x,y,w,h">
<template>...</template>
</div>
<!-- Custom property names -->
<div data-pool="sprites" data-key="id" data-pool-cull="50" data-pool-cull-props="posX,posY,width,height">
<template>...</template>
</div>
When only x,y are specified, the pool measures a template clone once at setup to determine default entity dimensions. When w,h are specified, entity size is read from each entity's properties. Use this when entities have varying sizes.
Data-based culling uses display: none for culled entities (removing them from the layout tree entirely), compared to the legacy path which uses visibility: hidden.
data-pool-cull. The data-pool-cull-props attribute must be used alongside data-pool-cull (the padding value). Without data-pool-cull, no culling occurs regardless of cull-props.
data-pool-static
Names an entity property that marks static entities. When this property is truthy on an entity, the pool skips per-frame binding evaluation after the initial add. The entity is rendered once and never re-evaluated. Static entities are also culled only when the camera/viewport changes, not every frame.
<div data-pool="world" data-key="id"
data-pool-cull="100" data-pool-cull-props="x,y"
data-pool-static="isStatic">
<template>...</template>
</div>
// Trees, rocks, flowers: rendered once, never re-evaluated
pool.add({ id: 1, x: 100, y: 200, emoji: 'ðģ', isStatic: true });
// Bees, butterflies: evaluated every frame
pool.add({ id: 2, x: 50, y: 75, emoji: 'ð', isStatic: false });
This is a major performance optimization for large worlds with many static background entities and fewer moving entities. The flush loop only iterates the dynamic entity array each frame. Static entities are stored in a separate array that is only processed when setCullBounds() reports a viewport change.
data-pool-sort
Automatically sets each entity's z-index based on a numeric entity property. This is essential for 2D games with depth sorting: entities lower on screen (higher Y value) should render on top of entities higher on screen.
<!-- Sort by Y position, ascending (lower Y = lower z-index = behind) -->
<div data-pool="sprites" data-key="id" data-pool-sort="y">
<template>
<div class="sprite" data-bind-style="{ left: x + 'px', top: y + 'px' }">
<img data-bind-attr="{ src: imgSrc }">
</div>
</template>
</div>
<!-- Sort descending (higher value = lower z-index = behind) -->
<div data-pool="layers" data-key="id" data-pool-sort="depth:desc">
...
</div>
| Syntax | Behavior |
|---|---|
data-pool-sort="y" | Ascending: z-index = Math.round(entity.y) |
data-pool-sort="y:desc" | Descending: z-index = -Math.round(entity.y) |
The sort property is read from each entity on every flush frame, so you can change an entity's depth dynamically by updating the property value.
Pool Definition
A pool is declared inside a component, store, or plugin's pools block. The definition object describes the pool's shape, defaults, and behavior; the framework creates the PoolHandle for you and exposes it as this.pools.name.
wildflower.component('game', {
pools: {
enemies: {
// Each pool has an optional `entity` block: the shape of the
// entities it holds. Same structure as components and stores:
// state, computed, methods.
entity: {
state: { hp: 100, maxHp: 100, eState: 'follow' },
computed: {
isDead() { return this.hp <= 0; },
hpPercent() { return Math.round((this.hp / this.maxHp) * 100); }
},
takeDamage(n) { this.hp -= n; }
},
// Pool-level shared state, visible to every entity's template as props.*
props: { paused: false },
// Pool lifecycle hooks. See below.
onAdd(entity) { /* ... */ },
onRemove(entity) { /* ... */ }
}
}
})
entity.state
Default fields merged into every new entity at push() time. The merge is shallow: spawn-provided values always win, and any field the spawn omits is filled in from the template.
// With the enemies pool above:
this.pools.enemies.push({ id: 1 });
// Entity: { id: 1, hp: 100, maxHp: 100, eState: 'follow' }
this.pools.enemies.push({ id: 2, hp: 250 });
// Entity: { id: 2, hp: 250, maxHp: 100, eState: 'follow' }
Nested objects in the template are shared by reference across entities. If you need per-entity nested objects, pass them at spawn time.
entity.computed
Functions declared in entity.computed become property getters on each entity, with this bound to the entity. Template bindings read computed names the same way they read plain properties.
entity: {
state: { hp: 100, maxHp: 100 },
computed: {
isDead() { return this.hp <= 0; },
hpPercent() { return Math.round((this.hp / this.maxHp) * 100); }
}
}
<template>
<div data-bind-class="isDead ? 'enemy dying' : 'enemy'">
HP: <span data-bind="hpPercent"></span>%
</div>
</template>
Computed properties recompute on every read. There is no cache. Entity mutations are visible to the next binding read with no invalidation step.
60 Ξs per entity per flush (a function call plus the expression body). Fine for event-driven pools (todos, dashboards, low-to-mid hundreds of entities on user actions). Not fine for hot animation loops at 60 fps with many entities: 800 entities à 60 fps à 60 Ξs is about 3 ms/frame. In those cases, prefer a plain data property that you assign at mutation time.
Entity methods
Any function property on the entity block that isn't state or computed is an entity method. Methods are installed on every entity at push() time; this is the entity itself.
entity: {
state: { hp: 100 },
// Method at top level of the entity block
takeDamage(n) { this.hp -= n; },
heal(n) { this.hp = Math.min(this.hp + n, this.maxHp); }
}
Methods are callable two ways:
// Directly on the entity
this.pools.enemies.get(1).takeDamage(25);
<!-- Via data-action inside the template, the clicked entity is `this` -->
<template>
<button data-action="takeDamage">Hit</button>
</template>
When both the component and an entity define a method with the same name, the entity's wins. Methods not defined on the entity fall back to the component method.
this bound to the entity at call time, so arrow functions are rejected at pool registration with a clear error. Use shorthand method syntax: takeDamage(n) { ... }, not takeDamage: (n) => { ... }.
this is the entity itself. The framework doesn't inject a pool handle, pool props, or a parent-component reference into that call site. Module-level values and globals like wildflower.getStore(...) remain reachable through ordinary JavaScript scope. See Entity scope in the guide for the full rule and the common split between entity-local behavior and component-level coordination.
props
Pool-level state shared across all entities in the pool. See the full reference at pool.props.
Lifecycle hooks
onAdd, onRemove, onClear, and onChange fire at the corresponding pool events. See the full reference at pool.onChange and Lifecycle Hooks.
JavaScript API
The PoolHandle exposed on this.pools.name provides the following methods and properties:
pool.push(obj) / pool.push(array)
pool.add is an alias for pool.push. Identical behavior and performance.
Add one or more entities to the pool. Each object must have the key property specified by data-key. Pass a single object for individual adds, or an array for bulk insertion via DocumentFragment (single DOM operation).
// Single add: returns the same object for chaining
const enemy = this.pool('enemies').add({
id: this.nextId++,
x: 100,
y: 0,
hp: 50,
maxHp: 50,
imgSrc: 'goblin.png',
className: 'enemy'
});
enemy.x += 10; // The returned object IS the entity
// Bulk add: array of objects, single DOM operation
this.pools.enemies.add([
{ id: 1, x: 0, y: 0, hp: 50, className: 'enemy' },
{ id: 2, x: 100, y: 0, hp: 50, className: 'enemy' },
{ id: 3, x: 200, y: 0, hp: 50, className: 'enemy' }
]);
When the first entity is added to any pool (or a component defines tick()), WildflowerJS automatically starts the shared rAF rendering loop. The template is cloned, initial bindings are applied, and the element is appended to the container. Bulk add uses a DocumentFragment so all elements are inserted in a single DOM operation, significantly faster for large collections.
pool.remove(key)
Remove an entity by its key value. Returns true if the entity was found and removed, false otherwise. The entity's DOM element is detached and recycled for reuse by future add() calls (see Entity Recycling).
// Remove enemy with id 42
const wasRemoved = this.pool('enemies').remove(42);
// Common pattern: remove from end of items array in reverse
const pool = this.pool('projectiles');
for (let i = pool.items.length - 1; i >= 0; i--) {
if (pool.items[i].offScreen) {
pool.remove(pool.items[i].id);
}
}
items array order may change when entities are removed. Do not rely on insertion order in pool.items after removals.
pool.update(key, props)
Patch an entity's properties by key. Uses Object.assign to merge the provided properties into the entity object. Returns the entity, or null if no entity with that key exists. For live pools, changes are reflected on the next rAF flush. For static pools (data-pool-static), bindings are applied synchronously; the DOM updates immediately.
// Move an entity
pool.update(enemy.id, { x: 100, y: 200 });
// Update multiple properties at once
pool.update(tower.id, {
level: tower.level + 1,
damage: tower.damage * 1.5,
imgSrc: getSprite('tower-upgraded')
});
// Returns null for nonexistent keys
const result = pool.update(999, { x: 0 }); // null
pool.get(key)
Retrieve an entity by its key. Returns the entity object, or undefined if no entity with that key exists.
const enemy = pool.get(42);
if (enemy) {
enemy.hp -= 10;
}
pool.setCullBounds(left, top, right, bottom)
Set custom cull bounds for data-based culling. Use this for pannable or zoomable worlds where entity coordinates do not map directly to screen position. The bounds define the visible region in entity coordinate space.
// In a pannable/zoomable game world:
function updateCamera() {
worldEl.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;
// Tell the pool what region of the world is visible
pool.setCullBounds(
-panX / zoom, // left edge in world coords
-panY / zoom, // top edge
(-panX + viewportW) / zoom, // right edge
(-panY + viewportH) / zoom // bottom edge
);
}
When setCullBounds is not called, the pool derives cull bounds from the container's parent element dimensions automatically. setCullBounds is only needed when a CSS transform (pan/zoom) makes the container larger than the visible area.
The pool tracks whether bounds have changed. Static entities (see data-pool-static) are only re-culled when the bounds actually change, not every frame.
pool.clear()
Remove all entities from the pool. This is a fast operation: it uses replaceChildren() to clear the DOM in one call, then resets the internal data structures.
// Reset the game: clear all entity pools
this.pool('enemies').clear();
this.pool('projectiles').clear();
this.pool('loots').clear();
When all pools across all components are empty after a clear() or remove(), and no components have tick() hooks, the shared rAF loop stops automatically to avoid wasting CPU.
pool.items
The raw array of entity objects. You can iterate this array, mutate entity properties, read values, or use array methods. Changes are reflected in the DOM on the next rAF flush.
// Iterate and mutate
for (const enemy of this.pool('enemies').items) {
enemy.x += enemy.speedX;
enemy.y += enemy.speedY;
}
// Find an entity
const boss = this.pool('enemies').items.find(e => e.type === 'boss');
// Check array length
if (this.pool('enemies').items.length === 0) {
this.nextWave();
}
pool.items is the pool's internal array. Do not reassign it (pool.items = [...]). To add and remove entities, use the add(), remove(), and clear() methods, which keep the DOM in sync.
pool.length
pool.size is an alias for pool.length. Identical behavior.
Returns the current number of entities in the pool. Always accurate, including mid-removal.
if (this.pool('enemies').size > 0) {
// Still enemies alive
}
// Use for HUD display
document.getElementById('enemy-count').textContent = this.pool('enemies').size;
pool.length and pool.size are plain JavaScript getters; they do not pass through a reactive proxy. A computed property that reads them will evaluate once on first eval (seeing the not-yet-populated pool, typically 0) and will NEVER re-evaluate when entities are added or removed. This is by design â pools intentionally bypass reactivity for performance.
Pattern that silently misbehaves:
// BAD â computed body never re-runs when the pool changes
wildflower.component('hud', {
computed: {
count() { return this.pool('enemies').length; }
}
});
Correct pattern â drive the count from explicit reactive state:
wildflower.component('hud', {
state: { count: 0 },
computed: {
label() { return this.state.count + ' enemies'; }
},
init() {
const p = this.pool('enemies');
p.add({ id: 1 });
this.state.count = p.length; // explicit mutation triggers cascade
}
});
For imperative HUD-style updates (the example above), pool.length is fine because you're writing to the DOM directly â there's no reactive observer expecting to be notified.
Array-like readers
Pools expose a read-only array surface directly on the handle. These delegate to the underlying items array; no wrapper allocation per call.
pool.at(i); // entity at DOM-order index i (undefined if out of range)
pool.find(fn); // first entity matching predicate, or undefined
pool.filter(fn); // new array of matching entities
pool.map(fn); // new array of mapped values
pool.forEach(fn); // iterate, no return value
pool.some(fn); // true if any match
pool.every(fn); // true if all match
pool.reduce(fn, initial); // fold over entities
for (const entity of pool) { ... } // pool is iterable
Use at(i) when you need positional access; use get(key) when you need key-based access. at reflects DOM order and is stable across removals. The iteration order of find, filter, map, forEach, some, every, reduce, and for...of follows the internal items array, which reshuffles on removal via swap-with-last. Order is not stable, but these methods don't depend on it.
splice, pop, indexOf, and slice are intentionally omitted. They only make sense on arrays with stable index positions, which pools don't provide (swap-with-last removal reshuffles the items array). Use remove(key) to delete and at(i) with DOM-order for positional reads.
pool.recycleSize
Returns the number of DOM nodes currently in the recycle free list, available for reuse by future add() calls. Read-only.
console.log(pool.recycleSize); // 0 initially
pool.add({ id: 1, name: 'A' });
pool.remove(1);
console.log(pool.recycleSize); // 1, DOM node available for reuse
pool.add({ id: 2, name: 'B' }); // Reuses recycled node
console.log(pool.recycleSize); // 0, node was consumed
pool.getElement(key)
Get the DOM element for a specific entity by its key value. Returns undefined if no entity with that key exists. Useful for attaching event listeners, measuring positions, or applying one-off DOM manipulations.
// Flash an enemy red when hit
const el = this.pool('enemies').getElement(enemy.id);
if (el) {
el.classList.add('hit-flash');
setTimeout(() => el.classList.remove('hit-flash'), 200);
}
// Measure an entity's screen position
const rect = this.pool('towers').getElement(tower.index)?.getBoundingClientRect();
Lifecycle Hooks
Declare hooks in the pools block to manage external resources tied to entity lifetime:
| Hook | Arguments | When |
|---|---|---|
onAdd |
(item) |
After entity added to pool |
onRemove |
(item) |
Before individual entity removed (not during clear()) |
onClear |
(items[]) |
Once before bulk clear() (skips onRemove) |
pools: {
balls: {
onAdd: 'createPhysicsBody', // string â component method
onRemove: 'destroyPhysicsBody', // string â component method
onClear: 'clearPhysicsWorld' // string â component method
}
}
// Or inline functions:
pools: {
dots: {
onAdd(item) { item.created = Date.now(); }
}
}
onClear is not defined, pool.clear() fires onRemove for each item individually. Define onClear when you need efficient bulk teardown without per-item side effects.
Lifecycle
Initialization
Pools are set up during component initialization. When the framework encounters a data-pool element, it:
- Extracts the
<template>and compiles its bindings into fast evaluator functions - Removes the
<template>from the DOM (same pattern asdata-list) - Creates a
PoolHandleregistered under the pool name - Reads optional attributes (
data-pool-fps,data-pool-cull,data-pool-sort)
The pool is ready to use in init(). Cache the handle for use in tick():
wildflower.component('my-game', {
init() {
this._pool = this.pool('enemies');
this._pool.add({ id: 1, x: 0, y: 0 });
},
// Called automatically each animation frame
tick(dt) {
for (const e of this._pool.items) {
e.x += e.vx * dt;
}
}
});
The tick(dt) Hook
Components with a tick method get called once per animation frame by the framework's shared rAF loop. No manual requestAnimationFrame setup or teardown is needed.
dt: milliseconds since the last frame (clamped to 250ms to prevent spiral-of-death after tab switches)now(second argument): theperformance.now()timestamp for this frametickruns BEFORE pool flush; entity mutations made in tick are visible in the DOM on the same frame- Works with or without pools, useful for canvas, Three.js, or any per-frame logic
- Automatically stops when the component is destroyed
Cleanup
Pools and tick hooks are automatically cleaned up when the owning component is destroyed. All entities are removed, DOM elements are cleared, the tick callback is unregistered, and the rAF loop stops if no other pools or tickable components remain active. You do not need to manually clean up pools or cancel animation frames.
Multiple Pools
A component can have any number of pools. Each pool has its own name, template, and configuration. All pools share the same rAF loop, with no overhead from having multiple pools.
<div data-component="game-world">
<div data-pool="background-particles" data-key="id" data-pool-fps="15">
<template><div class="bg-particle" data-bind-style="{ left: x + 'px', top: y + 'px', opacity: alpha }"></div></template>
</div>
<div data-pool="enemies" data-key="id" data-pool-sort="y" data-pool-cull="100">
<template>...</template>
</div>
<div data-pool="projectiles" data-key="id">
<template>...</template>
</div>
<div data-pool="damage-numbers" data-key="id" data-pool-fps="30">
<template><span class="dmg" data-bind="text" data-bind-style="{ left: x + 'px', top: y + 'px', opacity: alpha }"></span></template>
</div>
</div>
Typical game architecture uses separate pools for different entity types, each with appropriate performance settings. Background particles at 15fps, enemies with culling and depth sorting at 60fps, damage numbers at 30fps.
Event Handling
Pool templates do not support data-action on individual items. Instead, use data-pool-action on the container to delegate events to pool items:
data-pool-action
Adds delegated event listeners on the pool container. Supports multiple declarations separated by semicolons, with optional CSS selector targeting for different elements within a pool item.
Basic Usage
<div data-pool="enemies" data-key="id" data-pool-action="click:onEnemyClick">
<template>
<div data-bind-class="className"
data-bind-style="{ left: x + 'px', top: y + 'px' }">
<span data-bind="name"></span>
</div>
</template>
</div>
onEnemyClick(item, event) {
// item is the plain entity object from pool.items
item.hp -= 10;
if (item.hp <= 0) {
this.pool('enemies').remove(item.id);
}
}
Multiple Actions with Selectors
Use the extended syntax to dispatch to different methods based on which element was clicked within a pool item:
<div data-pool="tasks" data-key="id"
data-pool-action="click:.edit-btn:onEdit; click:.delete-btn:onDelete; mouseover:onHover">
<template>
<div class="task">
<span data-bind="title"></span>
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
</div>
</template>
</div>
onEdit(item, event) { /* clicked .edit-btn */ },
onDelete(item, event) { /* clicked .delete-btn */ },
onHover(item, event) { /* mouseover anywhere in item */ }
Format Reference
Each declaration follows one of three formats, separated by semicolons:
method: click (default event), any element in itemevent:method: specified event, any element in itemevent:.selector:method: specified event, only when click target matches CSS selector
When multiple declarations share the same event type, selector-specific handlers take priority over catch-all handlers. If a click matches .edit-btn, the catch-all handler is not called.
event.target up to the container's direct child to find the entity, then matches selectors within the entity's DOM subtree. Clicks on the container itself (not on an item) do not fire any handler.
pool.onChange
A callback property that fires synchronously on add(), remove(), and clear(). The pool's size is already updated when the callback fires.
init() {
const pool = this.pool('enemies');
pool.onChange = (p) => {
// Update a HUD counter when entities are added or removed
document.getElementById('enemy-count').textContent = p.size;
// Show/hide empty state
document.getElementById('no-enemies').style.display = p.size === 0 ? '' : 'none';
};
}
Set pool.onChange = null to remove the callback. There is no overhead when no callback is set.
pool.props
A plain object of shared data available to all pool item expressions via the props. prefix. Declared in the pools block and accessible at this.pools.name.props. Changes to props are picked up on the next rAF flush (or immediately for static pools via update()).
// Declare props in component definition
pools: {
boids: {
props: { shape: 'arrow', showLabels: true }
}
}
// Read/write at runtime
this.pools.boids.props.shape = 'circle'; // all items update on next flush
console.log(this.pools.boids.props.showLabels); // true
<!-- Use in template expressions with props. prefix -->
<div data-pool="boids" data-key="id">
<template>
<div data-bind-class="className + ' ' + props.shape"
data-bind-style="{ left: x + 'px', top: y + 'px' }">
<span data-bind="name" data-show="props.showLabels"></span>
</div>
</template>
</div>
Use props for values shared across all items: themes, palettes, locale settings, permission flags, display modes. This avoids stamping the same data onto every item object: one object for the entire pool, zero per-item memory cost.
Pools without a props declaration have an empty props object by default.