Manipulating new item via its component instance


#1

Suppose I have a my-list and a my-list-item components with the obvious relationship: an instance of my-list contains a list of my-list-item instances. The data object of my-list contains a list items of data entries, each rendered as a my-list-item component instance.
There is a method addNewItem in my-list which appends a new data entry to items, causing a new instance of my-list-item to be created / rendered.

https://jsfiddle.net/e29hnu0c/1/embedded/result,js,html,css

The Question:
What is the recommended way to finetune (manipulate) the newly created item? How can one get a reference to the item’s component instance?
The solution presented in the above jsfiddle is to attach a callback (named onCreated) to the item’s data entry which gets called (and removed) in the created hook of my-list-item. Is this an acceptable practice? What is the recommended solution?


#2

Can you explain what you are trying to achieve? Like, what is the end result you are looking for here.


#3

What I am trying to achieve is manipulating the state/value of the newly added item. The last user-added item should be distinguished from the items that were initially present in the list and possibly also from the previously user-added items. Furthermore, I’d like to perform such a manipulation via the item’s component instance (vm). This allows for the use of instance methods and computed properties for the manipulation. The problem is that the instance is not available at the time when the item’s data is pushed onto the list.
I have solved this by attaching the onCreated callback:

addNewItem : function (by_way)  {
      var onCreated; // callback to manipulate the added item
                     // via its component instance (vm)
      switch (by_way) {
        case 'A':
          onCreated = function (vm) { vm.beActive(); }; break;
        case 'B':
          onCreated = function (vm) { vm.beBold(); }; break;
        case 'C':
          onCreated = function (vm) {
            vm.beBold();
            vm.beActive();
            vm.timeoutReset(1500);
          }; break;
      }
      // create new item data entry
      var item_entry = { 
        title : `Item #${this.items.length}`
      };
      //attach the onCreated callback to the item's data entry
      item_entry.onCreated = onCreated; 
      
      // add the data entry to the list
      this.items.push (item_entry);  
    },
  },

(See jsfiddle.net/vdsepazg/5.)

I’d like to know whether the requirement to perform the item’s state/value manipulation via the item’s instance (instead of purely via the item’s data) is a sign of some misconception on my side.


#4

Short answer - yes. You should change the data and let the component render that change.

Generally you should not need to access a component instance. You should be modeling your application state using data structures and then using components to render those data structures to the screen in a form that is human readable. Vue components are not designed to be used as data stores.

You could certainly wire up a vue instance to act as a data store and then use that vue instance to encapsulate some methods or something


#5

Interesting. Judging from your answer, it looks like the Vue.js recommended approach is to stay away from using OOP encapsulation. In Vue, the component instance’s data/state should be manipulated directly – instead of via methods. This sounds like antithesis of OOP. It seems to be in opposition to the commonly held view that GUI components are a good fit for object orientation [1] [2].


#6

You are still not understanding. The vue component does not represent the state of the application, it represents some UI element.

The abstract application can be modeled using some data and we refer to this as the “application state”.

Let’s look at an example.

Imagine you want to show all your favorite foods on the screen. We can model the entire state of this application using an array:

const foods = [
  "pizza",
  "waffles",
  "tacos"
];
<ul>
  <li v-for="food in foods">{{ food }}</li>
</ul>

If we decide we also like ice cream we can simply push that onto the array:

foods.push("ice cream")

But what if we want to show an indicator for the new foods we add?

Well lets think about this for a second before we start getting into components and created hooks and trying to manipulate things. Let’s ask ourselves a question before we start implementing.

How can we model this application state?

How can we make some abstract representation of the information we are trying to display? Well, we could change the items in the array into objects:

const foods = [
  {
    value: "pizza",
    isNew: false
  },
  {
    value: "waffles",
    isNew: false
  },
  {
    value: "tacos",
    isNew: false
  }
];

And our template changes slightly:

<ul>
  <li v-for="food in foods">
    {{ food.value }}<template v-if="food.isNew"> (new)</template>
  </li>
</ul>

Then when we add a new item:

foods.push({
  value: "ice cream",
  isNew: true
})

Now we are pretty happy with this, but what if we want “new” indicator to fade after a few seconds?

Before we start getting crazy and making child components that set timeout in the created hook and compare time created or something crazy lets again ask ourselves a question.

How can we model this application state?

Instead of worrying about what text is shown in the “new” indicator or where it appears or what component renders it, lets think about how we would model this feature in abstract data.

Its pretty simple really. We already have everything we need - we just need some logic that changes the value of a food’s isNew property after a few seconds. Again, we aren’t using components, we aren’t doing anything fancy. We are just using some simple data to create an abstract representation of application state.

const iceCream = {
  value: "ice cream",
  isNew: true
};

setTimeout(() => {
  iceCream.isNew = false;
}, 3000);

foods.push(iceCream)

Now when we add ice cream to the list it will show the “new” indicator for only a few seconds. Note that our component for rendering the actual list item has not changed at all.

But this part where you add ice cream should actually be a function that accepts a value

Right, that looks like this:

addFood (value) {
  const newFood = {
    value,
    isNew: true
  };

  setTimeout(() => {
    newFood.isNew = false;
  }, 3000);

  foods.push(newFood)
}

But my actual feature is much more complicated than a simple timeout

So make some fancy special thing that encapsulates all that logic and use that to store the application state:

const foods = TimedList([
  "pizza",
  "waffles",
  "tacos"
])

And use some method for adding stuff:

addFood (value) {
  foods.addItem(value);
}

Its all well and good to say all this stuff, but does it actually work?

Yes. https://codepen.io/autumnwoodberry/pen/EMdGVo?editors=1010


#7

Thank you for your elaborated answer. The lesson I took from it is that in Vue, data items should not be encapsulated via Vue component instances. Instead, other means should be used for possible data encapsulation. It is recommended that Vue components be decoupled from data manipulation logic.


#8

Sometimes it makes sense to have a component mutate state. Lets say you have a dropdown menu that should show when you click a button. Probably you want a <DropdownMenu> component that has the button and the menu inside, in which case that component would have a local state to represent whether or not the actual menu was visible. In this case, you would have the Dropdown menu handle mutating that state.