In my angular application I have several panels which have a shared structure, identical html.
Inside the panel the content and behavior changes, basically each one is a separate directive, let's call them panel-content.
This is the close to the solution I think is optimal but I have some architectural doubts.
Since I have directive (which has transclude true set):
<panel></panel>
It's template looks like this:
<div>
Content
<ng-transclude></ng-transclude>
</div>
I have to repeat the panels
<ul>
<li ng-repeat="panel in panels">
<panel>
<panel-content></panel-content>
</panel>
</li>
</ul
This is all fine, but what would be a reasonably good way of "choosing" on each iteration which <panel-content> should I show?
Lets say I have a panel.id I can use.
I notice I can achieve it in several ways, I could do an ng-switch inside the <panel-content> view using the panel id, I could set up that the templateUrl of has a dynamic part and links to different URLs depending on the panel.id.
But for some reason, I am convinced I am missing something nicer more straightforward?
Please not that this architecture is not set in stone, if there is another structure that would better fit my needs please let me know.
So, the question again is, how do I choose? Or rather, who is responsible for choosing which <panel-content> should be displayed.
If I understand you right, I would use ng-include in directive that changes each time the template by id:
Something like:
<ul>
<li ng-repeat="panel in panels">
<panel type-id="panel.id">
</panel>
</li>
</ul>
and Directive:
app.directive('panel',
function() {
return {
restrict: 'E',
replace: true,
template: '<ng-include src="getTemplateUrl()"/>',
link: function(scope,elem,attrs) {
scope.getTemplateUrl = function() {
var url;
if(attrs.typeId !== undefined){
var url = $rootScope.list_templates[attrs.typeId].value; // TODO: add validation
}
return url;
};
}
});
app
$rootScope.list_templates = [
{ id: 0, value: 'partials/upcoming_0.html'},
{ id: 1, value: 'partials/upcoming_1.html'},
{ id: 2, value: 'partials/upcoming_2.html'}
];
I am having an ng-repeat block and with it i am writing an ng-click with a direct expression of setting a $scope variable to true.. but it doesn't work.. can someone plz help.. here is the plnkr
HTML:
selected: {{selected}}
<ul>
<li ng-repeat="t in t.header" ng-click="selected = true;">{{t.a1}}</li>
</ul>
JS:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.selected = false;
$scope.t = {
header: [
{
a1:'a1'
},
{
a1:'a1'
}
]
}
});
for now i am having a workaround to have a function call on its click and set the variable that is required, but curious to know whats wrong with the other approach?
Use $parent because ng-repeat creates it's own scope
<li ng-repeat="t in t.header" ng-click="$parent.selected = true;">{{t.a1}}</li>
Plunker
It's working, but because of the ngRepeat, selected is binded to the new scope. You can instead put selected in an object, so it won't create a new selected property on the child scope:
<li ng-repeat="t in t.header" ng-click="selectedObj.selected = true;">{{t.a1}}
$scope.selectedObj = { selected: false };
Check this plunker.
Try this:
//$scope.selected = false;
$scope.model = {};
$scope.model.selected = false;
plnkr
What happens is that the child scope gets its own property that hides/shadows the parent property of the same name. This is not something AngularJS is doing – this is how JavaScript prototypal inheritance works.
For better understanding:
https://github.com/angular/angular.js/wiki/Understanding-Scopes
Just use $parent scope, like this:
<li ng-repeat="t in t.header" ng-click="$parent.selected = true;">
Every item in ng-repat has his own scope.
I have a <ul> as such with <li>'s:
<li ng-repeat="x in xs" ng-class-odd="'x'" ng-class-even="'y'">
// inject here
<span>
{{x.v}}
</span>
</li>
I'd like on a certain event to inject a context menu (DOM position described above) that looks like this:
<ul id="context" class="col">
<li class="three"><span>1</span></li>
<li class="three"><span>2</span></li>
<li class="three"><span>3</span></li>
</ul>
What's the best way to implement this? 1, 2 and 3 above have the same functions to handle the repeated list items in the parent container. So I'm not sure if injecting the context menu as described above is a smart idea since It would generate unseeded repetitions of the context menu.
Thanks.
Here's a really basic example of a set of contextmenu directives where menu is inserted once in body.
One directive is used to bind the contenxtmenu event and send data to the directive that controls the menu itself.
The item selected and the mouse position get passed as data in the broadcast
HTML
<ul>
<li ng-repeat="item in items" context="item">{{item.title}}</li>
</ul>
<ul menu id="context" ng-show="menu_on">
<li ng-click="itemAlert('id')">Alert ID</li>
<li ng-click="itemAlert('title')">Alert Title</li>
</ul>
JS
app.directive('context', function($rootScope) {
return {
restrict: 'A',
scope: {
item: '=context'
},
link: function(scope, elem) {
elem.bind('contextmenu', function(evt) {
evt.preventDefault();
var data = {
pos: { x: evt.clientX, y: evt.clientY},
item: scope.item
}
scope.$apply(function(){
$rootScope.$broadcast('menu', data);
});
});
}
}
})
app.directive('menu', function($rootScope) {
return {
restrict: 'A',
link: function(scope, elem) {
scope.$on('menu', function(evt, data) {
scope.menu_on = true;
scope.item = data.item;
var cssObj = {
left: data.pos.x + 20 + 'px',
top: data.pos.y - 40 + 'px'
};
elem.css(cssObj)
});
scope.itemAlert = function(prop) {
scope.menu_on = false;
alert(scope.item[prop])
}
}
}
});
Would need some additional document listeners to close menu when user clicks outside of it. Objective here was to just create basic mechanics of being able to display menu and pass data.
I haven't looked but there are probably some open source directives already available that are far more advanced than this.
DEMO
I have very little javascript experience. I need to add a menu on click of an item. We have been asked to build it from scratch without using any library like bootstrap compoments or JQuery.
We are using Angularjs. In angular I want to know the correct method to create new elements. Something like what we did not document.createElement.
I am adding some of the code for you guys to have a better idea what I want to do.
Menu Directive
.directive('menu', ["$location","menuData", function factory(location, menuData) {
return {
templateUrl: "partials/menu.html",
controller: function ($scope, $location, $document) {
$scope.init = function (menu) {
console.log("init() called");
console.log("$document: " + $document);
if (menu.selected) {
$scope.tabSelected(menu);
}
}
$scope.creteMenu = function(menuContent){
//This is to be called when the action is an array.
}
$scope.tabSelected = function(menu){
$location.url(menu.action);
$scope.selected = menu;
}
$scope.click = function (menu) {
if (typeof (menu.action) == 'string') {
$scope.tabSelected(menu);
}
}
},
link: function (scope, element, attrs) {
scope.menuData = menuData;
}
};
}])
Menu data in service.
.value('menuData', [{ label: 'Process-IDC', action: [] }, { label: 'Dash Board', action: '/dashboard', selected: true }, { label: 'All Jobs', action: '/alljobs', selected: false }, { label: 'My Jobs', action: '/myjobs', selected: false }, { label: 'Admin', action: '/admin', selected: false }, { label: 'Reports', action: '/reports', selected: false }]);
If you notice the action of Process-IDC menu is an array it will contain more menu with actions in it and it should be opened in a sub menu.
Menu.html (partial)
<ul class="menu">
<li ng-class="{activeMenu: menu==selected}" ng-init="init(menu)" data-ng-click="click(menu)" data-ng-repeat="menu in menuData">{{menu.label}}</li>
</ul>
A few things come to mind. First of all, are you sure you need to actually create the element on click? If you are doing to to show a fixed element on click then the better approach would be to generate the element as normal, but not show it until you click. Something like:
<div ng-click="show_it=true">Show item</div>
<div ng-show="show_it">Hidden until the click. Can contain {{dynamic}} content as normal.</div>
If you need it to be dynamic because you might add several elements, and you don't know how many, you should look at using a repeat and pushing elements into a list. Something like this:
<div ng-click="array_of_items.push({'country': 'Sparta'})">Add item</div>
<div ng-repeat="item in array_of_items"> This is {{item.country}}</div>
Each click of the "Add item" text here will create another div with the text "This is Sparta". You can push as complex an item as you want, and you could push an item directly from the scope so you don't have to define it in the template.
<div ng-click="functionInControllerThatPushesToArray()">Add item</div>
<div ng-repeat="item in array_of_items"> This is {{item.country}}</div>
If neither of those options would work because it is a truly dynamic object, then I would start looking at using a directive for it like others have suggested (also look at $compile). But from what you said in the question I think a directive would be to complicate things needlessly.
I recommend you read the ngDirective and the angular.element docs.
Hint: angular.element has an append() method.
This is both really simple, but some what complex if you don't know where to start - I really recommend looking at the Tutorial, and following it end to end: http://docs.angularjs.org/tutorial/ - As that will introduce you to all the concepts around Angular which will help you understand the technical terms used to describe the solution.
If you're creating whole new menu items, if in your controller your menu is something like:
// An Array of Menu Items
$scope.menuItems = [{name: 'Item One',link: '/one'},{name: 'Item Two',link:'/two'}];
// Add a new link to the Array
$scope.addMenuItem = function(theName,theLink){
$scope.menuItems.push({name: theName,link:theLink});
}
And in the template, use the array inside ng-repeat to create the menu:
<ul>
<li ng-repeat="menuItem in menuItems">{{menuItem.name}}</li>
</ul>
If you just want to toggle the display of an item that might be hidden, you can use ng-if or ng-show
Assuming that you are doing it in a directive and you have angular dom element, you can do
element.append("<div>Your child element html </div>");
We can use $scope in App Controller to create Div Elements and then we can append other Div elements into it similarly.
Here's an Example:
$scope.div = document.createElement("div");
$scope.div.id = "book1";
$scope.div.class = "book_product";
//<div id="book1_name" class="name"> </div>
$scope.name = document.createElement("div");
$scope.name.id = "book1_name";
$scope.name.class= "name";
// $scope.name.data="twilight";
$scope.name.data = $scope.book.name;
$scope.div.append($scope.name);
console.log($scope.name);
//<div id="book1_category" class="name"> </div>
$scope.category = document.createElement("div");
$scope.category.id = "book1_category";
$scope.category.class= "category";
// $scope.category.data="Movies";
$scope.category.data=$scope.book.category;
$scope.div.append($scope.category);
console.log("book1 category = " + $scope.category.data);
//<div id="book1_price" class="price"> </div>
$scope.price = document.createElement("div");
$scope.price.id = "book1_price";
$scope.price.class= "price";
// $scope.price.data=38;
$scope.price.data=$scope.book.price;
$scope.div.append($scope.price);
console.log("book1 price = " + $scope.price.data);
//<div id="book1_author" class="author"> </div>
$scope.author = document.createElement("div");
$scope.author.id = "book1_author";
$scope.author.class= "author";
// $scope.author.data="mr.book1 author";
$scope.author.data=$scope.book.author;
$scope.div.append($scope.author);
console.log("book1 author = " + $scope.author.data);
//adding the most outer Div to document body.
angular.element(document.getElementsByTagName('body')).append($scope.div);
For more illustration, Here each book has some attributes (name, category, price and author) and book1 is the most outer Div Element and has it's attributes as inner Div elements.
Created HTML element will be something like that
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.