Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

lolsokje's avatar

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?

0 likes
0 replies

Please or to participate in this conversation.