Why can't I access the new DOM element created by Angular? - javascript

HTML:
<div class="list-group link-list" ng-show="linksForPerson">
<a href="" class="list-group-item" ng-repeat="link in linksForPerson" ng-click="showLinkDetail(link)" ng-class="{active: isSelectedLink(link)}">
<h4 class="list-group-item-heading">[[ link.engine.name ]]</h4>
<p class="list-group-item-text">[[ link.engine.base_url ]]</p>
<p class="list-group-item-text" ng-show="link.user_sync_id">[[ link.user_sync_id ]]</p>
<p class="list-group-item-text" ng-show="link.group_sync_id">[[ link.group_sync_id ]]</p>
</a>
<span class="glyphicon glyphicon-plus"></span> Add a new link
</div>
Controller:
appModuleLightDashboard.controller('ManageLinksController',
function($scope, $http, $timeout) {
$scope.addLink = function(event) {
$scope.linksForPerson.push({});
// Error: [$rootScope:inprog] http://errors.angularjs.org/1.3.0-rc.1/$rootScope/inprog?p0=%24apply
$('.link-list .list-group-item').eq(-2).trigger('click');
// But this works ---- why?
// $timeout( function(){$('.link-list .list-group-item').eq(-2).trigger('click')} , 0);
}
});
I have changed the interpolate symbol to [[]] as it conflicts with Django
The problem:
A new list item will be created when the user clicks on the "Add a new link". I wanted to select this new list item automatically.
But it looks like I couldn't select that new DOM element created by Angular ( i.e. $('.link-list .list-group-item') doesn't return the new one ), unless I wrap the code with $timeout. Anyone knows why?
Also, please advise if there is a more Angular way to achieve it:)

Your question is "why". The answer is because at the moment you are trying to use jQuery to find the element, it hasn't yet been added to the DOM. That doesn't happen until the digest cycle runs.
$timeout works because the function call is now deferred until after the next digest cycle. The problem with that solution is that there are cases where the DOM still won't yet have been modified.
Looking in more detail, this will have several failure modes. The error you are showing is sent because you are actually triggering a click in the second to last element already added, and you are doing it from inside of a digest cycle. If you already have two or more items added to the collection, this triggers angular's ng-click on the second to last one (which happens to not be the one you think), which assumes it is called outside of a digest cycle and calls $apply, which fails with the error you see because it's actually inside of a digest cycle.
The "angular way" to achieve what you want is to use a directive.
.directive('triggerClick', function($parse) {
return {
restrict: 'A',
link: function(scope, elem, attr) {
var fn = $parse(attr['triggerClick']);
if(scope.$last) { //or some other logic
fn(scope);
}
}
}
})
div class="list-group link-list" ng-show="linksForPerson">
<a href="" class="list-group-item" ng-repeat="link in linksForPerson" ng-click="showLinkDetail(link)" ng-class="{active: isSelectedLink(link)}" trigger-click="showLinkDetail(link)">
<h4 class="list-group-item-heading">[[ link.engine.name ]]</h4>
<p class="list-group-item-text">[[ link.engine.base_url ]]</p>
<p class="list-group-item-text" ng-show="link.user_sync_id">[[ link.user_sync_id ]]</p>
<p class="list-group-item-text" ng-show="link.group_sync_id">[[ link.group_sync_id ]]</p>
</a>
<span class="glyphicon glyphicon-plus"></span> Add a new link
</div>
This works because the link function of the directive will be called after the node has been constructed and added to the DOM. Note the addition of "trigger-click" to your ng-repeat element.
elem in the directive is a jQuery object wrapped around the instance of the ng-repeat item. Angular will call the link function for every instance of the directive, which in this case is every instance of the ng-repeat.
Even more "angular" would be to not use a click event at all. You don't include the implementation of showLinkDetail, but rather than trigger a click, just call it in your controller.
As a general "angular" rule, anything that looks like jQuery should only happen in a directive.
EDIT: With more info on what you need, you can do this without need to do any DOM manipulation at all (no directives).
appModuleLightDashboard.controller('ManageLinksController',
function($scope, $http, $timeout) {
$scope.activeLink = undefined;
$scope.addLink = function(event) {
$scope.activeLink = {};
$scope.linksForPerson.push($scope.activeLink);
}
$scope.showLinkDetail = function(link){
$scope.activeLink = link
}
$scope.isSelectedLink = function(link){
return $scope.activeLink === link;
}
});
<div class="list-group link-list" ng-show="linksForPerson">
<a href="" class="list-group-item" ng-repeat="link in linksForPerson" ng-click="showLinkDetail(link)" ng-class="{active: isSelectedLink(link)}">
<h4 class="list-group-item-heading">[[ link.engine.name ]]</h4>
<p class="list-group-item-text">[[ link.engine.base_url ]]</p>
<p class="list-group-item-text" ng-show="link.user_sync_id">[[ link.user_sync_id ]]</p>
<p class="list-group-item-text" ng-show="link.group_sync_id">[[ link.group_sync_id ]]</p>
</a>
<span class="glyphicon glyphicon-plus"></span> Add a new link
</div>

