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 $watch
for $scope.foo
y actualizará el HTML cada vez que $scope.foo
cambie.
<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.foo
se actualiza de tal manera que $digest
no se desencadena un ciclo en la actualización.
Problema 1: referencias obsoletas
Teniendo en cuenta la primera posibilidad, suponiendo que $digest
se esté aplicando a, si aService.foo
siempre fue la misma matriz, el conjunto automático $watch
detectarí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.foo
no se actualiza cuando aService.foo
cambia, pero la ng-repeat adjunta a aService2.foo
sí . Esto se debe a que nuestra referencia a aService.foo
está desactualizada, pero nuestra referencia a aService2.foo
no 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.foo
ya 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 String
o 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, $watch
se resolverá aService.foo
en 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 $watch
en el controlador que pone explícitamente foo
en el $scope
cada vez que cambia. No necesita ese extra $watch
cuando se adjunta en aService
lugar de aService.foo
al $scope
y se vincula explícitamente aService.foo
en el marcado.
Ahora eso está muy bien suponiendo $digest
que se esté aplicando un ciclo. En mis ejemplos anteriores, utilicé el $interval
servicio de Angular para actualizar las matrices, que inicia automáticamente un $digest
bucle 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 $digest
activa 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.foo
referencia 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 service
objeto.
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.foo
se 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.