AngularJS: a lista de repetição de ng não é atualizada quando um elemento de modelo é unido a partir da matriz de modelo

100

Eu tenho dois controladores e compartilho dados entre eles com uma função app.factory.

O primeiro controlador adiciona um widget na matriz de modelo (pluginsDisplayed) quando um link é clicado. O widget é colocado na matriz e essa mudança é refletida na visualização (que usa ng-repeat para mostrar o conteúdo da matriz):

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

O widget é baseado em três diretivas, k2plugin, remove e redimensiona. A diretiva remove adiciona um intervalo ao modelo da diretiva k2plugin. Quando a referida extensão é clicada, o elemento certo na matriz compartilhada é excluído com Array.splice(). A matriz compartilhada é atualizada corretamente, mas a alteração não é refletida na visualização. No entanto, quando outro elemento é adicionado, após a remoção, a visualização é atualizada corretamente e o elemento excluído anteriormente não é mostrado.

O que estou entendendo de errado? Você poderia me explicar por que isso não funciona? Existe uma maneira melhor de fazer o que estou tentando fazer com o AngularJS?

Este é meu 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 é meu 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);
        })
    };
});
janesconference
fonte

Respostas:

131

Sempre que você faz alguma forma de operação fora do AngularJS, como fazer uma chamada Ajax com jQuery ou vincular um evento a um elemento como o que você fez aqui, você precisa informar ao AngularJS para se atualizar. Aqui está a mudança de código que você precisa fazer:

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

Aqui está a documentação sobre ele: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply

Mathew Berg
fonte
4
Considere mover o escopo.remove (elemento) e o escopo.resize (elemento) dentro de uma expressão / função passada para $ apply.
Per Hornshøj-Schierbeck
1
@ PerHornshøj-Schierbeck Eu concordo, caso contrário, a Angular não terá conhecimento de erros se eles ocorrerem.
Jim Aho,
2
seja cuidadoso ! normalmente, o Angular chama o ciclo de resumo quando precisa e $ apply o chama manualmente. Geralmente, é uma má prática chamá-lo manualmente, porque podemos cometer erros de otimização e isso pode consumir recursos.
Alex
Não entendo o que é essa remoção ou redimensionamento que você está colocando aí.
Desarrollo Desafio de Guerrero
53

Se você adicionar um $scope.$apply();direito depois $scope.pluginsDisplayed.splice(index,1);, ele funciona.

Não sei por que isso está acontecendo, mas basicamente quando o AngularJS não sabe que o $ escopo mudou, ele exige que você chame $ apply manualmente. Também sou novo no AngularJS, então não posso explicar isso melhor. Eu também preciso olhar mais para isso.

Eu encontrei este artigo incrível que explica isso muito bem. Observação: acho que pode ser melhor usar ng-click (docs) em vez de vincular a "mousedown". Eu escrevi um aplicativo simples aqui ( http://avinash.me/losh , fonte http://github.com/hardfire/losh ) baseado em AngularJS. Não é muito limpo, mas pode ajudar.

avk
fonte
7

Eu tive o mesmo problema. O problema era porque 'ng-controller' foi definido duas vezes (no roteamento e também no HTML).

shreedhar bhat
fonte
1

Remova "faixa por índice" do ng-repeat e isso atualizaria o DOM

Bassem Zaitoun
fonte
0

Existe uma maneira fácil de fazer isso. Muito fácil. Desde que notei que

$scope.yourModel = [];

remove toda a lista de array $ scope.yourModel que você pode fazer assim

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

O $ scope.yourModel será atualizado com clonedObjects.

Espero que ajude.

user3856437
fonte