Home > database >  Conditional rendering inside v-for with Vue
Conditional rendering inside v-for with Vue

Time:09-21

Problem

I'm building a project with Vue, where I have a list of projects, each with an open and delete button. Now I need to add Tags to each project with a separate component. I want to toggle this component's rendering for each list item individually to create a list with collapsable items.

My first try looked something like this:

<b-list-group v-if="!isLoading">
    <b-list-group-item v-for="project in projects" :key="project.path"> 
      {{ project.name }} 
      <b-btn variant="danger" size="sm" class="list-button" @click="deleteProject(project.path)">Löschen</b-btn>
      <b-btn variant="secondary" size="sm" class="list-button" @click="openProject(project.path)">Öffnen</b-btn>
      <b-btn variant="secondary" size="sm" class="list-button" @click="project.showTags = !project.showTags">Tags</b-btn>
      <search-tags v-if="project.showTags" style="margin-top: 1em;"></search-tags>
    </b-list-group-item>

If I console.log my this.projects, I can see that the value for each project changes correctly. Nevertheless, the v-if doesn't render even if project.showTags == true.

I then read, that you're not supposed to use v-if inside v-for, so I tried the Bootstrap-Vue collapse component:

<b-list-group v-if="!isLoading">
    <b-list-group-item v-for="project in projects" :key="project.path"> 
      {{ project.name }} 
      <b-btn variant="danger" size="sm" class="list-button" @click="deleteProject(project.path)">Löschen</b-btn>
      <b-btn variant="secondary" size="sm" class="list-button" @click="openProject(project.path)">Öffnen</b-btn>
      <b-btn variant="secondary" size="sm" class="list-button" @click="project.showTags = !project.showTags">Tags</b-btn>
      <b-collapse v-model="project.showTags">
        <search-tags style="margin-top: 1em;"></search-tags>
      </b-collapse>
    </b-list-group-item>

But this also dosen't work.

I think this cannot be so difficult and I'm missing something here. Any help is appreciated.

Edit 1

As seen in my code, the whole list is put inside <b-list-group> which is conditionally rendered depending on isLoading. If I force a reload by flipping isLoading, then flipping project.showTags and then flipping isLoading back, it works. But I feel like this solution is super shady and hacky.

Working but ugly code:

<b-list-group-item v-for="project in projects" :key="project.path"> 
      {{ project.name }} 
      <b-btn variant="danger" size="sm" class="list-button" @click="deleteProject(project.path)">Löschen</b-btn>
      <b-btn variant="secondary" size="sm" class="list-button" @click="openProject(project.path)">Öffnen</b-btn>
      <b-btn variant="secondary" size="sm" class="list-button" @click="test(project)">Tags</b-btn>
      <search-tags v-show="project.showTags" style="margin-top: 1em;"></search-tags>
    </b-list-group-item>
  </b-list-group>

with:

test(project) {
  this.isLoading = !this.isLoading;
  project.showTags = !project.showTags;
  console.log(this.projects);
  this.isLoading = !this.isLoading;
},

Edit 2

My data is first defined like this:

data() {
return {
  projects: [],
  tags: [],
  newProject: false,
  isLoading: true,
  isError: false,
  form: {
    name: '',
  },
};

},

then on created() I call getAllProjects() to fetch my projects form backend:

created() {
this.getAllProjects();
},

My projects come without the showTags prop, so I assign it directly after fetching:

async getAllProjects() {
  try {
    this.isLoading = true;
    const res = await fetch(store.state.urlEndpoints.getProjects, {
      ...AuthPost(), // request config object
    });

    this.projects = JSON.parse(await res.text());
    this.projects.forEach((project) => { project.showTags = false; });
    console.log(this.projects);
    this.isLoading = false;
  } catch (err) {
    this.isError = true;
    this.isLoading = false;
  }
},

I console.log this.projects on each click on the "Tags" button, as seen above in the test() function. The values flip just like I intend them to.

CodePudding user response:

@Rouben you need to sort how you want to set this.projects as right now in your created you assume that you'll get the fixed array of projects back from getAllProjects.

Try doing something like this:

async getAllProjects() {
  try {
    this.isLoading = true;
    const res = await fetch(store.state.urlEndpoints.getProjects, {
      ...AuthPost(), // request config object
    });

    return await res.json().map(project => {
         project.showTags = false;
         return project;
    });

also you could try this instead inside the @click:

<b-list-group-item v-for="(project, index) in projects" :key="project.path"> 
  {{ project.name }} 
  <b-btn variant="danger" size="sm" class="list-button" @click="deleteProject(project.path)">Löschen</b-btn>
  <b-btn variant="secondary" size="sm" class="list-button" @click="openProject(project.path)">Öffnen</b-btn>
  <b-btn variant="secondary" size="sm" class="list-button" @click="flipShowTags(index, project)">Tags</b-btn>
  <search-tags v-show="project.showTags" style="margin-top: 1em;"></search-tags>
</b-list-group-item>
methods: {
  flipShowTags(index, obj) {
    this.$set(this.projects, index, {...obj, showTags: !obj.showTags})
  }
}

if it was because of it not being reactive

CodePudding user response:

The problem is here...

this.projects = JSON.parse(await res.text());
this.projects.forEach((project) => { project.showTags = false; });

Adding the showTags property like this is not reactive which is why Vue doesn't react as you expect to changes.

See Change Detection Caveats

Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.

You need to have all reactive properties present when you assign the value to your projects data property. For example

export default {
  data: () => ({
    projects: [],
    isLoading: true,
    // etc
  }),
  methods: {
    async loadAllProjects () {
      try {
        this.isLoading = true;
        const res = await fetch(store.state.urlEndpoints.getProjects, {
          ...AuthPost(), // request config object
        });

        if (!res.ok) {
          throw new Error(`${res.status}: ${await res.text()}`)
        }
    
        // note that the value assigned to `this.projects` now has the
        // `showTags` property at the time of assignment
        this.projects = (await res.json()).map(project => ({
          ...project,
          showTags: false
        }))
      } catch (err) {
        this.isError = true;
      }
      this.isLoading = false
    }
  },
  created () {
    this.loadAllProjects()
  }
}
  • Related