Atrasando a mudança de rota do AngularJS até o modelo ser carregado para evitar cintilação

Eu estou querendo saber se existe uma maneira (semelhante ao Gmail) para o AngularJS atrasar a exibição de uma nova rota até depois de cada modelo e seus dados terem sido buscados usando seus respectivos serviços.

Por exemplo, se houvesse um ProjectsController que listasse todos os Projetos e project_index.html que era o modelo que mostrava esses Projetos, Project.query() seria buscado completamente antes de mostrar a nova página.

Até então, a página antiga ainda continuaria sendo mostrada (por exemplo, se eu estivesse navegando em outra página e depois decidisse ver este índice do Projeto).

A propriedade de resolução $ routeProvider permite atrasar a alteração da rota até que os dados sejam carregados.

Primeiro defina uma rota com atributo de resolve como este.

 angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']). config(['$routeProvider', function($routeProvider) { $routeProvider. when('/phones', { templateUrl: 'partials/phone-list.html', controller: PhoneListCtrl, resolve: PhoneListCtrl.resolve}). when('/phones/:phoneId', { templateUrl: 'partials/phone-detail.html', controller: PhoneDetailCtrl, resolve: PhoneDetailCtrl.resolve}). otherwise({redirectTo: '/phones'}); }]); 

observe que a propriedade resolve está definida na rota.

 function PhoneListCtrl($scope, phones) { $scope.phones = phones; $scope.orderProp = 'age'; } PhoneListCtrl.resolve = { phones: function(Phone, $q) { // see: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4 var deferred = $q.defer(); Phone.query(function(successData) { deferred.resolve(successData); }, function(errorData) { deferred.reject(); // you could optionally pass error data here }); return deferred.promise; }, delay: function($q, $defer) { var delay = $q.defer(); $defer(delay.resolve, 1000); return delay.promise; } } 

Observe que a definição do controlador contém um object de resolução que declara coisas que devem estar disponíveis para o construtor do controlador. Aqui, os phones são injetados no controlador e são definidos na propriedade resolve .

A function resolve.phones é responsável por retornar uma promise. Todas as promises são coletadas e a mudança de rota é adiada até que todas as promises sejam resolvidas.

Demonstração de trabalho: http://mhevery.github.com/angular-phonecat/app/#/phones Fonte: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831

Aqui está um exemplo mínimo de trabalho que funciona para o Angular 1.0.2

Modelo:

  

JavaScript:

 function MyCtrl($scope, datasets) { $scope.datasets = datasets; } MyCtrl.resolve = { datasets : function($q, $http) { var deferred = $q.defer(); $http({method: 'GET', url: '/someUrl'}) .success(function(data) { deferred.resolve(data) }) .error(function(data){ //actually you'd want deffered.reject(data) here //but to show what would happen on success.. deferred.resolve("error value"); }); return deferred.promise; } }; var myApp = angular.module('myApp', [], function($routeProvider) { $routeProvider.when('/', { templateUrl: '/editor-tpl.html', controller: MyCtrl, resolve: MyCtrl.resolve }); });​​ 

http://jsfiddle.net/dTJ9N/3/

Versão simplificada:

Como $ http () já retorna uma promise (também conhecida como deferred), na verdade não precisamos criar o nosso próprio. Então podemos simplificar o MyCtrl. resolver para:

 MyCtrl.resolve = { datasets : function($http) { return $http({ method: 'GET', url: 'http://fiddle.jshell.net/' }); } }; 

O resultado de $ http () contém dados , status , headers e objects de configuração , então precisamos mudar o corpo do MyCtrl para:

 $scope.datasets = datasets.data; 

http://jsfiddle.net/dTJ9N/5/

Eu vejo algumas pessoas perguntando como fazer isso usando o método angular.controller com injeção de dependência amigável de minificação. Desde que eu comecei a trabalhar, me senti obrigado a voltar e ajudar. Aqui está a minha solução (adotada a partir da pergunta original e da resposta de Misko):

 angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']). config(['$routeProvider', function($routeProvider) { $routeProvider. when('/phones', { templateUrl: 'partials/phone-list.html', controller: PhoneListCtrl, resolve: { phones: ["Phone", "$q", function(Phone, $q) { var deferred = $q.defer(); Phone.query(function(successData) { deferred.resolve(successData); }, function(errorData) { deferred.reject(); // you could optionally pass error data here }); return deferred.promise; ] }, delay: ["$q","$defer", function($q, $defer) { var delay = $q.defer(); $defer(delay.resolve, 1000); return delay.promise; } ] }, }). when('/phones/:phoneId', { templateUrl: 'partials/phone-detail.html', controller: PhoneDetailCtrl, resolve: PhoneDetailCtrl.resolve}). otherwise({redirectTo: '/phones'}); }]); angular.controller("PhoneListCtrl", [ "$scope", "phones", ($scope, phones) { $scope.phones = phones; $scope.orderProp = 'age'; }]); 

Como esse código é derivado da pergunta / resposta mais popular, ele não foi testado, mas deve enviar você na direção correta se você já tiver entendido como fazer um código angular amigável à minição. A única parte que meu próprio código não exigia era uma injeção de “Telefone” na function de resolução para “telefones”, nem usei nenhum object de “atraso”.

Eu também recomendo este vídeo do youtube http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp , que me ajudou bastante

Se lhe interessar, decidi também colar o meu próprio código (escrito em coffeescript) para que possa ver como o consegui a trabalhar.

FYI, com antecedência eu uso um controlador genérico que me ajuda a fazer CRUD em vários modelos:

 appModule.config ['$routeProvider', ($routeProvider) -> genericControllers = ["boards","teachers","classrooms","students"] for controllerName in genericControllers $routeProvider .when "/#{controllerName}/", action: 'confirmLogin' controller: 'GenericController' controllerName: controllerName templateUrl: "/static/templates/#{controllerName}.html" resolve: items : ["$q", "$route", "$http", ($q, $route, $http) -> deferred = $q.defer() controllerName = $route.current.controllerName $http( method: "GET" url: "/api/#{controllerName}/" ) .success (response) -> deferred.resolve(response.payload) .error (response) -> deferred.reject(response.message) return deferred.promise ] $routeProvider .otherwise redirectTo: '/' action: 'checkStatus' ] appModule.controller "GenericController", ["$scope", "$route", "$http", "$cookies", "items", ($scope, $route, $http, $cookies, items) -> $scope.items = items #etc .... ] 

Esta confirmação , que faz parte da versão 1.1.5 e acima, expõe o object $promise do $resource . Versões do ngResource incluindo este commit permitem resolver resources como este:

$ routeProvider

 resolve: { data: function(Resource) { return Resource.get().$promise; } } 

controlador

 app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) { $scope.data = data; }]); 

Este snippet é amigável para injeção de dependência (eu até mesmo o uso em combinação de ngmin e uglify ) e é uma solução baseada em domínio mais elegante.

O exemplo abaixo registra um recurso de telefone e uma constante phoneRoutes , que contém todas as informações de roteamento para esse domínio (telefone). Algo que eu não gostei na resposta fornecida foi a localização da lógica de resolução – o módulo principal não deve saber de nada ou ser incomodado sobre a maneira como os argumentos de recurso são fornecidos ao controlador. Desta forma, a lógica permanece no mesmo domínio.

Nota: se você estiver usando o ngmin (e se não estiver: você deve), você só precisa escrever as funções de resolução com a convenção da matriz DI.

 angular.module('myApp').factory('Phone',function ($resource) { return $resource('/api/phone/:id', {id: '@id'}); }).constant('phoneRoutes', { '/phone': { templateUrl: 'app/phone/index.tmpl.html', controller: 'PhoneIndexController' }, '/phone/create': { templateUrl: 'app/phone/edit.tmpl.html', controller: 'PhoneEditController', resolve: { phone: ['$route', 'Phone', function ($route, Phone) { return new Phone(); }] } }, '/phone/edit/:id': { templateUrl: 'app/phone/edit.tmpl.html', controller: 'PhoneEditController', resolve: { form: ['$route', 'Phone', function ($route, Phone) { return Phone.get({ id: $route.current.params.id }).$promise; }] } } }); 

A próxima parte é injetar os dados de roteamento quando o módulo está no estado de configuração e aplicá-lo ao $ routeProvider .

 angular.module('myApp').config(function ($routeProvider, phoneRoutes, /* ... otherRoutes ... */) { $routeProvider.when('/', { templateUrl: 'app/main/index.tmpl.html' }); // Loop through all paths provided by the injected route data. angular.forEach(phoneRoutes, function(routeData, path) { $routeProvider.when(path, routeData); }); $routeProvider.otherwise({ redirectTo: '/' }); }); 

Testar a configuração da rota com esta configuração também é muito fácil:

 describe('phoneRoutes', function() { it('should match route configuration', function() { module('myApp'); // Mock the Phone resource function PhoneMock() {} PhoneMock.get = function() { return {}; }; module(function($provide) { $provide.value('Phone', FormMock); }); inject(function($route, $location, $rootScope, phoneRoutes) { angular.forEach(phoneRoutes, function (routeData, path) { $location.path(path); $rootScope.$digest(); expect($route.current.templateUrl).toBe(routeData.templateUrl); expect($route.current.controller).toBe(routeData.controller); }); }); }); }); 

Você pode ver isso com toda a glória no meu mais recente experimento . Embora este método funcione bem para mim, eu realmente me pergunto por que o $ injector não está atrasando a construção de nada quando detecta injeção de qualquer coisa que seja um object promissor ; isso tornaria as coisas muito mais fáceis.

Edit: usado Angular v1.2 (rc2)

Atrasar a exibição da rota certamente levará a um emaranhado asynchronous … por que não simplesmente rastrear o status de carregamento de sua entidade principal e usá-la na exibição. Por exemplo, em seu controlador, você pode usar os retornos de chamada de sucesso e erro no ngResource:

 $scope.httpStatus = 0; // in progress $scope.projects = $resource.query('/projects', function() { $scope.httpStatus = 200; }, function(response) { $scope.httpStatus = response.status; }); 

Então, na visão, você poderia fazer o que fosse:

 
Loading
Real stuff
...
Error, not found, etc. Could distinguish 4xx not found from 5xx server error even.

Eu trabalhei no código de Misko acima e foi o que fiz com isso. Esta é uma solução mais atual, já que $defer foi alterado para $timeout . Substituir $timeout no entanto, irá aguardar o período de tempo limite (no código de Misko, 1 segundo), e então retornar os dados esperando que sejam resolvidos a tempo. Com este caminho, retorna o mais cedo possível.

 function PhoneListCtrl($scope, phones) { $scope.phones = phones; $scope.orderProp = 'age'; } PhoneListCtrl.resolve = { phones: function($q, Phone) { var deferred = $q.defer(); Phone.query(function(phones) { deferred.resolve(phones); }); return deferred.promise; } } 

Usando o AngularJS 1.1.5

Atualizando a function ‘telefones’ na resposta de Justen usando a syntax AngularJS 1.1.5 .

Original:

 phones: function($q, Phone) { var deferred = $q.defer(); Phone.query(function(phones) { deferred.resolve(phones); }); return deferred.promise; } 

Atualizada:

 phones: function(Phone) { return Phone.query().$promise; } 

Muito mais curto graças à equipe Angular e colaboradores. 🙂

Esta é também a resposta de Maximilian Hoffmann. Aparentemente, esse commit chegou em 1.1.5.

Você pode usar a propriedade de resolução $ routeProvider para atrasar a mudança de rota até que os dados sejam carregados.

 angular.module('app', ['ngRoute']). config(['$routeProvider', function($routeProvider, EntitiesCtrlResolve, EntityCtrlResolve) { $routeProvider. when('/entities', { templateUrl: 'entities.html', controller: 'EntitiesCtrl', resolve: EntitiesCtrlResolve }). when('/entity/:entityId', { templateUrl: 'entity.html', controller: 'EntityCtrl', resolve: EntityCtrlResolve }). otherwise({redirectTo: '/entities'}); }]); 

Observe que a propriedade resolve está definida na rota.

EntitiesCtrlResolve e EntityCtrlResolve são objects constantes definidos no mesmo arquivo que os controladores EntitiesCtrl e EntityCtrl .

 // EntitiesCtrl.js angular.module('app').constant('EntitiesCtrlResolve', { Entities: function(EntitiesService) { return EntitiesService.getAll(); } }); angular.module('app').controller('EntitiesCtrl', function(Entities) { $scope.entities = Entities; // some code.. }); // EntityCtrl.js angular.module('app').constant('EntityCtrlResolve', { Entity: function($route, EntitiesService) { return EntitiesService.getById($route.current.params.projectId); } }); angular.module('app').controller('EntityCtrl', function(Entity) { $scope.entity = Entity; // some code.. }); 

Eu gosto da idéia do darkporter porque será fácil para um time de desenvolvimento novo no AngularJS entender e trabalhar imediatamente.

Eu criei essa adaptação que usa 2 divs, um para a barra do carregador e outro para o conteúdo real exibido depois que os dados são carregados. O tratamento de erros seria feito em outro lugar.

Adicione um sinalizador “pronto” ao escopo $:

 $http({method: 'GET', url: '...'}). success(function(data, status, headers, config) { $scope.dataForView = data; $scope.ready = true; // <-- set true after loaded }) }); 

Na visão HTML:

 

Veja também: Docs da barra de progresso do Boostrap

Eu gostei de respostas acima e aprendi muito com eles, mas há algo que está faltando na maioria das respostas acima.

Eu estava preso em um cenário semelhante, onde eu estava resolvendo o URL com alguns dados que são buscados na primeira solicitação do servidor. O problema que enfrentei foi o que se a promise for rejected .

Eu estava usando um provedor personalizado que costumava retornar um Promise que foi resolvido pela resolve de $routeProvider no momento da fase de configuração.

O que eu quero enfatizar aqui é o conceito de when faz algo assim.

Ele vê o url na barra de url e, em seguida, respectivo when bloco chamado controller e view é referido tão bem.

Digamos que eu tenha o seguinte código de fase de configuração.

 App.when('/', { templateUrl: '/assets/campaigns/index.html', controller: 'CampaignListCtr', resolve : { Auth : function(){ return AuthServiceProvider.auth('campaign'); } } }) // Default route .otherwise({ redirectTo: '/segments' }); 

Na raiz do url no navegador primeiro bloco de execução get chamado caso otherwise é chamado.

Vamos imaginar um cenário que eu acertei no rootUrl na barra de endereços que a function AuthServicePrivider.auth() é chamada.

Vamos dizer Promessa retornou está em estado de rejeição que então ??

Nada é renderizado.

Otherwise bloco não será executado como é para qualquer URL que não esteja definida no bloco de configuração e seja desconhecida para a fase de configuração do angularJs.

Teremos que lidar com o evento que é acionado quando essa promise não for resolvida. Na falha, $routeChangeErorr é triggersdo em $rootScope .

Pode ser capturado como mostrado no código abaixo.

 $rootScope.$on('$routeChangeError', function(event, current, previous, rejection){ // Use params in redirection logic. // event is the routeChangeEvent // current is the current url // previous is the previous url $location.path($rootScope.rootPath); }); 

IMO Geralmente, é uma boa ideia colocar o código de acompanhamento de events no bloco de execução do aplicativo. Esse código é executado logo após a fase de configuração do aplicativo.

 App.run(['$routeParams', '$rootScope', '$location', function($routeParams, $rootScope, $location){ $rootScope.rootPath = "my custom path"; // Event to listen to all the routeChangeErrors raised // by the resolve in config part of application $rootScope.$on('$routeChangeError', function(event, current, previous, rejection){ // I am redirecting to rootPath I have set above. $location.path($rootScope.rootPath); }); }]); 

Desta forma, podemos lidar com falhas de promise no momento da fase de configuração.

Eu tive uma interface de painel deslizante multi-nível complexo, com camada de canvas desativada. Criando diretiva na camada de canvas de desativação que criaria o evento click para executar o estado como

 $state.go('account.stream.social.view'); 

estavam produzindo um efeito de flicking. history.back () em vez de funcionou ok, no entanto, não é sempre de volta na história no meu caso. Então, o que eu descobri é que, se eu simplesmente criar o atributo href na minha canvas de desativação em vez de no state.go, funcionaria como um encanto.

  

Diretiva ‘voltar’

 app.directive('back', [ '$rootScope', function($rootScope) { return { restrict : 'A', link : function(scope, element, attrs) { element.attr('href', $rootScope.previousState.replace(/\./gi, '/')); } }; } ]); 

app.js Acabei de salvar o estado anterior

 app.run(function($rootScope, $state) { $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) { $rootScope.previousState = fromState.name; $rootScope.currentState = toState.name; }); }); 

Uma solução possível pode ser usar a diretiva ng-cloak com o elemento em que estamos usando os modelos, por exemplo

 
Value in myModel is: {{myModel}}

Eu acho que este leva menos esforço.