Get reactive data from dynamic component using Vue3's composition API
I'm using Laravel 9, InertiaJS and Vue 3 and the composition API.
I'm working on a text-, RNG- and web-based motorsport simulator. As part of this I want the user to be able to select different types of qualifying formats, each with distinctive configuration requirements. The user will be shown a dropdown with all different types of qualifying formats, and the correct format form fields are shown based on a dynamic component.
My main Qualifying.vue component;
<template>
<form @submit.prevent="form.post(route('seasons.configuration.qualifying.store', [season]));">
<div v-if="form.errors">
<p v-for="error in form.errors">{{ error }}</p>
</div>
<div class="mb-3">
<label for="format" class="form-label">Qualifying format</label>
<select id="format" v-model="format" class="form-control" required>
<option v-for="(format, key) in formats" :key="key" :value="key">{{ format }}</option>
</select>
</div>
<component :is="qualifyingComponent" @updateFormatDetails="handleFormatDetailsUpdate"
:existingFormatDetails="props.season?.format"/>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</template>
<script setup>
import {markRaw, ref, watch} from 'vue';
import {useForm} from "@inertiajs/inertia-vue3";
import ThreeSessionElimination from "@/Shared/QualifyingFormats/ThreeSessionElimination";
import SingleSession from "@/Shared/QualifyingFormats/SingleSession";
const props = defineProps({
season: {
type: Object,
required: true,
},
formats: {
type: Object,
required: true,
},
});
const components = {
three_session_elimination: ThreeSessionElimination,
single_session: SingleSession,
};
const qualifyingComponent = ref(null);
const format = ref(null);
const form = useForm({
selected_format: null,
format_details: {},
});
const handleFormatDetailsUpdate = (formatDetails) => {
form.format_details = {};
for (const [key, value] of Object.entries(formatDetails)) {
form.format_details[key] = value;
}
};
watch(format, (newFormat) => {
qualifyingComponent.value = markRaw(components[newFormat]);
form.selected_format = newFormat;
});
</script>
Each qualifying format component has a formatDetails reactive object. I've tried multiple different ways of accessing this object's data before or on form submit (using qualifyingFormat.value.formatDetails, or using refs and trying to access those), but I simply can't seem to access it. I've now settled for emitting an even whenever a field in the component's form has been updated, but that's led to a lot of repeated code (part of which has been moved to a composable, but not everything);
SingleSession.vue
<template>
<div class="row mb-3">
<div class="col-4">
<label for="runs_per_session" class="form-label">Runs per session</label>
<input type="number" class="form-control" id="runs_per_session" v-model="formatDetails.runs_per_session"
required>
</div>
<div class="col-4">
<label for="min_rng" class="form-label">Min RNG per run</label>
<input type="number" class="form-control" id="min_rng" v-model="formatDetails.min_rng" required>
</div>
<div class="col-4">
<label for="max_rng" class="form-label">Max RNG per run</label>
<input type="number" class="form-control" id="max_rng" v-model="formatDetails.max_rng" required>
</div>
</div>
</template>
<script setup>
import {onMounted, reactive, watch} from "vue";
import {assignFormatDetails} from "@/Composables/useQualifyingFormat";
const props = defineProps({
existingFormatDetails: {
type: Object,
required: false,
},
});
const formatDetails = reactive({
runs_per_session: null,
min_rng: null,
max_rng: null,
});
onMounted(() => {
assignFormatDetails(formatDetails, props?.existingFormatDetails);
});
const emit = defineEmits(['updateFormatDetails']);
const handleFormatDetailsUpdate = () => {
emit('updateFormatDetails', formatDetails);
};
watch(formatDetails, () => {
handleFormatDetailsUpdate();
});
</script>
ThreeSessionElimination.vue is pretty much the exact same, apart from different keys in the formatDetails object;
const formatDetails = reactive({
q2_driver_count: null,
q3_driver_count: null,
runs_per_session: null,
min_rng: null,
max_rng: null,
});
assignFormatDetails
export function assignFormatDetails(defaultFormatDetails, storedFormatDetails) {
if (!storedFormatDetails) {
return;
}
Object.keys(storedFormatDetails).forEach(key => {
if (defaultFormatDetails.hasOwnProperty(key)) {
defaultFormatDetails[key] = storedFormatDetails[key];
}
});
}
All of this works, but the more formats I add, the more I'll have to repeat myself. I also can't move defineEmits to a composable since it's only usable within <script setup>.
Is there a way of either 1. accessing a dynamic child's component data directly, or 2. moving the emit logic to an external file to make it reusable?
Please or to participate in this conversation.