Skip to content

Core declarations: actions, records, regions, state ​

Pling exposes a page to a model through four kinds of declarations. They answer different questions and are deliberately not interchangeable:

DeclarationQuestion it answersWrite path
ActionWhat can be done here?Yes
RecordWhich object is this, and what does it contain?No
RegionWhere is this, and is it currently reachable?No
StateWhat view is currently in effect?No

Only actions write. Records, regions, and state describe; they never become a second execution channel.

Actions ​

An action is a stable id, a description written for the model, a risk kind, and an execution route. The route is either a DOM primitive (click, fill, select, press, hover), an injected run, or a handoff that asks the user to complete the step.

FieldRequiredDefaultNotes
idRecommendedGenerated once for element bindingsStable ids are mandatory for list rows and task continuity
descYes—The model's only explanation of the operation
kindNo"write""read", "write", or "danger"
paramsNo—Array shorthand or JSON Schema; array params are required unless optional: true
runNoDOM primitiveReceives validated, unmasked arguments
availableNo—false or a string makes the action unavailable; the string is shown to the model
expectNo—Waits for a result; failure becomes EXPECTATION_FAILED
confirmNoDerived from the policytrue or "always"
handoffNo—The user completes the step; run is not executed
action / target / autoTarget / fidelityNoInferredControl how a DOM primitive resolves its target
ts
const submit = { id: "add_task", desc: "Add a task from the composer", kind: "write" };
tsx
const submit = usePlingTool<HTMLButtonElement>({
  id: "add_task",
  desc: "Add a task from the composer",
  kind: "write",
});
ts
const off = pling.tool(document.querySelector("#add")!, {
  id: "add_task",
  desc: "Add a task from the composer",
  kind: "write",
});
ts
// <button v-tool="{ id: 'add_task', desc: 'Add a task from the composer' }">
// The directive accepts the same ToolOptions shape.

Declaring an action does not guarantee it is visible. Disabled elements, covered elements, inactive regions, and failed available() checks remove it from the model's action list and attach a reason. This is intentional: “not registered” and “temporarily unavailable” are different states and should remain distinguishable.

Records ​

A record gives a row an identity and a bounded set of fields. The identity must come from the business object, not from its position in the array.

ts
// <li v-for="task in tasks" :key="task.id" v-record="{ data: task, label: task.title }">
tsx
const row = usePlingRecord({ data: task, label: task.title });
return <li ref={row}>{task.title}</li>;
ts
const off = pling.record(rowElement, { data: task, label: task.title });
ts
// <li v-for="task in tasks" :key="task.id" v-record="{ data: task, label: task.title }">

Defaults:

  • id: data.id, then data.key;
  • fields: the data object without the id field;
  • scope: the nearest enclosing region;
  • order: page order when the adapter can determine it.

Records are read-only. A row action is still an action and still goes through availability, policy, execution, and expectation checks. Do not try to encode “mark complete” as a record field change.

Regions ​

A region gives the page a small structure tree. It also participates in visibility and reachability.

FieldRequiredNotes
idYesStable, language-independent
labelNoHuman-readable name shown to the model
descriptionNoWhat the region is for
roleNomain, dialog, toolbar, list, and the other RegionRole values
parentNoExplicit parent when DOM ancestry is not the right tree
ts
// <section v-scope="{ id: 'todo_list', label: 'Task list', role: 'list' }">
tsx
const listScope = usePlingScope<HTMLUListElement>({
  id: "todo_list",
  label: "Task list",
  role: "list",
});
return <ul ref={listScope}>…</ul>;
ts
const off = pling.scope(listElement, { id: "todo_list", label: "Task list", role: "list" });
ts
// <ul v-scope="{ id: 'todo_list', label: 'Task list', role: 'list' }">

A region is not a permission boundary. It reduces and explains what the model can reach at this moment. Authorization still belongs to the action implementation or the server.

State ​

A state view is a named, read-only object that explains the current projection: active filter, applied search, counts, pagination, or the selected tab. It has no setter.

ts
useAgentState("task_view", () => ({
  filter: filter.value,
  search: appliedSearch.value,
  matched: visibleTasks.value.length,
  total: tasks.value.length,
}), { description: "Current filter, applied search, and task counts" });
tsx
usePlingState("task_view", () => ({
  filter,
  search: appliedSearch,
  matched: visibleTasks.length,
  total: tasks.length,
}), { description: "Current filter, applied search, and task counts" });
ts
const off = pling.state("task_view", () => ({
  filter,
  search: appliedSearch,
  matched: visibleTasks.length,
  total: tasks.length,
}), { description: "Current filter, applied search, and task counts" });
ts
useAgentState("task_view", () => ({
  filter: this.filter,
  search: this.appliedSearch,
  matched: this.visibleTasks.length,
  total: this.tasks.length,
}));

The reader runs for every snapshot. A plain object is evaluated once at registration and will freeze the initial values. Expose the view the user is seeing, not the whole store.

How the four declarations compose ​

A reliable page usually needs all four in the same region:

  • the region identifies the list and controls whether it is reachable;
  • records identify the rows and provide the data needed to choose one;
  • actions describe the operations available on the list or a row;
  • state explains filters and counts that are not visible from the rows alone.

This is the difference between “the model can click a button” and “the model can understand the current screen before choosing an action”.

Failure modes ​

SymptomCauseFix
The model invents an action idThe action was never registered, or its id changes between rendersDeclare it explicitly and use a stable id
A row action targets the wrong rowThe id came from the array indexUse the business key
A count is correct once and then staleA plain object captured setup-time valuesRead through a function
A dialog action remains available behind the overlayThe dialog has no region, or the overlay is not detectablePut the dialog in a dialog region and verify hit testing
The model tries to write through stateThe corresponding action is missing or poorly describedAdd the action the user would use; keep state read-only
Two rows share an action idThe id lacks the row keyUse toggle@${task.id} or a namespaced id

Version and compatibility ​

ItemValue
Applies to@plingjs/core 0.1.0-alpha.2
Introduced0.1.0-alpha.2 (first published alpha)
RuntimeBrowsers with ES2022 modules; Node.js 20 or later for Node-side use
Peer dependenciesNone
StabilityPre-1.0 alpha; a minor release may change the API

All four declarations are present in each adapter at this version. The adapter table below is the compatibility boundary; a change to a declaration's meaning would be a breaking change, not a documentation adjustment.

AdapterActionsRecordsRegionsState
Vue 3SupportedSupportedSupportedSupported
ReactSupportedSupportedSupportedSupported
Vanilla / jQuerySupportedSupportedSupportedSupported
Vue 2.7SupportedSupportedSupportedSupported

The status differs by adapter. The support tables in the Vue 3 adapter, React adapter, Vanilla / jQuery adapter, and Vue 2.7 adapter references define the exact boundary for each framework. Vue 2.7 is a real adapter, but only for the subset listed on its page.

Where to continue ​