I am currently working on a fairly simple Vue-3 app, but experiencing an issue I’m not sure how to find the root cause of. In my app I have a view which is being rendered via the router. This view has some simple state (an object ref for storing the values of some text fields, and a boolean ref for toggling a modal on and off), and also makes use of a single state object from a pinia store.
The very first time I load the view I can edit the text fields which are bound to the object ref with ease, but the very first time I attempt to open the modal by clicking a button two things happen: The text fields are cleared, and the modal fails to open. After the button has been clicked the very first time, everything works the way I expect, and I can open and close the modal without disrupting the state of the view.
I have attempted adding watchers to all of the internal state refs so I can see if the values are changing, but I am not seeing those watchers triggered when the text fields are cleared after the first button click, and I’m at a loss for figuring out what might be causing this reactivity to occur.
For context, here is the code of the view (TemplateEditorView.vue):
<script setup>
import TemplateSelectorComponent from '../components/TemplateSelectorComponent.vue'
import useTemplate from '../composables/useTemplate'
import { computed, ref, watch } from 'vue'
import AddSectionModal from '../components/AddSectionModal.vue'
import { useTemplateStore } from '../stores/template'
import { storeToRefs } from 'pinia'
const templateStore = useTemplateStore()
const { templates } = storeToRefs(templateStore)
const { selectedTemplate, selectedTemplateKey, updateSelectedTemplateKey } = useTemplate()
const newTemplate = ref({
id: '',
name: '',
templateText: '',
sections: []
})
const showModal = ref(false)
const handleModalSubmit = function (section) {
newTemplate.value.sections.push(section)
showModal.value = false
}
const handleSave = function () {
if (saveEnabled.value) {
templates.value[newTemplate.value.id] = newTemplate.value
clearForm()
}
}
function clearForm() {
newTemplate.value = {
id: '',
name: '',
templateText: '',
sections: []
}
}
const handleDelete = function () {
if (deleteEnabled.value) {
delete templates.value[selectedTemplateKey.value]
clearForm()
}
}
const deleteEnabled = computed(() => {
return Object.hasOwn(templates.value, selectedTemplateKey.value)
})
const saveEnabled = computed(() => {
return newTemplate.value.id && newTemplate.value.name && newTemplate.value.templateText
})
watch(selectedTemplate, async () => {
newTemplate.value = selectedTemplate.value
})
</script>
<template>
<div id="template-editor-view-content" class="row p-1 pt-4">
<div class="col">
<div class="row">
<div class="col"><h1>Template Editor</h1></div>
</div>
<div class="row">
<div id="options-column" class="col-4 primary-bordered vh-85">
<div class="row pt-3">
<div class="col"><h2 class="text-center">Template Options</h2></div>
</div>
<TemplateSelectorComponent @selected-template-changed="updateSelectedTemplateKey" />
<div class="row">
<div class="col">
<h3 class="text-center">Global Variables</h3>
</div>
</div>
<div class="row">
<div class="col">
<div id="globalVarsAccordion" class="accordion">
<div class="accordion-item">
<h2 id="companyNameAccordionHeader" class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#companyNameAccordionBody"
aria-expanded="false"
aria-controls="companyNameAccordionBody"
>
Company Name
</button>
</h2>
<div
id="companyNameAccordionBody"
class="accordion-collapse collapse"
aria-labelledby="companyNameAccordionHeader"
data-bs-parent="#globalVarsAccordion"
>
<div class="accordion-body">
<ul class="list-group list-group-horizontal">
<li class="list-group-item">isSelected</li>
<li v-pre class="list-group-item">{{ companyName.isSelected }}</li>
</ul>
<ul class="pt-2 list-group list-group-horizontal">
<li class="list-group-item">value</li>
<li v-pre class="list-group-item">{{ companyName.value }}</li>
</ul>
</div>
</div>
</div>
<div class="accordion-item">
<h2 id="jobTitleAccordionHeader" class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#jobTitleAccordionBody"
aria-expanded="false"
aria-controls="jobTitleAccordionBody"
>
Job Title
</button>
</h2>
<div
id="jobTitleAccordionBody"
class="accordion-collapse collapse"
aria-labelledby="jobTitleAccordionHeader"
data-bs-parent="#globalVarsAccordion"
>
<div class="accordion-body">
<ul class="list-group list-group-horizontal">
<li class="list-group-item">isSelected</li>
<li v-pre class="list-group-item">{{ jobTitle.isSelected }}</li>
</ul>
<ul class="pt-2 list-group list-group-horizontal">
<li class="list-group-item">value</li>
<li v-pre class="list-group-item">{{ jobTitle.value }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<template v-if="newTemplate.sections">
<div class="row">
<div class="col">
<h3 class="text-center">Template Variables</h3>
</div>
</div>
<div class="row">
<div class="col">
<div id="templateVarsAccordion" class="accordion">
<div
v-for="section in newTemplate.sections"
:key="section.key"
class="accordion-item"
>
<h2 id="`${section.key}AccordionHeader`" class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#`${section.key}AccordionBody`"
aria-expanded="false"
aria-controls="`${section.key}AccordionBody`"
>
{{ section.label }}
</button>
</h2>
<div
id="`${section.key}AccordionBody`"
class="accordion-collapse collapse"
aria-labelledby="`${section.key}AccordionHeader`"
data-bs-parent="#templateVarsAccordion"
>
<div class="accordion-body">
<ul class="list-group list-group-horizontal">
<li class="list-group-item">isSelected</li>
<li class="list-group-item">
<span v-pre>{{</span> {{ section.key }}.isSelected <span v-pre>}}</span>
</li>
</ul>
<ul class="pt-2 list-group list-group-horizontal">
<li class="list-group-item">value</li>
<li class="list-group-item">
<span v-pre>{{</span> {{ section.key }}.value <span v-pre>}}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
<div id="working-area" class="col vh-85">
<form class="row gx-3 align-items-center pb-3 pt-2">
<div class="col-auto">
<label for="templateKeyInput" class="col-form-label-lg">Template ID</label>
</div>
<div class="col">
<input
id="templateKeyInput"
v-model="newTemplate.id"
type="text"
class="form-control form-control-lg"
/>
</div>
<div class="col-auto">
<label for="templateNameInput" class="col-form-label-lg">Template Name</label>
</div>
<div class="col">
<input
id="templateNameInput"
v-model="newTemplate.name"
type="text"
class="form-control form-control-lg"
/>
</div>
</form>
<form class="row text-center justify-content-center">
<div class="col-2">
<button class="btn btn-secondary" @click="showModal = true">Add Section</button>
</div>
<div class="col-2">
<button class="btn btn-success" :disabled="!saveEnabled" @click="handleSave">
Save Template
</button>
</div>
<div class="col-2">
<button class="btn btn-danger" :disabled="!deleteEnabled" @click="handleDelete">
Delete Template
</button>
</div>
</form>
<div class="row pt-3">
<div class="col">
<textarea
id="templateTextArea"
v-model="newTemplate.templateText"
class="form-control"
></textarea>
</div>
</div>
</div>
</div>
</div>
<AddSectionModal :show="showModal" @close="showModal = false" @submit="handleModalSubmit" />
</div>
</template>
<style scoped>
// Some css
</style>
The modal which is being created is defined as follows (AddSectionModal.vue):
<script setup>
import GenericModal from './GenericModal.vue'
import { computed, ref } from 'vue'
defineProps({
show: Boolean
})
const emit = defineEmits(['close', 'submit'])
const section = ref({
key: '',
label: '',
text: '',
isSelected: false
})
function clearState() {
section.value = {
key: '',
label: '',
text: '',
isSelected: false
}
}
const submitEnabled = computed(() => {
return section.value.key && section.value.text && section.value.label
})
const handleClose = function () {
clearState()
emit('close')
}
const handleSubmit = function () {
if (submitEnabled.value) {
const value = section.value
clearState()
emit('submit', value)
}
}
</script>
<template>
<Teleport to="body">
<GenericModal :show="show">
<template #header>
<h5 class="mx-auto">Add New Section</h5>
</template>
<template #body>
<div class="container">
<div class="row gx-3 align-items-center">
<div class="col-3">
<label for="sectionKey" class="col-form-label-lg">Section Key</label>
</div>
<div class="col">
<input
v-model="section.key"
name="sectionKey"
type="text"
class="form-control form-control-lg"
/>
</div>
</div>
<div class="row pt-2 gx-3 align-items-center">
<div class="col-3">
<label for="sectionLabel" class="col-form-label-lg">Section Label</label>
</div>
<div class="col">
<input
v-model="section.label"
name="sectionLabel"
type="text"
class="form-control form-control-lg"
/>
</div>
</div>
<div class="row pt-2 gx-3 align-items-center">
<div class="col-3">
<label for="sectionText" class="col-form-label-lg">Section Text</label>
</div>
<div class="col">
<input
v-model="section.text"
name="sectionText"
type="text"
class="form-control form-control-lg"
/>
</div>
</div>
</div>
</template>
<template #footer>
<div class="row gx-1 float-end">
<div class="col">
<button class="btn btn-danger" @click="handleClose">Cancel</button>
</div>
<div class="col">
<button class="btn btn-success" :disabled="!submitEnabled" @click="handleSubmit">
Submit
</button>
</div>
</div>
</template>
</GenericModal>
</Teleport>
</template>
And the GenericModal implementation is as follows (GenericModal.vue):
<template>
<Transition name="modal">
<div v-if="show" class="modal-mask">
<div class="modal-container">
<div class="modal-header">
<slot name="header">default header</slot>
</div>
<div class="modal-body">
<slot name="body">default body</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" @click="$emit('close')">OK</button>
</slot>
</div>
</div>
</div>
</Transition>
</template>
<style>
// Some CSS
</style>
What strategies could I use to track down the source of the unexpected reactivity?