Summary#
A control's contract has always run one way: props in, events out. An app can hand a control values and be told when something happened, but it has no way to name something the control DOES.
A commands { } block is the other direction. It declares the verbs the control publishes — SelectAll,
ExportCsv(string filename) — as a typed, checked part of its contract rather than a method a caller has to know
about from documentation.
A control declares its verbs, the generated typings require the shim to implement them, and the platform refuses a shim that does not. An app reaches them two ways: from content it writes inside one of the control's own chrome slots, and by handing a verb back as a value — an entry in a list the control renders. Both name a verb; neither hands the app a callable.
The block is optional, and a control that declares none is unaffected.
Signature#
control <Name> {
commands {
<CommandName>;
<CommandName>(<Type> <param>, …);
}
}Description#
Declaring the verbs a control publishes#
control DataGrid {
contractVersion "1.1"
participation headless
props { string title; }
events { rowPicked(int index); }
commands {
/// Select every row.
SelectAll;
ExportCsv(string filename);
}
}Events and commands are the same idea pointing opposite ways, and the generated typings read that way — one typed
emit overload per event on the host, one typed method per command on the handle:
export interface DataGridHost {
emit(event: "rowPicked", index: number): void; // OUT — the platform provides it
}
export interface DataGridHandle extends ControlHandle<DataGridProps> {
commands: { // IN — the shim implements it
selectAll(): void;
exportCsv(filename: string): void;
};
}A command is declared PascalCase (a name written in Osy#) and implemented camelCase (a JavaScript method). Both sides derive that mapping from the declared name, so they cannot drift.
A command and an event cannot share a name: one name would mean both "tell the app this happened" and "ask the control to do this".
The shim must implement them#
Commands land on what mount RETURNS, not on the host, because they are things the control provides:
export function mount(el, props, host) {
return {
update(next) { … },
destroy() { … },
commands: {
selectAll() { … },
exportCsv(filename) { … },
},
};
}The platform checks them when it takes the handle — at the mount that produced it, exactly as it checks update
and destroy. A verb a control publishes and never writes fails there, loudly, naming the missing method. The
alternative is that it fails the first time anyone asks for it, which is a user, in a browser, a long way from the
code at fault.
Publishing a verb is a decision#
A command is not a way for an app to reach into a control. It is a named, typed verb the control chose to make part of its contract, so what an app can ask for is exactly what the control decided to publish — and it can be versioned like anything else in the block.
Prefer a prop for state (readOnly, density) and a command for an action that has no resting value.
"Selected" is a state; "select everything, now" is not.
Invoking a command — a chrome slot#
A control declares a slots { } block naming regions of its own interface that an app fills. The content of such
a slot is handed the control's commands, so the app writes the chrome and the control keeps the behaviour behind
it:
control MarkdownEditor {
contractVersion "1.1"
participation headless
props { string title; }
commands { FindNext; FindPrev; ReplaceAll(string text); }
slots { FindBar; }
}
[Page("/doc")] [AllowAnonymous]
component DocPage() {
action Close() { }
render {
MarkdownEditor(title: "Doc") {
slot FindBar { c =>
Row {
Button("Previous", onPress: c.FindPrev);
Button("Next", onPress: c.FindNext);
Button("Replace all", onPress: () => c.ReplaceAll("draft"));
Button("Done", onPress: Close);
}
}
}
}
}Values ride as props, verbs as commands. A search box has a resting value, so it is state the app owns and hands
in (findQuery:); "replace every match, now" has no resting value, so it is a command. That split is what lets a
whole find bar — boxes, count, buttons, and whether it is open at all — be the app's, while the matching and the
document edits stay the control's. The count comes back as an ordinary event.
c is the control's commands. Naming them is the point: the app's own actions stay reachable in the same content
(onClick: Close), and which of the two a name means is never in doubt. Had the commands simply been in scope
unqualified, one could shadow a page action — and the failure would be a button that works and does the wrong thing.
c.FindNxt is a compile error listing the commands the control does declare. A command with parameters cannot be
named bare — there would be nothing to supply them — so it is CALLED instead, in the form every event handler already
uses: onClick: () => c.ReplaceAll("draft"), as above. The arguments are evaluated where they are written, when the
content is built.
Where it renders is the control's decision. The shim asks for the slot and positions it; an app cannot put content somewhere the control did not offer, and a shim that ignores a slot simply shows nothing.
What an app never gets is the callable itself. It names a verb; the platform is what turns that name into a call on the control. So a slot's content can reach exactly the set of verbs the control published, and nothing else.
Invoking a command — a verb as a VALUE#
A chrome slot covers app content that CALLS the control. The other direction is a verb the app hands back — an entry in a list the control renders itself. That is what a menu wants: the control owns the menu's look and its keyboard model, and the app supplies what should be in it.
A control declares its item shape as an ordinary class, with an Action-typed member for the verb:
class MenuEntry {
public string Label;
public Action Run;
}
control MarkdownEditor {
contractVersion "1.1"
participation headless
props {
string title;
MenuEntry[] extraItems = [];
}
commands {
DeleteBlock;
TurnIntoHeading(int level);
}
}
[Page("/doc")] [AllowAnonymous]
component DocPage() {
action Archive() { }
render {
MarkdownEditor(title: "Doc", extraItems: [
new MenuEntry { Label = "Archive", Run = Archive },
new MenuEntry { Label = "Delete block", Run = MarkdownEditor.DeleteBlock },
new MenuEntry { Label = "Make it a heading", Run = () => MarkdownEditor.TurnIntoHeading(2) }
]);
}
}One member takes either kind of verb, and that is the point. A real menu is a mix: some entries run the app's own code, some run the control's. Split into separate props and every app has to reassemble one ordered list from two — and the order between them is lost.
Action is the type, the same one a component parameter uses for a callback (component X(Action onPress)). It
is always nullary; arguments are bound at the call site.
The four spellings, and nothing else:
| Written | Runs |
|---|---|
Run = Archive | an action/method/callback parameter of this component, taking no arguments |
Run = () => Archive(doc.Id) | the same, with arguments — evaluated where they are written |
Run = MarkdownEditor.DeleteBlock | a command of the control being called |
Run = () => MarkdownEditor.TurnIntoHeading(2) | the same, with arguments |
The qualifier must be the control being called: naming another control's verb is a compile error, because this entry is handed to this control and nothing else can run it. A verb that does not exist, a parameterised one named bare, the wrong number of arguments, and a plain value where a verb belongs are each refused with the working spelling named.
An Action member is only settable in a control's props at a call site. It holds a verb, not a value: there is no
storage for it, no wire form to the server, and away from a call site nothing that could run it. For the same reason
it is legal only on a class — a persisted entity refuses it.
The shim receives a plain function. Its generated typings say Run: () => void, and that is the whole contract:
it cannot tell an app action from one of its own commands, which is exactly why one member takes both.
Reaching a control from ANYWHERE else#
Not yet possible — both forms above put the app's name for a verb inside the control's own call. An app action calling a mounted instance from somewhere else entirely is a separate design (it needs instance naming and a "not mounted yet" answer), and it is deliberately unbuilt until something real cannot be expressed without it. The shim-side contract does not change if it lands.
What NOT to do meanwhile is fake it with a prop that means "go" — an incrementing counter the control watches for changes. It reads as state, it is not, and it breaks the moment two callers want the verb in one render.
Examples#
A media player, where the split between props and commands is the clearest:
control Player {
contractVersion "1.1"
participation opaque
props {
string src;
bool loop = false;
}
events { ended(bool natural); }
commands {
Play;
Pause;
SeekTo(int seconds);
}
}loop is a resting value the app owns, so it is a prop. Play has no resting value — asking twice means asking
twice — so it is a command.
See also#
- control — foreign UI controls (charts, grids, maps) — the
controlblock these are declared in - styles — a control's own look knobs — the other opt-in rung: a control's own look knobs
- probe — what a control says about itself — the other opt-in rung a shim implements on its handle: what the control says about itself
- Slot (child content) — slots in general, including a component's own