I have been reading many threads on using a directive or $last to call a function at the end of an ng-repeat, even two level nested ng-repeats, but nothing regarding the level of nested ng-repeats I have. I think the issue is related to autoloading a new page/view while the ng-repeat is still rendering elements...the new page loads but going back to the list the ng-repeat created causes weird things to happen.
Simple example:
<div ng-repeat="products in categories" ng-init="$last && listLoaded()"> // length = 12
<div ng-repeat="product in products">
<div ng-if="product.type == 1" ng-repeat="feature in product.features"> // length = 20
<div ng-if="feature.specs == 1" ng-repeat="specs in feature.specs"> // length = 40
</div>
<div ng-if="feature.desc == 1" ng-repeat="desc in feature.desc"> // length = 4
</div>
<div ng-if="feature.reviews > 0" ng-repeat="review in feature.reviews"> // length = 125
<div>{{review.review}}
<div ng-if="review.pictures > 0" ng-repeat="pictures in review.pictures"> // length = 3
<img src="blah.com/imgs/{{picture.picURL}}" id="picture.pictureID" ng-click="enlargeImage(picture.pictureID)">
</div>
</div>
</div>
</div>
</div>
$scope.listLoaded = function() {
if ($stateParams.prodID) {
//external ID passed in, autoload featurePage
$state.go("tabs/search/" + featurePage", {product : productID}) ;
}
}
$scope.enlargeImage = function(id) {
var el = document.getElementById(id) ;
el.style.border = "1px solid red" ;
el.style.display = "block" ;
console.log(el) ;
angular.element('#'+id).border = "1px solid red" ;
angular.element('#'+id.display = "block" ;
}
As you can see, there are multiple nested ng-repeats, all of varying lengths that will all have vastly different rendering times. I need to execute a function when ALL items, from all nested ng-repeats are done rendering, the function listLoaded calls to another tab/view/page.
When an external ID is passed in it autoloads a feature page. It all works, featurePage is loaded with all the correct info. However, when I go back to the list page, the full page is there, but when clicking on the images, though the function is executing, the images aren't getting the styles applied to them.
The console.log output of el shows the styles applied to the element, but in Developer Tools, in the Elements -> Styles section, the styles aren't actually being applied to the element and visually none of the image styles are changing.
console.log output:
<img src="blah.com/imgs/{{picture.picURL}}" id="picture.pictureID" ng-click="enlargeImage(picture.pictureID)" style="border:1px solid red;display:block;">
After an autoload to the featurePage, if I refesh the product list page, the same elements that weren't rendering style changes still have issues, they won't change. The view is cached. When I remove page caching - everything works properly. So, the issue must be related to the DOM elements not fully rendering or being added to the DOM.
When I comment out the listLoaded() function and don't force an autoload of the featurePage, the full list renders and all the ng-clicks work properly. I can then manually click on a specific product to go to the featurePage - and then back to the list again and everything still works. I think the issue has something to do with the angular.element not fully loading before the page view is auto-loaded to the featurePage - but I am not certain. All I know is everything works when I don't auto-redirect to the featurePage
I even tried a directive on the first ng-repeat and still have the same issues:
.directive('listRepeatDirective', function() {
return function(scope, element, attrs) {
angular.element(element).css('color','blue');
if (scope.$last){
scope.listLoaded() ;
}
};
})
Related
I need to select the element by class 'gm-style-iw' to fix some styles in Infowindow. The selection is happening inside of the angularjs directive.
<div ui-view="full-map" id="full-map" class="mainMap col-xs-6"></div>
ui-view - loading a directive with IW content. map is initialized inside that directive.
on the directive's controller initialization i have to edit element with class 'gm-style-iw'.
var iwElem = $document[0].getElementsByClassName("gm-style-iw")
does return the correct element.
console.log(iwElem) result is:
[]
length: 1
0: div.gm-style-iw
__proto__: HTMLCollection
However i'm stuck after it.
it as an HTMLCollection, which is the array of HTML elements, as i understand. => i must be able to get this 0 element by iwElem[0], the strange thing is that iwElem[0] returns undefined.
Also tried with jquery selectors:
$('.gm-style-iw') => length:0
$('div.gm-style-iw') => length:0
If you are sure that your html is something like
<div ui-view="full-map" id="full-map" class="mainMap col-xs-6">
...some codes
<div class="gm-style-iw"></div>
... other codes
</div>
And if your are using JQuery, how about trying the following selector?
$('.mainMap .gm-style-iw')
which means getting the child node of class .gm-style-iw under the parent node of class .mainMap
The problem is partially solved.
$timeout(function() {
removeIwStandardStyles()
},300);
function removeIwStandardStyles () {
var iwOuter = $(iwOuterTemp);
while (iwOuterTemp.length < 1){
iwOuterTemp = document.getElementsByClassName("gm-style-iw");
}
var iwBackground = iwOuter.prev();
iwBackground.children(':nth-child(2)').css({'display' : 'none'});
iwBackground.children(':nth-child(4)').css({'display' : 'none'});
}
$document[0].getElementsByClassName("gm-style-iw") was catching a corrent elements, but seems like they was not applied to DOM yet, or some other load issues.
However, the solution is not complete (see a while) - i suppose i can do that cause that element is definitely going to load. $timeout itself isn't helping completely - sometimes it does wait till complete IW load, sometimes not.
Let's say I have a lot (3000+) of items I want to render (in a ng-repeat) in a div with a fixed height and overflow: auto, so I'd get N visible items and a scrollbar for the rest of them.
I'm guessing doing a simple ng-repeat with so many items will probably take a lot of time. Is there a way I can make AngularJS render only those visible N items?
Edit:
An infinite scroll is not what I want. I want the user to be able to scroll to any point of the list, so I literally want a text editor-like behavior. Said with other words: I'd like the scroll to contain the "height" of all the items, but place in the DOM just a few ones.
This answer provides an approach for lazy-rendering only items currently in-view, as defined by the edit to the original question. I want the user to be able to scroll to any point of the list, so I literally want a text editor-like behavior. Said with other words: I'd like the scroll to contain the "height" of all the items, but place in the DOM just a few ones.
Install the angular-inview plugin before trying this.
In order to get your scrollheight you'd need something holding the space for your array items. So I'd start with an array of 3000 simple items (or combine with infinite scroll to whatever extent you want.)
var $app = angular.module('app', ['infinite-scroll']);
$app.controller('listingController', function($scope, $window) {
$scope.listOfItems = new Array($window._bootstrappedData.length);
$scope.loadItem = function($index,$inview) {
if($inview) {
$scope.listOfItems[$index] = $window._bootstrappedData[$index];
}
}
});
Since we're talking about flexible heights, I would create a placeholder for what your content looks like pre-render.
<div ng-controller="listingController">
<ul>
<li ng-repeat="item in listOfItems track by $index" in-view="loadItem($index,$inview)" style="min-height:100px"><div ng-if="item">{{item.name}}</div></li>
</ul>
</div>
Using ng-if will prevent rendering logic from being run unnecessarily. When you scroll an item into view, it'll automatically display. If you want to wait a second to see if the user is still scrolling you could set a timeout in the loadItem method that cancels if the same index gets pushed out of view within a reasonable time period.
Note: If you truly wanted to avoid putting anything in the DOM, you could set your scrollable area to a specific multiple of your "placeholder" height. Then you could create a directive that uses that height to determine the indexes of the items that should be displayed. As soon as you display new items, you'd need to add their heights to the total and make sure you position them at the right spot and make sure your directive knows how to interpret those heights into evaluating the next set of displayed elements. But I think that's way too radical and unnecessary.
Expanding on Grundy's point of using .slice().
I use ngInfiniteScroll when I need to lazy-render/lazy-load data.
I would keep those 3000 records out of your scope to prevent weighing down your digest performance unnecessarily and then append them to your scope data as you need them. Here's an example.
var $app = angular.module('app', ['infinite-scroll']);
$app.controller('listingController', function($scope, $window) {
/*
* Outside of this controller you should bootstrap your data to a non-digested array.
* If you're loading the data via Ajax, save your data similarly.
* For example:
* <script>
* window._bootstrappedData = [{id:1,name:'foo'},{id:2,name:'bar'},...];
* </script>
*/
var currentPage, pageLength;
$scope.listOfItems = [];
currentPage = 0;
pageLength = 100;
$scope.nextPage = function() {
// make sure we don't keep trying to slice data that doesn't exist.
if (currentPage * pageLength >= $window._bootstrappedData.length) {
return false;
}
// append the next data set to your array
$scope.listOfItems.push($window._bootstrappedData.slice(currentPage * pageLength, (currentPage + 1) * pageLength));
currentPage++;
};
/*
* Kickstart this data with our first page.
*/
return $scope.nextPage();
});
And your template:
<div ng-controller="listingController" infinite-scroll="nextPage()" infinite-scroll-distance="3">
<ul>
<li ng-repeat="item in listOfItems">{{item.name}}</li>
</ul>
</div>
I'm trying to render and refresh every few seconds a list of complicated objects using Angular. An issue I've found is that when the refresh happens, even if a particular HTML subcomponent hasn't changed, the HTML is updated and if you'd selected some of the text (e.g. you were trying to copy it) the selection goes away.
I know there is a general issue with changing the html that contains a selection, but I'm wondering if Angular has some solution to the problem that I'm just not aware of. Basically what I'm looking for is for only the HTML that actually changed being updated. I could do that if I was writing view code manually in jQuery, but every other part of doing it manually is awful
JS:
angular.module('items', [])
.factory('itemList', ['$http', function($http) {
var items = [];
var refresh = function() {
// imagine that this makes an HTTP call to get the new list
// of items
items.length = 0;
for (var i = 0; i < 10; i++) {
items.push("item " + Math.random(1, 10))
}
}
refresh();
return {
items: items,
refresh: refresh
};
}]);
var app = angular.module('app', [
'items'
]);
app.controller('ItemListController',
['$scope', 'itemList', '$interval',
function($scope, itemList, $interval) {
this.items = itemList.items;
$interval(itemList.refresh, 2000)
}
]);
HTML:
<body ng-app="app">
<div ng-controller="ItemListController as controller">
<h3>Items</h3>
<div ng-model="active">
<div ng-repeat="item in controller.items">
<div class="header">Header</div>
<div>{{item}}</div>
<hr/>
</div>
</div>
</div>
</body>
As you're wholesale replacing itemList on each refresh, angular has no option but to re-create all the elements in the ng-repeat, which is fine if you don't mind losing the selection and the refresh isn't too large and expensive. To prevent this though, you could try writing a merge in that factory that diffs the previous against the new and adds/removes items without replacing the whole reference. Then only if the item you selected no longer exists would you lose the selection.
Also, if the list is long and the differences from refresh to refresh are small, then this will probably be more efficient.
$interval refreshes entire DOM So selection is get refreshed and new value will appear.
if you want to select then cancel the interval for particular time $interval.cancel(milliseconds) then restart the timer with selected range.
$scope.$watch("refresh", function(){
$interval.cancel(5000);
p = $interval(itemsList.refresh(), 2000);
})
I figured it out. Thor's answer was on the right track. The issue was that I wasn't using "track by FOO" on my ng-repeat, so it had no way of knowing that the elements in my actual case with complex objects were the same and thus it redrew them
I'm looking for a way to integrate something like ng-repeat with static content. That is, to send static divs and to have them bound to JS array (or rather, to have an array constructed from content and then bound to it).
I realize that I could send static content, then remove and regenerate the dynamic bits. I'd like not to write the same divs twice though.
The goal is not only to cater for search engines and people without js, but to strike a healthy balance between static websites and single page applications.
I'm not sure this is exactly what you meant, but it was interesting enough to try.
Basically what this directive does is create an item for each of its children by collecting the properties that were bound with ng-bind. And after it's done that it leaves just the first child as a template for ng-repeat.
Directive:
var app = angular.module('myApp', []);
app.directive('unrepeat', function($parse) {
return {
compile : function (element, attrs) {
/* get name of array and item from unrepeat-attribute */
var arrays = $parse(attrs.unrepeat)();
angular.forEach(arrays, function(v,i){
this[i] = [];
/* get items from divs */
angular.forEach(element.children(), function(el){
var item = {}
/* find the bound properties, and put text values on item */
$(el).find('[ng-bind^="'+v+'."]').each(function(){
var prop = $(this).attr('ng-bind').split('.');
/* ignoring for the moment complex properties like item.prop.subprop */
item[prop[1]] = $(this).text();
});
this[i].push(item);
});
});
/* remove all children except first */
$(element).children(':gt(0)').remove()
/* add array to scope in postLink, when we have a scope to add it to*/
return function postLink(scope) {
angular.forEach(arrays, function(v,i){
scope[i] = this[i];
});
}
}
};
});
Usage example:
<div ng-app="myApp" >
<div unrepeat="{list:'item'}" >
<div ng-repeat="item in list">
<span ng-bind="item.name">foo</span>
<span ng-bind="item.value">bar</span>
</div>
<div ng-repeat="item in list">
<span ng-bind="item.name">spam</span>
<span ng-bind="item.value">eggs</span>
</div>
<div ng-repeat="item in list">
<span ng-bind="item.name">cookies</span>
<span ng-bind="item.value">milk</span>
</div>
</div>
<button ng-click="list.push({name:'piep', value:'bla'})">Add</button>
</div>
Presumable those repeated divs are created in a loop by PHP or some other backend application, hence why I put ng-repeat in all of them.
http://jsfiddle.net/LvjyZ/
(Note that there is some superfluous use of $(), because I didn't load jQuery and Angular in the right order, and the .find on angular's jqLite lacks some features.)
You really have only one choice for this:
Render differently for search engines on the server, using something like the approach described here
The problem is you would need to basically rewrite all the directives to support loading their data from DOM, and then loading their templates somehow without having them show up in the DOM as well.
As an alternative, you could investigate using React instead of Angular, which (at least according to their website) could be used to render things directly on the web server without using a heavy setup like phantomjs.
I am creating a mobile app (Phonegap/Cordova 1.5.0, JQM 1.1.0) and testing on iOS 5.1. I have a list of items that the user "owns" or wants to own. Throughout the app, the user can edit their list by adding and removing items. Whenever items are added or removed, the list updates, and it is displaying fine, with all of the JQuery CSS intact except the corners are no longer rounded (I'm thinking because data-inset is getting set to "false").
Here is my html for the list-headers:
<div data-role="page" id="profile">
<div data-role="header" data-position="fixed">
<...>
</div><!-- /header -->
<div data-role="content" data-theme="a">
<...>
<ul id="user-wants-list" data-role="listview" data-inset="true" data-theme="d" data-dividertheme="d" >
</ul> <!--/Wants list-->
</br>
<ul id="user-haves-list" data-role="listview" data-inset="true" data-theme="d" data-dividertheme="d" >
</ul> <!--/Has list-->
</br></br>
</div> <!--/content-->
</div> <!--/Profile-->
And here is the Javascript where I remove the old list and dynamically add the new one (the parameter 'haves' is an array of objects):
function displayHaves(haves){
var parent = document.getElementById('user-haves-list');
removeChildrenFromNode(parent);
parent.setAttribute('data-inset','true');
$(parent).listview("refresh");
var listdiv = document.createElement('li');
listdiv.setAttribute('id','user-haves-list-divider');
listdiv.setAttribute('data-role','list-divider');
listdiv.innerHTML = "I Have (" + haves.length + ")";
parent.appendChild(listdiv);
//create dynamic list
for(i=0;i<haves.length;i++){
var sellListing = haves[i].listing;
var userInfo = haves[i].user;
var itemData = haves[i].item;
//create each list item
var listItem = document.createElement('li');
listItem.setAttribute('id','user-haves-list-item-'+i);
parent.appendChild(listItem);
var link = document.createElement('a');
link.setAttribute('id','user-haves-link-' + i);
new FastButton(link, function(listing) {
return function() { displaySellListingPage(listing); }
}(sellListing));
listItem.appendChild(link);
var link = document.getElementById('user-haves-link-' + i);
var pic = document.createElement('img');
pic.setAttribute('src',itemData.pictureURL);
pic.setAttribute('width','80px');
pic.setAttribute('height','100px');
pic.setAttribute('style','padding-left: 10px');
link.appendChild(pic);
var list = document.getElementById('user-haves-list');
$(list).listview("refresh");
}
}
and my function removeChildrenFromNode(parent) is as follows:
function removeChildrenFromNode(node){
while (node.hasChildNodes()){
node.removeChild(node.firstChild);
}
}
So my question is, why does the listview lose the data-inset attribute?
Or, equally valid: is there another way I could/should be achieving corner rounding besides "data-inset='true'"?
Here are things I have tried:
using .trigger("create") on both the listview and the page
adding the listview with explicit styling each time by using $("#page-ID").append(...)
I read another post on StackOverflow that said that JQM creates some inner elements when you create an item (this post had to do with dynamic buttons not being the right size), and that there are some classes (like .ui-btn) that you can access (that may be losing styling when I remove the children from the node?), but I was unable to make any headway in that direction.
Thanks in advance for the help!
I figured out the answer to my question, but not a solution (yet).
$(list).listview('refresh') was getting called on some elements before they had been put on the page, so it was essentially being called on nothing (or another way to think about it is that each list item being appended happens after the refresh call, so it overrides some of the visual styling).
I know the problem has to do with asynchronous loading in javascript. Essentially, the .listview('refresh) executes before the earlier code, which creates the elements but takes longer to execute. I understand the reasoning behind the design, but is there some way to get around this in this case?
I am thinking some conditional that I could set, like:
var doneLoading = false;
//Then when finished set doneLoading to 'true'
if(doneLoading) $(list).listview('refresh');
but if the refresh gets called first, I figure that doneLoading will just evaluate to false and then not execute once the list is actually done loading.
Is there any kind of onComplete callback I can use, or a way to make it happen synchronously?
Try calling listview(refresh) after updating the HTML.