@deviltea/widget-vue

Cross-cutting Diagnostic/Result/Failure/Error conventions are maintained in Widget API conventions.
1,156
Install
npm install @deviltea/widget-vue
Latest Version:0.0.6
Vue:3
License:MIT
Last Updated:Oct 21, 2026
Links: GitHub  ·  npm
Maintainer: deviltea

@deviltea/widget-vue

Cross-cutting Diagnostic/Result/Failure/Error conventions are maintained in Widget API conventions.

ESM-only package.

npm version npm downloads bundle License

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 writable Ref<T | null>s. The setter delegates to the Runtime; on rejection the ref rolls back to the Runtime's own value (a rejected v-model candidate is rolled back visually for free).
  • useProperties() returns readonly Ref<T | null>s: ExecutionResult success projects to its value, failure projects to null — never a last-successful fallback.
  • useMethods() returns stable callables shaped (...args: Parameters<Fn>) => ReturnType<Fn> | null. Semantic failure projects to null; implementation-contract/disposed-runtime exceptions propagate unchanged.
  • useStateDiagnostics() / usePropertyDiagnostics() / useMethodDiagnostics() mirror the corresponding Runtime primitive's getDiagnostics()/subscribeDiagnostics() as a separate reactive channel, keyed the same way as their value counterpart.
  • emit (only when the plugin declares events) exposes one void-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 yields undefined.
  • widgetId / widgetType are always present, plain readonly values (not refs) identifying the current widget; the adapter stamps no DOM attributes from them.
  • useDiagnostics() mirrors RuntimeWidget.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 .value read (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

MIT License © 2023-PRESENT DevilTea

Related Packages

@vue-hooks-form/core

Vue Composition API for validating form.

2,489 12
vue-composable-function-tester

A test utility for testing vue composable functions

936 1
pai-build-widget

A flyrise build widget project

765
@vue/apollo-composable

Apollo GraphQL for Vue Composition API

6,282,241 6,025
@astrojs/vue

Use Vue components within Astro

4,883,748 63,526

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