javascriptvue.jsvue-component

Updating/ Removing recursive child components


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:

example directory browser

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.

example directory browser bug result

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>

StackBlitz Example

Is there a better solution that does not rely on nextTick() and an added component + prop?


Solution

  • 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>