user2509942
user2509942

Reputation: 33

Changes to scope on click are not being updated across my app

Started using Angular last week, read/watched many tutorials and I'm currently trying to build a newsfeed type application.

Here's the skinny: I have a service that gets data from the server. On the newsfeed itself I have two controllers: one that has the entire newsfeed in its scope and another that has an instance for each newsfeed article. If the user clicks an icon on an individual post it should call a service that has been injected into both controllers and then broadcasts a message that the main controller picks up. The main controller then updates a variable in a filter, filtering the newsfeed content based on the user's selection.

Here's the problem: Everything works fine except that the main controller doesn't update the bound variable in the HTML. I have read close to every SO article on two-way binding within an ng-repeat and the related struggles, but in my case the bound variable falls outside an ng-repeat, hence why I'm posting.

The code:

services.factory('filterService', function() {
var filterService = {};

filterService.filterKey = '';

filterService.getFilter = function() {
    return filterService.filterKey;
};

filterService.setFilter = function(name) {
    filterService.filterKey = name;
    $rootScope.$broadcast('changeFilter');
};

return filterService;

});

app.controller('CommentCtrl', function($scope, $timeout, $http, filterService) {

$scope.setSearchParam = function(param) {
    alert('clicked: ' + param)
    filterService.setFilter(param);
}



app.controller('FeedCtrl', function($scope, articles, filterService, $timeout) {
$scope.articles = articles;
$scope.model = {
    value: ''
};
$scope.$on('changeFilter', function() {
    console.log(filterService.filterKey);
    $scope.model.value = filterService.filterKey
    }  
}); 

});

<div class="articles">
    <div class="articleStub" ng-repeat="article in articles|filter:model.value">

        <div ng-controller="CommentCtrl">
            <div class="{{article.sort}}">
                <div class="leftBlock">
                    <a href="#" ng-click="setSearchParam(article.sort)">
                        <div class="typeIcon">
                            <i ng-class="{'icon-comments':article.question, 'icon-star':article.create, 'icon-ok-sign':article.notype}"></i>
                        </div>
                    </a>

Note: the FeedCtrl controller is called in the app.config $routeprovider function thing whatever its called

Edited to add: the alert and console checks both work, so I'm assuming the issue is not in the filterService or CommentCtrl.

Here's the Plnkr: http://plnkr.co/edit/bTit7m9b04ADwkzWHv88?p=preview

Upvotes: 3

Views: 2547

Answers (2)

Ed_
Ed_

Reputation: 19098

I'm adding another answer as the other is still valid, but is not the only problem!

Having looked at your code, your problems were two fold:

  • You had a link to href="#"

    This was causing the route code to be re-run, and it was creating a new instance of the controller on the same page, but using a different scope. The way I found this out was by adding the debug line: console.log("running controller init code for $scope.$id:" + $scope.$id); into script.js under the line that blanks the model.value. You'll notice it runs on every click, and the $id of the scope is different every time. I don't fully understand what was happening after that, but having two of the same controller looking after the same bit of the page can't be a good thing!

So, with that in mind, I set href="". This ruins the rendering of the button a bit, but it does cure the problem of multiple controllers being instantiated. However, this doesn't fix the problem... what's the other issue?

  • angular.element.bind('click', ....) is running 'outside the angular world'

    This one is a bit more complicated, but basically for angular data-bindings to work, angular needs to know when the scope gets changed. Most of the time it's handled automagically by angular functions (e.g. inside controllers, inside ng-* directives, etc.), but in some cases, when events are triggered from the browser (e.g. XHR, clicks, touches, etc.), you have to tell angular something has changed. You can do this with $scope.$apply(). There are a few good articles on the subject so I'd recommend a bit of reading (try here to begin with).

There are two solutions to this - one is to use the ng-click directive which wraps the native click event with $scope.$apply (and has the added advantage that your markup is more semantic), or the other is to do it yourself. To minimise the changes to your code, I just wrapped your click code in scope.$apply for you:

element.bind('click', function() {
  // tell angular that it needs to 'digest' the changes you're about to make.
  scope.$apply(function(){
    var param = scope.article.sort;
    filterService.setFilter(param);
  })                
});

Here's a working version of your code: http://plnkr.co/edit/X1AK0Bc4NZyChrJEknkN?p=preview

Note I also set up a filter on the list. You could easily ad a button to clear it that is hidden when there's no filter set:

<button ng-click="model.value=''" ng-show="model.value">Clear filter</button>

Hope this helps :)

Upvotes: 2

Ed_
Ed_

Reputation: 19098

I actually think the problem is not that your model.value isn't getting updated - all that code looks fine.

I think the problem lies in your filter.

<div class="articleStub" ng-repeat="article in articles|filter:model.value">

This filter will match any object with any field that contains model.value. What you actually want to do is the following:

<div class="articleStub" 
  ng-repeat="article in articles|filter:{sort: model.value}:true">

To specify that you only want to match against the sort property of each article. The final true parameter means that it'll only allow strict matches as well, so ed wouldn't match edward.

Note that | filter:{sort: model.value}:true is an angular expression, the :s are like JavaScript commas. If you were to imagine it in JavaScript it would be more like: |('filter',{sort:model.value}, true) where | is a special 'inject a filter here' function..

EDIT:

I'm finding it hard to debug your example without having the working code in front of me. If you can make it into a plunker I can help more, but in the meantime, I think you should try to make your code less complicated by using a different approach.

I have created a plunker that shows an easy way to filter a list by the item that you click. I've used very little code so hopefully it's quite easy to understand?

I would also recommend making your feed items into a directive. The directives can have their own controller so it would prevent you having to do the rather ugly repeating of a ng-controller.

Upvotes: 0

Related Questions