Select New DOM elements created by Angularjs ng-repeat - angularjs

I am using Angularjs for a web application. I have tried searching to find a solution to my problem, and it seems that Angularjs do not facilitate an easy way to access newly created DOM elements within ng-Repeat.
I have prepared a jsfiddle to show the actual problem.
here is the link: http://jsfiddle.net/ADukg/956/
Please let me know how to select the new DOM element within ng-repeat.

To expand on my comment, I have updated your fiddle to show a simple implentation of a directive that alerts the class of the element.
http://jsfiddle.net/ADukg/994/
Original comment:
Regarding dom manipulation in the controller, it says here that you should not do it at all. It should go in a directive. The controller should only contain business logic.
Why it doesn't work, I don't know, but it's probably because angular is still running its own stuff at this point in time.

There are two ways to do this:
1 ng-init
function controller($scope) {
$scope.items = [{id: 1, name: 'one'}, {id: 2, name: 'two'}];
$scope.initRepeaterItem(index, item) {
console.log('new repeater item at index '+index+':', item);
}
}
<ul>
<li ng-repeat="item in items" ng-init="initRepeaterItem($index, item)"></li>
</ul>
2 MutationObserver slightly more complex
Do this inside a directive, on the element whose parent gets new children (<ul> in this case)
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// you get notified about DOM mutations here
// check mutation.type if it was an insertion
console.log(mutation.target.nodeName, mutation.type, mutation);
});
});
var config = {childList: true, attributes: false, characterData: false,
subtree: false, attributeOldValue: false, characterDataOldValue: false};
observer.observe(element[0], config);
Demo http://plnkr.co/h6kTtq
Documentation https://developer.mozilla.org/en/docs/Web/API/MutationObserver
Browser support http://caniuse.com/mutationobserver

Related

Expose element of parent component to child

I have a main component that handles the toolbar and sidnav of my angular application. I would like to make a div inside the toolbar available to child components (and controllers) to customize so that they can do things like change the toolbar title text and add contextual buttons. This feels sort of like the opposite of transposition where a parent component can customize part of a child component (e.g. a menu component customizing the content of a button). One option would be to have the toolbar managed by a service, but even then I can't think of a great way to customize the content of the toolbar without doing a decent amount of javascript that builds up dom elements (one of the things I always try to avoid in angular).
In Angular 1.6.x components use isolate scope only:
Components only control their own View and Data: Components should
never modify any data or DOM that is out of their own scope. Normally,
in AngularJS it is possible to modify data anywhere in the application
through scope inheritance and watches. This is practical, but can also
lead to problems when it is not clear which part of the application is
responsible for modifying the data. That is why component directives
use an isolate scope, so a whole class of scope manipulation is not
possible.
So, to make this work you would need to use a directive instead of a component. The div you want to include would need to be a directive itself to be able to alter the parent scope of the toolbar directive. What you would be doing is transcluding one directive inside another, and using shared scope to alter the parent scope.
This article is a really good resource for what you are trying to accomplish. I would start here: https://www.airpair.com/angularjs/posts/transclusion-template-scope-in-angular-directives
I have altered the example Codepen from that article to show you how it might work: http://codepen.io/jdoyle/pen/aJQpYo
If you select items in the list you can see that the header name changes to whatever is selected.
angular.module("ot-components", [])
.controller("AppController",($scope)=> {
//Normally, this data would be wrapped in a service. For example only.
$scope.header = "Marketing";
$scope.areas = {
list: [
"Floorplan",
"Combinations",
"Schedule",
"Publish"
],
current: "Floorplan"
};
})
.directive("otList", ()=> {
return {
scope: false, // this is one of the major changes
template:
`<ul class="ot-list">
<li class="ot-list--item"
ng-repeat="item in items"
ng-bind="item"
ng-class="{'ot-selected': item === selected}"
ng-click="selectItem(item)">
</li>
</ul>`,
link: (scope, elem, attrs) => {
scope.items = JSON.parse(attrs.items);
scope.selected = attrs.selected;
scope.selectItem = (item) => {
scope.selected = item;
scope.$parent.header = item; // this is the other major change
};
}
};
})
.directive("otSite", ()=> {
return {
scope: true, // another major change
transclude: true,
template:
`<div class="ot-site">
<div class="ot-site--head">
<img class="ot-site--logo" src="//guestcenter.opentable.com/Content/img/icons/icon/2x/ot-logo-2x.png">
<h1>{{header}}</h1>
</div>
<div class="ot-site--menu">
</div>
<div class="ot-site--body" ng-transclude>
</div>
<div class="ot-site--foot">
© 2015 OpenTable, Inc.
</div>
</div>`
};
});