you should not put your "add new link" inside the div with ngShow because when the linksForPerson array is empty, you will not be able to add a new link . Also, putting it outside the div will ease up every other manipulation (based on what you want to achieve"
linksForPerson is an array, use ng-show="linksForPerson.length" instead
you should initialize your arrays before pushing anything into it $scope.linksForPerson=[]
use of ng-bind is a better alternative to {{}} or [[]]
I refactored your code.
// ---- controller
appModuleLightDashboard.controller('ManageLinksController',
function($scope, $http, $timeout) {
var activeLink;
// you should initiate your array
$scope.linksForPerson = [];
$scope.isSelectedLink = function (link) {
return activeLink === link;
};
$scope.addLink = function(event) {
activeLink = {
engine: {
name : "engine" + ($scope.linksForPerson.length + 1),
base_url : " someUrl"
}
};
$scope.linksForPerson.push(activeLink);
};
});
and html (note use of ng-bind)
<div ng-controller="ManageLinksController">
<div class="list-group link-list" ng-show="linksForPerson.length">
<a href="#" class="list-group-item" ng-repeat="link in linksForPerson" ng-click="showLinkDetail(link)" ng-class="{active: isSelectedLink(link)}">
<h4 class="list-group-item-heading" ng-bind="link.engine.name"></h4>
<p class="list-group-item-text" ng-bind="link.engine.base_url"></p>
<p class="list-group-item-text" ng-show="link.user_sync_id" ng-bind="link.user_sync_id"></p>
<p class="list-group-item-text" ng-show="link.group_sync_id" ng-bind="link.group_sync_id"></p>
</a>
</div>
<span class="glyphicon glyphicon-plus"></span> Add a new link
</div>
here's jsfiddle for you to play with

Related

AngularJS 1.5 - Iterate and set child components' property

I have a parent component that creates "n" child components within an ng-repeat. Each child component has an accordion element (from ui-bootstrap directives) in its template. From the parent component I would like to collapse or expand all accordions using a link in the parent component level. Each child accordion can be expanded/collapsed individually setting the local vm.isAccordionExpanded variable.
I am planning to use $scope.$broadcast() form the parent to notify the children, each of them will intercept the events with $scope.$on() and set a local boolean variable vm.isAccordionExpanded to open/close the accordion respectively.
Parent component template:
<span id="accordionListCommands" ng-if="vm.pastVisits.totalResults > 0">
<span id="collapseAllAccordion">
<a ng-click="vm.collapseAll()" href="">
<i class="fa fa-minus-square" aria-hidden="true"></i></a>
</span>
<span id="expandAllAccordion">
<a ng-click="vm.expandAll()" href="">
<i class="fa fa-plus-square" aria-hidden="true"></i></a>
</span>
</span>
<div ng-repeat="visitItem in vm.pastVisits.data">
<visits-list-component visit="visitItem"></visits-list-component>
</div>
Parent component js file:
$scope.$on('collapse-all-accordion', function () {
vm.isAccordionExpanded = false;
});
$scope.$on('expand-all-accordion', function () {
vm.isAccordionExpanded = true;
});
Child template:
<uib-accordion close-others="false">
<div uib-accordion-group is-open="vm.isAccordionExpanded">
//Rest of the template
Is there a better or more performant way to achieve this?
The way you are doing this is not right and its not the angular way to write it.
Instead use one way data binding or two way data binging:
bindings: {
visit: '<' // or ('=') respectivly
}
and then implement your collapseAll function like follows:
angular.forEach( vm.pastVisits.data,function(visitItem) {
visitItem.isAccordionExpanded = false;
});
then in the visit component you can write:
vm.$onChanges = function() {
if(changes.visit) {
vm.isAccordionExpanded = changes.visit.currentValue.isAccordionExpanded;
}
}
or even better without having to put $onChanges listener :
<uib-accordion close-others="false">
<div uib-accordion-group is-open="vm.visit.isAccordionExpanded">

Angular ui-router prevent state trigger inside directive

I have a directive clickable-tag for which i am passing my data as the tag's name (tag.tag):
<a class="item item-avatar"
ui-sref="nebula.questionData({questionId: question.id})"
ng-repeat="question in questionsData.questions">
<img src="{{question.user.profile_photo || '../img/avatar.jpg'}}">
<h2 class="question-title">{{question.title}}</h2>
<p>{{question.description}}</p>
<div class="question-tags-list" ng-repeat="tag in question.tags" clickable-tag data="{{tag.tag}}">
<button type="submit" class="tag">{{tag.tag}}</button>
</div>
</a>
The directive clickable-tag is inside a ui-sref (on the outer a tag). Inside the directive, I want the outer ui-sref to be prevented and instead the user should be directed to another state (the one i am specifying in the directive below).
.directive("clickableTag", function($state) {
return {
restrict: "A",
scope: {
data: "#"
},
link: function(scope, elem, attrs) {
elem.bind('click', function(ev) {
console.log('scope.tagName: ', scope.tagName);
if (scope.data) {
$state.go('nebula.tagData', {tagName: scope.data});
}
});
}
};
})
The problem is that only the resolve of the state specified inside the directive runs. The view which is actually rendered is of the state specified by the outer ui-sref.
Any solutions as to how to prevent the outer ui-sref from being triggered. and instead trigger a state change as specified inside the directive ?
Any help would be appreciated. Thanks.
Note: I have already tried preventDefault(), stopPropagation(), return false inside my directive.
Move the ng-repeat outside and above the <a> tag and move the close of the <a> tag above the button.
<div ng-repeat="question in questionsData.questions">
<a class="item item-avatar"
ui-sref="nebula.questionData({questionId: question.id})">
<img src="{{question.user.profile_photo || '../img/avatar.jpg'}}">
<h2 class="question-title">{{question.title}}</h2>
<p>{{question.description}}</p>
</a> <!--Put close of A tag here --->
<div class="question-tags-list" ng-repeat="tag in question.tags"
ng-click="$state.go('nebula.tagData', {tagName: tag.tag})">
<button type="submit" class="tag">{{tag.tag}}</button>
</div>
</div>
For more information see the AngularJS ng-click API Docs

AngularJS: Set element to class active by default

I've created a custom tabbed element using the following code:
<div class="row step">
<div class="col-md-4 arrow active" ui-sref-active="active">
<a ui-sref="dashboard.create.key_elements" ui-sref-opts="{ reload: true }">
<span class="number">1</span>
<span class="h5">Key Elements</span>
</a>
</div>
<div class="col-md-4 arrow" ui-sref-active="active">
<a ui-sref="dashboard.create.questions" ui-sref-opts="{ reload: true }">
<span class="number">2</span>
<span class="h5">Questions</span>
</a>
</div>
<div class="col-md-4 arrow" ui-sref-active="active">
<a ui-sref="dashboard.create.publish" ui-sref-opts="{ reload: true }">
<span class="number">3</span>
<span class="h5">Publish</span>
</a>
</div>
</div>
As you can see I'm using ui-sref-active="active" to add a class of active to an element when it is clicked. My issue is getting the first element to display with a class of active when the page is first loaded as currently it only happens when an item is clicked. I've tried manually adding active to the first element but this seems to be ignored.
The problem is that the first route dashboard.create.key_elements is not the current route, so ui-router disables it as "active".
Solution:
Add another class in the CSS e.g. "newclassname" to have the same behavior of "active" class
Add ng-class to the first element conditioned to a variable in $scope and ng-click on the other elements so to disable it
In the JS:
$scope.firstActive = true;
$scope.changeFirst = function() {
$scope.firstActive = false;
};
EDIT:
Better yet, instead of dabbling with ng-click, you can simply inject the variable when you define the routes. E.g. from a snippet of my own code
.state('ordini', {
url: '/ordini/:pdv',
templateUrl: 'ordini/ordini.html',
controller: 'OrdiniController',
resolve : {
CartValue: ['$rootScope', '$stateParams', 'CartService', function($rootScope, $stateParams, CartService){
return CartService.getCartValue($rootScope.user.MachCode, $stateParams.pdv);
}]
}
See documentation

AngularJS - change parent scope from directive?

I'm building my first Angular app, but am having a bit of trouble getting something to work. I have a video container that will be hidden until $scope.video.show = true; I'm trying to set this value when I click on a link. I'm trying to make that happen in a directive. Any help would be appreciated.
html:
<div ng-controller="AppCtrl">
<div ng-cloak
ng-class="{'show':video.show, 'hide':!video.show}">
// youtube iframe content, for example
</div>
<div>
<ul>
<li>
<h3>Video Headline 1</h3>
<button type="button"
video-show
data-video-id="jR4lLJu_-wE">PLAY NOW 〉</button>
</li>
<li>
<h3>Video Headline 2</h3>
<button type="button"
video-show
data-video-id="sd0f9as8df7">PLAY NOW 〉</button>
</li>
</ul>
</div>
</div>
javascript:
var thisViewModel = angular.module("savings-video", [])
.controller('SavingsVideoController', function($scope) {
$scope.video = {
show : false,
videoId : ""
};
};
thisViewModel.directive("videoShow", function(){
return{
restrict: 'A',
link: function(scope , element){
element.bind("click", function(e){
var $this = angular.element(element);
$this.closest('li').siblings().addClass('hide'); // hide the other one
$this.closest('li').removeClass('hide'); // keep me open
scope.video.show = true; // doesn't work.
// what is the best way to do this?
});
}
}
});
I see a few things you can improve.
Checkout ngShow/ngHide and ngIf; they'll give you toggle-ability more easily than trying to do it from scratch.
Think in angular. Rather than trying to use logic to modify the DOM on your own, simply setup your rules using angular directives, and let the framework do the rest for you.
For example, it seems like this is more what you want.
<div ng-controller="AppCtrl">
<div ng-cloak ng-show='video.show">
// youtube iframe content, for example
</div>
<div>
<ul ng-switch="video.videoId">
<my-video my-video-id="jR4ABCD" my-headline="Video Headline 1" ng-switch-when="myVideoId" my-video-manager="video" />
<my-video my-video-id="al1jd89" my-headline="Video Headline 2" ng-switch-when="myVideoId" my-video-manager="video"/>
</ul>
</div>
</div>
What I changed is making your iframe show conditionally with ngShow, and using ngSwitch to control which video appears (the appearing video is based on the $scope's video.videoId). Then, I turned your <li>s into a directive called my-video, which ends up looking like this
thisViewModel.directive("my-video", function(){
return{
restrict: 'E',
replace: true,
scope: {
myVideoId = "=",
myHeadline = "=",
myVideoManager = "="
},
template = '<li><h3>{{myHeadline}}</h3><button type="button" ng-click="play()">PLAY NOW 〉</button></li>',
link: function(scope , element){
scope.play = function(){
myVideoManager.show = true;
/*whatever you want here, using scope.myVideoId*/
}
}
}
});
This directive does exactly what your old HTML did, but brings it into the angular framework so you can access the properties you're looking for. By using the raw angular directives, I eliminate the need for any manual UI logic; I don't need to access element at all anymore, and both my HTML and JavaScript are cleaner. There's certainly room for improvement here, even, but I would say that this is closer to the right track.
It takes practice to get more familiar with, but following the guidelines in the SO link above will help.
EDIT
Sorry, think I missed a requirement the first time around. If you want both videos to show when none are selected, don't use ng-switch; just set up some manual ng-shows.
<div>
<ul>
<my-video my-video-id="jR4ABCD" my-headline="Video Headline 1" ng-show="myVideoId == video.videoId" my-video-manager="video" />
<my-video my-video-id="al1jd89" my-headline="Video Headline 2" ng-show="myVideoId == video.videoId" my-video-manager="video"/>
</ul>
</div>
Since ng-switch is really just a shortcut for ng-show anyways, it amounts to the same thing; the logic just got moved into the ng-show attribute instead.
Also, if you have an array of videos, checkout out ng-repeat; it will let you repeat your video tag multiple times automatically, instead of by hand.
<ul>
<my-video ng-repeat='aVideo in myVideoArray' my-video-id='aVideo.videoId' my-headline...(and so on)>
</ul>
Well your controller names don't match up. Try changing AppCtrl to SavingsVideoController.
You only need a very simple solution.
HTML
<div ng-controller="AppCtrl">
<div ng-cloak ng-show="view.show">
<!-- Use ng-show is more convenient -->
</div>
<div>
<ul>
<li>
<h3>Video Headline 1</h3>
<button type="button"
ng-click="view.show = true"
data-video-id="jR4lLJu_-wE">PLAY NOW 〉</button>
<!-- You don't need an extra directive to change view.show -->
</li>
<li>
<h3>Video Headline 2</h3>
<button type="button"
ng-click="view.show = true"
data-video-id="sd0f9as8df7">PLAY NOW 〉</button>
</li>
</ul>
</div>
</div>
JS
var thisViewModel = angular.module("savings-video", [])
.controller('SavingsVideoController', function($scope) {
$scope.video = {
show : false,
videoId : ""
};
};
// No need to create another directive

Running click functions on every instance of listing instead of current

I have a listing of articles here, and I can't figure out how to execute the ng-click function calls on every new article inside the ng-repeat. Right now it works for existing articles, but when new articles are added dynamically (via AJAX), I need those to have the same functionality too.
For example: the ng-click function calls on the "+" sign to reveal social buttons seem to not work once new articles are inserted via AJAX (ie: delete articles, and let list be populated again with new elements)
Does AngularJS provide any tools to do that?
<div>
<div>
<input type="text" ng-model="search">
<span>{{filtered.length}} article(s)</span>
</div>
<div article-listing ng-repeat="article in filtered = (wikiArticles | filter:search)">
<!--Individual article begin-->
<span>
{{article.title}}
</span>
<div>
<a ng-click="articles.removeArticle($index)" title="Delete">
<span>✖</span>
</a>
<a ng-click="articles.toggleShare(article)">
<span class="plus-sign" title="Share">✖</span>
<div social-share ng-show="article.socialShare">
<div ng-click="socialShare = !socialShare" class="addthis_toolbox addthis_default_style addthis_32x32_style"
addthis:title="{{article.title}}" addthis:description="{{article.extract}}" addthis:url="{{article.url}}">
<a class="addthis_button_facebook"></a>
<a class="addthis_button_twitter"></a>
<a class="addthis_button_google_plusone_share"></a>
<a class="addthis_button_reddit"></a>
<a class="addthis_button_hackernews"></a>
</div>
</div>
</a>
</div>
<div>{{article.extract}}</div>
<!--Individual article end-->
</div>
</div>
Code for ng-click calls that don't seem to work for new article insertions
$scope.articles = (function() {
return {
shuffleArticles : function() {
$scope.wikiArticles.reverse();
},
removeArticle : function(index) {
$scope.wikiArticles.splice(index, 1);
$scope.fireAPICalls();
},
toggleShare : function(currArticle) {
var previousState = currArticle.socialShare;
angular.forEach($scope.wikiArticles, function(article) {
article.socialShare = false;
});
currArticle.socialShare = previousState ? false : true;
}
}
})();
Your ng-click calls are actually working- you can watch the ng-show toggle in the debugger.
The problem is that there is nothing to display on the new items you add.
The articles you initially add all have their icons populated with the .addthis classes, for instance here's your Facebook icon element:
<a class="addthis_button_facebook at300b" title="Facebook" href="#">
<span class=" at300bs at15nc at15t_facebook">
<span class="at_a11y">Share on facebook</span>
</span>
</a>
at300bs includes the following css which displays the image:
background: url(widget058_32x32.gif) no-repeat left!important;
However as you add new items, you aren't including the needed .addthis classes to them. Their elements look like this:
<a class="addthis_button_facebook"></a>
So ng-show has nothing to display (it shows a 0x0 div).
Add the .addthis classes to your new elements as you add them and you'll be all set.

Categories

Resources