延迟AngularJS路线更改,直到加载模型以防止闪烁
我想知道是否有一种方式(类似于Gmail)的AngularJS 延迟显示一个新的路线,直到每个模型及其数据已被提取使用其各自的服务。
例如,如果有一个ProjectsController
列出所有的项目, project_index.html
是显示这些项目的模板,那么Project.query()
将在显示新页面之前被完全提取。
在此之前,旧的页面仍然会继续显示(例如,如果我正在浏览另一个页面,然后决定看到这个项目索引)。
$ routeProvider resolve属性允许延迟路由改变,直到数据被加载。
首先像这样定义一个带有resolve
属性的路由。
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'}); }]);
请注意, resolve
属性是在路由上定义的。
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; } }
请注意,控制器定义包含一个parsing对象,该对象声明了应该可用于控制器构造函数的东西。 这里的phones
注入到控制器中,并在resolve
属性中定义。
resolve.phones
函数负责返回一个promise。 所有的承诺都被收集起来,路由的变更被延迟了,直到所有的承诺都解决了。
工作演示: http : //mhevery.github.com/angular-phonecat/app/#/phones资料来源: https : //github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831
这是一个适用于Angular 1.0.2的最小工作示例
模板:
<script type="text/ng-template" id="/editor-tpl.html"> Editor Template {{datasets}} </script> <div ng-view> </div>
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()已经返回一个promise(又名deferred),我们实际上不需要创build自己的。 所以我们可以简化MyCtrl。 解决:
MyCtrl.resolve = { datasets : function($http) { return $http({ method: 'GET', url: 'http://fiddle.jshell.net/' }); } };
$ http()的结果包含数据 , 状态 , 头文件和configuration对象,所以我们需要将MyCtrl的主体改为:
$scope.datasets = datasets.data;
我看到一些人问如何使用angular.controller方法与最小化友好的dependency injection。 因为我刚刚做这个工作,我觉得有必要回来帮忙。 这是我的解决scheme(从原来的问题和米斯科的回答中采用):
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'; }]);
由于这个代码是从问题/最受欢迎的答案派生出来的,所以它是没有经过testing的,但是如果你已经知道如何制作小型化的angular码,它应该给你正确的方向。 我自己的代码并不需要的一个部分是将“电话”注入到“电话”的parsing函数中,我也没有使用任何“延迟”对象。
我也推荐这个youtubevideohttp://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp ,这对我有点帮助
如果它感兴趣,我决定也粘贴我自己的代码(用coffeescript编写),这样你就可以看到我是如何工作的。
仅供参考,我使用一个通用控制器,帮助我在几个型号上做CRUD:
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 .... ]
该提交是版本1.1.5及以上版本的一部分,公开$resource
的$promise
对象。 包含此提交的ngResource版本允许像这样parsing资源:
$ routeProvider
resolve: { data: function(Resource) { return Resource.get().$promise; } }
调节器
app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) { $scope.data = data; }]);
这段代码是dependency injection友好的(我甚至使用它结合ngmin和uglify ),这是一个更优雅的域驱动的解决scheme。
下面的示例注册一个电话 资源和一个固定的电话路由,其中包含该(电话)域的所有路由信息。 在提供的答案中,我不喜欢的东西是parsing逻辑的位置 – 主模块不应该知道任何东西,或者对将资源参数提供给控制器的方式感到困扰。 这样逻辑保持在同一个域中。
注意:如果你正在使用ngmin (如果你不是,你应该),你只需要用DI数组约定来编写parsing函数。
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; }] } } });
下一部分是在模块处于configuration状态时将路由数据注入并将其应用到$ 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: '/' }); });
使用此设置testing路由configuration也非常简单:
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); }); }); }); });
你可以在我最近的(即将到来的)实验中看到它的光彩。 虽然这个方法对我来说工作的很好,但是我真的好奇为什么当$ inject注入的东西是一个承诺对象的时候,为什么不会延迟构造任何 东西 ; 它会使事情soooOOOOOOOOOOOO容易得多。
编辑:使用Angular v1.2(rc2)
延迟显示路由肯定会导致asynchronous纠结……为什么不简单地跟踪主实体的加载状态并在视图中使用它。 例如,在你的控制器中,你可能在ngResource上同时使用成功和错误callback:
$scope.httpStatus = 0; // in progress $scope.projects = $resource.query('/projects', function() { $scope.httpStatus = 200; }, function(response) { $scope.httpStatus = response.status; });
然后在视图中你可以做任何事情:
<div ng-show="httpStatus == 0"> Loading </div> <div ng-show="httpStatus == 200"> Real stuff <div ng-repeat="project in projects"> ... </div> </div> <div ng-show="httpStatus >= 400"> Error, not found, etc. Could distinguish 4xx not found from 5xx server error even. </div>
我从Misko的代码开始工作,这就是我所做的。 由于$defer
已被更改为$timeout
所以这是一个更新的解决scheme。 然而,代替$timeout
会等待超时时间(在Misko的代码中,1秒),然后返回希望数据及时解决的数据。 用这种方式,它将尽快恢复。
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; } }
使用AngularJS 1.1.5
使用AngularJS 1.1.5语法在Justen的答案中更新'phones'函数。
原版的:
phones: function($q, Phone) { var deferred = $q.defer(); Phone.query(function(phones) { deferred.resolve(phones); }); return deferred.promise; }
更新:
phones: function(Phone) { return Phone.query().$promise; }
非常感谢Angular团队和贡献者。 🙂
这也是马克西米利安·霍夫曼的答案。 显然这个提交到1.1.5。
您可以使用$ routeProvider resolve属性来延迟路由更改,直到数据被加载。
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'}); }]);
请注意, resolve
属性是在路由上定义的。
EntitiesCtrlResolve
和EntityCtrlResolve
是在与EntitiesCtrl
和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.. });
我喜欢darkporter的想法,因为AngularJS的新开发团队很容易理解和直接工作。
我创build了这个适配,使用2个div,一个用于加载器栏,另一个用于加载数据后显示的实际内容。 error handling将在别处完成。
给$ scope添加一个'ready'标志:
$http({method: 'GET', url: '...'}). success(function(data, status, headers, config) { $scope.dataForView = data; $scope.ready = true; // <-- set true after loaded }) });
在html视图中:
<div ng-show="!ready"> <!-- Show loading graphic, eg Twitter Boostrap progress bar --> <div class="progress progress-striped active"> <div class="bar" style="width: 100%;"></div> </div> </div> <div ng-show="ready"> <!-- Real content goes here and will appear after loading --> </div>
另请参阅: Boostrap进度条文档
我喜欢上面的答案,并从他们身上学到了很多东西,但是在上面的大部分答案中都缺less一些东西。
我被困在一个类似的情况下,我解决了在服务器的第一个请求中获取的一些数据的URL。 如果承诺被rejected
我面临的问题是什么。
我正在使用一个自定义的提供程序,用于返回一个Promise
,它是在configuration阶段由$routeProvider
的resolve
的。
我想在这里强调的是什么when
这样做的概念。
它看到url栏中的url,然后在被调用的控制器和视图中的块分别被引用到目前为止很好。
可以说我有以下configuration阶段代码。
App.when('/', { templateUrl: '/assets/campaigns/index.html', controller: 'CampaignListCtr', resolve : { Auth : function(){ return AuthServiceProvider.auth('campaign'); } } }) // Default route .otherwise({ redirectTo: '/segments' });
否则在浏览器第一个块的根url上被调用otherwise
被调用。
让我们来想象一下我在地址栏中打到rootUrl的场景AuthServicePrivider.auth()
函数被调用。
让我们说承诺返回在拒绝状态然后呢?
没有得到任何呈现。
Otherwise
block不会被执行,因为它对于没有在configuration块中定义的任何URL,并且对于angularJs config阶段是未知的。
当这个承诺没有解决时,我们将不得不处理被解雇的事件。 失败$routeChangeErorr
在$rootScope
上被触发。
它可以被捕获,如下面的代码所示。
$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); });
国际海事组织这是一个好主意,把事件跟踪代码在运行应用程序块。 这段代码在应用程序的configuration阶段之后运行。
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); }); }]);
这样我们可以在configuration阶段处理诺言失败。
我有一个复杂的多级滑动面板界面,禁用屏幕层。 在禁用屏幕上创build指令,创build点击事件来执行状态
$state.go('account.stream.social.view');
正在产生轻弹效果。 history.back(),而不是它工作正常,但它并不总是在我的情况历史。 所以我发现如果我只是在我的禁用屏幕上创build属性href而不是state.go,就像一个魅力。
<a class="disable-screen" back></a>
指令“返回”
app.directive('back', [ '$rootScope', function($rootScope) { return { restrict : 'A', link : function(scope, element, attrs) { element.attr('href', $rootScope.previousState.replace(/\./gi, '/')); } }; } ]);
app.js我只保存以前的状态
app.run(function($rootScope, $state) { $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) { $rootScope.previousState = fromState.name; $rootScope.currentState = toState.name; }); });
一个可能的解决scheme可能是在我们使用模型的元素中使用ng-cloak指令
<div ng-cloak=""> Value in myModel is: {{myModel}} </div>
我觉得这个花费最less。