Skip to content

Vue integration

j3w1 edited this page Sep 9, 2026 · 1 revision

Vue 3 JavaScript integration

Use the official Custom Elements directly from Vue SFCs. You do not need a separate Vue component implementation. This guide assumes an existing Vue 3 + Vite JavaScript app; Quick start covers installing the pinned release archive.

Configure the Vue compiler

Merge this setting into your existing vite.config.js, keeping unrelated plugins and configuration:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue({
    template: { compilerOptions: {
      isCustomElement: tag => tag.startsWith('j3w1-'),
    } },
  })],
});

The setting belongs in the SFC compiler configuration. Adding it only to a runtime app instance does not configure already compiled SFCs.

A working settings form

Use this as src/App.vue in a small example app. It adapts the maintained text-field and button markup, adds an application-owned status select, and uses the supported enhancement so the select opens in j3w1 colors. The save action only displays local form data.

<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/text-field.css';
import '@j3w1/ui/styles/button.css';
import '@j3w1/ui/styles/controls.css';
import '@j3w1/ui/register/text-field';
import '@j3w1/ui/register/button';
import { enhanceControls } from '@j3w1/ui/enhance/choice';

const root = ref(null);
const displayName = ref('Atlas');
const status = ref('ready');
const saved = ref('');
let controls;
onMounted(() => { controls = enhanceControls(root.value); });
onBeforeUnmount(() => controls?.destroy());

function save(event) {
  const data = new FormData(event.currentTarget);
  saved.value = JSON.stringify(Object.fromEntries(data), null, 2);
}
function reset() {
  displayName.value = 'Atlas';
  status.value = 'ready';
  saved.value = '';
}
</script>

<template>
  <main ref="root" class="settings-example">
    <h1>Project settings</h1>
    <p>Local example; no data is sent to a server.</p>
    <form @submit.prevent="save" @reset.prevent="reset">
      <j3w1-text-field id="settings-name">
        <div class="text-field">
          <label class="text-field-label" for="settings-name-input">
            Project name
            <span class="text-field-required" aria-hidden="true">*</span>
            <span class="text-field-required sr-only"> required</span>
          </label>
          <div class="text-field-root">
            <input id="settings-name-input" class="text-field-input"
              v-model="displayName" name="projectName" type="text" required
              aria-describedby="settings-name-help settings-name-message">
            <span class="text-field-loading" aria-hidden="true">⋯</span>
          </div>
          <p id="settings-name-help" class="text-field-help">Shown in your workspace.</p>
          <p id="settings-name-message" class="text-field-message">Enter a project name.</p>
        </div>
      </j3w1-text-field>

      <label for="settings-status">Status</label>
      <select id="settings-status" v-model="status" name="status">
        <option value="ready">Ready</option>
        <option value="review">Review</option>
      </select>

      <j3w1-button id="settings-save">
        <button class="button" type="submit">
          <span class="button-loading" aria-hidden="true">⋯</span>
          <span class="button-label">Save settings</span>
        </button>
      </j3w1-button>
      <button type="reset">Reset example</button>
    </form>
    <pre aria-live="polite">{{ saved }}</pre>
  </main>
</template>

<style>
body { margin: 0; background: var(--color-surface-canvas); }
.settings-example { max-width: 42rem; margin-inline: auto; padding: var(--space-16); }
.settings-example form { display: grid; gap: var(--space-12); }
.settings-example h1 { color: var(--color-text-prose); }
.settings-example pre { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>

Your usual Vue browser entry still mounts App:

import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');

The wrapper does not implement Vue's modelValue convention. In this example, Vue owns the native input/select values. The reset handler updates that Vue state explicitly; it is not an example of submitting data to a backend.

Properties, events and refs

Need Vue pattern
Native input value v-model on the native input/select
Object/array custom-element property :data.prop="series", using the component's declared data shape
Documented custom event @j3w1-change="onChange"; read event.detail
Imperative API Element ref, after mount, then the documented method
Replaced native child structure After nextTick(), call the component's refresh()
Existing raw controls Enhance an explicit root after mount; destroy on unmount

Do not assume every component emits the same event or accepts data. Read its generated API first. chart.data, for example, is an array of { label, value } with finite non-negative values and a documented row limit.

Use stable keys when Vue renders lists. Avoid giving both Vue and a component ownership of reordering the same children. Do not let a second enhancement root independently manage the same controls.

Before shipping

Test required validation, actual FormData, reset, dynamic values/options, unmount/remount and narrow layout. For a dialog test Escape and focus return; for choices test the visible themed menu with mouse and keyboard. Keep unique IDs across repeated instances.

The full Vue demo shows larger compositions. Its source is a package consumer, not a backend template. See Portal and demo for its boundaries.

Source: official consumption guide.

Clone this wiki locally