angular array item not updating on scope change

Spent a few hours on this already, sifted through numerous stack posts and blogs but can't seem to get this to make my model update. More specifically, I am trying to update an array item (ng-repeat). In the simple case below, I iterate over venues list, and upon toggling a "like" button, I update the server appropriately, and reflect the change on the venues item on the $scope.
in my search.html I have a directive:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
and search controller I have:
app.controller('bleh', function(Service) {
...
$scope.venues = [{ id: 1, name: 'Venue1', like: false },{ id: 2, name: 'Venue2', like: false }];
...
});
Nothing unusual there. Then in my venues-list directive:
app.directive('venues-list', function() {
function venueListController($scope, Service) {
$scope.likeToggle = function(venue, $index) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venues[$index].like= !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueListController,
scope: {
venues: '='
}
}
});
then in my venue.html I have:
<div ng-repeat="venue in venues">
<p>{{venue.name}}</p>
<button ng-click="likeToggle(venue, $index)">Like</button>
</div>
I have tried many different options:
$scope.$apply() // after updating the $scope or updating scope within apply's callback function;
$scope.$digest()
$timeout(function() { // $scope.venues[$index] .... }, 0);
safe(s,f){(s.$$phase||s.$root.$$phase)?f():s.$apply(f);}
so
safe($scope, function() { $scope.venues[$index].like = !venue.like });
I haven't yet used the link within the directive, but my venues.html template is obviously a little more elaborate than presented here.
EDIT:
Just to keep the discussion relevant, perhaps I should have mentioned - the data is coming back from the server with no issues, I am handling errors and I am definitely hitting the portion of the code where the $scope is to be updated. NOTE: the above code is a small representation of the full app, but all the fundamental pieces are articulated above.
Search Controller
Venues Service
venue-list directive and venue.html template to accompany the directive
directive controller
EDIT #2
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
Just to keep it even simpler, the above doesn't work - so really, the bottom line is my items within an array are not reflecting the updates ...
EDIT #3
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
My apologies - just to re-iterate what I was trying to say above - the above is changing the scope, however, the changes are not being reflected on the view.
Perhaps the issue is with your service and promise resolution.. Can you put a console.log there and see if the promise resolution is working fine? or Can you share that code bit. Also where are you checking for scope update? within directive or outside
OK after some refactoring I finally got it working.
The "fix" (if you want to call it that) to my specific problem was:
instead of passing an array of venues, I was iterating over the array on the parent controller, passing in a venue as an element attribute that would bind (two-way) on the isolated scope of the directive.
so, instead of:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
I now have:
<ion-content>
<venues-list ng-repeat="venue in venues" venue="venue"></venues-list>
</ion-content>
and my directive now looks like:
app.directive('venues-list', function() {
function venueController($scope, Service) {
$scope.likeToggle = function(venue) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venue.like = !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueController,
scope: {
venue: '='
}
}
});
This did the trick!

Delimiting some sibling nodes without additional element

What is the equivalent angularjs template for the following handlebar template? Is there any way to achieve same result without wrapping the if block with another tag?
(foo is false)
<ul>
<li>a</li>
{{if foo}}
<li>b</li>
…
<li>c</li>
{{/if}}
<li>d</li>
</ul>
The rendered template should be exactly:
<ul>
<li>a</li>
<li>d</li>
</ul>
ng-if with one time binding(if you are in version 1.3.x else resort to some other libraries like bindonce to avoid any unnecessary watches) might be more appropriate for you. But ideally it is clearly unclear because you can solve this with many ways in angular. It does not even has to get to the view, you could just filter it out from the controller itself while setting up the view model which is used to repeat (ng-repeat) the lis. ng-show can also be used if you are trying to show and hide them. Difference between ng-if and ng-show/ng-hide is that ng-if removes the element completely from dom (and it cannot be animated with nganimate). ng-show just sets the css property display:none if condition set is false.
<ul>
<li>a</li>
<li ng-if="::foo">b</li><!-- Using :: for one time binding V1.3.x so no more watchers -->
<li ng-if="::foo">c</li>
<li>d</li>
</ul>
Update based on the comment that OP is looking for "a block statement to show/hide a bunch of elements together without adding a container tag".
Angular is not just a templating library like handlebars. So first thing before providing any specific answer is to recommend to learn how angular works. It is much more than a templating engine, it binds data to DOM that is already rendered and view is more of a reflection of the view model/model built from the controller. So in your case, as i explained earlier you just have to filter out the data based on a specific condition. Take a look at ng-repeat, event DOM filters that can be used with ng-repeat. So in short looking for a a block statement to show/hide a bunch of elements together without adding a container tag in angular (just what you would in handlebars) is thinking in wrong direction in my opinion. A possible solution for you can as well just be to identify when foo becomes true do not event provide those items (to be filtered out) to be rendered to view (or works case use filters in the view). And adding a block statement can just result in an invalid html in your case and browser will just strip it off before even angular has a chance to process it (unlike handlerbars where you transform your template to html before even rendering).
Here is one possible, better way (Using view filters are bad if filtering is one time, if it is just one time do the filtering in the controller) to do this in my opinion.
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.items = [{
name: 'a',
hideWhenFoo: false
}, {
name: 'b',
hideWhenFoo: false
}, {
name: 'c',
hideWhenFoo: true
}, {
name: 'd',
hideWhenFoo: true
}, {
name: 'e',
hideWhenFoo: true
}, {
name: 'f',
hideWhenFoo: false
}, {
name: 'g',
hideWhenFoo: false
}];
$scope.foo = true; // due to some condition
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<ul>
<li ng-repeat="item in items | filter:{hideWhenFoo:!foo}">{{item.name}}</li>
</ul>
</div>
The following works, similar to ng-repeat-start and ng-repeat-end. However, I did not find it on the docs.
<ul>
<li>a</li>
<li ng-if-start='foo'>b</li>
…
<li ng-if-end>c</li>
<li>d</li>
</ul>

AngularJS directive timing issue

I'm trying to figure out the order of operations here, and I'm obviously failing.
I'm trying to implement a jquery image gallery plugin as a directive. The plugin takes a set of images, uses $.load to load them into the DOM, and then upon completion creates a nice gallery.
You can see my example here. If you click "Load Images", it will pull 20 images from Flickr. What I'd like to have happen is, on load, the $.gridnav function is called. I am demonstrating that functionality when you click "Display GridNav", but obviously don't want that second button click as a requirement.
Now, my directive sits outside of my ng-repeat which is bound to the property getting set on successful callback from flickr. So I understand why it's not getting called again, I'm just not sure what to do to make this function as I'd like.
I'm not entirely sure how gridnav is supposed to work, so I'm not sure if this works fully, but what about calling gridnav anytime the array changes? You can pass the array into the directive via the view and then $watch the vale on the scope inside the directive.
HTML:
<div class="well clearfix giftCardList tj_container" id="giftCardContainer"
gridnav="giftCards">
...
JS:
link: function ($scope, element, attrs) {
// watch for the value passed in to gridnav attribute to change
$scope.$watch(attrs.gridnav, function(value){
// don't do anything unless there's actually a value
if (!value) return;
// once it's changed, call gridnav on the next loop
$timeout(function() {
$(element).gridnav({
rows: 1,
navL: '#giftCard_prev',
navR: '#giftCard_next',
type: {
mode: 'disperse',
speed: 400,
easing: '',
factor: '',
reverse: ''
}
}, 0);
}, true);
});
}
Plunker: http://plnkr.co/edit/lMYvIJEkEPRAaNIAuNvF?p=preview

How directives in Angularjs works?

I am struggle with one question and can not figure out why it happens.
I have a module (portfolioModule), service (Menu) and a directive (niMenu). The directive should render a menu item with its subitems.
HTML:
<div ng-app="portfolioModule">
<script id="menuItem" type="text/ng-template">
<li>
<a>{{item.name}}</a>
<ul>
<li ng-repeat="item in menu.getKids(item.id)" ng-include="'menuItem'"></li>
</ul>
</li>
</script>
<div>
<div ni-menu="test">test1</div>
</div>
</div>
JavaScript:
var portfolioModule = angular.module('portfolioModule', []);
portfolioModule.factory('Menu', function() {
var items = [
{
"parent_id": null,
"id": 1,
"name": "foo"
}, {
"parent_id": 1,
"id": 2,
"name": "foo-bar"
}
];
this.getKids = function(parent_id) {
var result = [];
parent_id = parent_id || null;
console.log('getKids', parent_id);
angular.forEach(items, function(value) {
if (value.parent_id === parent_id) {
result.push(value);
}
});
return result;
};
return this;
}
);
portfolioModule.directive('niMenu', function(Menu) {
var niMenu;
return niMenu = {
template: '<ul ng-include="\'menuItem\'" ng-repeat="item in menu.getKids()"></ul>',
restirt: 'A',
link: function(scope, element, attrs) {
console.log('link');
scope.menu = Menu;
}
};
}
);
Working demo: http://jsfiddle.net/VanSanblch/Ng7ef.
In html I call niMenu by ni-menu. The directive has a template and a link function that put Menu-service into a scope of module. In directive's template I use Menu.getKids() and get all top level items. Later, in template that used by ng-include I call Menu.getKids(item.id) and get all children of particular item.
Everything works excellent except one small detail. If you open console then you can observe that there are much more calls of getKids than I am expected. For example, for array of two elements the number of getKids calls is nine.
Could someone explain why on earth that happens?
Ok, so the reason it's executing more than once is because that's how the digest cycle works: it continuously executes all view expressions (like the one you pass to ngRepeat) until it expresses no change. See this answer for more information, but the takeaway is this: it will always execute at least once, but oftentimes more.
When using ngRepeat, you generally want to avoid fetching the data from a function because it negatively impacts performance; why call the same function more than once when the data never changed? A better approach is to have your controller (or in this case directive) execute your function and store the results on the scope, so your ngRepeat looks like ng-repeat="item in items" instead of ng-repeat="item in getItems()".
Unfortunately, that means you have to restructure the way you have your directive working. This turns out to be a good idea anyway, because your directive can be rewritten to be a little bit simpler - you don't need any ngInclude, for example.
What you want to do is create two directives because you have two templates: one to represent the overall menu and one to represent a child item (which, in turn, can have child items). The menu directive should repeat over the top-level menu items, drawing a menuItem for each one. The menuItem directive should check for children and repeat over those, if necessary, with more menuItems. Etc. Here's an example created by Andy Joslin of how you can accomplish a recursive directory: http://plnkr.co/edit/T0BgQR.
To implement something like this moves beyond the scope of this question, but take a stab at it and post a new question if you need help.
Good luck!

Resources