Decorators
Decorators are reusable behavior helpers that wrap a type's handlers without requiring you to hand-roll the same interception logic every time. They fit naturally into Inglorious Web's array-based composition model and are a good fit for concerns such as debouncing, cleanup, validation, or other cross-cutting behavior.
withDebounce
The built-in withDebounce decorator wraps selected handlers with per-entity debouncing. It is especially useful for handlers that should not fire repeatedly while a user is typing or interacting quickly.
import { withDebounce } from "@inglorious/web/decorators/with-debounce"
const types = {
AutosaveForm: [Form, withDebounce(500, ["saveData"])],
SearchBox: [Combobox, withDebounce({ optionsLoad: 300, saveQuery: 800 })],
}
2
3
4
5
6
How it works
- A delay can be provided for all wrapped handlers or per handler.
- Debouncing is scoped per entity so each entity keeps its own pending timer state.
- Pending work is cancelled when the entity is destroyed, avoiding stale callbacks.
When to use it
Use withDebounce for handlers such as:
- save operations
- search requests
- autosuggest lookups
- any other event that should coalesce rapid input
withThrottle
The built-in withThrottle decorator wraps selected handlers with per-entity throttling. Unlike debouncing (which delays and deduplicates), throttling ensures a handler runs at most once every delay milliseconds during rapid-fire events. By default, the first call in a burst runs immediately (leading edge). Optionally, you can enable hasTrailing to also invoke the handler once more after the delay with the final call's arguments, ensuring no state is lost.
import { withThrottle } from "@inglorious/web/decorators/with-throttle"
const types = {
// Leading-edge only (default)
ScrollTracker: [Base, withThrottle(200, ["scroll"])],
// Leading + trailing
ResizablePanel: [
Base,
withThrottle({ resize: 100 }, undefined, { hasTrailing: true }),
],
}
2
3
4
5
6
7
8
9
10
11
How it works
- A delay can be provided for all wrapped handlers or per handler.
- By default, the handler fires on the leading edge (first call immediately), and subsequent calls are throttled for the delay period.
- With
hasTrailing: true, the handler also fires once more after the delay with the most recent suppressed call's arguments. - Throttling is scoped per entity so each entity keeps its own throttle window.
- Pending work is cancelled when the entity is destroyed, avoiding stale callbacks.
When to use it
Use withThrottle for handlers such as:
- scroll or resize events (frequent, need responsive leading edge)
- mouse move tracking
- any high-frequency event that needs rate limiting while staying responsive
Use hasTrailing: true when you need to ensure the final state of a burst is never dropped (e.g., ending a resize or scroll and needing to capture the final position).
withErrorBoundary
The built-in withErrorBoundary decorator wraps a type's render in an error boundary. If the wrapped render throws, the caught error is handed to a fallback function and its template is rendered instead — so a single failing entity degrades to a fallback UI rather than taking down the whole app.
Because every entity is rendered through the same render, composing a boundary onto a type is the idiomatic way to isolate that subtree.
import { html } from "@inglorious/web"
import { withErrorBoundary } from "@inglorious/web/decorators/with-error-boundary"
const types = {
Chart: [
ChartBase,
withErrorBoundary((err) => html`<p>Chart failed: ${err.message}</p>`),
],
}
2
3
4
5
6
7
8
9
The fallback receives the caught error, the entity, and the render api, so it can render anything a normal template can — including a retry action:
withErrorBoundary(
(error, entity, api) => html`
<div class="chart-error">
<p>${entity.title ?? "This widget"} could not be displayed.</p>
<button @click=${() => api.notify(`#${entity.id}:reload`)}>Retry</button>
</div>
`,
)
2
3
4
5
6
7
8
If you omit the fallback, the decorator logs the error and renders nothing, which is still enough to keep the rest of the app alive.
How it works
- Composes after the base type so it wraps the base
render; every other handler on the type is left untouched. - Catches errors thrown synchronously during render and returns the fallback instead of letting them propagate.
- The wrap is deterministic, so the same error produces the same fallback during SSR and client hydration — it never introduces a hydration mismatch, and works out of the box with SSX.
When to use it
Use withErrorBoundary for any type whose render depends on data that might be malformed or incomplete:
- charts and data-visualization widgets
- third-party or user-provided content
- dashboard tiles where one broken widget shouldn't blank the page
Limitations
Like React and Svelte error boundaries, this only catches errors thrown during render. It does not catch:
- errors thrown in event handlers (they fire outside render)
- errors from async work / thunks (they run in the store phase, before render)
- errors thrown by lit-html directives committed after
renderreturns
Those belong to the store phase — handle them as entity.error state inside your handlers (see Error Handling). For a last-resort net around the root render, see the root-level guard.
Adding more decorators
As the collection grows, additional decorators can be added in the same style. Keep each decorator focused on a single concern, return a behavior object that mirrors the wrapped type's handlers, and let the composed type decide how to use it.
Inglorious Web