DEV Community

Jeffrey Biles
Jeffrey Biles

Posted on • Updated on

In-template computed property declaration in VueJS

Computed Properties are one of the best features of VueJS.

However, they don't work in all situations.

For example, let's say that you're looping over an array in a template and need to calculate something based on the index.

<span v-for="(item, index) in new Array(5)" :key="index">
  <slot :isFilled="index + 1 <= rating">
    <font-awesome-icon icon="star" v-if="index + 1 <= rating" />
    <font-awesome-icon :icon="['far', 'star']" v-else />
  </slot>
</span>

This example is from a rating-display component which I was using in my recent course on Component Slots. It takes in a rating prop, then displays either a set of stars (default) or whatever icons you choose to put into the slot.

Alt Text

The issue here is that index + 1 <= rating is repeated, and in the second usage with the v-if, the intent of the line is not clear.

However, we can't use a traditional computed property, because we're relying on index for the calculation.

And using a method, while it successfully solves the intent issue, is still pretty messy.

<span v-for="(item, index) in new Array(5)" :key="index">
  <slot :isFilled="isFilled(rating, index)">
    <font-awesome-icon icon="star" v-if="isFilled(rating, index)" />
    <font-awesome-icon :icon="['far', 'star']" v-else />
  </slot>
</span>

Wouldn't it be cool if, instead, you could just assign stuff to a property mid-template?

<!-- not actual code -->
<span v-for="(item, index) in new Array(5)" :key="index">
  let isFilled = index + 1 <= rating
  <slot :isFilled="isFilled">
    <font-awesome-icon icon="star" v-if="isFilled" />
    <font-awesome-icon :icon="['far', 'star']" v-else />
  </slot>
</span>

The answer is yes, that would be awesome.

Good news: we can do it.

Kind of.

<span v-for="(item, index) in new Array(5)" :key="index">
  <Let :val="index + 1 <= rating" v-slot="{val: isFilled}">
    <slot :isFilled="isFilled">
      <font-awesome-icon icon="star" v-if="isFilled" />
      <font-awesome-icon :icon="['far', 'star']" v-else />
    </slot>
  </Let>
</span>

So it's probably some crazy complex meta-programming magic, right?

See for yourself:

<template>
  <span>
    <slot :val="val" />
  </span>
</template>

<script>
  export default {
    //this can be anything, so it's a rare case where this style of prop declaration is preferred
    props: ['val'] 
  }
</script>

<style lang="scss" scoped>

</style>

That's the entire thing.

I even left the empty css block in there, so I could say I didn't cut a single line.

If you want to know more, check out my courses on Slots on VueScreencasts.com. The Let block was pulled from the first course, and I'll be adding a second course in the series -- with even more crazy uses of slots -- in a week.

--Jeffrey

Oldest comments (4)

Collapse
 
johnhalsey profile image
John Halsey

What if you created a separate component for the items you're looping over, and let itself work the necessary value on a computed property. That way you don't need the ifs and whats and buts in the loop.

Collapse
 
jeffreybiles profile image
Jeffrey Biles • Edited

It's certainly possible to create a new component for content within loops, and oftentimes that is the correct solution, but that comes with its own set of downsides.

The biggest of those downsides are prop-drilling and, if the stick-it-in-a-new-component technique is used often, the proliferation of one-use components.

Collapse
 
johnhalsey profile image
John Halsey • Edited

I’d take prop drilling over confusing loop logic any day. At least that way, it’s much easier to read from a maintainable point of view. And if it’s a component inside a loop, technically it’s not a single use component. From a SOLID point of view and code responsibility, the extra component makes more sense to me. But I see the argument for it the other way around.

Thread Thread
 
jeffreybiles profile image
Jeffrey Biles • Edited

I'm guessing that you imagined a solution as follows:

<RatingFill :index="index" :rating="rating" v-for="(item, index) in new Array(5)" :key="index" />

However, part of the requirements are that we must preserve the slot where we can put in custom html for filled or unfilled rating items, so that solution doesn't work.

Let's compare these side by side.

<span v-for="(item, index) in new Array(5)" :key="index">
  <Let :val="index + 1 <= rating" v-slot="{val: isFilled}">
    <slot :isFilled="isFilled">
      <font-awesome-icon icon="star" v-if="isFilled" />
      <font-awesome-icon :icon="['far', 'star']" v-else />
    </slot>
  </Let>
</span>

vs

<span v-for="(item, index) in new Array(5)" :key="index">
  <RatingFillCalculator :index="index" :rating="rating" v-slot="{isFilled}">
    <slot :isFilled="isFilled">
      <font-awesome-icon icon="star" v-if="isFilled" />
      <font-awesome-icon :icon="['far', 'star']" v-else />
    </slot>
  </RatingFillCalculator>
</span>

The difference is that in the second case, we're creating an extra component and hiding the calculation. The Let seems more clear and more concise.