Does $last mean that ng-repeat elements are already rendered? - javascript

The Problem
if(scope.$last)
{
//get container height
}
I've been thinking that it's the proper way of getting height of container which has ng-repeated elements inside. I've reached the conclusion that it's not the proper approach.
Have a look at my directive:
AdminApp.directive("ngExpander", function($rootScope, $timeout){
var GetProperContainerHeight = function(container, content){
var container = $(container);
var content = $(content);
container.height(content.outerHeight());
return container.height();
}
return{
restrict:'A',
link:function(scope, elem, attr){
if(scope.$last){
$timeout(function(){
$rootScope.ContainerHeight = GetProperContainerHeight(attr.container, attr.content);
}, 500);
}
}
}
});
If I hadn't added $timeout, the directive wouldn't have worked properly, because it wouldn't have returned proper container height (some negative value I achieved then).
The Background
Directive works here:
<div class="SwitchContent" data-ng-show="ShowContent" id="testId">
<div class="user-list-element"
data-ng-repeat="user in UserList | filter:userFilter track by $index"
data-ng-click="GetUserDetails(user);"
data-ng-expander
data-container="div.UserPanel"
data-content="div[id=testId]">
<i class="fa fa-user fa-lg padding-r-10"></i> {{ user.name + ' ' + user.surname }}
</div>
</div>
How can I achieve proper results without $timeout?

