0

I am writing my first AngularJS app and I'm trying to get a directive to update its view when an array it received from the service changed.

My directive looks like this:

angular.module('Aristotle').directive('ariNotificationCenter', function (Notifications) {
    return {
        replace: true,
        restrict: 'E',
        templateUrl: 'partials/ariNotificationCenter.html',
        controller: function ($scope) {
            $scope.notifications = Notifications.getNotifications();

            $scope.countUnread = function () {
                return Notifications.countUnread();
            };
        }
    };
});

The partial is quite simply:

<p>Unread count: {{countUnread()}}</p>

While my Notifications service looks like this:

function Notification (text, link) {
    this.text = text;
    this.link = link;
    this.read = false;
}

var Notifications = {
    _notifications: [],

    getNotifications: function () {
        return this._notifications;
    },

    countUnread: function () {
        var unreadCount = 0;

        $.each(this._notifications, function (i, notification) {
            !notification.read && ++unreadCount;
        });

        return unreadCount;
    },

    addNotification: function (notification) {
        this._notifications.push(notification);
    }
};

// Simulate notifications being periodically added
setInterval(function () {
    Notifications.addNotification(new Notification(
        'Something happened!',
        '/#/somewhere',
        Math.random() > 0.5
    ));
}, 2000);

angular.module('Aristotle').factory('Notifications', function () {
    return Notifications;
});

The getNotifications function returns a reference to the array, which gets changed by the setInterval setup when addNotification is called. However, the only way to get the view to update is to run $scope.$apply(), which stinks because that removes all the automagical aspect of Angular.

What am I doing wrong?

Thanks.

2 Answers 2

2

I believe the only problem with you code is that you are using setInterval to update the model data, instead of Angular built-in service $interval. Replace the call to setInterval with

$interval(function () {
    Notifications.addNotification(new Notification(
        'Something happened!',
        '/#/somewhere',
        Math.random() > 0.5
    ));
}, 2000);

And it should work without you calling $scope.$apply. Also remember to inject the $interval service in your factory implementation Notifications.

angular.module('Aristotle').factory('Notifications', function ($interval) {

$interval internally calls $scope.$apply.

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

2 Comments

I was just discovering this about the same time from this article: jimhoskins.com/2012/12/17/angularjs-and-apply.html . Thanks so much, it makes much more sense that the only way for Angular to be aware that the scope was modified is if it was told to listen for the update.
Yes this in an excellent article. Angular needs to know when the model changed. It watches for some triggers to determine that. These triggers include remote request, interval and timeout requests, keyboard and mouse events.
1

I'm not an expert at Angular yet, but it looks like your problem may be in the partial.

<p>Unread count: {{countUnread()}}</p>

I don't think you can bind to a function's results. If this works, I believe it will only calculate the value once, and then it's finished, which appears to be the issue you are writing about.

Instead, I believe you should make a variable by the same name:

$scope.countUnread = 0;

And then update the value in the controller with the function.

Then, in your partial, remove the parentheses.

<p>Unread count: {{countUnread}}</p>

As long as $scope.countUnread is indeed updated in the controller, the changes should be reflected in the partial.

And as a side note, if you take this approach, I'd recommend renaming either the variable or the function, as that may cause issues, or confusion at the very least.

5 Comments

countUnread is a function though, it's a method attached to the $scope (defined in my first block of code in the question).
Right after I posted that, I reviewed the code samples again and saw that. I just completely overhauled my answer for what I believe is now the correct approach. Sorry about that.
Okay, so other controllers (or potentially a backend) might be calling methods on the Notifications service that will add notifications. If I just store a number in the controller, how can I make it so that when another separate controller pushes a notification to the Notifications service, this directive will recalculate the unread count?
You can bind to a function result, and it will be evaluated on every digest cycle.
There are two main ways (that I currently know of) to share data how it seems you want to. The first is to pass the data as parameters in the querystring when you navigate to the page, such as /page.html?unread=0. Another way is to have multiple pages sharing the same controller. Not sure how "recommended" the latter is though...

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.