Dyrecteddyrected
Integrations

Vue 3 Integration

Use Dyrected in a standard Vue 3 application with composables, live preview, and the Admin UI bridge.

The @dyrected/vue package provides Vue 3 composables, a React-in-Vue Admin bridge, and live preview support for any Vite-based or framework-agnostic Vue 3 app. If you are using Nuxt, use @dyrected/nuxt instead — it re-exports everything from this package and adds Nuxt-specific glue.


Installation

pnpm add @dyrected/vue @dyrected/sdk

Embedding the Admin UI

@dyrected/vue ships a <DyrectedAdmin /> component that mounts the React-based Admin UI into your Vue app using a React-in-Vue bridge:

<!-- src/pages/Admin.vue -->
<script setup lang="ts">
import { DyrectedAdmin } from '@dyrected/vue'
</script>

<template>
  <DyrectedAdmin
    base-url="https://api.dyrected.cloud"
    api-key="sk_live_..."
  />
</template>

Props

Prop

Type

Passing Vue components as custom field inputs

You can inject Vue components as custom admin field inputs. The bridge wraps them automatically so they render inside the React-based Admin UI.

Register the key in your collection schema

In your dyrected.config.ts, add admin.component to any field. The value is a string key that maps to the component you provide to <DyrectedAdmin>:

// dyrected.config.ts
import { defineConfig, defineCollection } from '@dyrected/core'

export default defineConfig({
  collections: [
    defineCollection({
      slug: 'products',
      fields: [
        { name: 'title', label: 'Title', type: 'text' },
        {
          name: 'brandColor',
          label: 'Brand Color',
          type: 'text',
          admin: {
            component: 'products.brandColor',  // unique key — matches the prop below
            description: 'Brand hex color, e.g. #6366f1',
          },
        },
      ],
    }),
  ],
})

Write your Vue component

The bridge passes the following props to your component:

Prop

Type

<!-- components/BrandColorPicker.vue -->
<script setup lang="ts">
const props = defineProps<{
  value: string | null
  onChange: (v: string) => void
  disabled: boolean
}>()
</script>

<template>
  <div class="color-picker-wrapper">
    <input
      type="color"
      :value="value ?? '#000000'"
      :disabled="disabled"
      @input="(e) => onChange((e.target as HTMLInputElement).value)"
    />
    <span>{{ value ?? 'None selected' }}</span>
  </div>
</template>

Pass the component to <DyrectedAdmin>

<!-- pages/admin.vue -->
<script setup lang="ts">
import { DyrectedAdmin } from '@dyrected/vue'
import BrandColorPicker from '~/components/BrandColorPicker.vue'
</script>

<template>
  <ClientOnly>
    <DyrectedAdmin
      :components="{
        fields: {
          'products.brandColor': BrandColorPicker,
        }
      }"
    />
  </ClientOnly>
</template>

The key ('products.brandColor') must exactly match the admin.component value in your schema.

Virtual fields (computed/read-only display)

You can combine a custom component with an afterRead hook to display a virtual/computed field that is never stored in the database:

// dyrected.config.ts
{
  name: 'fullName',
  type: 'text',
  admin: {
    component: 'users.fullName',
    readOnly: true,
    description: 'Automatically computed from first and last name.',
  },
  access: { update: false },   // prevent API writes
  hooks: {
    afterRead: [
      ({ value, doc }) => `${doc.firstName ?? ''} ${doc.lastName ?? ''}`.trim() || value,
    ],
  },
}

copyable — Copy-to-clipboard fields

Add a copy button to any read-only field by wrapping value in your component:

<!-- components/CopyableField.vue -->
<script setup lang="ts">
const props = defineProps<{ value: string | null }>()
const copied = ref(false)

async function copy() {
  if (!props.value) return
  await navigator.clipboard.writeText(props.value)
  copied.value = true
  setTimeout(() => (copied.value = false), 2000)
}
</script>

<template>
  <div class="copyable-field">
    <span>{{ value ?? '—' }}</span>
    <button type="button" @click="copy">
      {{ copied ? '✓ Copied' : 'Copy' }}
    </button>
  </div>
</template>

Then in your schema:

{
  name: 'apiKey',
  type: 'text',
  admin: {
    component: 'users.apiKey',
    readOnly: true,
  },
  access: { update: false },
}

Advanced: Custom Admin Components in Vue

When building rich custom field components in Vue 3 that run inside the React-based Admin UI:

  1. How the Bridge Works: The React-in-Vue bridge mounts your Vue component dynamically inside the React render tree. Properties (value, onChange, field, etc.) are converted to reactive Vue props automatically.
  2. Accessing the Document ID: Since the document id is read-only metadata (not a registered form field), it is not present in standard form watch values. You can read the current document ID in two ways:
    • Access the prop context.siblingData.id (which Dyrected injects automatically on Edit forms).
    • If the component runs on an older version or outside form context, you can parse it from window.location.hash as a fallback.
  3. Reacting to Router Navigation: Because the Admin UI runs on React Router, Vue's computed() properties or route watchers (like Nuxt/Vue Router) will not trigger when the URL changes. If your custom component needs to react to URL changes, listen to the browser hashchange event directly:
    import { onMounted, onUnmounted } from 'vue'
    
    onMounted(() => {
      window.addEventListener('hashchange', handleHashChange)
    })
    onUnmounted(() => {
      window.removeEventListener('hashchange', handleHashChange)
    })
  4. Bridge Context Properties: The context prop passed to your custom Vue component contains useful metadata:
    • context.user: The currently authenticated administrator user.
    • context.schemas: The loaded Dyrected collection configuration schemas.
    • context.siblingData: Object containing current sibling field values in the form, including the document id.

