Performance of ng-repeat with dynamically generated array of data - javascript

Let's say I have an array of objects and that each object has a unique ID and a text property.
Now, let's say I have a ng-repeat like this one:
Controller
$scope.data = my_arr;
View
<div ng-repeat="for elem in data track by elem.id">
{{ elem.text }}
</div>
Special note on the track by.
What would happen if I assigned a completely new array to the data variable ($scope.data = new_arr;), if the content of this new array:
is completely identical to the previous one
it contains 1 new element
it contais all elements except 1
Will AngularJS be smart enough to:
not re-render all div elements
append/insert only the new div element to the DOM
hide/delete only the div that isn't contained in the new array

Since you’re using track by elem.id, Angular will reuse DOM elements. It will only add or remove one element to the document tree. Per the docs:
Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones.

Related

Lit-element that is sorting dynamic childs

I have 2 lit-element components. The parent content is :
<s-sortablelist>
${this.renderChildren()}
</s-sortablelist>
The renderChildren function is looping over an array property children = [1, 2] and creating <div> elements. The rendered page might be something like :
<s-sortablelist>
<div id="1"></div>
<div id="2"></div>
</s-sortablelist>
The sortablelist component allows the user to reorder the <div> tags using drag and drop. After dnd, the rendered layout might become (2 and 1 are reverted) :
<s-sortablelist>
<div id="2"></div>
<div id="1"></div>
</s-sortablelist>
After the user changed the order, if I change the property children=[3,4], the result is different from my expectations :
I'm expecting to see a new list with the children 3,4
I'm seeing a list with 3,4, and some other elements (1, 2) depending on the dnd operations I made before
So my question is how is it supposed to work ?
If the children array changes, because it is a property, the parent component will render.
I'm expecting also the sotablelist component to be rerendered, but why would I have extra children from a previous render?
You can't mutate the DOM under control of lit-html this much. lit-html places comment nodes into the DOM to keep track of where the dynamic template parts are, and by moving elements around you're breaking the bookkeeping.
The right way to do this is to not move nodes in the drag and drop operation, but right before you would have actually changed the DOM, instead change the data that rendered the DOM. Then lit-html can render the list on the new order, but keep all the comment node and other internal data in sync.

Removing last cloned element

Similar to this thread, I am trying to be able to add and remove select boxes for different parts of my document. However, when I call the remove function, it removes the first instance of my cloned object, instead of the last. I have tried using :last and :last-child, but they do not seem to be working(May just be a syntax error, as I am new to Javascript/Jquery)
Also, should I be assigning different id's to each of my cloned objects? My goal is for each g:select to select a database object, and compile all of the different objects text into 1 big string (each object has a 'documentBody' field that I want to compile). Since I am basically doing the same thing to each object, is it necessary for me to assign specific id's to each select, or will just cloning them be sufficient?
Here is what I currently have implemented
<div id="selects">
<g:select name="intro"
id= "intro" from="${package.name.Subtag.findAllWhere(tag: package.name.Tag.get(2))}" noSelection="['': 'Please choose Subtag']"/>
</div>
<button onclick="addSelect()">Add</button>
<button onclick="removeSelect(intro)">Remove</button>
and
<g:javascript library="jquery"/>
<g:javascript>
function addSelect(){
var cloner = $("#intro").clone();
$("#selects").append(cloner);
}
function removeSelect(id){
$("#intro:last-child").remove();
}
</g:javascript>
As Jai has mentioned, the issue is that you are cloning and appending an element with an id, creating a duplicate id on the page. When jQuery searches for an element by id, it stops at the first one found, regardless of any other pseudo classes.
The issue is that a duplicate id on the page means that the HTML document is invalid, so all bets are off. Older browsers may even throw an error. Using a class rather than an id prevents the .clone() function from copying the dupe id, but if you still need the first element's id, you can always remove it from your cloned object before appending it to the page:
cloner.removeAttr('id');

Understanding the ngRepeat 'track by' expression

I'm having difficulties understanding how the track by expression of ng-repeat in angularjs works. The documentation is very scarce: http://docs.angularjs.org/api/ng/directive/ngRepeat
Can you explain what the difference between those two snippets of code is in terms of databinding and other relevant aspects?
with: track by $index
<!--names is an array-->
<div ng-repeat="(key, value) in names track by $index">
<input ng-model="value[key]">
</div>
without (same output)
<!--names is an array-->
<div ng-repeat="(key, value) in names">
<input ng-model="value[key]">
</div>
You can track by $index if your data source has duplicate identifiers
e.g.: $scope.dataSource: [{id:1,name:'one'}, {id:1,name:'one too'}, {id:2,name:'two'}]
You can't iterate this collection while using 'id' as identifier (duplicate id:1).
WON'T WORK:
<element ng-repeat="item.id as item.name for item in dataSource">
// something with item ...
</element>
but you can, if using track by $index:
<element ng-repeat="item in dataSource track by $index">
// something with item ...
</element>
a short summary:
track by is used in order to link your data with the DOM generation (and mainly re-generation) made by ng-repeat.
when you add track by you basically tell angular to generate a single DOM element per data object in the given collection
this could be useful when paging and filtering, or any case where objects are added or removed from ng-repeat list.
usually, without track by angular will link the DOM objects with the collection by injecting an expando property - $$hashKey - into your JavaScript objects, and will regenerate it (and re-associate a DOM object) with every change.
full explanation:
http://www.bennadel.com/blog/2556-using-track-by-with-ngrepeat-in-angularjs-1-2.htm
a more practical guide:
http://www.codelord.net/2014/04/15/improving-ng-repeat-performance-with-track-by/
(track by is available in angular > 1.2 )
If you are working with objects track by the identifier(e.g. $index) instead of the whole object and you reload your data later, ngRepeat will not rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones.

