AngularJS: Como assistir variáveis de serviço?
Eu tenho um serviço, diga:
factory('aService', ['$rootScope', '$resource', function ($rootScope, $resource) {
var service = {
foo: []
};
return service;
}]);
e eu gostaria de usar foo
para controlar uma lista que é renderizada em HTML:
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
para que o controlador detecte quando aService.foo
é actualizado, eu compus este padrão onde adiciono aService ao $scope
do controlador e depois uso $scope.$watch()
:
function FooCtrl($scope, aService) {
$scope.aService = aService;
$scope.foo = aService.foo;
$scope.$watch('aService.foo', function (newVal, oldVal, scope) {
if(newVal) {
scope.foo = newVal;
}
});
}
Isto parece ser de mão longa, e tenho-o repetido em todos os controladores que usam as variáveis do serviço. Existe uma maneira melhor de realizar a observação compartilhada variáveis?
21 answers
Podes sempre usar o bom e velho padrão de observador se quiseres evitar a tirania e a sobrecarga de $watch
.
Em serviço:
factory('aService', function() {
var observerCallbacks = [];
//register an observer
this.registerObserverCallback = function(callback){
observerCallbacks.push(callback);
};
//call this when you know 'foo' has been changed
var notifyObservers = function(){
angular.forEach(observerCallbacks, function(callback){
callback();
});
};
//example of when you may want to notify observers
this.foo = someNgResource.query().$then(function(){
notifyObservers();
});
});
E no controlador:
function FooCtrl($scope, aService){
var updateFoo = function(){
$scope.foo = aService.foo;
};
aService.registerObserverCallback(updateFoo);
//service now in control of updating foo
};
Num cenário como este, onde objectos múltiplos/desconhecidos possam estar interessados em alterações, use $rootScope.$broadcast
do item a ser alterado.
Em vez de criar o seu próprio registo de ouvintes (que têm de ser limpos em vários $Destroyers), você deve ser capaz de $broadcast
a partir do serviço em questão.
Você ainda deve codificar os $on
manipuladores em cada ouvinte, mas o padrão é dissociado de várias chamadas para $digest
e assim evita o risco de observadores de longa duração.
Desta forma, também, os ouvintes podem vir e partir do DOM e/ou diferentes âmbitos de crianças sem que o serviço mude o seu comportamento.
** actualização: exemplos * *
As transmissões fariam mais sentido em serviços "globais" que poderiam ter impacto em inúmeras outras coisas na sua aplicação. Um bom exemplo é um serviço de usuário onde há uma série de eventos que podem ocorrer, tais como login, logout, update, idle, etc. Creio que é aqui que as transmissões fazem mais sentido. porque qualquer escopo pode ouvir para um evento, sem mesmo injetar o serviço, e ele não precisa avaliar quaisquer expressões ou resultados de cache para inspecionar para mudanças. Ele apenas dispara e esquece (por isso certifique-se de que é uma notificação de fogo e esquecimento, não algo que requer ação).factory('UserService', [ '$rootScope', function($rootScope) {
var service = <whatever you do for the object>
service.save = function(data) {
.. validate data and update model ..
// notify listeners and provide the data that changed [optional]
$rootScope.$broadcast('user:updated',data);
}
// alternatively, create a callback function and $broadcast from there if making an ajax call
return service;
}]);
O serviço acima transmitiria uma mensagem para cada âmbito quando a função save() estivesse completa e os dados fossem válidos. Alternativamente, se for um recurso $ou um envio ajax, mova a transmissão ligue para o callback para que ele dispare quando o servidor tiver respondido. As transmissões se adequam a esse padrão particularmente bem, porque cada ouvinte apenas espera pelo evento sem a necessidade de inspecionar o escopo em cada $digest. O ouvinte pareceria:
.controller('UserCtrl', [ 'UserService', '$scope', function(UserService, $scope) {
var user = UserService.getUser();
// if you don't want to expose the actual object in your scope you could expose just the values, or derive a value for your purposes
$scope.name = user.firstname + ' ' +user.lastname;
$scope.$on('user:updated', function(event,data) {
// you could inspect the data to see if what you care about changed, or just update your own scope
$scope.name = user.firstname + ' ' + user.lastname;
});
// different event names let you group your code and logic by what happened
$scope.$on('user:logout', function(event,data) {
.. do something differently entirely ..
});
}]);
Um dos benefícios disto é a eliminação de vários relógios. Se você estivesse combinando campos ou derivando valores como o exemplo acima, você teria que assistir tanto as propriedades do primeiro nome e do último nome. A observar o a função getUser () só funcionaria se o objeto do usuário fosse substituído em atualizações, ele não dispararia se o objeto do usuário simplesmente tivesse suas propriedades atualizadas. Nesse caso você teria que fazer um relógio profundo e isso é mais intensivo.
A$broadcast envia a mensagem do scope que é chamada para qualquer scope infantil. Então chamá-lo de $rootScope vai disparar em cada Mira. Se você fosse emitir $do escopo do seu controlador, por exemplo, ele dispararia apenas nos scopes que herda do teu controlador. $emit vai na direção oposta e se comporta de forma semelhante a um evento DOM em que ele borbulha a cadeia de escopo.
Tenha em mente que existem cenários em que a $broadcast faz muito sentido, e há cenários em que o $watch é uma opção melhor - especialmente se num âmbito isolado com uma expressão de relógio muito específica.
app.service('myService', function($q) {
var self = this,
defer = $q.defer();
this.foo = 0;
this.observeFoo = function() {
return defer.promise;
}
this.setFoo = function(foo) {
self.foo = foo;
defer.notify(self.foo);
}
})
Então, onde quer que apenas use o método myService.setFoo(foo)
para actualizar foo
em serviço. No seu controlador pode usá-lo como:
myService.observeFoo().then(null, null, function(foo){
$scope.foo = foo;
})
Os dois primeiros argumentos de then
são sucesso e erros callbacks, o terceiro é notificar callback.
JavaScript:
angular.module("Demo", [])
.factory("DemoService", function($timeout) {
function DemoService() {
var self = this;
self.name = "Demo Service";
self.count = 0;
self.counter = function(){
self.count++;
$timeout(self.counter, 1000);
}
self.addOneHundred = function(){
self.count+=100;
}
self.counter();
}
return new DemoService();
})
.controller("DemoController", function($scope, DemoService) {
$scope.service = DemoService;
$scope.minusOneHundred = function() {
DemoService.count -= 100;
}
});
HTML
<div ng-app="Demo" ng-controller="DemoController">
<div>
<h4>{{service.name}}</h4>
<p>Count: {{service.count}}</p>
</div>
</div>
Este JavaScript funciona à medida que estamos a passar um objecto de volta do serviço em vez de um valor. Quando um objeto JavaScript é devolvido de um serviço, Angular adiciona Relógios a todas as suas propriedades.
Note também que estou a usar 'var self = this' porque preciso de manter uma referência ao objecto original quando o '$timeout ' executar, caso contrário, 'isto' irá referir-se ao objecto da janela.
function FooCtrl($scope, aService) {
$scope.foo = aService.foo;
}
Se alguma, outra variável neste mesmo Ctrl é dependente da mudança do foo, então sim, você precisaria de um relógio para observar o foo e fazer alterações a essa variável. Mas desde que seja uma simples referência ver é desnecessário. Espero que isto ajude.
Quando uma expressão angular como a que usou está presente no HTML, o Angular configura automaticamente um $watch
para $scope.foo
, e irá actualizar o HTML sempre que o $scope.foo
mudar.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
A questão não dita aqui é que uma de duas coisas está afetando aService.foo
de tal forma que as mudanças não são detectadas. Estas duas possibilidades são:
-
aService.foo
está a ajustar-se a uma nova matriz cada vez, fazendo com que a referência a ela esteja desactualizada. -
aService.foo
está a ser actualizado de forma a que um ciclo$digest
não seja activado na actualização.
Problema 1: Referências Ultrapassadas
Considerando a primeira possibilidade, assumindo que um $digest
está a ser aplicado, se aService.foo
fosse sempre a mesma matriz, o conjunto automático $watch
iria detectar as alterações, como mostrado no excerto de código abaixo.
Solução 1-A: certifique - se que a matriz ou objecto é o mesmo objecto em cada actualizaçã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 pode ver, a ng-repeat supostamente ligada a aService.foo
não actualiza quando aService.foo
muda, mas a ng-repeat ligada a aService2.foo
faz . Isto porque a nossa referência a {[13] } está desactualizada, mas a nossa referência a aService2.foo
não está. Nós 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 que $scope.foo
já não referenciou a matriz que queríamos.
No entanto, embora existam várias formas de garantir que a referência inicial é mantida em tacto, por vezes pode ser necessário alterar o objecto ou a matriz. Ou se a propriedade de serviço faz referência a um primitivo como um String
ou Number
? Nesses casos, não podemos simplesmente confiar numa referência. Então o que podemos fazer?
Várias das respostas dadas anteriormente já dava algumas soluções para esse problema. No entanto, sou pessoalmente a favor de usar o método simples sugerido por Jin e as semanas nos comentários:
Apenas referência ao serviço.foo no HTML markup
Solução 1-b: anexar o serviço ao âmbito e referência {service}.{property}
no HTML.
Ou seja, faz isto:
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="[email protected]" 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>
Assim, o $watch
irá resolver aService.foo
em cada $digest
, o que irá obter o valor correctamente actualizado.
$watch
desnecessário no controlador que coloca explicitamente foo
No $scope
sempre que ele muda. Não precisas desse extra. você anexou aService
em vez de aService.foo
ao $scope
, e se vinculou explicitamente a aService.foo
na marcação.
Isso é bom, assumindo que um ciclo está a ser aplicado. Em meus exemplos acima, eu usei Angular's
$interval
serviço para actualizar as matrizes, que inicia automaticamente um ciclo $digest
após cada actualização. Mas e se as variáveis de serviço (por qualquer razão) não estão sendo atualizadas dentro do "mundo Angular". Em outras palavras, nós não temos um $digest
o ciclo é activado automaticamente sempre que a propriedade do serviço muda?
Problema 2: Desaparecido $digest
Muitas das soluções aqui resolverão este problema, mas concordo com o Sussurrador de código.:
A razão pela qual estamos a usar uma estrutura como Angular é para não cozinharmos os nossos próprios padrões de observação.
Portanto, eu preferiria continuar a usar a referência aService.foo
na marcação HTML como mostrado no segundo exemplo acima, e não ter que registrar uma callback adicional dentro do controlador.
Solução 2: utilizar um setter e getter com $rootScope.$apply()
Fiquei surpreendido por ainda ninguém ter sugerido o uso de umsetter egetter . Esta capacidade foi introduzida na ECMAScript5, e, portanto, já existe há anos. Claro, isso significa que se, por qualquer razão, você precisa apoiar navegadores realmente velhos, então este método não vai funcionar, mas eu sinto como getters e setters são muito subutilizado em JavaScript. Neste caso específico, 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 adicionei uma variável 'privada' na função de serviço: realFoo
. Esta obtenção é actualizada e recuperada usando as funções get foo()
e set foo()
respectivamente no objecto service
.
Note a utilização de $rootScope.$apply()
na função set. Isto garante que o Angular esteja ciente de quaisquer alterações a service.foo
. Se você obter erros 'inprog' veja em esta página de referência útil , ou se usar o Angular > = 1, 3 pode apenas usar $rootScope.$applyAsync()
.
Tenha também Cuidado com isto se aService.foo
estiver a ser actualizado com muita frequência, uma vez que isso pode ter um impacto significativo no desempenho. Se o desempenho fosse um problema, você poderia configurar um padrão de observador semelhante às outras respostas aqui usando o setter.
Pode inserir o serviço em $rootScope e ver:
myApp.run(function($rootScope, aService){
$rootScope.aService = aService;
$rootScope.$watch('aService', function(){
alert('Watch');
}, true);
});
No seu controlador:
myApp.controller('main', function($scope){
$scope.aService.foo = 'change';
});
Outra opção é usar uma biblioteca externa como: https://github.com/melanke/Watch.JS
Funciona com: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rinoceronte 1, 7+
Você pode observar as mudanças de um, muitos ou todos os atributos do objeto.
Exemplo:
var ex3 = {
attr1: 0,
attr2: "initial value of attr2",
attr3: ["a", 3, null]
};
watch(ex3, function(){
alert("some attribute of ex3 changes!");
});
ex3.attr3.push("new value");
angular.module('MyApp').factory('aFactory', function ($rootScope) {
// Define your factory content
var result = {
'key': value
};
// add a listener on a key
$rootScope.$watch(function () {
return result.key;
}, function (newValue, oldValue, scope) {
// This is called after the key "key" has changed, a good idea is to broadcast a message that key has changed
$rootScope.$broadcast('aFactory:keyChanged', newValue);
}, true);
return result;
});
Depois no seu controlador:
angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {
$rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) {
// do something
});
}]);
Desta forma você coloca todo o código de fábrica relacionado dentro da sua descrição então você só pode confiar na transmissão de fora
==actualizado==
Muito simples agora em $ watch.HTML:
<div class="container" data-ng-app="app">
<div class="well" data-ng-controller="FooCtrl">
<p><strong>FooController</strong></p>
<div class="row">
<div class="col-sm-6">
<p><a href="" ng-click="setItems([ { name: 'I am single item' } ])">Send one item</a></p>
<p><a href="" ng-click="setItems([ { name: 'Item 1 of 2' }, { name: 'Item 2 of 2' } ])">Send two items</a></p>
<p><a href="" ng-click="setItems([ { name: 'Item 1 of 3' }, { name: 'Item 2 of 3' }, { name: 'Item 3 of 3' } ])">Send three items</a></p>
</div>
<div class="col-sm-6">
<p><a href="" ng-click="setName('Sheldon')">Send name: Sheldon</a></p>
<p><a href="" ng-click="setName('Leonard')">Send name: Leonard</a></p>
<p><a href="" ng-click="setName('Penny')">Send name: Penny</a></p>
</div>
</div>
</div>
<div class="well" data-ng-controller="BarCtrl">
<p><strong>BarController</strong></p>
<p ng-if="name">Name is: {{ name }}</p>
<div ng-repeat="item in items">{{ item.name }}</div>
</div>
</div>
JavaScript:
var app = angular.module('app', []);
app.factory('PostmanService', function() {
var Postman = {};
Postman.set = function(key, val) {
Postman[key] = val;
};
Postman.get = function(key) {
return Postman[key];
};
Postman.watch = function($scope, key, onChange) {
return $scope.$watch(
// This function returns the value being watched. It is called for each turn of the $digest loop
function() {
return Postman.get(key);
},
// This is the change listener, called when the value returned from the above function changes
function(newValue, oldValue) {
if (newValue !== oldValue) {
// Only update if the value changed
$scope[key] = newValue;
// Run onChange if it is function
if (angular.isFunction(onChange)) {
onChange(newValue, oldValue);
}
}
}
);
};
return Postman;
});
app.controller('FooCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
$scope.setItems = function(items) {
PostmanService.set('items', items);
};
$scope.setName = function(name) {
PostmanService.set('name', name);
};
}]);
app.controller('BarCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
$scope.items = [];
$scope.name = '';
PostmanService.watch($scope, 'items');
PostmanService.watch($scope, 'name', function(newVal, oldVal) {
alert('Hi, ' + newVal + '!');
});
}]);
Com base na resposta dedo dtheodor Você poderia usar algo semelhante ao abaixo para garantir que você não se esqueça de desbloquear o callback... Alguns podem opor-se a passar o $scope
para um serviço embora.
factory('aService', function() {
var observerCallbacks = [];
/**
* Registers a function that will be called when
* any modifications are made.
*
* For convenience the callback is called immediately after registering
* which can be prevented with `preventImmediate` param.
*
* Will also automatically unregister the callback upon scope destory.
*/
this.registerObserver = function($scope, cb, preventImmediate){
observerCallbacks.push(cb);
if (preventImmediate !== true) {
cb();
}
$scope.$on('$destroy', function () {
observerCallbacks.remove(cb);
});
};
function notifyObservers() {
observerCallbacks.forEach(function (cb) {
cb();
});
};
this.foo = someNgResource.query().$then(function(){
notifyObservers();
});
});
Matriz.Remover é um método de extensão que se parece com este:
/**
* Removes the given item the current array.
*
* @param {Object} item The item to remove.
* @return {Boolean} True if the item is removed.
*/
Array.prototype.remove = function (item /*, thisp */) {
var idx = this.indexOf(item);
if (idx > -1) {
this.splice(idx, 1);
return true;
}
return false;
};
mainApp.service('aService',[function(){
var self = this;
var callbacks = {};
this.foo = '';
this.watch = function(variable, callback) {
if (typeof(self[variable]) !== 'undefined') {
if (!callbacks[variable]) {
callbacks[variable] = [];
}
callbacks[variable].push(callback);
}
}
this.notifyWatchersOn = function(variable) {
if (!self[variable]) return;
if (!callbacks[variable]) return;
angular.forEach(callbacks[variable], function(callback, key){
callback(self[variable]);
});
}
this.changeFoo = function(newValue) {
self.foo = newValue;
self.notifyWatchersOn('foo');
}
}]);
No Seu Controlador
function FooCtrl($scope, aService) {
$scope.foo;
$scope._initWatchers = function() {
aService.watch('foo', $scope._onFooChange);
}
$scope._onFooChange = function(newValue) {
$scope.foo = newValue;
}
$scope._initWatchers();
}
FooCtrl.$inject = ['$scope', 'aService'];
var myApp = angular.module("myApp",[]);
myApp.factory("randomService", function($timeout){
var retValue = {};
var data = 0;
retValue.startService = function(){
updateData();
}
retValue.getData = function(){
return data;
}
function updateData(){
$timeout(function(){
data = Math.floor(Math.random() * 100);
updateData()
}, 500);
}
return retValue;
});
myApp.controller("myController", function($scope, randomService){
$scope.data = 0;
$scope.dataUpdated = 0;
$scope.watchCalled = 0;
randomService.startService();
$scope.getRandomData = function(){
return randomService.getData();
}
$scope.$watch("getRandomData()", function(newValue, oldValue){
if(oldValue != newValue){
$scope.data = newValue;
$scope.dataUpdated++;
}
$scope.watchCalled++;
});
});
Cheguei a esta questão, mas afinal o meu problema era que eu estava a usar setInterval quando devia estar a usar o fornecedor do intervalo angular$. Este também é o caso do setTimeout (use $timeout em vez disso). Sei que não é a resposta à pergunta da operação, mas pode ajudar alguns, pois ajudou-me.
Basicamente a solução lá diz para não usar $watch
Como é uma solução muito pesada. em vez disso propõem-se usar $emit
e $on
.
O meu problema era ver uma variável no meu Serviço e reaja na Directiva. E com o método acima é muito fácil!
O meu exemplo de Módulo/serviço:
angular.module('xxx').factory('example', function ($rootScope) {
var user;
return {
setUser: function (aUser) {
user = aUser;
$rootScope.$emit('user:change');
},
getUser: function () {
return (user) ? user : false;
},
...
};
});
Então basicamente euassisto o meu user
- sempre que estiver definido para um novo valor I $emit
a user:change
estado.
angular.module('xxx').directive('directive', function (Auth, $rootScope) {
return {
...
link: function (scope, element, attrs) {
...
$rootScope.$on('user:change', update);
}
};
});
Agora na Directiva ouço as $rootScope
esobre A DADA mudança - eu reajo, respectivamente. Muito fácil e elegante!
function watch(target, callback, deep) {
$rootScope.$watch(function () {return eval(target);}, callback, deep);
};
/ / Serviço: (nada de especial AQUI)
myApp.service('myService', function() {
return { someVariable:'abc123' };
});
/ / ctrl:
myApp.controller('MyCtrl', function($scope, myService) {
$scope.someVariable = myService.someVariable;
// watch the service and update this ctrl...
$scope.$watch(function(){
return myService.someVariable;
}, function(newValue){
$scope.someVariable = newValue;
});
});
Um pouco feio, mas adicionei o registo de variáveis de âmbito ao meu serviço para uma toggle:
myApp.service('myService', function() {
var self = this;
self.value = false;
self.c2 = function(){};
self.callback = function(){
self.value = !self.value;
self.c2();
};
self.on = function(){
return self.value;
};
self.register = function(obj, key){
self.c2 = function(){
obj[key] = self.value;
obj.$apply();
}
};
return this;
});
E depois no controlador:
function MyCtrl($scope, myService) {
$scope.name = 'Superhero';
$scope.myVar = false;
myService.register($scope, 'myVar');
}
A função de observação observa as alterações de referência (tipos primitivos) se quiser ver algo como o 'array push', basta usar:
someArray.push(someObj); someArray = someArray.splice(0);
Isto irá actualizar a referência e actualizar o relógio de qualquer lado. Incluindo um método de getter de serviços. Tudo o que for primitivo será actualizado. automaticamente.
Controlador
$scope.foo = function(){
return aService.foo;
}
Acho que isto vai fazer o que quiseres. O meu controlador continua a verificar o valor do meu serviço com esta implementação. Honestamente, isto é muito mais simples do que a resposta selecionada.
Se quiser saltar a longa explicação, pode ir directamente para jsfiddle
- WatchObj
mod.service('WatchObj', ['$rootScope', WatchObjService]);
function WatchObjService($rootScope) {
// returns watch function
// obj: the object to watch for
// fields: the array of fields to watch
// target: where to assign changes (usually it's $scope or controller instance)
// $scope: optional, if not provided $rootScope is use
return function watch_obj(obj, fields, target, $scope) {
$scope = $scope || $rootScope;
//initialize watches and create an array of "unwatch functions"
var watched = fields.map(function(field) {
return $scope.$watch(
function() {
return obj[field];
},
function(new_val) {
target[field] = new_val;
}
);
});
//unregister function will unregister all our watches
var unregister = function unregister_watch_obj() {
watched.map(function(unregister) {
unregister();
});
};
//automatically unregister when scope is destroyed
$scope.$on('$destroy', unregister);
return unregister;
};
}
Este serviço é utilizado no controlador da seguinte forma:: Suponha que você tem um serviço "testService" com as propriedades 'prop1', 'prop2', 'prop3'. Você quer assistir e atribuir ao escopo 'prop1' e "prop2". Com o serviço de vigilância vai parecer assim:
app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);
function TestWatchCtrl($scope, testService, watch) {
$scope.prop1 = testService.prop1;
$scope.prop2 = testService.prop2;
$scope.prop3 = testService.prop3;
watch(testService, ['prop1', 'prop2'], $scope, $scope);
}
- aplicar Veja obj é ótimo, mas não é suficiente se você tem código assíncrono em seu serviço. Para esse caso, eu uso um segundo utilitário que se parece com isso:
mod.service('apply', ['$timeout', ApplyService]);
function ApplyService($timeout) {
return function apply() {
$timeout(function() {});
};
}
app.service('TestService', ['apply', TestService]);
function TestService(apply) {
this.apply = apply;
}
TestService.prototype.test3 = function() {
setTimeout(function() {
this.prop1 = 'changed_test_2';
this.prop2 = 'changed2_test_2';
this.prop3 = 'changed3_test_2';
this.apply(); //trigger $digest loop
}.bind(this));
}
// TEST app code
var app = angular.module('app', ['watch_utils']);
app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);
function TestWatchCtrl($scope, testService, watch) {
$scope.prop1 = testService.prop1;
$scope.prop2 = testService.prop2;
$scope.prop3 = testService.prop3;
watch(testService, ['prop1', 'prop2'], $scope, $scope);
$scope.test1 = function() {
testService.test1();
};
$scope.test2 = function() {
testService.test2();
};
$scope.test3 = function() {
testService.test3();
};
}
app.service('TestService', ['apply', TestService]);
function TestService(apply) {
this.apply = apply;
this.reset();
}
TestService.prototype.reset = function() {
this.prop1 = 'unchenged';
this.prop2 = 'unchenged2';
this.prop3 = 'unchenged3';
}
TestService.prototype.test1 = function() {
this.prop1 = 'changed_test_1';
this.prop2 = 'changed2_test_1';
this.prop3 = 'changed3_test_1';
}
TestService.prototype.test2 = function() {
setTimeout(function() {
this.prop1 = 'changed_test_2';
this.prop2 = 'changed2_test_2';
this.prop3 = 'changed3_test_2';
}.bind(this));
}
TestService.prototype.test3 = function() {
setTimeout(function() {
this.prop1 = 'changed_test_2';
this.prop2 = 'changed2_test_2';
this.prop3 = 'changed3_test_2';
this.apply();
}.bind(this));
}
//END TEST APP CODE
//WATCH UTILS
var mod = angular.module('watch_utils', []);
mod.service('apply', ['$timeout', ApplyService]);
function ApplyService($timeout) {
return function apply() {
$timeout(function() {});
};
}
mod.service('WatchObj', ['$rootScope', WatchObjService]);
function WatchObjService($rootScope) {
// target not always equals $scope, for example when using bindToController syntax in
//directives
return function watch_obj(obj, fields, target, $scope) {
// if $scope is not provided, $rootScope is used
$scope = $scope || $rootScope;
var watched = fields.map(function(field) {
return $scope.$watch(
function() {
return obj[field];
},
function(new_val) {
target[field] = new_val;
}
);
});
var unregister = function unregister_watch_obj() {
watched.map(function(unregister) {
unregister();
});
};
$scope.$on('$destroy', unregister);
return unregister;
};
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class='test' ng-app="app" ng-controller="TestWatch">
prop1: {{prop1}}
<br>prop2: {{prop2}}
<br>prop3 (unwatched): {{prop3}}
<br>
<button ng-click="test1()">
Simple props change
</button>
<button ng-click="test2()">
Async props change
</button>
<button ng-click="test3()">
Async props change with apply
</button>
</div>
<div ng-app="myApp">
<div ng-controller="FirstCtrl">
<input type="text" ng-model="Data.FirstName"><!-- Input entered here -->
<br>Input is : <strong>{{Data.FirstName}}</strong><!-- Successfully updates here -->
</div>
<hr>
<div ng-controller="SecondCtrl">
Input should also be here: {{Data.FirstName}}<!-- How do I automatically updated it here? -->
</div>
</div>
// declare the app with no dependencies
var myApp = angular.module('myApp', []);
myApp.factory('Data', function(){
return { FirstName: '' };
});
myApp.controller('FirstCtrl', function( $scope, Data ){
$scope.Data = Data;
});
myApp.controller('SecondCtrl', function( $scope, Data ){
$scope.Data = Data;
});