Eu me deparei com essa pergunta procurando algo semelhante, mas acho que ela merece uma explicação completa do que está acontecendo, além de algumas soluções adicionais.
Quando uma expressão angular como a que você usou está presente no HTML, o Angular configura automaticamente um $watch
para $scope.foo
e atualiza o HTML sempre $scope.foo
que for alterado.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
O problema não dito aqui é que uma das duas coisas está afetando, de aService.foo
modo que as alterações não são detectadas. Essas duas possibilidades são:
aService.foo
está sendo definido para uma nova matriz a cada vez, fazendo com que a referência a ela fique desatualizada.
aService.foo
está sendo atualizado de forma que um $digest
ciclo não seja acionado na atualização.
Problema 1: referências desatualizadas
Considerando a primeira possibilidade, assumindo que a $digest
está sendo aplicada, se aService.foo
sempre foi a mesma matriz, o conjunto automaticamente$watch
detectaria as alterações, conforme mostrado no snippet de código abaixo.
Solução 1-a: verifique se a matriz ou o objeto é o mesmo objeto em cada atualização
angular.module('myApp', [])
.factory('aService', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
$interval(function() {
if (service.foo.length < 10) {
var newArray = []
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.factory('aService2', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Keep the same array, just add new items on each update
$interval(function() {
if (service.foo.length < 10) {
service.foo.push(Math.random());
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
'aService2',
function FooCtrl($scope, aService, aService2) {
$scope.foo = aService.foo;
$scope.foo2 = aService2.foo;
}
]);
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Array changes on each update</h1>
<div ng-repeat="item in foo">{{ item }}</div>
<h1>Array is the same on each udpate</h1>
<div ng-repeat="item in foo2">{{ item }}</div>
</div>
</body>
</html>
Como você pode ver, o ng-repeat supostamente anexado ao aService.foo
não é atualizado quando aService.foo
muda, mas o ng-repeat anexado ao aService2.foo
faz . Isso ocorre porque nossa referência a aService.foo
está desatualizada, mas nossa referência a aService2.foo
não é. Criamos uma referência à matriz inicial com $scope.foo = aService.foo;
, que foi descartada pelo serviço na próxima atualização, o que significa$scope.foo
não faz mais referência à matriz que queríamos mais.
No entanto, embora existam várias maneiras de garantir que a referência inicial seja mantida intacta, às vezes pode ser necessário alterar o objeto ou a matriz. Ou, e se a propriedade service fizer referência a um primitivo como a String
ou Number
? Nesses casos, não podemos simplesmente confiar em uma referência. Então, o que pode fazer?
Várias das respostas dadas anteriormente já fornecem algumas soluções para esse problema. No entanto, sou pessoalmente a favor do uso do método simples sugerido por Jin e nas semanas seguintes nos comentários:
basta referenciar aService.foo na marcação html
Solução 1-b: anexe o serviço ao escopo e faça referência {service}.{property}
no HTML.
Significado, basta fazer o seguinte:
HTML:
<div ng-controller="FooCtrl">
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
JS:
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
angular.module('myApp', [])
.factory('aService', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
$interval(function() {
if (service.foo.length < 10) {
var newArray = []
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
]);
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js@1.4.7" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Array changes on each update</h1>
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
</body>
</html>
Dessa forma, a $watch
resolução será resolvida aService.foo
em cada$digest
, obtendo o valor atualizado corretamente.
Isso é o que você estava tentando fazer com a solução alternativa, mas de uma maneira muito menos complicada. Você adicionou uma desnecessária $watch
no controlador que coloca explicitamente foo
na $scope
sempre que muda. Você não precisa desse extra $watch
quando anexar em aService
vez de aService.foo
ao $scope
e vincular explicitamente à aService.foo
marcação.
Agora está tudo bem desde que um $digest
ciclo esteja sendo aplicado. Nos meus exemplos acima, usei o $interval
serviço da Angular para atualizar as matrizes, que inicia automaticamente um $digest
loop após cada atualização. Mas e se as variáveis de serviço (por qualquer motivo) não estiverem sendo atualizadas dentro do "mundo Angular". Em outras palavras, não temos um $digest
ciclo sendo ativado automaticamente sempre que a propriedade do serviço muda?
Problema 2: ausente $digest
Muitas das soluções aqui resolverão esse problema, mas eu concordo com o Code Whisperer :
A razão pela qual estamos usando uma estrutura como Angular é não criar nossos próprios padrões de observadores
Portanto, eu preferiria continuar usando o aService.foo
referência na marcação HTML, como mostrado no segundo exemplo acima, e não precisar registrar um retorno de chamada adicional no Controller.
Solução 2: use um levantador e um levantador com $rootScope.$apply()
Fiquei surpreso que ninguém ainda tenha sugerido o uso de um setter e getter . Esse recurso foi introduzido no ECMAScript5 e, portanto, existe há anos. Claro, isso significa que, por qualquer motivo, você precisa oferecer suporte a navegadores realmente antigos, esse método não funcionará, mas eu sinto que getters e setters são muito subutilizados no JavaScript. Nesse caso em particular, eles podem ser bastante úteis:
factory('aService', [
'$rootScope',
function($rootScope) {
var realFoo = [];
var service = {
set foo(a) {
realFoo = a;
$rootScope.$apply();
},
get foo() {
return realFoo;
}
};
// ...
}
angular.module('myApp', [])
.factory('aService', [
'$rootScope',
function($rootScope) {
var realFoo = [];
var service = {
set foo(a) {
realFoo = a;
$rootScope.$apply();
},
get foo() {
return realFoo;
}
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
setInterval(function() {
if (service.foo.length < 10) {
var newArray = [];
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
]);
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Using a Getter/Setter</h1>
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
</body>
</html>
Aqui eu adicionei uma variável 'privada' na função de serviço: realFoo
. Este get é atualizado e recuperado usando as funções get foo()
e set foo()
respectivamente noservice
objeto.
Observe o uso de $rootScope.$apply()
na função definida. Isso garante que o Angular esteja ciente de quaisquer alterações no service.foo
. Se você receber erros 'inprog', consulte esta página de referência útil ou se você usa Angular> = 1.3, pode simplesmente usar $rootScope.$applyAsync()
.
Também tenha cuidado com isso, se aService.foo
estiver sendo atualizado com muita frequência, pois isso pode afetar significativamente o desempenho. Se o desempenho for um problema, você pode configurar um padrão de observador semelhante às outras respostas aqui usando o configurador.