Vue Integration
Using the Notations library in Vue applications
This guide shows how to create Vue components for rendering Carnatic music notation.
Installation
npm install notations
Basic NotationViewer Component
Create a reusable Vue component (Composition API):
Basic NotationViewer Component
<!-- NotationViewer.vue -->
<template>
<div ref="container" :class="className"></div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import * as N from 'notations';
const props = defineProps({
source: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
});
const emit = defineEmits(['error']);
const container = ref(null);
let view = null;
function renderNotation() {
if (!container.value) return;
if (!view) {
const table = document.createElement('table');
table.classList.add('notation-table');
container.value.appendChild(table);
view = new N.Carnatic.NotationView(table);
}
const [notation, beatLayout, errors] = N.load(props.source);
if (errors.length === 0) {
view.tableElement.innerHTML = '';
view.renderNotation(notation, beatLayout);
} else {
emit('error', errors);
}
}
onMounted(() => {
renderNotation();
});
watch(() => props.source, () => {
renderNotation();
});
</script>
Component with Source Display and Editing
A more complete component that shows source code and allows editing:
Editable NotationBlock Component
<!-- NotationBlock.vue -->
<template>
<div class="notation-block">
<div v-if="showSource" class="notation-source">
<div v-if="caption" class="notation-caption">{{ caption }}</div>
<template v-if="isEditing">
<textarea
v-model="editedSource"
class="notation-textarea"
></textarea>
<div class="notation-actions">
<button @click="applyEdit">Apply</button>
<button @click="cancelEdit">Cancel</button>
</div>
</template>
<template v-else>
<pre><code>{{ currentSource }}</code></pre>
<div class="notation-actions">
<button @click="copyToClipboard">Copy</button>
<button @click="startEdit">Edit</button>
</div>
</template>
</div>
<div class="notation-output">
<strong>Output:</strong>
<div ref="container"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import * as N from 'notations';
const props = defineProps({
source: { type: String, required: true },
caption: { type: String, default: '' },
showSource: { type: Boolean, default: false }
});
const container = ref(null);
const isEditing = ref(false);
const currentSource = ref(props.source);
const editedSource = ref(props.source);
let view = null;
function renderNotation() {
if (!container.value) return;
if (!view) {
const table = document.createElement('table');
table.classList.add('notation-table');
container.value.appendChild(table);
view = new N.Carnatic.NotationView(table);
}
const [notation, beatLayout, errors] = N.load(currentSource.value);
if (errors.length === 0) {
view.tableElement.innerHTML = '';
view.renderNotation(notation, beatLayout);
}
}
function startEdit() {
editedSource.value = currentSource.value;
isEditing.value = true;
}
function applyEdit() {
currentSource.value = editedSource.value;
isEditing.value = false;
}
function cancelEdit() {
editedSource.value = currentSource.value;
isEditing.value = false;
}
async function copyToClipboard() {
await navigator.clipboard.writeText(currentSource.value);
}
onMounted(() => renderNotation());
watch(currentSource, () => renderNotation());
</script>
Options API Version
<!-- NotationViewer.vue (Options API) -->
<template>
<div ref="container" :class="className"></div>
</template>
<script>
import * as N from 'notations';
export default {
name: 'NotationViewer',
props: {
source: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
emits: ['error'],
data() {
return {
view: null
};
},
mounted() {
this.initView();
this.renderNotation();
},
watch: {
source() {
this.renderNotation();
}
},
methods: {
initView() {
const table = document.createElement('table');
table.classList.add('notation-table');
this.$refs.container.appendChild(table);
this.view = new N.Carnatic.NotationView(table);
},
renderNotation() {
const [notation, beatLayout, errors] = N.load(this.source);
if (errors.length === 0) {
this.view.tableElement.innerHTML = '';
this.view.renderNotation(notation, beatLayout);
} else {
this.$emit('error', errors);
}
}
}
};
</script>
TypeScript with Vue 3
<!-- NotationViewer.vue -->
<template>
<div ref="container" :class="className"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import * as N from 'notations';
interface Props {
source: string;
className?: string;
}
const props = withDefaults(defineProps<Props>(), {
className: ''
});
const emit = defineEmits<{
(e: 'error', errors: N.ParseError[]): void;
}>();
const container = ref<HTMLDivElement | null>(null);
let view: N.Carnatic.NotationView | null = null;
function renderNotation(): void {
if (!container.value) return;
if (!view) {
const table = document.createElement('table');
table.classList.add('notation-table');
container.value.appendChild(table);
view = new N.Carnatic.NotationView(table);
}
const [notation, beatLayout, errors] = N.load(props.source);
if (errors.length === 0) {
view.tableElement.innerHTML = '';
view.renderNotation(notation, beatLayout);
} else {
emit('error', errors);
}
}
onMounted(() => renderNotation());
watch(() => props.source, () => renderNotation());
</script>
Error Handling
<template>
<div>
<NotationViewer :source="source" @error="handleErrors" />
<div v-if="errors.length" class="error-display">
<h4>Parsing Errors:</h4>
<ul>
<li v-for="(err, i) in errors" :key="i">
Line {{ err.line }}, Col {{ err.column }}: {{ err.message }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import NotationViewer from './NotationViewer.vue';
const source = ref('...');
const errors = ref([]);
function handleErrors(errs) {
errors.value = errs;
}
</script>