You cannot do it without timeout, because Angular rendering happens asynchronously after your directive is compiled. But what you don't need is a number 500 which you probably don't like.
Just use the $timeout without second parameter.
$timeout(function(){
$rootScope.ContainerHeight = SetProperContainerHeight(attr.container, attr.content);
});
This puts the task in the browser queue without any delay. You will still have some small time between the moment when browser stopped rendering and your timeout start but this is the closest you can do.
EDIT
Probably the problem is in your scope.$last method. What you can do instead of if(scope.$last){ is watching the HTML content:
link:function(scope, elem, attr){
scope.$watch(function() {
return elem.html();
}, function() {
$timeout(function(){
$rootScope.ContainerHeight = GetProperContainerHeight(attr.container, attr.content);
});
});
}
Although it can look ugly it always works for me. Whenever HTML of the element is changed (and it will be as long as ng-repeat changes the element's inner content) the watcher event happens.
You still need a timeout here because HTML is changed but it is not rendered yet, so you need to put it to asynchronous browser queue with the timeout.
This also has another advantage: current height is always reflected in your rootScope property while your method will only do it once on the directive load.

Related

angularjs - get ng-repeat element using element.html() inside a jsPanel directive

I am trying to create a directive which will take the html content and place it n a jsPanel element.
So far I am able to compile the scope and able get elements with the scope. Then i tried to get html tag with ng-repeat, where i got stuck.
Following is the code used to create the directive
mainApp.directive('jspanelcontent', function($compile) {
console.log("loading");
var directive = {};
directive.restrict = 'AE';
directive.compile = function(element, attributes) {
var linkFunction = function($scope, element, attributes) {
console.log(element.html());
var contenthtml = element.html();
contenthtml = angular.element($compile(contenthtml)($scope));
element.html("");
$scope.jspanleinstance = $.jsPanel({
position: {
my: "left-bottom",
at: "left-bottom",
offsetY: 15
},
theme: "rebeccapurple",
contentSize: {
width: 600,
height: 350
},
headerTitle: attributes.panelcaption,
content: contenthtml,
});
// });
}
return linkFunction;
}
return directive;
});
And using like following in html
<div jspanelcontent panelcaption="list">
<div ng-controller="ListController">
{{test}}
<div ng-repeat="user in users">
<span>{{user.id}}</span>
<span>{{user.name}}</span>
<span>{{user.email}}</span>
<br />
</div>
</div>
The console log returns as
Output i am getting (jsPanel)
As you see the "test" variable is properly bind and the ng-repeat element is display as commented, I am not aware why it's returning like that. If anyone can figure out the issue, then i might get it working.
I know i can access the users data as $scope.users inside the directive. The problem here is that user able to use the directive commonly, so i have to assume that i don't the variables in the $scope.
I stuck in this place, and couldn't find any solutions to try. Any suggestions or solutions will be more helpful. :)
NOTE: No syntax errors, outside the directive the data is displaying (Tested)

Rich tabs and transcluded content

I currently have this problem: I made 3 directives (check plunkr for a 'reduced' test case, dont mind the closures they come from Typescript) that control tabs, using a controller to keep them grouped, since I can have more than one tabbed content on the current view. The problem happens when the tab itself has some bindings that are located outside of the scope, and when the tab is 'transcluded' in place, the binding never updates because the scope is different.
Here is the plunkr http://plnkr.co/edit/fnw1oV?p=preview and here is the "tab transclude" part that is important
this.link = function (scope, element, attr) {
var clickEvent, el, item;
item = scope.item;
console.log(scope);
el = item.element.filter('.is-tab');
el.addClass('picker-tabs-item');
clickEvent = 'tabs.select(item);';
if (el.is('[ng-click]')) {
clickEvent += el.attr('ng-click') + ';';
}
el.attr('ng-click', clickEvent);
el.attr('ng-class', 'tabs.classes(item)');
item.element = $compile(item.element)(scope);
element.replaceWith(item.element);
};
The current approach feels hacky (keeping the original scope and element in an array). Plus in my app, the data is loaded after the tabs were loaded, so it can't even retain some initial state. The tabs look like this right now:
and the way it should look like (but doesn't work, as you can see clicking one tab select all of them):
A real tab code from my app:
<div class="clearfix" login-space user-space>
<div class="picker clearfix" ng-class="{'to-hide': user.data.get('incomplete') || checkout.checkoutForm.$invalid}">
<div class="pick-title icon icon-pay">Forma de Pagamento</div>
<div class="for-hiding">
<div tabs="pagamento">
<div tab="/templates/tabs/plans/credito" selected="true">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('credito')" type="button">
Cartão
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('credito')"></span>
</button>
</div>
<div tab="/templates/tabs/plans/debito">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('debito')" type="button">
Débito
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('debito')"></span>
</button>
</div>
<div tab="/templates/tabs/plans/boleto">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('boleto')" type="button">
Boleto
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('boleto')"></span>
</button>
</div>
</div>
</div>
</div>
</div>
login-space and user-space are directives just to assign it to the login and user controllers. checkout is the current controller inside ui-view.
$stateProvider.state('planos.checkout', {
url: '/checkout',
templateUrl: '/templates/partials/plans/checkout',
controllerAs: 'checkout',
controller: Controllers.Checkout,
data: {
allowed: false,
enclosed: true
}
});
since the checkout controller must be instantiated only once, I can't reinstantiate it, but still need to access it's functions and bound data.
'/templates/partials/plans/checkout' contains the tab code above (so yes, technically it's in the same scope as checkout controller)
In your plunker, changing your tabs to this:
<span data-subctrl="">{{ subctrl.sum('credito') }}</span>
Showed the sum. I looked at what subctrl was and you have it as a directive, that's why subctrl.sum is not working. Plunker with it working: http://plnkr.co/edit/qwEHMqfzJ6pC79hM8cDj?p=preview
If that's not what's wrong with your application, then please describe it a little more.
Solved this by removing the html content of the tab, and applying a different scope to the inner content, then reattaching to the original element.
this.link = function (scope, element, attr) {
var clickEvent, el, item;
item = scope.item;
console.log(scope);
el = item.element.filter('.is-tab');
var contents = el.html(); //added
el.empty(); // added
el.addClass('picker-tabs-item');
clickEvent = 'tabs.select(item);';
if (el.is('[ng-click]')) {
clickEvent += el.attr('ng-click') + ';';
}
el.attr('ng-click', clickEvent);
el.attr('ng-class', 'tabs.classes(item)');
item.element = $compile(item.element)(scope);
item.element.append($compile('<div>' + contents + '</div>')(item.scope)); //added
element.replaceWith(item.element);
};

Add element to DOM via directive and bind scope watching from controller (angular.js)

Here is the little app in which you can add items, edit them and delete - all described in controller, based on angular 2 way data-bind.
http://cssdeck.com/labs/schedule-of-the-red-hood
Everytime the element is added, in the calednar-bar programmatically should be inserted an element, described in directive's template:
template: '<div ng-repeat="event in events" class="event">' +
'<h3 ng-model="event.Name">{{event.Name}}</h3>' +
'<span ng-model="event.StartTime; event.EndTime" class="time">{{event.StartTime}}-{{event.EndTime}}</span>' +
'</div>'
Though I can't get it how to link the scope from controller, bind the element insertion from the controller:
$scope.addEvent = function(event, attrs) {
$scope.events.push(event);
$scope.event = {};
var eventGrid = angular.element(document.createElement('eventGrid')),
el = $compile(eventGrid)($scope);
angular.element(document.body).append(eventGrid);
$scope.insertHere = el;
}
as I understand now, my code creates an element in the DOM, but doesn't use template from directive... How can I do it? Is the chosen structure of code appropriate to this goal?
You need to be more data driven, forget about adding things to the dom. E.g.
$scope.addEvent = function(event, attrs) {
$scope.events.push(event);
$scope.event = {};
/*var eventGrid = angular.element(document.createElement('eventGrid')),
el = $compile(eventGrid)($scope);
angular.element(document.body).append(eventGrid);
$scope.insertHere = el;*/
}
//add an order by to your ng-repeat
<li ng-repeat="event in events|orderBy:StartTime">
Instead of Saving StartTime as a string save it as a number, and use a filter to convert from seconds to your friendly HH:MM:AM|PM.
The conversion from string to seconds is the hardest part, but it is the right way to do it.

How to add a hover background image style on a div the Angularjs way

I have a background image on a div that I want to have switch on hover. I can't change the class because I'm using a bound property to fill in the information. As far as I know I don't see any way to add hover with styles inside of the html and I found a way to do it in jquery but it just doesn't seem like the angular way.
Method #1: No controller, everything in template.
<div ng-init="bg = ''"
ng-mouseenter="bg = 'http://www.gravatar.com/avatar/b76f6e92d9fc0690e6886f7b9d4f32da?s=100'"
ng-mouseleave="bg = ''"
style="background-image: url({{bg}});">
</div>
Method #2: Using vars to store the values (uses a controller)
<div ng-mouseenter="bg = imageOn"
ng-mouseleave="bg = imageOff"
style="background-image: url({{bg1}});">
</div>
Controller:
function myCtrl($scope){
$scope.bg1 = "" // this is the default image.
$scope.imageOn = "http://www.gravatar.com/avatar/b76f6e92d9fc0690e6886f7b9d4f32da?s=100";
$scope.imageOff = ""; // image that would after after the mouse off.
}
Method #3: Saved the best for last! Using a directive!!
<div hover-bg-image="{{image}}"></div>
Directive (could be improved to revert back to original image if there is one... its basic to show example):
.directive('hoverBgImage',function(){
return {
link: function(scope, elm, attrs){
elm.bind('mouseenter',function(){
this.style.backgroundImage = 'url('+attrs.hoverBgImage+')';
});
elm.bind('mouseleave',function(){
this.style.backgroundImage = '';
})
}
};
});
Controller:
function withDirective($scope){
$scope.image = "http://www.gravatar.com/avatar/b76f6e92d9fc0690e6886f7b9d4f32da?s=100";
}
Note: The items in the controllers could/should/would be set dynamically.
Demos: http://jsfiddle.net/TheSharpieOne/kJgVw/1/

ng-repeat finish event

I want to call some jQuery function targeting div with table. That table is populated with ng-repeat.
When I call it on
$(document).ready()
I have no result.
Also
$scope.$on('$viewContentLoaded', myFunc);
doesn't help.
Is there any way to execute function right after ng-repeat population completes? I've read an advice about using custom directive, but I have no clue how to use it with ng-repeat and my div...
Indeed, you should use directives, and there is no event tied to the end of a ng-Repeat loop (as each element is constructed individually, and has it's own event). But a) using directives might be all you need and b) there are a few ng-Repeat specific properties you can use to make your "on ngRepeat finished" event.
Specifically, if all you want is to style/add events to the whole of the table, you can do so using in a directive that encompasses all the ngRepeat elements. On the other hand, if you want to address each element specifically, you can use a directive within the ngRepeat, and it will act on each element, after it is created.
Then, there are the $index, $first, $middle and $last properties you can use to trigger events. So for this HTML:
<div ng-controller="Ctrl" my-main-directive>
<div ng-repeat="thing in things" my-repeat-directive>
thing {{thing}}
</div>
</div>
You can use directives like so:
angular.module('myApp', [])
.directive('myRepeatDirective', function() {
return function(scope, element, attrs) {
angular.element(element).css('color','blue');
if (scope.$last){
window.alert("im the last!");
}
};
})
.directive('myMainDirective', function() {
return function(scope, element, attrs) {
angular.element(element).css('border','5px solid red');
};
});
See it in action in this Plunker.
If you simply want to execute some code at the end of the loop, here's a slightly simpler variation that doesn't require extra event handling:
<div ng-controller="Ctrl">
<div class="thing" ng-repeat="thing in things" my-post-repeat-directive>
thing {{thing}}
</div>
</div>
function Ctrl($scope) {
$scope.things = [
'A', 'B', 'C'
];
}
angular.module('myApp', [])
.directive('myPostRepeatDirective', function() {
return function(scope, element, attrs) {
if (scope.$last){
// iteration is complete, do whatever post-processing
// is necessary
element.parent().css('border', '1px solid black');
}
};
});
See a live demo.
There is no need of creating a directive especially just to have a ng-repeat complete event.
ng-init does the magic for you.
<div ng-repeat="thing in things" ng-init="$last && finished()">
the $last makes sure, that finished only gets fired, when the last element has been rendered to the DOM.
Do not forget to create $scope.finished event.
Happy Coding!!
EDIT: 23 Oct 2016
In case you also want to call the finished function when there is no item in the array then you may use the following workaround
<div style="display:none" ng-init="things.length < 1 && finished()"></div>
//or
<div ng-if="things.length > 0" ng-init="finished()"></div>
Just add the above line on the top of the ng-repeat element. It will check if the array is not having any value and call the function accordingly.
E.g.
<div ng-if="things.length > 0" ng-init="finished()"></div>
<div ng-repeat="thing in things" ng-init="$last && finished()">
Here is a repeat-done directive that calls a specified function when true. I have found that the called function must use $timeout with interval=0 before doing DOM manipulation, such as initializing tooltips on the rendered elements. jsFiddle: http://jsfiddle.net/tQw6w/
In $scope.layoutDone, try commenting out the $timeout line and uncommenting the "NOT CORRECT!" line to see the difference in the tooltips.
<ul>
<li ng-repeat="feed in feedList" repeat-done="layoutDone()" ng-cloak>
{{feed | strip_http}}
</li>
</ul>
JS:
angular.module('Repeat_Demo', [])
.directive('repeatDone', function() {
return function(scope, element, attrs) {
if (scope.$last) { // all are rendered
scope.$eval(attrs.repeatDone);
}
}
})
.filter('strip_http', function() {
return function(str) {
var http = "http://";
return (str.indexOf(http) == 0) ? str.substr(http.length) : str;
}
})
.filter('hostName', function() {
return function(str) {
var urlParser = document.createElement('a');
urlParser.href = str;
return urlParser.hostname;
}
})
.controller('AppCtrl', function($scope, $timeout) {
$scope.feedList = [
'http://feeds.feedburner.com/TEDTalks_video',
'http://feeds.nationalgeographic.com/ng/photography/photo-of-the-day/',
'http://sfbay.craigslist.org/eng/index.rss',
'http://www.slate.com/blogs/trending.fulltext.all.10.rss',
'http://feeds.current.com/homepage/en_US.rss',
'http://feeds.current.com/items/popular.rss',
'http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml'
];
$scope.layoutDone = function() {
//$('a[data-toggle="tooltip"]').tooltip(); // NOT CORRECT!
$timeout(function() { $('a[data-toggle="tooltip"]').tooltip(); }, 0); // wait...
}
})
Here's a simple approach using ng-init that doesn't even require a custom directive. It's worked well for me in certain scenarios e.g. needing to auto-scroll a div of ng-repeated items to a particular item on page load, so the scrolling function needs to wait until the ng-repeat has finished rendering to the DOM before it can fire.
<div ng-controller="MyCtrl">
<div ng-repeat="thing in things">
thing: {{ thing }}
</div>
<div ng-init="fireEvent()"></div>
</div>
myModule.controller('MyCtrl', function($scope, $timeout){
$scope.things = ['A', 'B', 'C'];
$scope.fireEvent = function(){
// This will only run after the ng-repeat has rendered its things to the DOM
$timeout(function(){
$scope.$broadcast('thingsRendered');
}, 0);
};
});
Note that this is only useful for functions you need to call one time after the ng-repeat renders initially. If you need to call a function whenever the ng-repeat contents are updated then you'll have to use one of the other answers on this thread with a custom directive.
Complementing Pavel's answer, something more readable and easily understandable would be:
<ul>
<li ng-repeat="item in items"
ng-init="$last ? doSomething() : angular.noop()">{{item}}</li>
</ul>
Why else do you think angular.noop is there in the first place...?
Advantages:
You don't have to write a directive for this...
Maybe a bit simpler approach with ngInit and Lodash's debounce method without the need of custom directive:
Controller:
$scope.items = [1, 2, 3, 4];
$scope.refresh = _.debounce(function() {
// Debounce has timeout and prevents multiple calls, so this will be called
// once the iteration finishes
console.log('we are done');
}, 0);
Template:
<ul>
<li ng-repeat="item in items" ng-init="refresh()">{{item}}</li>
</ul>
Update
There is even simpler pure AngularJS solution using ternary operator:
Template:
<ul>
<li ng-repeat="item in items" ng-init="$last ? doSomething() : null">{{item}}</li>
</ul>
Be aware that ngInit uses pre-link compilation phase - i.e. the expression is invoked before child directives are processed. This means that still an asynchronous processing might be required.
It may also be necessary when you check the scope.$last variable to wrap your trigger with a setTimeout(someFn, 0). A setTimeout 0 is an accepted technique in javascript and it was imperative for my directive to run correctly.
I did it this way.
Create the directive
function finRepeat() {
return function(scope, element, attrs) {
if (scope.$last){
// Here is where already executes the jquery
$(document).ready(function(){
$('.materialboxed').materialbox();
$('.tooltipped').tooltip({delay: 50});
});
}
}
}
angular
.module("app")
.directive("finRepeat", finRepeat);
After you add it on the label where this ng-repeat
<ul>
<li ng-repeat="(key, value) in data" fin-repeat> {{ value }} </li>
</ul>
And ready with that will be run at the end of the ng-repeat.
<div ng-repeat="i in items">
<label>{{i.Name}}</label>
<div ng-if="$last" ng-init="ngRepeatFinished()"></div>
</div>
My solution was to add a div to call a function if the item was the last in a repeat.
This is an improvement of the ideas expressed in other answers in order to show how to gain access to the ngRepeat properties ($index, $first, $middle, $last, $even, $odd) when using declarative syntax and isolate scope (Google recommended best practice) with an element-directive. Note the primary difference: scope.$parent.$last.
angular.module('myApp', [])
.directive('myRepeatDirective', function() {
return {
restrict: 'E',
scope: {
someAttr: '='
},
link: function(scope, element, attrs) {
angular.element(element).css('color','blue');
if (scope.$parent.$last){
window.alert("im the last!");
}
}
};
});
i would like to add another answer, since the preceding answers takes it that the code needed to run after the ngRepeat is done is an angular code, which in that case all answers above give a great and simple solution, some more generic than others, and in case its important the digest life cycle stage you can take a look at Ben Nadel's blog about it, with the exception of using $parse instead of $eval.
but in my experience, as the OP states, its usually running some JQuery plugins or methods on the finnaly compiled DOM, which in that case i found that the most simple solution is to create a directive with a setTimeout, since the setTimeout function gets pushed to the end of the queue of the browser, its always right after everything is done in angular, usually ngReapet which continues after its parents postLinking function
angular.module('myApp', [])
.directive('pluginNameOrWhatever', function() {
return function(scope, element, attrs) {
setTimeout(function doWork(){
//jquery code and plugins
}, 0);
};
});
for whoever wondering that in that case why not to use $timeout, its that it causes another digest cycle that is completely unnecessary
I had to render formulas using MathJax after ng-repeat ends, none of the above answers solved my problem, so I made like below. It's not a nice solution, but worked for me...
<div ng-repeat="formula in controller.formulas">
<div>{{formula.string}}</div>
{{$last ? controller.render_formulas() : ""}}
</div>
I found an answer here well practiced, but it was still necessary to add a delay
Create the following directive:
angular.module('MyApp').directive('emitLastRepeaterElement', function() {
return function(scope) {
if (scope.$last){
scope.$emit('LastRepeaterElement');
}
}; });
Add it to your repeater as an attribute, like this:
<div ng-repeat="item in items" emit-last-repeater-element></div>
According to Radu,:
$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {mycode});
For me it works, but I still need to add a setTimeout
$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {
setTimeout(function() {
mycode
}, 100); });
If you simply wants to change the class name so it will rendered differently, below code would do the trick.
<div>
<div ng-show="loginsuccess" ng-repeat="i in itemList">
<div id="{{i.status}}" class="{{i.status}}">
<div class="listitems">{{i.item}}</div>
<div class="listitems">{{i.qty}}</div>
<div class="listitems">{{i.date}}</div>
<div class="listbutton">
<button ng-click="UpdateStatus(i.$id)" class="btn"><span>Done</span></button>
<button ng-click="changeClass()" class="btn"><span>Remove</span></button>
</div>
<hr>
</div>
This code worked for me when I had a similar requirement to render the shopped item in my shopping list in Strick trough font.

Categories