0

I'm currently working on a simple module to display notifications to the user in the form of toasts, or popups, that appear in the lower right-hand corner of the screen. The following directive works great except I would prefer to

a) use .bind instead of ng-click to handle manual dismissing of a notification;

b) use replace: true help reduce some of the generated markup

toast.directive('toast', function() {
    return {
        link: function($scope, $element, $attrs, $location) {
            $scope.delayDismiss = function(index) {
                setTimeout(function() {
                    $scope.dismiss(index);
                    $scope.$apply();
                }, 5000);
            }

            $scope.dismiss = function(index) {
                $scope.messages[index].dismissed = true;
            }

            $element.bind('click', function() {
                console.log('This never triggers');
            });
        },
        replace: true,
        restrict: 'E',
        template: '<li ng-repeat="toast in messages" ng-click="dismiss($index)" ng-init="delayDismiss($index)" ng-if="!toast.dismissed">{{toast.sender}} says {{toast.message}}</li>'
    }
});

The problem is that, now, $element in my link() function refers to the generated Angular comment instead of the <li> resulting in the bound click event never triggering. Which doesn't make any sense to me.

Am I misunderstanding this whole directive thing?

1
  • "The problem is that, now, $element in my link() function refers to the generated Angular comment" is wrong. As expected, $element refers to the <li>. Can you provide a fiddle reproducing the problem? Notice that replace: true is a deprecated functionality since 1.3.0-beta.10. Commented May 31, 2014 at 18:19

1 Answer 1

2

a) use .bind instead of ng-click to handle manual dismissing of a notification;

By removing ngRepeat from your directive's template, you should be able to do this within the directive's link function. However, I would advise against doing this if it is only a matter of preference on your part.

See the answer to How do I “think in AngularJS” if I have a jQuery background? for more details as to why.

Your usage of ngRepeat is causing the linked element to refer to the generated comment node instead of the <li>.

By moving the ngRepeat directive outside your directive's template, the behavior is closer to what you're expecting:

<toast ng-repeat="toast in messages"></toast>

b) use replace: true help reduce some of the generated markup

That's fine, although note that the angular 1.3 documentation mentions that replace has been deprecated and will be removed from the next major release.


To make the directive more "re-usable", I would move the logic for an individual toast into its own factory instead of trying to handle it on the link function of the directive.

I would also add a ngTransclude to the template <li> and transclude: true to the directive's definition.

View

<ol>
  <toast ng-repeat="toast in messages">
    {{toast.sender}} says {{toast.message}}
  </toast>
</ol>

Controller

controller("MainCtrl", function ($scope, $toast) {      
  $scope.addToast = function(message, sender, timeout) {
    $toast.add(message, sender, parseInt(timeout));
  };
})

Directive

directive('toast', function() {
  return {
    replace: true,
    restrict: 'E',
    template: '<li ng-click="toast.dismiss()" ng-transclude></li>',
    transclude: true
  }
})

Factory

factory("$toast", function($rootScope, $timeout) {
  var messages = $rootScope.messages = [];
  var $toast = (function() {
    function $toast(message, sender, timeout) {
      var self = this;
      this.message = message;
      this.sender = sender;
      this.timeout = timeout || $toast.defaultTimeout;
      this.timer = $timeout(function() {
        self.dismiss();
      }, this.timeout);
    }
    $toast.prototype.dismiss = function() {
      if (this.timer) {
        $timeout.cancel(this.timer);
        this.timer = null;
      }
      var index = messages.indexOf(this);
      if (index !== -1) {
        messages.splice(index, 1);
      }
    };
    return $toast;
  })();
  $toast.defaultTimeout = 5000;
  $toast.add = function(message, sender, timeout) {
    messages.push(new $toast(message, sender, timeout));
    return $toast;
  };
  return $toast;
})

Here is a working example: http://plnkr.co/edit/52JiHWi2jgloIyDst74I?p=preview

Sign up to request clarification or add additional context in comments.

1 Comment

This is a far better answer than I ever expected! I suspect I will be referencing this whenever I'm thinking about how to structure a new component. Thank you so much!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.