Vue adapter
@textbus/adapter-vue renders Component block views with Vue 3 defineComponent + setup (or equivalent), and together with BrowserModule wires up the browser. The examples below match the VueAdapter + BrowserModule + Textbus wiring order from Getting started, written for current 5.x (e.g. new RootComponent({ slot }) only passes initial data; fromJSON uses Registry.createSlot).
Component / Slot DOM query APIs: Adapter.
For a full project layout and build setup, see textbus/vue-demo.
Dependencies
npm install @textbus/adapter-vueYou also need vue@3, @textbus/core, @textbus/platform-browser, reflect-metadata, and a bundler that can resolve @viewfly/core (used internally by the adapter for ReflectiveInjector).
View conventions
- Block views are
.vueSFCs: bindrootRefon the outer real DOM (:ref="rootRef"), render the slot subtree with<component :is="slotRender()" />; insideslotRender,inject(AdapterInjectToken)then calladapter.slotRender. In the factory, wrapchildrenwithcreateVNodefrom@textbus/core(same as the Viewfly adapter). Do not wrapchildrenwithh. importpaths below are relative (replace with your@/alias if configured).
VueAdapter + BrowserModule + Textbus
The mount passed to new VueAdapter runs only when editor.render(...) runs, so you can provide Textbus and VueAdapter inside mount—by then editor is assigned (see let editor below). The adapter referenced in the mount closure is fully constructed on the first render.
Recommended order: tokens.ts (InjectionKey) → inject(AdapterInjectToken) in block views → let editor → const adapter = new VueAdapter(...) → inside mount, createApp(root).provide(..., editor).provide(..., adapter) → new BrowserModule → editor = new Textbus(...) → void editor.render(root model).
The example splits into two models, two .vue block views, tokens, and a main entry; keep them in one directory and use the tabs to browse.
import 'reflect-metadata'
import { createApp } from 'vue'
import { BrowserModule } from '@textbus/platform-browser'
import { VueAdapter } from '@textbus/adapter-vue'
import { ContentType, Slot, Textbus } from '@textbus/core'
import { AdapterInjectToken, TextbusInjectToken } from './tokens'
import { ParagraphComponent } from './components/paragraph.component'
import ParagraphView from './components/paragraph.view.vue'
import { RootComponent } from './components/root.component'
import RootView from './components/root.view.vue'
let editor!: Textbus
const adapter = new VueAdapter(
{
[RootComponent.componentName]: RootView as any,
[ParagraphComponent.componentName]: ParagraphView as any,
},
(host, root) => {
const app = createApp(root)
.provide(TextbusInjectToken, editor)
.provide(AdapterInjectToken, adapter)
app.mount(host)
return () => app.unmount()
}
)
const browserModule = new BrowserModule({
adapter,
renderTo() {
return document.getElementById('editor') as HTMLElement
},
})
editor = new Textbus({
imports: [browserModule],
components: [RootComponent, ParagraphComponent],
})
const rootModel = new RootComponent({
slot: new Slot([ContentType.BlockComponent]),
})
void editor.render(rootModel)import { InjectionKey } from 'vue'
import { Textbus } from '@textbus/core'
import { VueAdapter } from '@textbus/adapter-vue'
export const TextbusInjectToken: InjectionKey<Textbus> = Symbol('Textbus')
export const AdapterInjectToken: InjectionKey<VueAdapter> = Symbol('Adapter')import {
Commander,
Component,
type ComponentStateLiteral,
ContentType,
onBreak,
Registry,
Selection,
Slot,
Textbus,
useContext,
useSelf,
} from '@textbus/core'
export interface ParagraphComponentState {
slot: Slot
}
export class ParagraphComponent extends Component<ParagraphComponentState> {
static componentName = 'ParagraphComponent'
static type = ContentType.BlockComponent
static fromJSON(textbus: Textbus, state: ComponentStateLiteral<ParagraphComponentState>) {
const slot = textbus.get(Registry).createSlot(state.slot)
return new ParagraphComponent({ slot })
}
override getSlots(): Slot[] {
return [this.state.slot]
}
override setup() {
const commander = useContext(Commander)
const selection = useContext(Selection)
const self = useSelf()
onBreak(ev => {
ev.preventDefault()
const nextContent = ev.target.cut(ev.data.index)
const p = new ParagraphComponent({ slot: nextContent })
commander.insertAfter(p, self)
selection.setPosition(nextContent, 0)
})
}
}import {
Component,
type ComponentStateLiteral,
ContentType,
onContentInsert,
Registry,
Selection,
Slot,
Textbus,
useContext,
} from '@textbus/core'
import { ParagraphComponent } from './paragraph.component'
export interface RootComponentState {
slot: Slot
}
export class RootComponent extends Component<RootComponentState> {
static componentName = 'RootComponent'
static type = ContentType.BlockComponent
static fromJSON(textbus: Textbus, state: ComponentStateLiteral<RootComponentState>) {
const slot = textbus.get(Registry).createSlot(state.slot)
return new RootComponent({ slot })
}
override getSlots(): Slot[] {
return [this.state.slot]
}
override setup() {
const selection = useContext(Selection)
onContentInsert(ev => {
if (typeof ev.data.content === 'string' || ev.data.content.type !== ContentType.BlockComponent) {
const slot = new Slot([ContentType.Text])
const p = new ParagraphComponent({ slot })
slot.insert(ev.data.content)
ev.target.insert(p)
selection.setPosition(slot, slot.index)
ev.preventDefault()
}
})
}
}<template>
<div :ref="rootRef" data-component="paragraph">
<component :is="slotRender()" />
</div>
</template>
<script lang="ts" setup>
import { inject, defineProps } from 'vue'
import { ViewComponentProps } from '@textbus/adapter-vue'
import { createVNode } from '@textbus/core'
import { AdapterInjectToken } from '../tokens'
import { ParagraphComponent } from './paragraph.component'
const props = defineProps<ViewComponentProps<ParagraphComponent>>()
const adapter = inject(AdapterInjectToken)!
function slotRender() {
const slot = props.component.state.slot
return adapter.slotRender(slot, children => {
return createVNode('p', null, children)
})
}
</script><template>
<div data-component="root" :ref="rootRef">
<component :is="slotRender()" />
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue'
import { ViewComponentProps } from '@textbus/adapter-vue'
import { createVNode } from '@textbus/core'
import { AdapterInjectToken } from '../tokens'
import { RootComponent } from './root.component'
export default defineComponent({
props: ['component', 'rootRef'],
setup(props: ViewComponentProps<RootComponent>) {
const adapter = inject(AdapterInjectToken)!
return {
slotRender() {
const slot = props.component.state.slot
return adapter.slotRender(slot, children => {
return createVNode('div', null, children)
})
},
}
},
})
</script>renderTo must return the outer page container (e.g. #editor); do not return adapter.host. On page teardown call editor.destroy() so the teardown returned from mount runs (app.unmount()).
slotRender and provide / inject
AdapterInjectToken:provide(AdapterInjectToken, adapter)inmount, theninject(AdapterInjectToken)!in block viewsetupbefore callingslotRender—no module-levelrefor a predeclaredlet adapter.TextbusInjectToken: lets youinject(TextbusInjectToken)in the Vue subtree for toolbars, status panels, etc.;Selection,Commander, and other kernel services are still accessed fromComponent.setup()viauseContext(@textbus/core).
ViewMount third argument
The current ViewMount signature is (host, root, context) where context is an Injector. The sample ignores the third parameter; to expose kernel Injector services to the Vue subtree, provide custom **InjectionKey**s in mount with values from context.get(...) (types per @textbus/adapter-vue and @textbus/core exports).
