Отмена изменения маршрута AngularJS до загрузки модели для предотвращения мерцания
Мне интересно, есть ли способ (аналогичный Gmail) для AngularJS для задержки, показывающий новый маршрут, пока после каждой модели и ее данных не будет получено, используя соответствующие службы.
Например, если есть ProjectsController
, в котором перечислены все проекты и project_index.html
, которые были шаблоном, который показывал эти проекты, Project.query()
был бы извлечен полностью, прежде чем показывать новую страницу.
До тех пор старая страница будет продолжать отображаться (например, если я просматриваю другую страницу, а затем решил увидеть этот индекс проекта).
Ответы
Ответ 1
$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
отвечает за возвращение обещания. Все promises собираются и изменение маршрута задерживается до тех пор, пока не будут разрешены все promises.
Рабочая демонстрация: http://mhevery.github.com/angular-phonecat/app/#/phones
Источник: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831
Ответ 2
Здесь минимальный рабочий пример, который работает для 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://jsfiddle.net/dTJ9N/3/
Оптимизированная версия:
Так как $http() уже возвращает обещание (aka offferred), нам фактически не нужно создавать свои собственные. Таким образом, мы можем упростить MyCtrl. разрешить:
MyCtrl.resolve = {
datasets : function($http) {
return $http({
method: 'GET',
url: 'http://fiddle.jshell.net/'
});
}
};
Результат $http() содержит данные, статус, заголовки и объекты конфигурации, поэтому нам нужно изменить тело MyCtrl на:
$scope.datasets = datasets.data;
http://jsfiddle.net/dTJ9N/5/
Ответ 3
Я вижу, как некоторые люди спрашивают, как это сделать, используя метод 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';
}]);
Так как этот код выведен из ответа на вопрос/самый популярный, он не проверен, но он должен отправить вас в правильном направлении, если вы уже понимаете, как сделать дружественный для пользователя код angular. Одна часть, которую не требовал мой собственный код, - это инъекция "телефона" в функцию разрешения для "телефонов", и я вообще не использовал какой-либо объект "delay".
Я также рекомендую это видео youtube http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp, что мне очень помогло
Если это вас заинтересовало, я решил также вставить свой собственный код (написанный в coffeescript), чтобы вы могли видеть, как я его работаю.
FYI, я заранее использую общий контроллер, который помогает мне делать 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 ....
]
Ответ 4
Этот коммит, являющийся частью версии 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;
}]);
Ответ 5
Этот фрагмент является дружественным к инъекциям (я даже использую его в комбинации ngmin и uglify), и это более элегантное решение на основе домена.
Ниже приведен пример ресурса Телефон и константы 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);
});
});
});
});
Вы можете увидеть это в полной славе в мой последний (предстоящий) эксперимент.
Хотя этот метод отлично подходит для меня, я действительно удивляюсь, почему инжектор $не задерживает строительство чего-либо, когда он обнаруживает инъекцию всего, что является обещанием; это сделало бы вещи soooOOOOoooOOOOO намного проще.
Изменить: используется Angular v1.2 (rc2)
Ответ 6
Отсрочка отображения маршрута обязательно приведет к асинхронному запуску... почему бы просто не отслеживать статус загрузки вашего основного объекта и использовать его в представлении. Например, в вашем контроллере вы можете использовать как успешные, так и обратные вызовы ошибок на 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>
Ответ 7
Я работал с кодом Misko выше, и это то, что я сделал с ним. Это более актуальное решение, так как $defer
было изменено на $timeout
. Подстановка $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;
}
}
Ответ 8
Использование AngularJS 1.1.5
Обновление функции "телефонов" в ответе Justen с помощью синтаксиса 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.
Ответ 9
Вы можете использовать свойство $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
- constant объекты, определенные в том же файле, что и контроллеры 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..
});
Ответ 10
Мне нравится идея темного фотографа, потому что команде разработчиков Dev будет легко разобраться в работе и разобраться с ней.
Я создал эту адаптацию, которая использует 2 divs, одну для панели загрузчика и другую для фактического содержимого, отображаемого после загрузки данных. Обработка ошибок будет выполнена в другом месте.
Добавьте флаг "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
Ответ 11
Мне понравились ответы выше и многому научились у них, но в большинстве приведенных выше ответов есть что-то, что отсутствует.
Я застрял в аналогичном сценарии, когда я исправлял URL-адрес с некоторыми данными, которые извлекаются в первом запросе с сервера. Проблема, с которой я столкнулся, была, если обещание rejected
.
Я использовал пользовательский поставщик, который использовал для возврата Promise
, который был разрешен resolve
of $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);
});
ИМО. Как правило, рекомендуется добавить код отслеживания событий в блок запуска приложения. Этот код запускается сразу после фазы конфигурации приложения.
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);
});
}]);
Таким образом, мы можем справиться с обещанием сбой во время фазы конфигурации.
Ответ 12
У меня был сложный многоуровневый интерфейс с панелью с отключенным слоем экрана. Создание директивы об отключении экранного слоя, который создавал бы событие click для выполнения состояния типа
$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;
});
});
Ответ 13
Одним из возможных решений может быть использование директивы ng-cloak с элементом, в котором мы используем модели, например.
<div ng-cloak="">
Value in myModel is: {{myModel}}
</div>
Я думаю, что это требует наименьших усилий.