AngularJS: ng-repeat list is not updated when a model element is spliced from the model array

159,429

Solution 1

Whenever you do some form of operation outside of AngularJS, such as doing an Ajax call with jQuery, or binding an event to an element like you have here you need to let AngularJS know to update itself. Here is the code change you need to do:

app.directive("remove", function () {
    return function (scope, element, attrs) {
        element.bind ("mousedown", function () {
            scope.remove(element);
            scope.$apply();
        })
    };

});

app.directive("resize", function () {
    return function (scope, element, attrs) {
        element.bind ("mousedown", function () {
            scope.resize(element);
            scope.$apply();
        })
    };
});

Here is the documentation on it: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply

Solution 2

If you add a $scope.$apply(); right after $scope.pluginsDisplayed.splice(index,1); then it works.

I am not sure why this is happening, but basically when AngularJS doesn't know that the $scope has changed, it requires to call $apply manually. I am also new to AngularJS so cannot explain this better. I need too look more into it.

I found this awesome article that explains it quite properly. Note: I think it might be better to use ng-click (docs) rather than binding to "mousedown". I wrote a simple app here (http://avinash.me/losh, source http://github.com/hardfire/losh) based on AngularJS. It is not very clean, but it might be of help.

Solution 3

I had the same issue. The problem was because 'ng-controller' was defined twice (in routing and also in the HTML).

Solution 4

Remove "track by index" from the ng-repeat and it would refresh the DOM

Share:
159,429

Related videos on Youtube

janesconference
Author by

janesconference

Updated on July 17, 2020

Comments

  • janesconference
    janesconference almost 4 years

    I have two controllers and share data between them with an app.factory function.

    The first controller adds a widget in the model array (pluginsDisplayed) when a link is clicked. The widget is pushed into the array and this change is reflected into the view (that uses ng-repeat to show the array content):

    <div ng-repeat="pluginD in pluginsDisplayed">
        <div k2plugin pluginname="{{pluginD.name}}" pluginid="{{pluginD.id}}"></div>
    </div>
    

    The widget is built upon three directives, k2plugin, remove and resize. The remove directive adds a span to the template of the k2plugin directive. When said span is clicked, the right element into the shared array is deleted with Array.splice(). The shared array is correctly updated, but the change is not reflected in the view. However, when another element is added, after the remove, the view is refreshed correctly and the previously-deleted element is not shown.

    What am I getting wrong? Could you explain me why this doesn't work? Is there a better way to do what I'm trying to do with AngularJS?

    This is my index.html:

    <!doctype html>
    <html>
        <head>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.min.js">
            </script>
            <script src="main.js"></script>
        </head>
        <body>
            <div ng-app="livePlugins">
                <div ng-controller="pluginlistctrl">
                    <span>Add one of {{pluginList.length}} plugins</span>
                    <li ng-repeat="plugin in pluginList">
                        <span><a href="" ng-click="add()">{{plugin.name}}</a></span>
                    </li>
                </div>
                <div ng-controller="k2ctrl">
                    <div ng-repeat="pluginD in pluginsDisplayed">
                        <div k2plugin pluginname="{{pluginD.name}}" pluginid="{{pluginD.id}}"></div>
                    </div>
                </div>
            </div>
        </body>
    </html>
    

    This is my main.js:

    var app = angular.module ("livePlugins",[]);
    
    app.factory('Data', function () {
        return {pluginsDisplayed: []};
    });
    
    app.controller ("pluginlistctrl", function ($scope, Data) {
        $scope.pluginList = [{name: "plugin1"}, {name:"plugin2"}, {name:"plugin3"}];
        $scope.add = function () {
            console.log ("Called add on", this.plugin.name, this.pluginList);
            var newPlugin = {};
            newPlugin.id = this.plugin.name + '_'  + (new Date()).getTime();
            newPlugin.name = this.plugin.name;
            Data.pluginsDisplayed.push (newPlugin);
        }
    })
    
    app.controller ("k2ctrl", function ($scope, Data) {
        $scope.pluginsDisplayed = Data.pluginsDisplayed;
    
        $scope.remove = function (element) {
            console.log ("Called remove on ", this.pluginid, element);
    
            var len = $scope.pluginsDisplayed.length;
            var index = -1;
    
            // Find the element in the array
            for (var i = 0; i < len; i += 1) {
                if ($scope.pluginsDisplayed[i].id === this.pluginid) {
                    index = i;
                    break;
                }
            }
    
            // Remove the element
            if (index !== -1) {
                console.log ("removing the element from the array, index: ", index);
                $scope.pluginsDisplayed.splice(index,1);
            }
    
        }
        $scope.resize = function () {
            console.log ("Called resize on ", this.pluginid);
        }
    })
    
    app.directive("k2plugin", function () {
        return {
            restrict: "A",
            scope: true,
            link: function (scope, elements, attrs) {
                console.log ("creating plugin");
    
                // This won't work immediately. Attribute pluginname will be undefined
                // as soon as this is called.
                scope.pluginname = "Loading...";
                scope.pluginid = attrs.pluginid;
    
                // Observe changes to interpolated attribute
                attrs.$observe('pluginname', function(value) {
                    console.log('pluginname has changed value to ' + value);
                    scope.pluginname = attrs.pluginname;
                });
    
                // Observe changes to interpolated attribute
                attrs.$observe('pluginid', function(value) {
                    console.log('pluginid has changed value to ' + value);
                    scope.pluginid = attrs.pluginid;
                });
            },
            template: "<div>{{pluginname}} <span resize>_</span> <span remove>X</span>" +
                           "<div>Plugin DIV</div>" +
                      "</div>",
            replace: true
        };
    });
    
    app.directive("remove", function () {
        return function (scope, element, attrs) {
            element.bind ("mousedown", function () {
                scope.remove(element);
            })
        };
    
    });
    
    app.directive("resize", function () {
        return function (scope, element, attrs) {
            element.bind ("mousedown", function () {
                scope.resize(element);
            })
        };
    });
    
  • Per Hornshøj-Schierbeck
    Per Hornshøj-Schierbeck about 10 years
    Consider moving the scope.remove(element) and scope.resize(element) inside a expression/function passed to $apply.
  • Jim Aho
    Jim Aho about 9 years
    @PerHornshøj-Schierbeck I agree, otherwise Angular will not be aware of errors if those occur.
  • Alex
    Alex over 8 years
    be careful ! normally, Angular call the digest cycle when he have to and $apply will call it manually. It's often a bad practice to call it manually, because we can make optimization errors, and it can be resource consuming.
  • Desarrollo Desafio de Guerrero
    Desarrollo Desafio de Guerrero almost 7 years
    I don't understand what is that remove or resize you're putting there.