@deviltea/widget-vue
@deviltea/widget-vue
Cross-cutting Diagnostic/Result/Failure/Error conventions are maintained in Widget API conventions.
ESM-only package.
A thin Vue 3 integration over @deviltea/widget-core
Runtime semantics: a keyed renderer registry, a recursive WidgetRenderer root
component, and the useWidget(Plugin) bridge that lazily projects a
RuntimeWidget into Vue-native Refs and callables.
Installation
To install the package, use either npm, yarn, or pnpm:
npm install @deviltea/widget-vue @deviltea/widget-core vue
yarn add @deviltea/widget-vue @deviltea/widget-core vue
pnpm add @deviltea/widget-vue @deviltea/widget-core vue
vue (^3.5) is a peer dependency; @deviltea/widget-core is a regular
dependency because this package's own published types are built on top of it.
Usage
Register one Vue renderer component per plugin type, then mount the root
component with a WidgetSystemRuntime:
// renderer.ts
import { createWidgetVueRenderer } from '@deviltea/widget-vue'
import { SectionRenderer, TextInputRenderer, TextRenderer } from './renderers'
import { system } from './system'
export const WidgetRenderer = createWidgetVueRenderer(
system,
renderers =>
renderers
.Text(TextRenderer)
.TextInput(TextInputRenderer)
.Section(SectionRenderer),
)
<!-- App.vue -->
<script setup lang="ts">
import { WidgetRenderer } from './renderer'
import { runtime } from './runtime'
</script>
<template>
<WidgetRenderer :runtime="runtime" />
</template>
A renderer component receives no widget prop. It calls useWidget(Plugin),
which is both the compile-time type witness (deriving the exact
state/property/method/slot surfaces from the plugin's declared interfaces) and
a runtime assertion that the currently rendered widget belongs to that exact
plugin instance:
<!-- TextInputRenderer.vue -->
<script setup lang="ts">
import { useWidget } from '@deviltea/widget-vue'
import { TextInputPlugin } from './plugins/text-input'
const { useState, useProperties, useMethods, useStateDiagnostics } = useWidget(TextInputPlugin)
const { value } = useState()
const { charCount } = useProperties()
const { clear } = useMethods()
const { value: valueDiagnostics } = useStateDiagnostics()
</script>
<template>
<div>
<input v-model="value">
<span>{{ charCount }} characters</span>
<button @click="clear()">
Clear
</button>
<p v-if="valueDiagnostics.length > 0">
{{ valueDiagnostics[0].message }}
</p>
</div>
</template>
useState()returns writableRef<T | null>s. The setter delegates to the Runtime; on rejection the ref rolls back to the Runtime's own value (a rejectedv-modelcandidate is rolled back visually for free).useProperties()returns readonlyRef<T | null>s:ExecutionResultsuccess projects to its value, failure projects tonull— never a last-successful fallback.useMethods()returns stable callables shaped(...args: Parameters<Fn>) => ReturnType<Fn> | null. Semantic failure projects tonull; implementation-contract/disposed-runtime exceptions propagate unchanged.useStateDiagnostics()/usePropertyDiagnostics()/useMethodDiagnostics()mirror the corresponding Runtime primitive'sgetDiagnostics()/subscribeDiagnostics()as a separate reactive channel, keyed the same way as their value counterpart.emit(only when the plugin declaresevents) exposes onevoid-returning callable per declared event, typed from its arguments. Looking up a member is passive; the first call acquires the Core event emitter and forwards the arguments. An undeclared name yieldsundefined.widgetId/widgetTypeare always present, plain readonly values (not refs) identifying the current widget; the adapter stamps no DOM attributes from them.useDiagnostics()mirrorsRuntimeWidget.getDiagnostics()/subscribeDiagnostics()— this widget's own aggregate, not the Runtime-wide collected aggregate.- Every accessor is gated on the plugin's declared capabilities: an absent capability drops the accessor entirely; an explicitly-declared-empty capability keeps it present with an empty keyed surface.
- Everything is lazy: obtaining
useState()or accessing a member never reads or subscribes the Runtime by itself — only the first.valueread (or method call) does, and exactly one Runtime subscription is created per bridge member, cleaned up when the owning component unmounts.
Slots
A widget with a slots capability exposes WidgetSlot from useWidget(),
typed to its exact declared slot-name union:
<!-- SectionRenderer.vue -->
<script setup lang="ts">
import { useWidget } from '@deviltea/widget-vue'
import { SectionPlugin } from './plugins/section'
const { WidgetSlot } = useWidget(SectionPlugin)
</script>
<template>
<section>
<header>
<WidgetSlot name="header" />
</header>
<WidgetSlot name="content" />
</section>
</template>
WidgetSlot is one shared internal component identity across the whole
package — using it never allocates a new component definition — and it
renders the current widget's semantic slot children recursively, in
declaration order, with no filtering/sorting/fallback semantics of its own.
Root lifecycle
WidgetRenderer's only semantic prop is runtime. It validates
runtime.blueprint.system against the exact WidgetSystem instance
createWidgetVueRenderer was bound to (a mismatch throws
WidgetVueIntegrationError with code runtime-system-mismatch, a
programmer/configuration exception, never a Widget Diagnostic), fully unmounts and remounts its internal tree whenever the
runtime prop identity changes (even for a structurally identical root), and
never calls runtime.dispose() itself — Runtime lifetime stays owned by the
caller.
Server rendering
Server-side rendering with a long-lived or shared Runtime is unsupported
(a declared non-goal for now). useWidget() activates Core subscriptions
lazily on the first .value read and releases them only through Vue's
onScopeDispose; Vue's server renderer never stops component effect scopes, so
every server render would leave subscriptions registered on the Runtime until
runtime.dispose(), accumulating on each request. There is no SSR-specific
code path or hydration guarantee. Mount WidgetRenderer on the client only
(client-only components inside an SSR application are fine). See
Server rendering
in the full guide for the reasoning and open prerequisites.
License
Related Packages
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.0.6 | ^3.5.0 | MIT | |
| 0.0.5 | ^3.5.0 | MIT | |
| 0.0.4 | ^3.5.0 | MIT | |
| 0.0.3 | ^3.5.0 | MIT | |
| 0.0.2 | ^3.5.0 | MIT | |
| 0.0.1 | ^3.5.0 | MIT | |
| 0.0.0 | ^3.5.0 | MIT |