difference between v-for structures - javascript

Could someone explain what is the difference between those 2 v-for structures:
<li v-for="item in items" :key="item">
</li>
and
<li v-for="(item, i) in items" :key="i">
</li>

Vue requires all items inside a v-for to be "key-ed". The key is used to uniquely identify each element. This doesn't mean Vue will break if you don't use a key. It will warn it might not be able to detect all changes.
The "key" is particularly useful and important to Vue, as it allows it to skip re-rendering items which have not changed.
When duplicate keys are detected across a rendered collection, Vue will issue a warning.
Another Vue recommendation is that the "key"s are primitives (strings or numbers). When you specify non-primitive keys Vue will, again, issue a warning.
Given all the above recommendations, when rendering an array of unique primitives, using
<div v-for="item in items" :key="item" />
...is perfectly acceptable, as it meets the requirements: each key is primitive and unique.
Therefore, if you change the order of the items, Vue will be able to re-use the existing DOM elements and perform any move transitions (if you specified any). Testing this is actually fun. Consider this example, taken from the Vue documentation. Open it and, using dev-tools, change any of the rendered items' color to red. Then click the shuffle button. You'll see Vue re-uses the element and your custom change is kept, as the item is moved around.
When you're dealing with collections of non-primitives, or with collections of non-unique primitives, Vue still expects you to provide a unique primitive key for each item. Ideally, you should have a unique identifier (e.g: item.id). This is the typical solution and meets all requirements.
Sometimes, you don't have a unique identifier on each item, and an easy solution is to just use the items' position in the array as identifier (key):
<div v-for="(item, index) in items" :key="index" />
However, remember this can become problematic, particularly in cases where you change the order of the items after they're rendered and expect Vue to react to this change. It won't! Because Vue only watches the keys. When you swap items, keys don't change. only values do, so Vue won't re-render.
Official docs here:
Vue2 list rendering: https://v2.vuejs.org/v2/guide/list.html
Vue3 list rendering: https://vuejs.org/guide/essentials/list.html#list-rendering
As an example of how useful and powerful keys are when rendering, have a look at this virtual scroller.
Open the dev tools, inspect any cell and then scroll, while keeping the devtools open. You'll notice rows and columns only get updated when they change (each element flashes swiftly when it gets updated).
If you look into the code, you'll notice keys are dynamic. So you'd expect the first <div> in rows to always render the contents of the first object in rows and swap contents whenever another object takes first place in the array.
Thanks to the keys, Vue keeps the same <div> throughout its journey across the screen and only discards it after it leaves the scrolling window. As you scroll up, new <div>s are prepended to parent and as you scroll down new <div>s are appended, while the top ones are discarded.
This allows scrolling 100k rows (and 1 billion cells) smoothly (well, only in theory; in practice - we're only rendering as many cells as the screen can fit + 1 extra row + 1 extra column).

In the first case, the v-for iterates over all elements of items. It assigns the :key to the item itself. This is not ideal if you have duplicate elements in items, because each :key should be unique.
In the second case, v-for also iterates over all elements of items, but it introduces another variable named i, which represents the numerical index of the item in items. This index is then assigned to :key. This is better, because the indexes can't be duplicate.

As the purpose of :key attribute is to give a hint for Vue virtual DOM algorithm about change detection happen. Essentially, it helps Vue identify what's changed and what hasn't.
To understand it better I am adding the uses of both the scenarios :
:key="item" - If item is having unique value. So that each iterated element will get assigned with unique key.
:key="index" - If item is not having unique value. Hence, to assigned unique key to each iterated element we are using index.

Related

Vue Draggable not working with filterBy?

I am using vue draggable component to drag and drop objects from one div to another. I am also using filterBy filtering the v-model. The problem is when I filter(and for example get 1 result) and try to drag and drop the item the very first from all items is dropped and not the dragged item.
Dragging from:
<draggable :list="available" class="draggable" :options=" group:'stuff'}">
<div v-for="people in filterBy(available, filter_available, 'name')" class="list-item">
<img :src="image" height="20"> {{ name }}
</div>
</draggable>
Dropping to:
<draggable :list="drop" class="draggable" :options="{group:'stuff'}">
<div v-for="people in filterBy(drop, filter_doppred, 'name')" class="list-item">
<img :src=image" height="20"> {{ name }}
</div>
</draggable>
On it's own both features are working fine. However, when filtering the results, it changes the index of the elements and when moving from one list to another - I move a different item, not the one dragged.
Any Ideas?
Well, you have to pass the same list to the draggable component that you use for the v-for - both need the filtered result.
You currently use a method to do the filter operation. You would have t call this method twice to get the same result to both places.
You should instead use a computed property to avoid doing the same operation twice.
Edit: the draggable component will mutate the provided list, which will not affect your source data when you use a computed property.
After looking at the docs for vue-draggable, you should probably use the changeevent provided by the component (https://github.com/SortableJS/Vue.Draggable#events) to update your source data.
I can't give you a concrete example because I don't know what your logic is to determine the new position of the moved element in the unfiltered source list.
Mostly I agree with #LinusBorg, with some minor differences:
1) draggable list props and v-for must use a collection
2) Create a different array for the filtered array. A computed property won´t work as draggable will try to change it. You can use a data field and use watch if the master array may change.
3) Listen the change event to propagate change on the master array.
I think that's happening because SortableJS uses indexes to get the currently dragged element.
// Sortable.js:341
// Get the index of the dragged element within its parent
startIndex = _index(target, options.draggable);
I have a similar case:
several sets of drag'n'drop groups should share the same list of items, and the items cannot repeat across sets.
I think I'll end up with adding/removing the elements manually.
I added a property(visible) to my object, when the filter change I set the property to true if is still visible, to false if not. Based on the property i hide or show the element to avoid create a new array with filterBy, filterBy is the reason the index changes and does not match with the original array (cause it creates a new one).

