Vue 3 composition API: issue with computed to filter array

Viewed 3110

I am learning the Vue 3 composition API and I did a simple post list (simply creating an array of post in the setup() with tags for each posts) as below first code section. Then I have a component PostList to display those posts and a TagCloud to show all the existing tags. I am trying to filter the posts by clicking on a tag.

  • I managed to emit the tag from the component TagCloud
  • I try to filter filter my list of posts (tils) with a computed but... it doesn't work

Below are the 3 elements working together, can anyone spot what's wrong?

The main view

<template>
  <div class="home">
    <h1>Today I learned (TIL)</h1>
    <div class="layout">
      <TagCloud :tils="tils" @tag="filterTils" />
      <PostList :tils="filterTils" />
    </div>
  </div>
</template>

<script>
import PostList from "../components/PostList.vue";
import TagCloud from "../components/TagCloud.vue";
import { ref, computed } from "vue";

export default {
  name: "Til",
  components: { PostList, TagCloud },
  setup() {
    const tils = ref([
      {
        title: "Symbol#to_proc conversion",
        description: "map(&:to_i) is exactly the same as map { |x| x.to_i }",
        tags: ["ruby"],
      },
      {
        title: "Symbol#to_proc conversion",
        description:
          "Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum ",
        tags: ["lorem"],
      },
      {
        title: "Symbol#to_proc conversion",
        description: "map(&:to_i) is exactly the same as map { |x| x.to_i }",
        tags: ["ruby"],
      },
    ]);

    const filterTils = computed((tag) => {
      return tils.value.filter((til) => til.tags.includes(tag));
    });
    
    return { tils, filterTils };
  }
};
</script>

The tag cloud component

<template>
  <div class="tag-cloud">
    <h3>Tags</h3>
    <div v-for="tag in tags" :key="tag">
      <div @click="filterTag(tag)" class="uniq-tag">#{{ tag }}</div>
    </div>
  </div>
</template>

<script>
import useTags from "../composables/useTags";

export default {
  props: ["tils"],
  setup(props, context) {
    const { tags } = useTags(props.tils);
    const filterTag = (tag) => {
      context.emit("tag", tag);
    };
    return { tags, filterTag };
  },
};
</script>

the post list component

<template>
  <div class="til-list">
    <div v-for="til in tils" :key="til">
      <div class="til">
        <h3>{{ til.title }}</h3>
        <p>{{ til.description }}</p>
        <span v-for="tag in til.tags" :key="tag"> #{{ tag }} </span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: ["tils"],
};
</script>
1 Answers

You're trying to use filterTils both as an event handler for the tag event, and as a computed prop that returns the filtered results, but those two things should be separate.

Also note that wrapping a method with computed() does not necessarily return another method. In the case of filterTils, it returns an Array.

Solution

Here's one way to solve the problem:

  1. Add a ref that holds the currently selected tag:

    import { ref } from 'vue'
    
    export default {
      setup() {
        const currentTag = ref()
      }
    }
    
  2. Change the @tag event handler to copy the current tag:

    <template>
      <TagCloud @tag="onTag" />
    </template>
    
    <script>
    export default {
      setup() {
        return {
          onTag: tag => currentTag.value = tag
        }
      }
    }
    </script>
    
  3. Update filterTils to compute the filtered results based on the current tag:

    const filterTils = computed(() => {
      return currentTag.value
        ? tils.value.filter(til => til.tags.includes(currentTag.value))
        : tils.value;
    })
    

demo

Related