0
votes

I'm trying to create a component with a 'typeahead' aspect to it and am experiencing some difficulties with it.

There is more to the component than this, but certain actions will trigger the parent component to pass typeaheadItems to this component, at which point it should display them. The user should have the ability to hide the typeahead though which is where I'm experiencing my problem.

My initial naive approach was as follows.

Attempt 1

Given the Vue component:

<template>
    <div
      class="typeahead-items"
      v-show="myTypeaheadItems.length">
        <div class="item close-typeahead">
          <i
            class="icon close"
            @click="closeTypeahead"></i>
        </div>
        <div
          class="item"
          v-for="item in typeaheadItems"
          :key="item">
            {{item}}
        </div>
  </div>
</template>

<script>
  export default {
    name: 'typeahead',
    props: {
      typeaheadItems: {
        type: Array,
        default: function () {
          return [];
        }
      }
    },
    data () {
      return {
      };
    },
    methods: {
      closeTypeahead: function () {
        this.typeaheadItems = [];
      }
    }
  };
</script>

This works as expected, showing and hiding appropriately the typeahead, but gives me the warning:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "typeaheadItems"

Attempt 2

I then tried to update the component as follows:

v-for="item in myTypeaheadItems"

data () {
      return {
        myTypeaheadItems: this.typeaheadItems
      };
    },
methods: {
      closeTypeahead: function () {
        this.myTypeaheadItems = [];
      }

This doesn't work at all, the typeahead never shows, and there are no warnings or errors.

Attempt 3

Lastly I tried adding a watch:

watch: {
      typeaheadItems: function (val) {
        this.myTypeaheadItems = val.splice(0);
      }
    },

This does not work and gives me the warning:

You may have an infinite update loop in watcher with expression "typeaheadItems"

I can't seem to figure out the Vue way to accomplish this. How can I have an array props coming in from another component and still manipulate it within its own component and have the view update appropriately without warnings/errors?

UPDATE

Upon further review Attempt 2 is the approach that seems like it might be the closest to the 'Vue way' and it is working in that the typeahead items are getting html elements rendered for them, but it appears as though nothing is happening because v-show="myTypeaheadItems.length" appears to not be updating. I'm not sure why this is the case other than possibly one of the caveats listed on Vue's site.

Given Franco Pino's comments below, I was able to get Attempt 3 to work changing splice to slice. Is this the proper way to move forward?

1

1 Answers

1
votes

(for the sake of honesty, the "don't mutate props" message is a warning you could ignore, although naturally, that's not generally recommended)

The short answer to the question is:

Hiding the reference with a this.myDataVersionOfSomeProp like you did in the first example can make the warnings disappear, but careful not to lose that reference like you did by reassigning the data to something else, instead you could do someList.length = 0 someList.splice(0). to empty the list by modification rather than by a reassignment to []. But I'd say the Vue way is "Props down, events up".

Longer answer (sorry about the length, to be honest):

Solution one doesn't work because while initially you are assigning the myTypeaheadItems to the prop passed by the parent (and therefore have a reference to it), in your method you are reassigning it to something else (in this case the reassignment is this.myTypeaheadItems = []), so then you stop having a reference to the prop and the child's myTypeaheadItems becomes disconnected from the original prop. Here you could use someList.splice(0) to empty the list without doing a reassignment of the whole list.

You probably meant slice rather than splice in that watcher. Splice will modify val, which has a reference to the original prop, so the watcher will get called again and so on...

As for "The Vue way": The relevant Vue motto here is "Props down, events up" (it's related to the whole "one way flow" deal). You pass the prop from the parent to the child, and if the child wants to modify it, it emits an event telling the parent that a modification is in order, after all, the parent component is the one that owns the object, so it should decide about its modifications itself.

See here for custom events, thou there are more relevant chapters in there (I didn't even mention computed properties, which the warning also talks about): https://vuejs.org/v2/guide/components.html#Custom-Events

Here's an example with three approaches in the children for modifying objects that come from a prop: https://jsfiddle.net/8jwp2mrk/3/

The first child button has the child component claiming ownership of the prop by renaming it in its data, but instead of just directly changing a value of the object, it reassigns its this.myObj to something else (which was the prblem in your first attempt at solving the warning. For this example I simply reassign this.myObj to a clone of itself before modifying). (EDIT: To be clear, this approach is wrong for the reason explained in the last paragraph, I did this one as an example of how a child's "myProp" might become disconnected from the parent's original prop, which is useful when you want the child to own its own private version of the passed prop, but might be undesired behavior if you intended parent and child to share the one state)

In the second one, the child also renames the prop to a data myObj, but instead of reassigning it, it modifies it directly, this works as expected and gets you no warnings.

The third follows "Props down, events up". When you click the button the child tells the parent component it should update the prop. This also works as expected (you could even use arguments in your event to pass the value the object should update to).

Click the first child button a few times, then use the button at the bottom to hide the children components, then click again to show them again and you'll see the value of the first one got reset to the value of the original prop in the parent, this is what Vue warns against when it tells you that "the value will be overwritten whenever the parent component re-renders", which can be undesirable behavior if you expected the child to manage the prop more independently from the parent, but still share that prop's value with the parent.