Me topé con esta pregunta buscando algo similar, pero creo que merece una explicación detallada de lo que está sucediendo, así como algunas soluciones adicionales.
Cuando una expresión angular como la que usó está presente en el HTML, Angular configura automáticamente un $watchfor $scope.fooy actualizará el HTML cada vez que $scope.foocambie.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
El problema no mencionado aquí es que una de las dos cosas está afectando de aService.foo tal manera que los cambios no se detectan. Estas dos posibilidades son:
aService.foo se establece en una nueva matriz cada vez, lo que hace que la referencia no esté actualizada.
aService.foose actualiza de tal manera que $digestno se desencadena un ciclo en la actualización.
Problema 1: referencias obsoletas
Teniendo en cuenta la primera posibilidad, suponiendo que $digestse esté aplicando a, si aService.foosiempre fue la misma matriz, el conjunto automático $watchdetectaría los cambios, como se muestra en el fragmento de código a continuación.
Solución 1-a: asegúrese de que la matriz u objeto sea el mismo objeto en cada actualización
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 puede ver, la ng-repeat supuestamente asociada a aService.foono se actualiza cuando aService.foocambia, pero la ng-repeat adjunta a aService2.foo sí . Esto se debe a que nuestra referencia a aService.fooestá desactualizada, pero nuestra referencia a aService2.foono lo está. Creamos una referencia a la matriz inicial con $scope.foo = aService.foo;, que luego fue descartada por el servicio en su próxima actualización, lo que significa que $scope.fooya no hace referencia a la matriz que queríamos.
Sin embargo, si bien hay varias maneras de asegurarse de que la referencia inicial se mantenga intacta, a veces puede ser necesario cambiar el objeto o la matriz. ¿O qué pasa si la propiedad del servicio hace referencia a una primitiva como a Stringo Number? En esos casos, no podemos simplemente confiar en una referencia. Entonces, ¿qué podemos hacer?
Varias de las respuestas dadas anteriormente ya dan algunas soluciones a ese problema. Sin embargo, personalmente estoy a favor de usar el método simple sugerido por Jin y thetallweeks en los comentarios:
solo referencia aService.foo en el marcado html
Solución 1-b: Adjunte el servicio al ámbito y haga referencia {service}.{property}en el HTML.
Es decir, solo haz esto:
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>
De esa manera, $watchse resolverá aService.fooen cada uno $digest, lo que obtendrá el valor actualizado correctamente.
Esto es algo de lo que estaba tratando de hacer con su solución, pero de una manera mucho menos redonda. Ha añadido una innecesaria $watchen el controlador que pone explícitamente fooen el $scopecada vez que cambia. No necesita ese extra $watchcuando se adjunta en aServicelugar de aService.fooal $scopey se vincula explícitamente aService.fooen el marcado.
Ahora eso está muy bien suponiendo $digestque se esté aplicando un ciclo. En mis ejemplos anteriores, utilicé el $intervalservicio de Angular para actualizar las matrices, que inicia automáticamente un $digestbucle después de cada actualización. Pero, ¿qué pasa si las variables de servicio (por cualquier razón) no se actualizan dentro del "mundo angular". En otras palabras, ¿ no se $digestactiva automáticamente un ciclo cada vez que cambia la propiedad del servicio?
Problema 2: falta $digest
Muchas de las soluciones aquí resolverán este problema, pero estoy de acuerdo con Code Whisperer :
La razón por la que estamos usando un marco como Angular es no cocinar nuestros propios patrones de observación
Por lo tanto, preferiría seguir usando la aService.fooreferencia en el marcado HTML como se muestra en el segundo ejemplo anterior, y no tener que registrar una devolución de llamada adicional dentro del Controlador.
Solución 2: use un setter y getter con $rootScope.$apply()
Me sorprendió que nadie haya sugerido el uso de un setter y getter . Esta capacidad se introdujo en ECMAScript5 y, por lo tanto, existe desde hace años. Por supuesto, eso significa que si, por cualquier razón, necesita admitir navegadores realmente antiguos, entonces este método no funcionará, pero siento que los captadores y configuradores están muy infrautilizados en JavaScript. En este caso particular, podrían ser bastante útiles:
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>
Aquí he añadido una variable 'privado' en la función de servicio: realFoo. Esto se actualiza y recupera utilizando las funciones get foo()y set foo()respectivamente en el serviceobjeto.
Tenga en cuenta el uso de $rootScope.$apply()en la función set. Esto asegura que Angular estará al tanto de cualquier cambio en service.foo. Si obtiene errores 'inprog', consulte esta página de referencia útil , o si usa Angular> = 1.3, simplemente puede usar $rootScope.$applyAsync().
También tenga cuidado con esto si aService.foose actualiza con mucha frecuencia, ya que eso podría afectar significativamente el rendimiento. Si el rendimiento fuera un problema, podría configurar un patrón de observación similar a las otras respuestas aquí usando el configurador.