What is "track by" in AngularJS and how does it work?

I don't really understand how track by works and what it does.
My main goal is to use it with ng-repeat to add some precision.
Using track by to track strings & duplicate values
Normally ng-repeat tracks each item by the item itself. For the given array objs = [ 'one', 'one', 2, 'five', 'string', 'foo'], ng-repeat attempts to track changes by each obj in the ng-repeat="obj in objs". The problem is that we have duplicate values and angular will throw an error. One way to solve that is to have angular track the objects by other means. For strings, track by $index is a good solution as you really haven't other means to track a string.
track by & triggering a digest & input focuses
You allude to the fact you're somewhat new to angular. A digest cycle occurs when angular performs an exhaustive check of each watched property in order to reflect any change to the correspodant view; often during a digest cycle it happens that your code modify other watched properties so the procedure needs to be performed again until angular detects no more changes.
For example: You click a button to update a model via ng-click, then you do somethings (i mean, the things you wrote in the callback to perform when an user makes a click), then angular trigger digest cycle in order to refresh the view. I'm not too articulate in explaining that so you should investigate further if that didn't clarify things.
So back to track by. Let's use an example:
call a service to return an array of objects
update an object within the array and save object
after save service, depending on what the API returns, you may:
replace the whole object OR
update a value on the existing object
reflect change in ng-repeat UI
How you track this object will determine how the UI reflects the change.
One of the most annoying UXs I've experienced is this. Say you have a table of objects, each cell has an input where you want to in-line edit those objects' properties. I want to change the value, then on-blur, save that object while moving to the next cell to edit while you might be waiting on the response. So this is an autosave type thing. Depending on how you setup your track by statement, you may lose current focus (e.g. the field you're currently editing) when the response gets written back into your array of objects.
When you add track by you basically tell angular to generate a single DOM element per data object in the given collection.
You can track by $index if your data source has duplicate identifiers.
If you do need to repeat duplicate items, you can substitute the default tracking behavior with your own using the track by expression.
Example:
[{id:1,name:'one'}, {id:1,name:'one too'}, {id:2,name:'two'}]
Try to use the duplicate values in ng-repeat, you will get an error such as:
Error: ngRepeat:dupes Duplicate Key in Repeater
To avoid this kind of problems you should use track by $index. For example:
<ul>
<li ng-repeat="item in [1, 2, 3, 3] track by $index">
{{ item }}
</li>
</ul>
Here is how you would get $index in nested ng-repeat:
<div ng-repeat="row in matrix">
<div ng-repeat="column in row">
<span>outer: {{$parent.$index}} inner: {{$index}}</span>
</div>
</div>
Here are some resources that may help you:
track by $index documentation
ngRepeat documentation
2014 codelord.net article about ng-repeat performance and track by
You should use track by only if you need to go against the default behaviour of ng-repeat which is to remove duplicate items.
You can track the items using the scope property $index or specifying a custom function.
For instance:
<div ng-repeat="x in [42, 42, 43, 43] track by $index">
{{x}}
</div>
Display all values of the array (42 is displayed twice).
For reference: https://docs.angularjs.org/api/ng/directive/ngRepeat
Let's say, we have the following list:
<ul>
<li ng-repeat="item in items">
{{ item }}
</li>
</ul>
where, an item has the following structure:
{ 'id'=>id, 'name'=>name, 'description'=>description }
There is no problem whatsoever in this list until we wish to update it. For our own convenience we replace the list of items, with another updated list of items, as such:
items = newItems;
However, in this new list, few items change. Most items remain the same. Unfortunately Angular does not know how to identify our items and map them to the respective <li> elements, so it just deletes all elements and creates them again. This is extremely performance-costly in some cases and here is where track by comes in use.
By adding the track by clause to the element
<li ng-repeat="item in items track by item.id">
we are informing Angular, that the unique identifier of our items is item.id. Now Angular knows not to recreate all items, but only items with new ids, and only updates the rest. The performance improvement is significant in most cases. Also, personally, I like that I can monitor my items easier on my browser's developer tools, because they don't disappear every time I update them.

Global Method to Clone object in vuejs rather then reference it, to avoid code duplication

I am building simple CRUD div list with Vuejs, which allows me to perform all operations on single page as search through results by the means of filter.
The problem I face is that in order to do perform update I have to have two containers for each data object in the loop.
So 1st container acts like view, 2nd has the form with same values as view for editing purposes. I basically toggle them to switch from view to edit mode
It looks something like that:
<input v-model="search">
<items v-for="product in products | filterBy search in searchFields">
<item>
<div v-show="!edit">
Display Data "div"
<div>
<div v-show="edit">
Edit Data "form"
<div>
<item>
</items>
I have a problem with using "filterBy" when looping through items to search through them. Because data in 1st and 2nd container is bound upon editing field in edit mode the element disappears. As filter does not care whether the item is in edit mode or in view mode. It simply filters items, and when the value of item is changed and no longer equal to search value its gone.
I have solved the problem by simply cloning object of view element and passed cloned object to form element instead of referencing it by adding objectClone method on child item. Which sort of fixes my problem as the data in edit mode no longer bound to view mode. But this creates another problem, now I have to copy objectClone method on each new child element of the loop in order for this to work. Is there a way to clone element within vuejs without creating a dedicated method or is it possible to create global method objectClone to use it across all Vue elements?
Additional problem I get with my approach is that element after editing now cloned and I cant remove it from parent data container. As it no longer references the previous element that got cloned.

How to re run dom-repeat with sort when bool property changed in Polymer element

How can I rerun sort and render element when something changed in my element or whenever I want from code or automatically? Thank you
<template is="dom-repeat" items="[[attachments]]" as="attach_item" sort="_sortItems"></template>
You need to set an observer for the fields you want to watch.
Quoted from polymer docs:
By default, the filter and sort functions only run when the array itself is mutated (for example, by adding or removing items).
To re-run the filter or sort functions when certain sub-fields of items change, set the observe property to a space-separated list of item sub-fields that should cause the list to be re-filtered or re-sorted.
Example:
<template is="dom-repeat" items="{{employees}}"
filter="isEngineer" observe="type manager.type">
More info here: https://www.polymer-project.org/1.0/docs/devguide/templates.html#filtering-and-sorting-lists
PS: you need to set your field using this.set("array.index.field", value), otherwise your observer won't be notified

Canjs – When to use which mustache tag when iterating lists?

There seems to be a few different ways to do the same thing in Can.js, which is great! But some of the ways work a little differently than others, and can affect how the DOM is rendered and updated. I'd appreciate it if someone could clear up the nuance.
I think the scenario where this choice becomes interesting is when you want to have some default text or a placeholder for an empty list.
{{#if list}} and {{#if list.length}}
These are not the same. Both an empty array and a can.List will render for {{#if list}}.
fiddle
{{#each list}}
So using what we learned with #if...
{{#if list.length}}
<ul>
{{#each list}}
<li>...<li>
{{/each}}
</ul>
{{else}}
<i>The list is empty.</i>
{{/if}}
{{#list}}
I think this is intended to be the best of both worlds. It only occurred to me today that since this is a block helper, it supports the {{else}}.
{{#list}}
rendered for each item in list
{{else}}
rendered once if list is empty
{{/list}}
The thing is, this can't produce the html we did with #each.
Wrap the whole thing in a <ul> tag, it gets rendered regardless of the list being empty or not
Stick the <ul> tag in the first block (positive block? affirmative?) and it renders every time
So the implementation seems to be dependent on the markup. Fair enough.
Here's the rub.
Supposedly, #each and #list update the DOM differently. From the docs for #each...
If the value of the key is a can.List, the resulting HTML is updated when the list changes. When a change in the list happens, only the minimum amount of DOM element changes occur.
So add one item to the list, only that item is rendered, remove an item, only that element is removed. The behavior of #list is not documented, but I'm under the impression it may re-render the entire block.
Questions
Which is best? Besides being more terse, I'm not sure #list has any advantages, so why do the authors suggest that it is preferred?
Assuming list is a can.List instance:
{{#if list}}
will check for the truthy value of list. This is akin to checking for the truthy value of any JS object and will result to true, regardless of list contents or length.
{{#if list.length}} will check for the truthy value of the length attribute. If you have an empty list, the length will be 0, so the #if will result to false and no contents will be rendered. If there is a length >= 1, this will result to true and the contents of the #if will be rendered.
#each and #list both iterate through an instance of can.List, however we setup the bindings differently.
#each will setup bindings on every individual item being iterated through, while #list will not. This makes a difference in two scenarios:
1) You have a large list, with minimal updates planned after initial render. In this case, #list might be more advantageous as there will be a faster initial render. However, if any part of the list changes, the entire #list area will be re-processed.
2) You have a large list, with many updates planned after initial render. A grid with many columns of editable fields, for instance. In this case, you many want to use #each, even though it will be slower on initial render(we're setting up more bindings), you'll have faster updates as there are now many sections.
Quick note on your case of if list has contents ..., else empty list area:
{{#each items}}
Looping through list items!
{{/each}}
{{^if items.length}}
Show if there are no items!
{{/if}}
The above should account for that scenario. If there's additional logic, I would consider writing a custom helper(depending on what the logic looks like).

Categories

Resources