77

Here is my current template:

<a-droppable v-for="n in curSize" :key="n - 1" :style="{width: `${99.99 / rowLenMap[orderList[n - 1]]}%`, order: orderList[n - 1]}">
  <a-draggable :class="{thin: rowLenMap[orderList[n - 1]] > 10}">
    <some-inner-element>{{rowLenMap[orderList[n - 1]]}}</some-inner-element>
  </a-draggable>
</a-droppable>

The problem is that i have to write rowLenMap[orderList[n - 1]] multiple times, and i'm afraid vue.js engine will also calculate it multiple times.

What i want is something like this:

<a-droppable v-for="n in curSize" :key="n - 1" v-define="rowLenMap[orderList[n - 1]] as rowLen" :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
  <a-draggable :class="{thin: rowLen > 10}">
    <some-inner-element>{{rowLen}}</some-inner-element>
  </a-draggable>
</a-droppable>

I think it's not difficult to implement technically because it can be clumsily solved by using something like v-for="rowLen in [rowLenMap[orderList[n - 1]]]". So is there any concise and official solution?

5
  • Do a method. I think that's the cleanest way to go without changing your data structure. Accessing to two array elements by index shouldn't be noticeable even if it's computed each time anyway. Commented May 16, 2017 at 11:07
  • There is no vue directive to change data structure from template. As solution you can watch and regenerate curSize depending on orderList/rowLenMap. n will become object with three fields: n: your n, list amf len Commented May 16, 2017 at 12:35
  • I think a method is not brief enough in case 2 or 3 or more parameters is required. @Cobaltway Commented May 18, 2017 at 8:44
  • Similar to the accepted answer. Maybe a computed prop is little better than a watcher. Thx. @KirillMatrosov Commented May 18, 2017 at 8:47
  • github.com/posva/vue-local-scope Commented Dec 4, 2023 at 16:03

12 Answers 12

194

I found a very simple (almost magical) way to achieve that, All it does is define an inline (local) variable with the value you want to use multiple times:

<li v-for="id in users" :key="id" :set="user = getUser(id)">
  <img :src="user.avatar" />
  {{ user.name }}
  {{ user.homepage }}
</li>

Note : set is not a special prop in Vuejs, it's just used as a placeholder for our variable definition.

Source: https://dev.to/pbastowski/comment/7fc9

CodePen: https://codepen.io/mmghv/pen/dBqGjM


Update : Based on comments from @vir us

This doesn't work with events, for example @click="showUser(user)" will not pass the correct user, rather it will always be the last evaluated user, that's because the user temp variable will get re-used and replaced on every circle of the loop.

So this solution is only perfect for template rendering because if component needs re-render, it will re-evaluate the variable again.

But if you really need to use it with events (although not advisable), you need to define an outer array to hold multiple variables at the same time :

<ul :set="tmpUsers = []">
  <li v-for="(id, i) in users" :key="id" :set="tmpUsers[i] = getUser(id)" @click="showUser(tmpUsers[i])">
    <img :src="tmpUsers[i].avatar" />
    {{ tmpUsers[i].name }}
    {{ tmpUsers[i].homepage }}
  </li>
</ul>

https://codepen.io/mmghv/pen/zYvbPKv

credits : @vir us

Although it doesn't make sense here to basically duplicate the users array, this could be handy in other situations where you need to call expensive functions to get the data, but I would argue you're better off using computed property to build the array then.

Sign up to request clarification or add additional context in comments.

21 Comments

This works, but it has a side effect of adding an attribute (set="[object Object]") to the output, which is probably not wanted (although harmless).
Not working for me. Goes into infinite loop, Console error says You may have an infinite update loop in a component render function.
Vue 2.5.2 version
it doesn't seem to be evaluated correctly if used inside @click for example. Seems like only the value evaluated for the first item of v-for is used inside all listeners. Any ideas on how to fix that?
oh, I see - you are talking about this particular example. Yes, it will copy the array since this is a very simple example. The idea was to demonstrate the approach of reusing "complex computation" though and this is how one can technically overcome a pitfall with listeners which might be handy. Of course you can pass the same function to perform the calculation one more time, which you've already mentioned. It's more a memory/time/readability tradeoff at this point. Not such a big deal, but just for the sake of completeness worth mentioning.
|
24

Today I needed this and used <template> tag and v-for like this
I took this code and

<ul>
  <li v-for="key in keys" 
      v-if="complexComputation(key) && complexComputation(key).isAuthorized">
    {{complexComputation(key).name}}
  </li>
</ul>

Changed it to this

<ul>
  <template v-for="key in keys">
    <li v-for="complexObject in [complexComputation(key)]"
        v-if="complexObject && complexObject.isAuthorized">
      {{complexObject.name}}
    </li>
  </template>
