Vue and Nuxt
alphaTab has no Vue wrapper: a component creates the AlphaTabApi on a template ref when it is mounted and destroys it when it is unmounted. This page shows that component with Vite and with Nuxt.
npm install @coderline/alphatab
npm install --save-dev @coderline/alphatab-vite
Vue with Viteโ
Register the alphaTab plugin next to the Vue plugin in vite.config.ts:
import { alphaTab } from '@coderline/alphatab-vite';
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [vue(), alphaTab()]
});
Create the component in src/App.vue:
<script setup lang="ts">
import { AlphaTabApi, synth } from '@coderline/alphatab';
import { onMounted, onUnmounted, ref, shallowRef, useTemplateRef } from 'vue';
const scoreElement = useTemplateRef<HTMLDivElement>('score');
const api = shallowRef<AlphaTabApi>();
const playerReady = ref(false);
const playing = ref(false);
onMounted(() => {
const instance = new AlphaTabApi(scoreElement.value!, {
core: {
file: `${import.meta.env.BASE_URL}song.gp`,
fontDirectory: `${import.meta.env.BASE_URL}font/`
},
player: {
enablePlayer: true,
enableCursor: true,
soundFont: `${import.meta.env.BASE_URL}soundfont/sonivox.sf2`
}
});
instance.playerReady.on(() => (playerReady.value = true));
instance.playerStateChanged.on(e => (playing.value = e.state === synth.PlayerState.Playing));
api.value = instance;
});
onUnmounted(() => {
api.value?.destroy();
api.value = undefined;
});
</script>
<template>
<button type="button" :disabled="!playerReady" @click="api?.playPause()">
{{ playing ? 'Pause' : 'Play' }}
</button>
<div ref="score"></div>
</template>
shallowRef, notref.ref()makes its value deeply reactive. Vue would wrap the API and every object reached through it, such as the score model, the settings and the renderer, in proxies. That costs performance and lets alphaTab's internals run on proxies instead of its own objects. Only the UI state usesref().- Lifecycle. The template ref points to the element in
onMounted;onUnmounteddestroys the API. - Asset paths. Vite writes the scripts to
assets/, socore.fontDirectoryandplayer.soundFontpoint to the copies the plugin makes, prefixed withimport.meta.env.BASE_URL.
See Installation (Vite) for the plugin. Sample: vue-3-vite.
Nuxtโ
Nuxt builds with Vite, so it uses the same plugin. Nitro serves the font and the SoundFont, in nuxt.config.ts:
import { createRequire } from 'node:module';
import path from 'node:path';
import { alphaTab } from '@coderline/alphatab-vite';
const alphaTabDist = path.dirname(createRequire(import.meta.url).resolve('@coderline/alphatab'));
export default defineNuxtConfig({
vite: {
plugins: [alphaTab({ assetOutputDir: false })]
},
nitro: {
publicAssets: [
{ baseURL: 'font', dir: path.join(alphaTabDist, 'font'), maxAge: 60 * 60 * 24 * 7 },
{ baseURL: 'soundfont', dir: path.join(alphaTabDist, 'soundfont'), maxAge: 60 * 60 * 24 * 7 }
]
}
});
vite.pluginsregisters the plugin for the client build: page, workers and worklet share one core chunk.nitro.publicAssetsserves/font/and/soundfont/straight fromnode_modules/@coderline/alphatab/distin development and copies them into.output/publiconnuxt build. The plugin's own copy is disabled withassetOutputDir: false: Nuxt turns Vite'spublicDiroff, so the plugin has no default target. If you let the plugin copy intopublic/instead, note that Nitro picks uppublic/only if it exists when the build starts.
Create the component as a client-only component, app/components/ScoreViewer.client.vue (Nuxt 3: components/ScoreViewer.client.vue):
<script setup lang="ts">
import { AlphaTabApi, synth } from '@coderline/alphatab';
const { app } = useRuntimeConfig();
const scoreElement = useTemplateRef<HTMLDivElement>('score');
const api = shallowRef<AlphaTabApi>();
const playerReady = ref(false);
const playing = ref(false);
onMounted(async () => {
await nextTick();
if (!scoreElement.value) {
return;
}
const instance = new AlphaTabApi(scoreElement.value, {
core: {
file: `${app.baseURL}song.gp`,
fontDirectory: `${app.baseURL}font/`
},
player: {
enablePlayer: true,
enableCursor: true,
soundFont: `${app.baseURL}soundfont/sonivox.sf2`
}
});
instance.playerReady.on(() => (playerReady.value = true));
instance.playerStateChanged.on(e => (playing.value = e.state === synth.PlayerState.Playing));
api.value = instance;
});
onUnmounted(() => {
api.value?.destroy();
api.value = undefined;
});
</script>
<template>
<div>
<button type="button" :disabled="!playerReady" @click="api?.playPause()">
{{ playing ? 'Pause' : 'Play' }}
</button>
<div ref="score"></div>
</div>
</template>
Use it from app/app.vue:
<template>
<ScoreViewer />
</template>
.client.vue. Nuxt renders the component only in the browser and leaves it out of the server bundle, so alphaTab is neither imported nor run during server-side rendering.<ClientOnly>around a normal component also skips rendering on the server, but the server bundle still imports alphaTab.nextTick(). A.clientcomponent renders its template only after it is mounted, so the template ref is set one tick later.- Base URL. The paths are prefixed with
useRuntimeConfig().app.baseURLand follow a custom base URL such asNUXT_APP_BASE_URL=/sub/.
nuxt build warns that a chunk is larger than 500 kB; that is alphaTab's core, about 300 kB gzipped.