Does ngFor directive re-render whole array on every mutation? - arrays

Let's say we have an array of items:
items = [
{ title: 'item 1'},
{ title: 'item 2'},
/* ... */
];
And there is a template that renders this array:
<ul>
<li *ngFor="let item of items">{{item.title}}</li>
</ul>
Wll angular2 rerender the whole array if I add/remove items via push/splice or will it only add/remove the markup for the corresponding items? If it does updates only, then is there any difference in mutation stategies -- should I prefer push/splice over array replacing? In other words, are these two approaches equivalent in term of rendering performance:
/* 1: mutation */
this.items.push({ title: 'New Item' });
/* 2: replacement */
var newArray = this.items.slice();
newArray.push({ title: 'New Item' });
this.items = newArray;

In addition to Gunter's answer, if you want to know which part of your UI is rendered/re-rendered you can with Chrome (even independent from any lib/framework) :
Open your debug panel
Menu (of debug panel) / More tools / Rendering
You should then see the following panel :
Toggle the Paint Flashing option on, and have some fun with your list.
If an area is flashing green, it has been painted / re-painted 👍.
EX : If you take the Plunkr in Gunter's answer : http://plnkr.co/edit/oNm5d4KwUjLpmmu4IM2K?p=preview and toggle the Paint Flashing on, add an item to the list and you'll see that previous items do not flash. (which means there's no repaint).

No , it re-renders only when the array itself is replaced by a different array instance.
update
Thanks to Olivier Boissé (see comments)
Even when a different array instance is passed, Angular recognizes if it contains the same item instances and doesn't rerender even then.
See also this StackBlitz example
If the used IterableDiffer recognizes and addition or removal at the beginning or in the middle, then an item is inserted/removed at that place without re-rendering all other items.
The animations demonstrated in Plunkers in answers of this question How can I animate *ngFor in angular 2? also demonstrate that. In fact this kind of animation was a driving factor to get this implemented this way (in addition to general optimization)

Related

React-slick Slider stays on same page if slides items are changed

I am stuck with an issue with react-slick. The subject line may not make sense, but I will try to explain the scenario in detail. See this example fiddle to see the issue in action.
var DemoSlider = React.createClass({
getSlides(count) {
var slides = [];
for(var i = 0; i < count; i++) {
slides.push((<img key={i} src='http://placekitten.com/g/400/200' />));
}
return slides;
},
render: function() {
var settings = {
dots: false,
infinite: false,
slidesToShow: 3,
slidesToScroll: 3
}
var slides = this.getSlides(this.props.count);
return (
<div className='container'>
<Slider {...settings}>
{ slides }
</Slider>
</div>
);
}
});
In this demo, initially the slider shows 20 slides (3 per page). The idea is that if you click the button, it will generate a random number, which would be the new number of slides. The code is fairly simple and self-explanatory.
To reproduce the problem,
1. keep on clicking Next arrow until you reach the last slide.
2. click on the button that says 'Click' to generate a new random number of slides.
My expectation was that the slide will go back to the first slide but not to stay on the page where it previously was. Even worse, if the new number of slides is lower than the previous number, the user will see a blank page with no slides. On clicking Previous error, he/she can go to the previous slides, and everything works normally but the initial display ruins the user experience.
Is there something I am missing to add in the code, or is this a bug?
Thanks,
Abhi.
I would say it is rather a buggy behavior, but still you can achieve what you want by forcing a redraw after new collection has been passed as props, by resetting react's key:
<DemoSlider key={Date.now()} count={this.state.count}/>
UPDATED FIDDLE
Quick workaround: When you change to a new set of slides, remove the component, and reconstruct it again. It would then start with the first slide. You can do this by incrementing the key attribute of DemoSlider.
For a proper fix, you'd need to tell the component to change the 'current slide', so that it's not looking at a slide index beyond the end, but a casual look at the source at https://cdnjs.cloudflare.com/ajax/libs/react-slick/0.11.0/react-slick.js suggests it currently does not allow this. A change would need to be made to InnerSlider.componentWillReceiveProps() to check state.currentSlide against props.slidesToShow and state.slideCount.
It would also benefit from allowing currentSlide to be passed as props.

Bootstrap dropdown in column header

I've been trying to use the ng-grid 3.0 (ui-grid). I have managed to populate my grid and it's been very responsive and it's features are really amazing. But I'm trying to customize my column headers, as I need more info there.
I can create a custom header cell template, as indicated in the docs, but I don't seem able to use a Bootstrap Dropdown there, it gets cropped and I can't use it at all. Some googling got me thinking it is probably some issue with the overflow attributes, but still I can't solve it. My grid options is as follows:
$scope.columnDefs = [
{ name:'name', displayName: 'Vdd', headerCellTemplate: 'headerTemplate.html' },
{ name:'gender', headerCellTemplate: 'headerTemplate.html' },
{ name:'company' }
]
$scope.gridOptions = {
columnDefs: $scope.columnDefs,
rowTemplate: 'rowTemplate.html',
data: 'data'
};
I have forked an example in plunkr and managed to reproduce my issue:
http://plnkr.co/edit/qdrFiifiz18fxB8w6Aja?p=preview
I want to replace the built-in dropdown menu (since it doesn't seem to allow nesting and sub-menus) and add another one (so in the end, I'd have two dropdown menus in each header cell)
Any help is appreciated =)
I am proud to say I think I've figured it out. I've been digging through ui-grid's source code and narrowed it down to this block (lines: 2847 - 2852).
function syncHorizontalHeader(scrollEvent){
var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
if (containerCtrl.headerViewport) {
containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
}
}
I noticed that containerCtrl.headerViewport.scrollLeft was never getting set to newScrollLeft. A quick google search led me to this StackOverflow thread which says that you can't set the scrollLeft property of an element if it's overflow is set to visible.
My solution was to replace containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); with containerCtrl.headerViewport.style.marginLeft = -gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid) + 'px'; which just sets a negative margin on the header. Then add an overflow:hidden; style to .ui-grid-render-container-body to hide headers that extend beyond the main grid viewport.
Doing this messed up the placement of column menus, but there is an easy fix. On line 514 replace var containerScrollLeft = renderContainerElm.querySelectorAll('.ui-grid-viewport')[0].scrollLeft; with var containerScrollLeft = renderContainerElm.querySelectorAll('.ui-grid-viewport')[0].style.marginLeft; to use the margin instead of the scroll value in the menu placement calculation.

