I have an application which needs to walk and display a directory structure similar to a filesystem. Below is an example of how this would work with a filesystem.
App.vue
<script setup>
import ListItem from './components/ListItem.vue';
const data = {
'C:/': {
'users/': {
'testUser/': {
'documents/': {},
},
},
'windows/': {
'system32': {},
},
},
'E:/': {
'test/': {},
},
};
</script>
<template>
<div style="display: flex; flex-direction: column">
<h1>Recursive Directory Browser</h1>
<ListItem title="Browse" :items="data" />
</div>
</template>
<style scoped></style>
ListItem.vue
<script setup>
import { defineProps, ref, nextTick } from 'vue';
const props = defineProps({
title: {
type: String,
required: true,
},
items: {
type: Object,
required: true,
},
});
const selectedName = ref();
const selectedValue = ref();
async function updateSelected(key, value) {
selectedName.value = key;
selectedValue.value = value;
}
</script>
<template>
<div style="border: 2px solid black; margin: 1em; padding: .25em;">
<h2>{{ title }}</h2>
<div style="display: flex; flex-direction: row;">
<template v-for="(value, key) in items">
<button @click="updateSelected(key, value)">{{ key }}</button>
</template>
</div>
</div>
<template v-if="selectedName">
<ListItem
:title="selectedName"
:items="selectedValue"
/>
</template>
</template>
<style scoped></style>
This code results in an App that looks like this:
The problem is that there is a bug here where a user can walk the directory several levels deep and then if the user navigates back to a different node on a parent directory, the child components for the already explored path remain.
I've solved this by adding a prop/ref called updating
on the ListItem
component and toggling it between a call to nextTick()
.
ListItem.vue using prop + component and nextTick()
<script setup>
import { defineProps, ref, nextTick } from 'vue';
const props = defineProps({
title: {
type: String,
required: true,
},
items: {
type: Object,
required: true,
},
updating: {
type: Boolean,
required: false,
default: false,
},
});
const updating = ref(props.updating);
const selectedName = ref();
const selectedValue = ref();
async function updateSelected(key, value) {
updating.value = true;
await nextTick();
selectedName.value = key;
selectedValue.value = value;
updating.value = false;
}
</script>
<template>
<div style="border: 2px solid black; margin: 1em; padding: .25em;">
<h2>{{ title }}</h2>
<div style="display: flex; flex-direction: row;">
<template v-for="(value, key) in items">
<button @click="updateSelected(key, value)">{{ key }}</button>
</template>
</div>
</div>
<template v-if="selectedName && !updating">
<ListItem
:title="selectedName"
:items="selectedValue"
:updating="updating"
/>
</template>
</template>
<style scoped></style>
Is there a better solution that does not rely on nextTick()
and an added component + prop?
updating
flag forces ListItem
to be remounted when data for the component hierarchy is reassigned, which is a workaround. The idiomatic way to do this is to use unique key, which is data itself in this case:
<template>
<div ...>...</div>
<ListItem
v-if="selectedName"
:key="selectedValue"
:title="selectedName"
:items="selectedValue"
/>
</template>