Se você já tem uma certa experiência com AngularJS, com certeza tem a mesma opinião que eu com relação ao binding: é algo mágico. Mas e quando por algum motivo obscuro ele não funciona, o que fazemos? Talvez você pode ter passado pelo mesmo problema que eu: callbacks de alguma instrução do Javascript pura (fora de algum módulo do AngularJS) que manipulam o $scope
.
Nessa hora você precisará entender algumas coisas como, por exemplo, o funcionamento de tudo por baixo dos panos. Vou tentar passar um breve overview disso e uma possível solução do problema do callback aqui neste post. Confira!
Cenário
Se o que expliquei no início ainda não soou familiar para você, então vamos ilustrar o seguinte cenário: temos uma aplicação que sua função é contar até 10. Qual a primeira coisa que vem na cabeça? Utilizar setInterval()
, fácil né? Aí que mora o perigo… vamos lá!
O controller CountdownController
, apresentado abaixo, irá inicializar o contador no $scope
para exibí-lo na View:
var timerApp = angular.module('timerApp', []);
timerApp.controller('CountdownController', ['$scope',
function($scope) {
$scope.count = 0;
}
]);
E a view irá apresentar o contador, enquanto ele for menor que 10:
<div class="count" ng-controller="CountdownController">
<h1 ng-show="count < 10">{{count}}</h1>
</div>
Por enquanto nada é atualizado, que tal testar se o binding funciona como o esperado? Um botão para incrementar o contador é adicionado.
<div class="count" ng-controller="CountdownController">
<h1 ng-show="count < 10">{{count}}</h1>
<button ng-click="increment()">Incrementar</button>
</div>
O método para fazer o incremento também é adicionado dentro do CountdownController
.
$scope.increment = function() { $scope.count++; };
Se conferir o resultado no browser tudo estará funcionando e quando o atributo count
chegar a 10 o contador irá sumir. Já que o count
será incrementado a cada segundo, e não ao clicar no botão, é hora de remover o botão do HTML e utilizar o setInterval()
para fazer o incremento.
setInterval($scope.increment, 1000);
Voltando para o browser, nada acontece, mesmo que a página fique aberta 5 minutos. Será que o código está sendo chamado? Vamos ver via console.log
.
$scope.increment = function() {
console.log($scope.count);
$scope.count++;
};
O código está sim sendo chamado, mas nada acontece.
Funcionamento do AngularJS e o $scope.$apply
Para resumir bem rapidamente como o AngularJS funciona: ele permite que qualquer valor seja alvo de binding e no término de cada Javascript que definimos em lugares “gerenciados” por ele, ocorrerá uma verificação se determinado valor mudou, caso sim aplica as mudanças na tela.
Providers, directives e services fornecem essa praticidade por baixo dos panos, quando definimos nossas próprias callbacks Javascript aí a história muda já que fica sob nossa responsabilidade aplicar as mudanças, e é aí que entra o $scope.$apply()
.
Na verdade, o $scope.$apply()
é uma API de alto nível que chamará uma outra função que é a responsável por fazer toda a mágica do binding: $scope.$digest()
. A documentação do AngularJS é bem clara quando diz: nunca chame direto o $scope.$digest(), então prefira sempre usar o $scope.$apply()
, e utilize com moderação!
Então para corrigir o código anterior basta simplesmente definir o método increment()
como:
$scope.increment = function() {
$scope.$apply(function() {
$scope.count++;
});
};
Mas, para esse caso, prefira usar o $timeout
ao invés de setInterval()
! Na verdade, se realmente precisar usar algo do Javascript, prefira o setTimeout()
:D
Confira agora a solução com o $timeout
:
timerApp.controller('CountdownController', ['$scope', '$timeout',
function($scope, $timeout) {
$scope.count = 0;
$scope.increment = function() {
$scope.count++;
if ($scope.count < 10) {
$timeout($scope.increment, 1000);
}
};
$scope.increment();
}
]);
Exemplo final: http://jsfiddle.net/v6xa54pk/
No meu cenário precisei do $scope.apply
em uma API de websockets puramente Javascript, mais especificamente a Stomp over Websockets, caso você esbarre com algum problema de binding não deixe de consultar a documentação do AngularJS e procurar por alternativas escritas especificamente para Angular!
Referências
- https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply
- http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
- http://stackoverflow.com/questions/9682092/databinding-in-angularjs#answer-9693933
Até a próxima.