Expose
Expose specification
What Is Expose?
Section titled “What Is Expose?”Expose is the Component → App Maker information channel in Proto UI. It lets a component promise which values, states, methods, or outward signals a component instance provides to its caller.
The caller here is the App Maker, not the User. User perception belongs to Feedback; App Maker configuration belongs to Props; App Maker access to component-provided capabilities belongs to Expose.
For developers used to React or Vue, Proto UI Props + part of Expose roughly overlaps with the common mix of props, refs, and event callbacks. Proto UI intentionally splits the directions: Props is App Maker → Component, while Expose is Component → App Maker.
Expose is not just a ref API. A ref is a host-level carrier in some environments; Expose is a formal protocol channel. Adapters may carry it through refs, methods, CustomEvents, messages, host handles, or other mechanisms, but they must preserve the semantic direction.
Setup Declaration and Runtime Surfaces
Section titled “Setup Declaration and Runtime Surfaces”Expose registration is setup-only.
setup-onlydef.expose.value('version', '1.0.0');def.expose.method('focus', () => focusInternalTarget());
const open = def.state.bool('open', false);def.expose.state('open', open);
def.expose.event('change', { payload: 'json' });These APIs declare the component instance’s outward capability surface. They are not render output and are not runtime writes. After setup exits, the component cannot dynamically add, remove, or replace expose entries.
Runtime may use already-declared surfaces:
runtime-onlydef.lifecycle.onMounted((run) => { run.expose.emit('change', { open: true });});run.expose.emit only applies to outward signals declared through def.expose.event. It does not re-register an expose entry and is not a runtime API of the Event channel.
Exposed state may also reflect internal state changes during runtime, but that changes the current value of a state slot, not the structure of the expose registry.
Exposes Record and Key Namespace
Section titled “Exposes Record and Key Namespace”Each component instance has one exposes record. Every entry is identified by a non-empty string key:
def.expose.value('label', 'Submit');def.expose.method('reset', () => reset());All expose classifications share one key namespace. Once a key is used for a value, state, method, or outward signal, it cannot be reused for another expose entry category:
def.expose.value('ready', true);def.expose.event('ready'); // invalid: duplicate keyAdapters must let the App Maker access registered exposes by key or read the full exposes record. The returned record should not be a live mutable object that directly mutates the internal registry; modifying a snapshot must not change the component’s internal expose registry.
Four Expose Surfaces
Section titled “Four Expose Surfaces”Expose v0 classifies outward surfaces into four kinds.
| Surface | Prototype author API | App Maker-facing semantics |
|---|---|---|
| value | def.expose.value(key, value) | readable outward information |
| state | def.expose.state(key, handle) | external state handle for internal state |
| method | def.expose.method(key, fn) | component capability the App Maker may trigger |
| outward signal | def.expose.event(key, spec?) | signal emitted by the component during runtime |
Expose values fit constants, static configuration, internal descriptions, or readonly information that does not need change notification.
def.expose.value('version', '1.0.0');def.expose.value('features', { dismissible: true });v0 does not require every expose value to satisfy the JSON value boundary. If a component exposes a mutable object or host-local object, portability, subscription semantics, and reactive update semantics must not be assumed automatically.
If the App Maker needs to know when a value changes, the component should expose state, an outward signal, or an explicitly subscription-capable API instead of relying on observation of a mutable value.
Expose state is the intersection module between Expose and State. A component can expose internal state as an App-Maker-facing external state handle:
const open = def.state.bool('open', false);def.expose.state('open', open);The App Maker does not receive the raw state slot, nor a prototype-author-side owned, borrowed, or observed view. It receives an adapter or translation-layer-defined external view.
External state handles provide at least:
| API | Semantics |
|---|---|
get() | reads the current value |
subscribe(cb) | subscribes to StateEvent-like changes |
unsubscribe(off) | unsubscribes, or uses an equivalent mechanism |
spec | readonly StateSpec metadata |
External handles do not provide set, setDefault, or another App Maker write capability. The App Maker may read and subscribe, but cannot directly rewrite the component’s internal state slot.
Method
Section titled “Method”Expose methods represent component capabilities the App Maker may trigger, such as focus(), reset(), open(), or close().
def.expose.method('focus', () => { focusInternalTarget();});In JavaScript/Web hosts, methods commonly appear as callable functions. The contract does not require every host to implement methods as direct function references. Adapters may translate them into ref methods, message invocation, command handles, or other host-native calling surfaces, as long as they preserve the meaning of “App Maker triggers a component capability.”
Outward Signal
Section titled “Outward Signal”def.expose.event declares that the component may actively broadcast an outward signal to the App Maker:
def.expose.event('change', { payload: 'json' });
def.lifecycle.onMounted((run) => { run.expose.emit('change', { checked: true });});Here event is the current API name of the expose surface. It is not the User → Component Event channel. Event lets a component receive User interaction; expose event lets a component emit a signal to the App Maker.
v0 payload specs carry only minimal semantic hints:
| payload | Semantics |
|---|---|
void | the signal should not depend on a payload |
json | the payload has JSON-compatible intent, but complete validation is not stabilized |
any or omitted | no strong portable payload boundary is promised |
options is currently reserved for adapters or future extensions and should not be interpreted as a portable behavior guarantee.
Adapter Surface
Section titled “Adapter Surface”Expose carrier surfaces are chosen by adapters.
The Web Component adapter may provide a getExposes() snapshot; React/Vue adapters may use refs, component instances, callbacks, or framework-idiomatic surfaces; non-Web hosts may map exposes to messages, commands, or native handles.
Adapter freedom is about the carrier, not the semantic direction. An adapter must not reinterpret Expose as User feedback, Props input, Event callbacks, or component-to-component Context. It also must not let the App Maker mutate the internal expose registry by mutating a returned record.
Lifecycle
Section titled “Lifecycle”Expose entries are bound to the component instance lifecycle.
While the instance is alive, registered expose entries must be accessible through adapter surfaces. After unmount/dispose, exposes must be cleared, invalidated, or made distinguishably unavailable.
This also applies to more specific expose surfaces:
- after dispose, reading the expose registry must not keep returning the old valid registry;
- old snapshots must not be interpreted as the current live instance exposes record;
- expose-state external handles must become invalid and clear external subscriptions;
- expose-event emit is valid only while the instance is alive.
Contract Preview
Section titled “Contract Preview”Expose test mapping is split into the base domain, outward signals, and the State intersection module:
| Test entity | Main coverage |
|---|---|
T-EXPOSE-0001 | channel identity, setup-only registration, shared key namespace, record snapshot, value/method, lifecycle |
T-EXPOSE-EVENT-0001 | expose event declaration, runtime emit, unregistered key failure, payload spec |
T-EXPOSE-STATE-0001 | external state handle shape, same-source updates, read-only boundary, lifecycle safety |
These tests keep Expose from collapsing into “just get a ref.” They make the protocol boundaries executable: who can access it, when it is declared, how keys conflict, what each surface means, and how it becomes unavailable after lifecycle disposal.
Relationship to Other Specifications
Section titled “Relationship to Other Specifications”Coredefines information channels, setup/runtime, and component/prototype foundations; Expose is the Component → App Maker channel.Propsis App Maker → Component configuration input; Expose is the opposite direction: component capability promises.Feedbackis Component → User; Expose does not carry User-perceivable presentation.Eventis User → Component;expose.eventkeeps the existing API name, but semantically represents a Component → App Maker outward signal.Statedefines internal state machines;Expose-stateprojects internal state into an App-Maker-facing readonly external state handle.Lifecycledefines invalidation rules for expose registries, external handles, subscriptions, and emit after dispose.