AngularJS ngOptions select list behaves inconsistently in IE 9

This jsFiddle hopefully demonstrates the problem I am facing with IE 9. I am roughly doing the same things I am doing in my real app. I have a list of stuff that I loop through using ngRepeat. Each item has a property that can be selected from a dropdown list which is rendered using ngOptions.
The problem in IE seems to be that selecting an item from the list jumps back to the previous item in the list. Chrome does not have this problem, the item selection is perfect. In the example, try selecting the value 'Imported' in the Fiddle - I could simply not do it in IE whereas it works perfectly in Chrome.
The only thing I can think of that could be a problem is the IDs for the dropdown which are currently just plain old string values like '20', '30' and '40'.
The code for ngOptions is
<select class="form-control" id="selProc" data-ng-model="currentPlant.ProcType" data-my-dropdown='Plants' data-ng-options="plant.SpecialProcurement as plant.Description for plant in Plants"></select>
currentPlant here is the looped variable.
The dropdown values are the following array:
var dropDownValues = [{
Plant: '2150',
SpecialProcurement: '20',
Description: 'External'
}, {
Plant: '2150',
SpecialProcurement: '30',
Description: '3rd Party'
}, {
Plant: '2150',
SpecialProcurement: '40',
Description: 'Imported'
}];
These dropdown values get populated in a custom directive's link function. In my real app the list gets fetched using $http.post() and the result is assigned to the dropdown variable using JSON.stringify. Regardless of whether this is good practice or not, the problem still persists. If the problem is because of the JSON.stringify, I would be keen to know. I have tried replacing that with a regular array assignment but the result is the same.
I'm stumped. Please help.

How do I prepend a new item to an ng-repeat list?

I have situations where I need to prepend an item to a list that is initially generated using ng-repeat. How do I do this?
<div ng-click="prependItem()>Click Here</div>
<div ng-repeat="item in items">
<div class="someClass">Item name: {{item.name}}</div>
<div class="anotherClass">Item type: {{item.type}}</div>
</div>
If I click on prependItem() I want want the new item to be added to the top of the list. Obviously, I don't want to regenerate the entire ng-repeat. I've been unable to find any documentation that would explain how to do this. Thank ahead of time for any help!
scope.prependItem = function (newItem) {
items.unshift(newItem);
};
AngularJS is smart enough to know the addition, and only create html element for it
http://plnkr.co/edit/qzIfzSP6buiQ49rDreNk?p=preview
You can see from console that only the newly added item will log messages
ng-repeat will rebuild the list if you add an item to the front of your array. If you add it to the end it's smarter in that it will only update the DOM to reflect the change for the one item you've added in.
Because you've added an item to the front, it has to move everything in the DOM so I think it just rebuilds it as it's easier to do than moving (don't quote me on that though lol). It isn't necessarily a bad thing to rebuild that list (unless it's huge, you won't even notice the refresh, if it is very big, I'd recommend showing a spinner whilst it rebuilds the html, that's the approach we've taken at work; since the user has clicked the button they're expecting the interaction so the spinner seemed like the best compromise whilst angular rerendered).

Is there a better way to have dynamic text appear next to a combobox?

I have a combobox, and based on the current selection, I would like dynamic text to appear beside the combobox.
My current solution works, but seems kludgy and fragile. It may not work at all depending on where the combobox appears in the DOM.
Here is the crux of my current solution (called when dropdown changes):
var child = owner.el.first().next().first().first().first().next().first();
if (child.dom.childNodes.length == 3) {
child.createChild({
tag: 'span',
html: c + Ext.id()
});
} else {
child.last().replaceWith({
tag: 'span',
html: c + Ext.id()
});
}
I'm mostly concerned about the first line...this can't be a good way of finding an insertion point.
Here is a pic of the combo box with the dynamic text appearing beside:
And here is what I looked at to find where I wanted to insert the text:
Can someone suggest a better way of achieving this effect? Thanks.
Have the span in the page already and grab it by its id.
Ext.get('combo-suffix').insertHTML(theText);
Or, grab the combo box element by its id and get/add its sibling.
Will it always be in a div called "ext-gen82"?
If so, why not use the jquery find method?
$("#ext-gen82").find("ext-gen107");
Alternatively you could give them their own class to help you find them as well:
$(".parentClass").find(".spanElement");
You could also use the children method:
$("#ext-gen82").children("span");
#OrangeDog pointed me the right direction, but I'll post a more detailed answer here for completeness/future reference.
var inserted = false; // need to set this up initially
...
var pos = Ext.getCmp('combo-element-id').el.next('.x-form-trigger');
if (inserted) {
pos.next().update(c + Ext.id());
} else {
inserted = true;
pos.insertSibling({
tag: 'span',
html: c + Ext.id()
}, 'after');
}

Resources