</ul>

And it worked and I was pleasantly surprised because I didn't know this was possible

3 Comments

nice trick!!! working fine, much thx. but one suggestion, please create a much simpler example when you provide an answer.
Very nice combined use of template tag and v-for="item in [singleton]". This solution has no unwanted side effect at all. If multiple shortcut variables are needed, we can further make the singleton an Object literal.
This is the best answer here.
17

Just tested using vue3 and works, i think it works universally

{{ (somevariable = 'asdf', null) }}
<span v-if="somevariable=='asdf'">Yey</span>
<span v-else>Ney</span>

It outputs nothing while setting your variable.

mandatory:

opening "("

set your variable

closing ", null)"

6 Comments

the same can also be achieved using the void operator {{ void(somevariable = 'asdf') }}
Note: the , null (or void) is necessary to prevent rendering the content ("asdf" in this example).
Also, to make Typescript not complain about this, I also had to define the type of my someVariable in my <script> somewhere.
best solution, imho
This isn't scope though, so if you do it somewhere, anywhere after in the template will have access to it
|
13

Judging by your template, you're probably best off with a computed property, as suggested in the accepted answer.

However, since the question title is a bit broader (and comes up pretty high on Google for "variables in Vue templates"), I'll try to provide a more generic answer.


Especially if you don't need every item of an array transformed, a computed property can be kind of a waste. A child component may also be overkill, in particular if it's really small (which would make it 20% template, 20% logic, and 60% props definition boilerplate).

A pretty straightforward approach I like to use is a small helper component (let's call it <Pass>):

const Pass = {
  render() {
    return this.$scopedSlots.default(this.$attrs)
  }
}

Now we can write your component like this:

<Pass v-for="n in curSize" :key="n - 1" :rowLen="rowLenMap[orderList[n - 1]]" v-slot="{ rowLen }">
  <a-droppable :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
    <a-draggable :class="{thin: rowLen > 10}">
      <some-inner-element>{{rowLen}}</some-inner-element>
    </a-draggable>
  </a-droppable>
</Pass>

<Pass> works by creating a scoped slot. Read more about scoped slots on the Vue.js documentation or about the approach above in the dev.to article I wrote on the topic.


Appendix: Vue 3

Vue 3 has a slightly different approach to slots. First, the <Pass> component source code needs to be adjusted like this:

const Pass = {
  render() {
    return this.$slots.default(this.$attrs)
  }
}

3 Comments

So this does not work with vue 3, I tried to migrate return this.$slots.default(this.$attrs); but I get Property "locale" was accessed during render but is not defined on instance. at <Var locale="en" >
Thanks for the pointer @phper. I have added the Vue 3 compatible component definition using $slots and also adjusted the usage code (migrated from slot-scope to v-slot) so it's compatible with Vue 2.6+ and Vue 3. :)
If you need to keep typing information about fallthrough attributes when using typescript: github.com/posva/vue-local-scope/issues/…
7

How about this:

<div id="app">
  <div
    v-for="( id, index, user=getUser(id) ) in users"
    :key="id"
  >
    {{ user.name }}, {{ user.age }} years old
    <span @click="show(user)">| Click to Show {{user.name}} |</span>
  </div>
</div>

CodePen: https://codepen.io/Vladimir-Miloevi/pen/xxJZKKx

Comments

5

This seems like the perfect use case of a child component. You can simply pass your complex computed value(s) as a property to the component.

https://v2.vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props

3 Comments

I see this a lot for Vue and I don't like this argument that "everything should be a component". I have plenty of "components" that are only used once. Vue coders should be able to make an "inline component" so that they don't need to manage a whole new file.
So do it, you can create a component anywhere.... vuejs.org/v2/guide/components.html
Or just add additional code to the template in question. Many of my component are "born" this way. Where I extract the template code from a larger component to reuse through out the application.
4

In some situations a v-define or v-set would indeed be helpful to prevent executing something repeatedly (although a computed property is sometimes better). You could use v-for with an array having just that value.

<template v-for="user in [getUser(id)]">
Hello {{ user.name }}!
</template>

would have the same effect as the hypothetical v-set:

<template v-set="user=getUser(id)">
Hello {{ user.name }}!
</template>

Comments

2
<template>
  <div>
    <div v-for="item in itemsList" :key="item.id">
      {{ item.name }}

      <input v-model="item.description" type="text" />
      <button type="button" @click="exampleClick(item.id, item.description)">
        Click
      </button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [
        {
          id: 1,
          name: 'Name1',
        },
        {
          id: 2,
          name: 'Name2',
        },
      ],
    }
  },
  computed: {
    itemsList() {
      return this.items.map((item) => {
        return Object.assign(item, { description: '' })
      })
    },
  },
  methods: {
    exampleClick(id, description) {
      alert(JSON.stringify({ id, description }))
    },
  },
}
</script>

