Home > Software engineering >  Correct way to mutate data in slotted child component
Correct way to mutate data in slotted child component

Time:10-31

I want to create a table component made out of three parts. The wrapper, the heads and the data.

While most of it works pretty well, I'm struggling with the order by functionality.

When the user clicks on a th tag, the data should be reordered and a little indicator should be shown. The ordering works but the indicator doesn't.

The actual problem
I know that it's bad to mutate a property inside the child although it's defined in the parent. Since I use slots, I can't use the normal $emit.
Using the approach shown here brings me Uncaught (in promise) TypeError: parent is null and Unhandled error during execution of scheduler flush.

Although I already know that my current approach is wrong, I don't know how to do it right.
Googling around, I found keywords like writable computed props and scoped slots, but I can't put it together.

So what's the right way to realize two-way-binding in a slotted environment?

My Table.vue file

<template>
    <div >
        <div >
            <div >

                <Search v-if="searchable" :filters="props.filters"></Search>

                <div >
                    <div >
                        <table >
                            <thead >
                            <tr>
                                <slot name="table-heads"></slot>
                            </tr>
                            </thead>
                            <tbody >
                                <slot name="table-body"></slot>
                            </tbody>
                        </table>

                        <Pagination v-if="paginationLinks" :links="paginationLinks"></Pagination>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script setup>
import Pagination from "@/Components/Tables/Pagination.vue";
import {provide} from "vue";
import Search from "@/Components/Tables/Search.vue";
import {Inertia} from "@inertiajs/inertia";

    let name = "Table";

    let props = defineProps({
        paginationLinks: Array,
        dataUrl: String,
        filters: Object,
        order: Object,
        searchable: Boolean
    });

    provide('dataUrl', props.dataUrl);
    provide('order', props.order);

</script>

<style scoped>

</style>

My TableHead.vue file

<template>
    <th @click="orderByClicked" scope="col"
        >
        <div >
            <slot></slot>
            <span v-if="order.orderBy === props.orderKey">
                <i v-if="order.orderDirection === 'asc'" ></i>
                <i v-if="order.orderDirection === 'desc'" ></i>
            </span>
        </div>
    </th>
</template>

<script setup>
import { inject } from "vue";
import { Inertia } from "@inertiajs/inertia";

let name = "TableHead";
let dataUrl = inject('dataUrl');
let order = inject('order');

let props = defineProps({
    orderKey: String,
    orderByClicked: Function
});

function orderByClicked() {
    if (props.orderKey) {
        if (order.orderBy === props.orderKey)
            order.orderDirection = order.orderDirection === 'asc' ? 'desc' : 'asc';
        else
            order.orderDirection = "asc"

        order.orderBy = props.orderKey;

        Inertia.get(dataUrl, {orderBy: props.orderKey, orderDirection: order.orderDirection}, {
            preserveState: true,
            replace: true
        });
    }
}

</script>


<style scoped>

</style>

My TableData.vue file (just to be complete)

<template>
    <td >
        <slot></slot>
    </td>
</template>

<script setup>
    let name = "TableData";
</script>

<style scoped>

</style>

putting it together

            <Table :pagination-links="props.users.links" :data-url="'/users'" :searchable="true" :filters="props.filters" :order="props.order">
                <template #table-heads>
                    <TableHead order-key="name">name</TableHead>
                    <TableHead order-key="email">email</TableHead>
                    <TableHead>Bearbeiten</TableHead>
                </template>

                <template #table-body>
                    <tr v-for="user in users.data" :key="user.id">
                        <TableData>{{ user.username }}</TableData>
                        <TableData>{{ user.email}}</TableData>
                    </tr>
                </template>
            </Table>

CodePudding user response:

It is likely to be a reactivity problem. Your indicators relies on the order property which you set at setup and seem to change correctly however I don't see anything to make it reactive for your template.

In Table.vue where you provide it, you might just need to import ref from Vue to make it reactive:

import { ref, provide } from 'vue'

// provide static value
provide('dataUrl', props.dataUrl);

// provide reactive value
const order = ref(props.order)
provide('order', order);

CodePudding user response:

I realized that the error only occurred in combination with FontAwesome, so I looked further.
By inspecting the code, I found out that FontAwesome manipulates the DOM.

It should have been clear in hindsight but anyway...

The cause
The point is, that when you insert a tag like <i ></i> it's converted to <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-up" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" data-fa-i2svg=""><path fill="currentColor" d="M182.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8H288c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"></path></svg>

That's fine as long as you don't attach own attributes to the i tag because they get destroyed!
That happened to my v-if. Unfortunately it didn't silently vanish but it caused some weird errors.

The solution
Wrap any tags which are getting manipulated by a third party (in this case FontAwesome) within a suitable parent tag.
I came up with the following solution:

<span v-if="someCondition">
   <i ></i>
</span>

Trivia
Interestingly this error didn't occur in https://sfc.vuejs.org for some reason. I couldn't reproduce it in any online tool. The components only crashed in my local dev environment.

  • Related