Vue 3 - How do I use an injected list in v-for loop?

Viewed 354

I have an app that, among other things, keeps track of items in lists. The following example, a slight extension of the docs example (using props), works properly.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Vue List Component Test</title>
    <script src="https://unpkg.com/vue@3"></script>
</head>
<body>
    <div id="app"></div>
    <script src="./app.js"></script>
</body>
</html>

app.js (with props)

const MyApp = {
    data() {
        return {
            items: []
        }
    },
    template: `<list-count></list-count><list :itemList="items"></list>`,
    methods: {
        addItem(i) {
            this.items.push(i)
        }
    },
    provide() {
        return {
            listLength: Vue.computed(() => this.items.length)
        }
    },
}

const app = Vue.createApp(MyApp)

const ItemListCount = {
    inject: ["listLength"],
    template: `<p> {{ this.listLength }}</p>`
}

const ItemList = {
    props: ["itemList"],
    template:`<ul>
                  <list-item v-for="(item, index) in itemList" :i="item" :idx="index"></list-item>
              </ul>`
}

const ItemListItem = {
    props: ["i", "idx"],
    template: `<li>{{ idx }}, {{ i.message }}</li>`
}

app.component("list", ItemList)
app.component("list-item", ItemListItem)
app.component("list-count", ItemListCount)
const vm = app.mount('#app')

Because the item lists sit in components that are subject to routing, I'd rather use Provide/Inject vs. having to figure out how to pass props down the chain and through the router. The following app.js, which uses provide/inject instead of props for the list, does not work.

app.js (with provide/inject) ... note the removal of the :itemList binding, the addition of the itemList in provide(), and the change in the ItemList component to use inject instead of props.

const MyApp = {
    data() {
        return {
            items: []
        }
    },
    template: `<list-count></list-count><list></list>`,
    methods: {
        addItem(i) {
            this.items.push(i)
        }
    },
    provide() {
        return {
            listLength: Vue.computed(() => this.items.length),
            itemList: Vue.computed(() => this.items)
        }
    },
}

const app = Vue.createApp(MyApp)

const ItemListCount = {
    inject: ["listLength"],
    template: `<p> {{ this.listLength }}</p>`
}

const ItemList = {
    inject: ["itemList"],
    template:`<ul>
                  <list-item v-for="(item, index) in itemList" :i="item" :idx="index"></list-item>
              </ul>`
}

const ItemListItem = {
    props: ["i", "idx"],
    template: `<li>{{ idx }}, {{ i.message }}</li>`
}

app.component("list", ItemList)
app.component("list-item", ItemListItem)
app.component("list-count", ItemListCount)
const vm = app.mount('#app')

The above raises the following error in console:

Uncaught TypeError: i is undefined

I assume the error is because the v-for loop isn't working properly with the inject: ["itemList"] whereas it seems to be working fine with a props: ["itemList"]. I can't find any relevant docs that would explain why this is the case. How do I fix the provide/inject version?

1 Answers

It looks like you have some unnecessary stuff inside your provide function.

This should do the job:

provide() {
    return {
      itemList: this.items
    };
  }

See this working codepen example

Update

Also, I'm assuming that you're using this code as an example of some more complex operation that actually justifies the use of provide/inject. Otherwise it's an overkill for a list component, and you should simplify it like this

const MyApp = {
  data() {
    return {
      items: []
    };
  },
  template: `
  <button @click="addItem(Math.floor(Math.random() * 100))">Add Item</button>
  <list :items="items"></list>`,
  methods: {
    addItem(i) {
      this.items.push(i);
    }
  }
};

const app = Vue.createApp(MyApp);

const ItemList = {
  props: { items: Array },
  template: `
  <p>{{ items.length }}</p>
  <ul>
  <li v-for="(item, index) in items">{{ index + ', ' + item }}</li>
  </ul>`
};

app.component("list", ItemList);

app.mount("#app");
Related