Vue

Step-by-step integration of the components into a Vue 3 or Nuxt application.

Vue 3 works with custom elements natively: it sets DOM properties, so objects and arrays need no wrapper. The one thing it must be told is which tags are custom elements, otherwise the compiler warns about unknown components.

1. Install

npm i tuix-webcomponents

2. Tell the compiler about the tags

Every tag starts with tuix-, so one rule covers the whole library.

vite.config.ts
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
 
export default defineConfig({
    plugins: [
        vue({
            template: {
                compilerOptions: {
                    isCustomElement: (tag) => tag.startsWith('tuix-'),
                },
            },
        }),
    ],
})

3. Register the elements

Import the library once, where the app is created:

src/main.ts
import { createApp } from 'vue'
import 'tuix-webcomponents'
import 'tuix-webcomponents/dist/assets/tuix-styles.css'
import App from './App.vue'
 
createApp(App).mount('#app')

The stylesheet also pulls Rubik in from the Tuix CDN.

In Nuxt, the import has to stay on the client, because the elements extend HTMLElement, which does not exist while the server renders:

plugins/tuix.client.ts
export default defineNuxtPlugin(async () => {
    await import('tuix-webcomponents')
})

The .client suffix is what keeps it out of the server bundle. Server-rendered markup still contains the tags; they upgrade when the plugin runs.

4. Use them

src/components/InvoiceForm.vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { DescriptionType, IconName, MainVariant } from 'tuix-webcomponents'
 
const countries = [
    { name: 'Switzerland', value: 'CH' },
    { name: 'Germany', value: 'DE' },
]
 
const name = ref('')
const country = ref<string>()
const error = ref<string>()
 
const selected = computed(() => countries.find((option) => option.value === country.value))
 
const onNameChange = (event: CustomEvent<{ value: string }>) => {
    name.value = event.detail.value
    error.value = undefined
}
 
const onSelect = (event: CustomEvent<{ value: string | number | boolean }>) =>
    (country.value = String(event.detail.value))
 
const save = () => {
    if (!name.value) return (error.value = 'A company name is required')
    console.log(name.value, country.value)
}
</script>
 
<template>
    <tuix-input
        title="Company name"
        :value="name"
        :iconRight="IconName.BUILDING"
        :description="error"
        :descriptionType="error ? DescriptionType.ERROR : DescriptionType.TEXT"
        @on-change="onNameChange"
    />
 
    <tuix-dropdown title="Country" :options="countries" :value="selected" @select="onSelect" />
 
    <tuix-button :variant="MainVariant.PRIMARY" :iconLeft="IconName.SAVE" @customClick="save">Save</tuix-button>
</template>

Properties vs attributes

With v-bind, Vue checks whether the name exists on the element and sets it as a property when it does. So bind with the property's own spelling:

<!-- property: works for strings, objects and arrays alike -->
<tuix-dropdown :options="countries" :iconRight="icon" />
 
<!-- static attribute: lowercase, as everywhere in HTML -->
<tuix-input iconright="search" descriptiontype="error" />

The .prop modifier forces it where necessary: :options.prop="countries".

Events

Every event binds with @ under its own name, camelCase ones included, once isCustomElement marks the tag as an element:

<tuix-input @on-change="onChange" @on-blur="onBlur" />
<tuix-dropdown @select="onSelect" />
<tuix-checkbox @change="onToggle" />
<tuix-table @next-page="onNextPage" @page-size-change="onPageSize" />
<tuix-button @customClick="save">Save</tuix-button>

@customClick works because Vue keeps the case of a listener name that contains a capital letter when the tag is an element rather than a component: "these may be custom elements' custom events", as the compiler puts it. Every other name is lowercase already, so nothing has to be spelled differently from the Events reference.

Do not reach for a dynamic name (@[clickEvent]) for this. It is the one form that breaks: a dynamic name compiles to onCustomClick, which Vue hyphenates to custom-click, and the handler then never runs. Bind @customClick directly.

A template ref works too, and reads better when several handlers are involved:

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
 
const button = ref<HTMLElement>()
const save = () => console.log('saved')
 
onMounted(() => button.value?.addEventListener('customClick', save))
onUnmounted(() => button.value?.removeEventListener('customClick', save))
</script>
 
<template>
    <tuix-button ref="button" variant="primary">Save</tuix-button>
</template>

v-model

v-model does not work on these elements, because they emit on-change instead of update:modelValue. The value plus the handler works, as does wrapping the element in a small Vue component that exposes modelValue:

src/components/TuixInputField.vue
<script setup lang="ts">
defineProps<{ modelValue: string; title?: string }>()
const emit = defineEmits<{ 'update:modelValue': [string] }>()
</script>
 
<template>
    <tuix-input :title="title" :value="modelValue" @on-change="emit('update:modelValue', $event.detail.value)" />
</template>
<TuixInputField v-model="name" title="Company name" />

Troubleshooting

SymptomCause
Failed to resolve component: tuix-buttonisCustomElement is not configured for the build tool in use
HTMLElement is not defined during a Nuxt buildThe library is imported on the server; move it into a .client plugin
A camelCase event never firesIt was bound with a dynamic name; bind @customClick directly
An array prop arrives as a stringIt was written as a static attribute instead of :bound
The field never changesThe value is not written back from the change handler

On this page