advice on datastructure for ajax/javascript/jquery page

This is a JavaScript/Ajax webpage (also using jQuery).
I have a nested structure I need to display. After displaying a top level element, users can click on it, and see levels below it (dynamically generated).
I don't want to pre-generate everything and hide it with display: none (the page is complex, I'm simplifying for this question) - I want to build the display from the javascript array that was fetched with ajax.
My question:
I have two options:
1: Create a flat array:
[ {id: xx, children: [ xx, xx, .. ] }, ....]
Then for the onclick of an element I get the id from the array, find the children, pull them up from the array and display them. (I guess I'll have to search through the array, since there are no associative arrays in javascript - or make an index.)
2: Create a nested array:
{ id: xx, children [ { id: xx, children : [....] }, {....} ] }
Then somehow bind the children in the array to the element when I display it.
I have two problems with this second approach:
A: I'm constantly copying large chunks of the array for each child when I create it. (At least I think I am. Do I need to use deep copy? Can I make a reference?)
B: I'm not sure how to bind the data to the child element. Normally I build the display using html strings with onClicks, then append the entire thing. But onClicks can only take an ID, not a copy of an array.
I did something similar recently where I had a very large nested structure (over 2000 nodes) - which I did not want to bulk append to the DOM.
What I ended up doing was taking the ajax loaded data and converting it into a nested structure...
<node id="1" title="a">
<node id="2" title="b />
<node id="3" title="b">
<node id="4" title="d" />
</node>
</node> etc...
...and storing this as a jQuery object (nodes), but never appending it to the DOM.
I could then select the immediate children of a node as I needed them relatively easily, for converting into html elements and appending to the DOM, adding data, etc...
$("#"+ID+">node", nodes).each(function() {
var node = $(this);
//do whatever...
});
I don't know if this is the most memory-efficient approach, but it certainly makes it very easy to select and append the immediate children of a node to the DOM as you need them.
I would prefer to use the second approach, for the reason it has a better structure as well as you can write less code as recursive comes into play.
You say that your not sure how to bind the child elements to the array without actually creating dom elements, well if you use <!DOCTYPE html> for html you elements can have html-* attributes allowing you to store data in an element, example:
<ul id="lists">
<li class="parent" id="root_22" data-children="{some object}">A Root Elelment</li>
</ul>
the problem with this method is that you would have to store every children of children in the root element, which more than likely is a overhead.
Another way is to bind the data using jQuery.data method, this will keep the DOM clean but will atatch data to an element.
Store arbitrary data associated with the specified element.
#see: http://api.jquery.com/jQuery.data/

How does JavaScript know where an element is the DOM?

I was writing some code in jQuery, like:
$(".middleContent").not(this).slideUp();
(this = currently active .middleContent element)
And I was wondering how JavaScript knew the elements index in the DOM.
I know each object is unique, but if you have a few elements with the same class how does it distinguish between them? It is to do which its index in the tree of all the elements, like how a program has an address in RAM?
Each dom element is an individual object and unique. The not does comparisons on the current execution context ( this ) to make sure that any element inside the array doesnt equal this.
I think you're underestimating what it means for a DOM element to be unique. It's not only the class, tag name or index within the current parent element that identifies a DOM element. Each DOM element internally has a unique identifier, which is not accessible to you. It's used by the browser to organize the DOM internally. There can be hundreds of seemingly identical <div class="middleContent" /> elements in your page, each single one has a unique internal identifier. If you compare one DOM element to another, the browser will always be able to tell whether it's the same element or one that just looks like it.
this refers to one specific DOM element, therefore jQuery is able to filter it out of a collection of seemingly similar elements.
The elements in the DOM are just objects themselves, organised into a tree structure, so they have next and previous siblings at the same level, their own list of children, a parent. From this you can walk around the structure of the tree and manipulate it.
You can obtain the object(s) inside a jQuery object by using indexing notation:
var caption = $('#caption');
var domElement = caption[0];
Then domElement will contain one of these.

Categories

Resources