killua8p
killua8p

Reputation: 314

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

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>
    <a href="" class="list-group-item" ng-click="addLink($event)"><span class="glyphicon glyphicon-plus"></span> Add a new link</a>
</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);
        }
    });

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:)

Upvotes: 0

Views: 736

Answers (2)

jfab fab
jfab fab

Reputation: 491

  1. 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"

  2. linksForPerson is an array, use ng-show="linksForPerson.length" instead

  3. you should initialize your arrays before pushing anything into it $scope.linksForPerson=[]

  4. 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>

    <a href="#" class="list-group-item" ng-click="addLink($event)"><span class="glyphicon glyphicon-plus"></span> Add a new link</a>
</div>

here's jsfiddle for you to play with

Upvotes: 0

Joe Enzminger
Joe Enzminger

Reputation: 11190

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>
    <a href="" class="list-group-item" ng-click="addLink($event)"><span class="glyphicon glyphicon-plus"></span> Add a new link</a>
</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>
    <a href="" class="list-group-item" ng-click="addLink($event)"><span class="glyphicon glyphicon-plus"></span> Add a new link</a>
</div>

Upvotes: 3

Related Questions