AngularJS: la lista ng-repeat no se actualiza cuando un elemento del modelo se empalma de la matriz del modelo

100

Tengo dos controladores y comparto datos entre ellos con una función app.factory.

El primer controlador agrega un widget en la matriz del modelo (pluginsDisplayed) cuando se hace clic en un enlace. El widget se inserta en la matriz y este cambio se refleja en la vista (que usa ng-repeat para mostrar el contenido de la matriz):

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

El widget se basa en tres directivas, k2plugin, remove y resize. La directiva remove agrega un espacio a la plantilla de la directiva k2plugin. Cuando se hace clic en dicho intervalo, se elimina el elemento correcto en la matriz compartida con Array.splice(). La matriz compartida se actualiza correctamente, pero el cambio no se refleja en la vista. Sin embargo, cuando se agrega otro elemento, después de la eliminación, la vista se actualiza correctamente y no se muestra el elemento eliminado anteriormente.

¿Qué me estoy equivocando? ¿Podrías explicarme por qué esto no funciona? ¿Hay una mejor manera de hacer lo que estoy tratando de hacer con AngularJS?

Este es mi 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>

Este es mi 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);
        })
    };
});
conferencia de janes
fuente

Respuestas:

131

Siempre que realice alguna forma de operación fuera de AngularJS, como hacer una llamada Ajax con jQuery, o vincular un evento a un elemento como el que tiene aquí, debe informar a AngularJS para actualizarse. Aquí está el cambio de código que debe hacer:

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();
        })
    };
});

Aquí está la documentación al respecto: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply

Mathew Berg
fuente
4
Considere mover scope.remove (elemento) y scope.resize (elemento) dentro de una expresión / función pasada a $ apply.
Per Hornshøj-Schierbeck
1
@ PerHornshøj-Schierbeck Estoy de acuerdo; de lo contrario, Angular no se dará cuenta de los errores si ocurren.
Jim Aho
2
ten cuidado ! normalmente, Angular llama al ciclo de resumen cuando tiene que hacerlo y $ apply lo llamará manualmente. A menudo es una mala práctica llamarlo manualmente, porque podemos cometer errores de optimización y puede consumir recursos.
Alex
No entiendo qué es esa eliminación o cambio de tamaño que está poniendo allí.
Desarrollo Desafio de Guerrero
53

Si agrega un $scope.$apply();derecho después $scope.pluginsDisplayed.splice(index,1);, funciona.

No estoy seguro de por qué sucede esto, pero básicamente cuando AngularJS no sabe que el $ scope ha cambiado, requiere llamar a $ apply manualmente. También soy nuevo en AngularJS, así que no puedo explicar esto mejor. También necesito investigar más.

Encontré este artículo asombroso que lo explica bastante bien. Nota: Creo que sería mejor usar ng-click (docs) en lugar de enlazar a "mousedown". Escribí una aplicación simple aquí ( http://avinash.me/losh , fuente http://github.com/hardfire/losh ) basada en AngularJS. No está muy limpio, pero podría ser de ayuda.

avk
fuente
7

Tuve el mismo problema. El problema fue porque 'ng-controller' se definió dos veces (en el enrutamiento y también en el HTML).

shreedhar bhat
fuente
1

Elimine "track by index" de ng-repeat y actualizará el DOM

Bassem Zaitoun
fuente
0

Hay una forma sencilla de hacerlo. Muy fácil. Desde que me di cuenta de que

$scope.yourModel = [];

elimina toda la lista de matrices $ scope.yourModel que puede hacer así

function deleteAnObjectByKey(objects, key) {
    var clonedObjects = Object.assign({}, objects);

     for (var x in clonedObjects)
        if (clonedObjects.hasOwnProperty(x))
             if (clonedObjects[x].id == key)
                 delete clonedObjects[x];

    $scope.yourModel = clonedObjects;
}

$ Scope.yourModel se actualizará con clonedObjects.

Espero que ayude.

user3856437
fuente