Задержка изменения маршрута AngularJS до загрузки модели для предотвращения мерцания
Мне интересно, есть ли способ (аналогичный Gmail) для AngularJS отложить показ нового маршрута до тех пор, пока каждая модель и ее данные не будут получены с использованием соответствующих сервисов.
Например, если бы был ProjectsController
который перечислил все проекты и project_index.html
который был шаблоном, который показал эти проекты, Project.query()
будет полностью загружен перед отображением новой страницы.
До тех пор старая страница все еще продолжала бы отображаться (например, если бы я просматривал другую страницу и затем решил посмотреть этот индекс проекта).
13 ответов
Свойство разрешения $ routeProvider позволяет отложить изменение маршрута до загрузки данных.
Сначала определите маршрут с 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;
}
}
Обратите внимание, что определение контроллера содержит объект разрешения, который объявляет вещи, которые должны быть доступны для конструктора контроллера. Здесь phones
вводится в контроллер, и это определяется в resolve
имущество.
resolve.phones
Функция отвечает за возврат обещания. Все обещания собраны, и изменение маршрута задерживается до тех пор, пока все обещания не будут решены.
Рабочая демонстрация: http://mhevery.github.com/angular-phonecat/app/ Источник: 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() уже возвращает обещание (иначе отложенное), нам на самом деле не нужно создавать свое собственное. Таким образом, мы можем упростить MyCtrl. принять решение:
MyCtrl.resolve = {
datasets : function($http) {
return $http({
method: 'GET',
url: 'http://fiddle.jshell.net/'
});
}
};
Результат $http() содержит данные, статус, заголовки и объекты конфигурации, поэтому нам нужно изменить тело MyCtrl на:
$scope.datasets = datasets.data;
Я вижу, как некоторые люди спрашивают, как это сделать, используя метод angular.controller с внедрением зависимостей, дружественных к минификации. Так как я только получил эту работу, я чувствовал себя обязанным вернуться и помочь. Вот мое решение (взято из исходного вопроса и ответа Миско):
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';
}]);
Поскольку этот код получен из вопроса / самого популярного ответа, он не проверен, но он должен направить вас в правильном направлении, если вы уже понимаете, как сделать дружественный угловой код минификацией. Единственная часть, которая не требовала моего собственного кода, - это вставка "Phone" в функцию разрешения для "телефонов", при этом я вообще не использовал объект "задержки".
Я также рекомендую это видео на YouTube http://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 и выше, предоставляет $promise
объект $resource
, Версии ngResource, включая этот коммит, позволяют разрешать ресурсы следующим образом:
$ routeProvider
resolve: {
data: function(Resource) {
return Resource.get().$promise;
}
}
контроллер
app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {
$scope.data = data;
}]);
Этот фрагмент кода удобен для внедрения зависимостей (я даже использую его в сочетании с ngmin и uglify), и это более элегантное решение на основе домена.
В приведенном ниже примере регистрируется ресурс Phone и константа phoneRoutes, которая содержит всю информацию о маршрутизации для этого (телефонного) домена. Что-то, что мне не понравилось в предоставленном ответе, было расположение логики разрешения - главный модуль не должен ничего знать или беспокоиться о том, как аргументы ресурса передаются контроллеру. Таким образом, логика остается в той же области.
Примечание: если вы используете ngmin (а если нет: вам следует), вам нужно только написать функции разрешения с соглашением о массиве 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;
}]
}
}
});
Следующим этапом является внедрение данных маршрутизации, когда модуль находится в состоянии конфигурации, и его применение к $ 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: '/' });
});
Тестирование конфигурации маршрута с этой настройкой также довольно просто:
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);
});
});
});
});
Вы можете увидеть это в полной славе в моем последнем (предстоящем) эксперименте. Хотя этот метод отлично работает для меня, мне действительно интересно, почему $ инжектор не задерживает создание чего-либо, когда он обнаруживает внедрение чего-либо, что является объектом обещания; это сделало бы вещи оооооооооооооо намного проще.
Редактировать: используется Angular v1.2(RC2)
Задержка показа маршрута обязательно приведет к асинхронному путанице... почему бы просто не отследить состояние загрузки вашего основного объекта и использовать его в представлении. Например, в вашем контроллере вы можете использовать как коллбэки успеха, так и ошибки в ngResource:
$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>
Я работал с кодом Миско выше, и это то, что я сделал с ним. Это более актуальное решение, так как $defer
был изменен на $timeout
, Подставляя $timeout
однако будет ожидать период ожидания (в коде Миско, 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.
Оригинал:
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, чтобы отложить изменение маршрута до загрузки данных.
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, будет легко разобраться и сразу поработать.
Я создал эту адаптацию, которая использует 2 div, один для панели загрузчика, а другой для фактического контента, отображаемого после загрузки данных. Обработка ошибок будет сделана в другом месте.
Добавьте флаг 'ready' в $scope:
$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, e.g. 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.
Мне понравились приведенные выше ответы и я многому научился у них, но в большинстве приведенных выше ответов чего-то не хватает.
Я застрял в похожем сценарии, где я решал URL с некоторыми данными, которые выбираются в первом запросе с сервера. Проблема, с которой я столкнулся, заключалась в том, что если rejected
,
Я использовал пользовательский поставщик, который использовал для возврата Promise
который был решен resolve
из $routeProvider
во время фазы конфигурации.
Здесь я хочу подчеркнуть, что концепция when
это делает что-то вроде этого.
Он видит URL в строке URL, а затем соответствующие when
блок в вызываемом контроллере и просмотр пока что хороший.
Допустим, у меня есть следующий код фазы конфигурации.
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
блок не будет выполнен так, как он есть для любого URL, который не определен в блоке конфигурации и неизвестен на этапе конфигурации angularJs.
Нам придется обработать событие, которое запускается, когда это обещание не выполнено. При неудаче $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);
});
IMO Обычно хорошая идея поместить код отслеживания событий в блок запуска приложения. Этот код запускается сразу после этапа настройки приложения.
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);
});
}]);
Таким образом, мы можем справиться с ошибкой обещания во время фазы конфигурации.
У меня был сложный многоуровневый интерфейс скользящей панели с отключенным слоем экрана. Создание директивы по отключению слоя экрана, которая создала бы событие щелчка, чтобы выполнить состояние как
$state.go('account.stream.social.view');
производили потрясающий эффект. history.back() вместо этого работала нормально, однако в моем случае это не всегда возвращается в историю. Итак, я обнаружил, что если я просто создаю атрибут 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;
});
});
Одним из возможных решений может быть использование директивы ng-cloak с элементом, в котором мы используем модели, например
<div ng-cloak="">
Value in myModel is: {{myModel}}
</div>
Я думаю, что это требует наименьших усилий.