Surely
Surely

Reputation: 1709

VueJs array replacement for v-for how to make it work

I have a very simple page that contains two tabs, each tab contains a list of reports with different status.

The way I am trying to do is, I have two different data source array, one for each tab and third array that is to denote current displaying array items.

When user click on the tab, I replace the third array to either first or second array according to the tab clicked.

However, the v-for section is not updated, but the v-show section is updated. I spent quite a few hours to try to solve it, have tried Vue.set(...) but still not working.

Anyone knows the detailed cause of the issue and what is the best solutions for it?

Following is the code, I use OnSenUI for Vue and Vue router, this is the complete component code:

<template id="main-page">
  <v-ons-page>
  <v-ons-toolbar>
    <div class="center">Reports</div>
  </v-ons-toolbar>

  <v-ons-bar>
    <p style="text-align: right; margin-right: 20px">
      <v-ons-button @click="$ons.notification.alert('TODO: implement this.')">
        + New Report
      </v-ons-button>
    </p>
  </v-ons-bar>

  <v-ons-card v-for="report in showingReports" :key="report.requestNo">
    <div class="title">
      {{report.requestNo}}
    </div>
    <div class="content">
      Submitted Date: {{report.submittedDate}}<br>
      Status: {{report.status}}<br>
      Subject: {{report.subject}}<br>
    </div>
  </v-ons-card>

  <v-ons-card v-show="!showingReports || showingReports.length == 0">
    <div class="content">
      <p class="center">
        No records found
      </p>
    </div>
  </v-ons-card>

      <v-ons-tabbar>
        <v-ons-tab label="Open" @click="selectTab(0)" :active="selectedTab == 0"></v-ons-tab>
        <v-ons-tab label="Closed" @click="selectTab(1)" :active="selectedTab == 1"></v-ons-tab>
      </v-ons-tabbar>
   </v-ons-page>
  </template>
 <script>
  export default {
  data() {
    return {
      showingReports: [],
      openedReports: [],
      closedReports: [],
      selectedTab: 0
    }
  },

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

  methods: {
    getReports() {
      this.allReports = [
        {
          requestNo: "REPORT18070102",
          submittedDate: Date(),
          status: "OPENED",
          subject: "Test1"
        },
        {
          requestNo: "REPORT18070103",
          submittedDate: Date(),
          status: "OPENED",
          subject: "Test2"
        }
      ];

      this.openedReports = this.allReports.filter(report => report.status == "OPENED");
      this.closedReports = this.allReports.filter(report => report.status != "OPENED");
      this.selectTab(0);
    },

    selectTab(tab) {
      this.selectedTab = tab;
      if(tab == 0){
        this.showingReports = this.openedReports;
      }else{
        this.showingReports = this.closedReports;
      }
    },
  }
}

To include more details, this is a simple starting project I started by reading some tutorials. This is the main.js:

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
   el: '#app',
   router,
   components: { App },
  template: '<App/>'
})

This is the App.vue:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

This is the index.js under router foler. ReportList is the component file, which is located under components folder:

import Router from 'vue-router'
import ReportList from '@/components/ReportList'

import 'onsenui/css/onsenui.css';
import 'onsenui/css/onsen-css-components.css';

import Vue from 'vue';
import VueOnsen from 'vue-onsenui';

Vue.use(Router)
Vue.use(VueOnsen)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'ReportList',
      component: ReportList
    }
  ]
})

The dependencies in package.json:

"scripts": {
    "prod": "webpack-dev-server --inline --progress --config build/webpack.prod.conf.js",
    "start": "npm run prod",
    "e2e": "node test/e2e/runner.js",
    "test": "npm run unit && npm run e2e",
    "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
    "build": "node build/build.js"
  },
  "dependencies": {
     "onsenui": "^2.10.4",
     "vue": "^2.5.2",
     "vue-onsenui": "^2.6.1",
     "vue-router": "^3.0.1"
  },

-----Removed the console log, it does not help---


Updated: I make it work through another way, render the whole list but use v-show to control whether to show it. I think the "v-for" is not very reactive compared to v-show.

<v-ons-card v-for="report in allReports" :key="report.requestNo" v-show="shouldShow(report)">
  <div class="title">
      {{report.requestNo}}
  </div>
  <div class="content">
    Submitted Date: {{report.submittedDate}}<br>
    Status: {{report.status}}<br>
    Subject: {{report.subject}}<br>
  </div>
</v-ons-card>

shouldShow(report){
    if(this.selectedTab == 0){
      return report.status == 'OPENED';
    }else{
      return report.status != 'OPENED';
    }
  },

  selectTab(tab) {
    this.selectedTab = tab;
    var hasItem = false;
    this.allReports.forEach(r => hasItem = hasItem || this.shouldShow(r));
    this.emptyList = !hasItem;
    console.log(this.emptyList);
  }

Upvotes: 3

Views: 3470

Answers (1)

Jonathan Lam
Jonathan Lam

Reputation: 17371

From List Rendering in the Vue docs:

When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. ...

This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).

To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item. This special attribute is a rough equivalent to track-by in 1.x, but it works like an attribute, so you need to use v-bind to bind it to dynamic values (using shorthand here)

Because of this, you should always add a unique key attribute (with v-bind) for every element in the list to let Vue know there was a change in state.

E.g., using the requestNo attribute of each list item:

<v-ons-card v-for="report in showingReports" :key="report.requestNo">

Update (continuing my comment above):

You might want to consider using Vue's computed properties. They're quite powerful and intelligently update properties when their dependencies are updated. This might also fix your current issue with the list not rendering.

For example:

HTML

<v-ons-card v-for="report in computedReports" :key="report.requestNo">
  <!-- ... -->
</v-ons-card>

JS

data: {
  selectedTab: 0
},
computed: {
  computedReports() {
    return this.allReports.filter(report => this.isOpenedReports ?report.status != "OPENED" : report.status == "OPENED");
  }
}
methods: {
  getReports() {
    this.allReports = [
      {
        requestNo: "REPORT18070102",
        submittedDate: Date(),
        status: "OPENED",
        subject: "Test1"
      },
      {
        requestNo: "REPORT18070103",
        submittedDate: Date(),
        status: "OPENED",
        subject: "Test2"
      }
    ];
    this.selectTab(0);
  },

  selectTab(tab) {
    this.selectedTab = tab;
  },
}

Upvotes: 2

Related Questions