Fetching Content — useDyrected

<script setup lang="ts">
import { useDyrected } from '@dyrected/vue'

const { doc, pending, error } = useDyrected('pages', 'home')
</script>

<template>
  <div v-if="pending">Loading...</div>
  <div v-else-if="error">{{ error.message }}</div>
  <div v-else>
    <h1>{{ doc?.title }}</h1>
  </div>
</template>

Live Preview — useLivePreview

The useLivePreview composable subscribes to postMessage events from the Dyrected Admin UI and updates your component data reactively.

<!-- pages/PostPreview.vue -->
<script setup lang="ts">
import { useLivePreview } from '@dyrected/vue'

const props = defineProps<{ initialPost: Post }>()

const { data: post, isLive } = useLivePreview({
  initialData: props.initialPost,
  serverURL: import.meta.env.VITE_CMS_ADMIN_URL,
})
</script>

<template>
  <div v-if="isLive" class="preview-banner">Preview Mode</div>
  <h1>{{ post?.title }}</h1>
</template>

Composable API:

const { data, isLive } = useLivePreview<T>({
  initialData: T,      // Shown until the first postMessage arrives
  serverURL?: string,  // Admin origin for origin validation (defaults to '*')
})

Prop

Type

Inline editing support

Add data-dy-path attributes to your template elements to enable click-to-field navigation in the Admin's Edit Mode:

<template>
  <article>
    <h1 data-dy-path="title">{{ post?.title }}</h1>
    <p data-dy-path="excerpt">{{ post?.excerpt }}</p>
    <img :src="post?.coverImage?.url" data-dy-path="coverImage" />
  </article>
</template>

When the editor activates Edit Mode in the preview toolbar, clicking any marked element sends the field path back to the Admin, which scrolls to and focuses the corresponding form input. The useLivePreview composable wires up all the hover and click listeners automatically.


Rendering components

@dyrected/vue ships ready-to-use components for the most common content types, so you don't have to hand-render media or icons.

<DyrectedImage>

A plain-<img> wrapper that accepts a Dyrected Media object or a URL string and handles width, height, and alt automatically.

<script setup lang="ts">
import { DyrectedImage } from '@dyrected/vue'
</script>

<template>
  <DyrectedImage :media="post.coverImage" :width="800" :height="400" />
</template>

<DyrectedMedia>

Renders the right element based on media type — YouTube embed, image, video, or a download link fallback.

<script setup lang="ts">
import { DyrectedMedia } from '@dyrected/vue'
</script>

<template>
  <DyrectedMedia :media="post.featuredMedia" :width="800" :height="450" />
</template>

<DyrectedIcon>

The icon field type stores the name of a Lucide icon (e.g. "ChartNoAxesCombined"), not SVG markup. <DyrectedIcon> resolves that name to the matching icon so you can render an icon field value directly.

<script setup lang="ts">
import { DyrectedIcon } from '@dyrected/vue'
</script>

<template>
  <!-- feature.icon === "ChartNoAxesCombined" -->
  <DyrectedIcon :name="feature.icon" class="w-6 h-6 text-primary" />
</template>

Standard Lucide attributes (size, color, stroke-width, class, …) fall through to the underlying icon.

Prop

Type

Browse valid icon names at lucide.dev/icons. The Admin UI's icon-picker field uses the same set, so any value an editor can choose will resolve here. In Nuxt these render components are auto-imported (<DyrectedIcon>, <DyrectedMedia>, <DyrectedBlocks>) — no import needed.

<Blocks>

Renders an array of blocks by blockType, scoping each item's data-dy-path for click-to-edit. See Building a page builder.


Reference — everything @dyrected/vue exports

ExportKindPurpose
DyrectedAdminComponentEmbeds the React-based Admin UI via the React-in-Vue bridge.
DyrectedImageComponent<img> wrapper for a Media object or URL.
DyrectedMediaComponentRenders image / video / YouTube / download by media type.
DyrectedIconComponentRenders an icon field value as a Lucide icon.
BlocksComponentRenders an array of blocks by blockType to your block components.
DyPathScopeComponentScopes a data-dy-path base for nested live-preview editing.
useDyrectedComposableFetch a document / collection / global.
useLivePreviewComposableSubscribe to Admin preview messages + inline editing.
useDyrectedAuthComposableAdmin authentication state + actions.
useDyPathComposableRead the current data-dy-path scope.

Package Structure

packages/vue/
├── src/
│   ├── components/
│   │   ├── DyrectedAdmin.vue      # React-in-Vue bridge component
│   │   ├── DyrectedImage.vue      # <img> wrapper for Media
│   │   ├── DyrectedMedia.vue      # Type-aware media renderer
│   │   ├── DyrectedIcon.vue       # Lucide icon renderer for `icon` fields
│   │   ├── Blocks.ts              # Block-array renderer
│   │   └── DyPathScope.ts         # data-dy-path scoping
│   ├── composables/
│   │   ├── useDyrected.ts         # Content fetching
│   │   └── useLivePreview.ts      # Live preview + inline editing
│   ├── bridge/
│   │   └── react-in-vue.ts        # React root mounting + prop sync
│   └── index.ts

On this page