Comments

2

Borrowing an example from Mohamed's answer. To get it working in Vue3 with TypeScript I used Array.prototype.map() to create new objects where I call the function.

const app = new Vue({
  el: '#app',
  data: {
    users: [1, 2, 3, 4],
    usersData: {
      1: {name: 'Mohamed', age: 29},
      2: {name: 'Ahmed', age: 27},
      3: {name: 'Zizo', age: 32},
      4: {name: 'John', age: 13},
    }
  },
  methods: {
    getUser(id) {
      return this.usersData[id];
    },
  },
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <div v-for="{ id, user } in users.map((id) => ({ id: id, user: getUser(id) }))" :key="id">{{ user.name }}, {{ user.age }} years old</div>
</div>

Comments

1

Vue 3:

Just a <template> from https://github.com/posva/vue-local-scope/issues/17

<template>
  <slot v-bind="$attrs" />
</template>

but this approach and Loilo's answer are based on fallthrough attributes currently can't persistent typing information about the original value of passing variable due to Vue `v-bind="$attrs"` breaks typing

Update: the author in the previous <template> has just provided another generic component variant which allows the type of props get passed by type parameter: https://github.com/posva/vue-local-scope/issues/17#issuecomment-1839385652

<template>
  <slot :scope="scope" />
</template>

<script setup lang="ts" generic="T">
  defineProps<{ scope: T }>();
</script>

will a different usage:

- <Pass v-for="n in curSize" :key="n - 1" :rowLen="rowLenMap[orderList[n - 1]]" v-slot="{ rowLen }">
+ <Pass v-for="n in curSize" :key="n - 1" :scope="{ rowLen: rowLenMap[orderList[n - 1]] }" v-slot="{ scope: { rowLen } }">

Also try createReusableTemplate() from vueuse:

<script>
const [DefinePass, ReusePass] = createReusableTemplate<{ rowLen: string }>()
</script>

<template>
  <DefinePass v-slot="{ rowLen }">
    <div>{{ rowLen }}</div>
  </DefinePass>
  <ReusePass v-for="n in curSize" :key="n - 1" :rowLen="rowLenMap[orderList[n - 1]]" />
</template>

Comments

1

Several methods have been published here already, but I don't think any answer adds the most performant and proper solutions for both Vue 2 and Vue 3.

The solutions below use functional components or plain functions, making sure no extra component instance is created, saving time and memory, which is specially important when considering that such mechanisms are usually used in loops. Also, I say they are proper as they are not gimmicky, using normal and uncontroversial Vue mechanisms.

Vue 2

// Vars.js
export default {
  functional: true,
  render: (h, { scopedSlots, data }) => scopedSlots.default(data.attrs)
}

This can be used in templates as below, after importing Vars.js in:

<template v-for="item in itemList">
    <vars 
        :selected="checkItemSelected(item)" 
        :disabled="!checkItemOk(item)"
        :dash-case="'Weird caveat with dash case, pay attention!'"
        v-slot="{ selected, disabled, ['dash-case']: dashCase }">
        <!-- 
            any template using `selected`, `disabled` and `dashCase` freely 
        -->
    </vars>
</template>

Vue 3

// Vars.js or Vars.ts
const Vars = (_, context) => {
  return context.slots.default(context.attrs)
}

export default Vars

This can be used in the template same as Vue 2, after importing the component file:

<template v-for="item in itemList">
    <Vars 
        :selected="checkItemSelected(item)" 
        :disabled="!checkItemOk(item)"
        :dash-case="'Same weird caveat from Vue 2 with dash case, pay attention!'"
        v-slot="{ selected, disabled, ['dash-case']: dashCase }">
        <!-- 
            any template using `selected`, `disabled` and `dashCase` freely 
        -->
    </Vars>
</template>

Comments

-3

curSize is an array. Your temporary values comprise a corresponding implied array sizedOrderList = curSize.map(n => orderList[n-1]). If you define that as a computed, your HTML becomes

<a-droppable v-for="n, index in sizedOrderList" :key="curSize[index]" :style="{width: `${99.99 / rowLenMap[n]}%`, order: n}">
  <a-draggable :class="{thin: rowLenMap[n] > 10}">
    <some-inner-element>{{rowLenMap[n]}}</some-inner-element>
  </a-draggable>
</a-droppable>

1 Comment

Create a computed to take place of the original list. A good solution for now.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.