Задержка изменения маршрута AngularJS до загрузки модели для предотвращения мерцания

314

Мне интересно, есть ли способ (аналогичный Gmail) для AngularJS для задержки, показывающий новый маршрут, пока после каждой модели и ее данных не будет получено, используя соответствующие службы.

Например, если есть ProjectsController, в котором перечислены все проекты и project_index.html, которые были шаблоном, который показывал эти проекты, Project.query() был бы извлечен полностью, прежде чем показывать новую страницу.

До тех пор старая страница будет продолжать отображаться (например, если я просматриваю другую страницу, а затем решил увидеть этот индекс проекта).

Теги:
angularjs-routing

13 ответов

386

$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

  • 0
    В примере вы используете $ defer. $ Defer был заменен на $ timeout? Насколько я понимаю из примера, это задерживает изменение маршрута не до загрузки данных, а на 1 сек. И мы можем только надеяться, что данные будут загружены за эту секунду ...
  • 0
    отсрочка / задержка используется только в качестве примера. Он задерживается как минимум на секунду или до момента передачи данных.
Показать ещё 21 комментарий
51

Здесь минимальный рабочий пример, который работает для 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/

  • 0
    Я пытаюсь сделать что-то подобное, но у меня возникают проблемы с введением «наборов данных», так как это не определено. Какие-нибудь мысли?
  • 0
    Привет, mb21, думаю, ты сможешь мне помочь с этим вопросом: stackoverflow.com/questions/14271713/…
Показать ещё 4 комментария
32

Я вижу, как некоторые люди спрашивают, как это сделать, используя метод 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 ....
    ]
  • 0
    Правильно ли я понимаю из вашего примера и моих неудачных попыток, что теперь невозможно ссылаться на функцию resolve в контроллере в последних версиях Angular? Так что должно быть объявлено прямо в конфиге, как здесь?
  • 0
    @XMLilley Я уверен, что это так. Этот пример был из 1.1.2, когда я написал его, я считаю. Я не видел никакой документации по размещению разрешения внутри контроллера
Показать ещё 2 комментария
18

Этот коммит, являющийся частью версии 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;

}]);
  • 0
    Какие версии включают этот коммит, пожалуйста?
  • 0
    Последняя нестабильная версия (1.1.5) включает этот коммит. ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js
Показать ещё 3 комментария
16

Этот фрагмент является дружественным к инъекциям (я даже использую его в комбинации 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)

  • 2
    Этот превосходный ответ кажется гораздо более подходящим для философии «Angular» (инкапсуляция и т. Д.). Мы все должны прилагать сознательные усилия, чтобы не допустить, чтобы логика распространялась по всей базе кода, как кудзу.
  • 0
    I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object Я предполагаю, что они опускают эту функциональность, потому что это может стимулировать шаблоны проектирования, которые негативно влияют на отзывчивость приложений. По их мнению, идеальное приложение - это действительно асинхронное приложение, поэтому решение должно быть на грани.
Показать ещё 2 комментария
11

Отсрочка отображения маршрута обязательно приведет к асинхронному запуску... почему бы просто не отслеживать статус загрузки вашего основного объекта и использовать его в представлении. Например, в вашем контроллере вы можете использовать как успешные, так и обратные вызовы ошибок на 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>
  • 5
    Возможно, предоставление статуса HTTP представлению не правильно, больше, чем работа с классами CSS и элементами DOM, принадлежащими контроллеру. Я бы, вероятно, использовал ту же идею, но не использовал абстрактный статус в isValid () и isLoaded ().
  • 0
    Это действительно не лучшее разделение проблем, плюс то, что он потерпит крах, если у вас есть вложенные контроллеры, которые зависят от конкретного объекта.
Показать ещё 2 комментария
7

Использование 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.

  • 1
    Я не могу найти что-нибудь о $promise в документах . Это могло быть удалено с версии 2.0+.
  • 0
    Это доступно только в 1.2
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;
  }
}
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 - 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..
});
3

Мне нравится идея темного фотографа, потому что команде разработчиков 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

  • 0
    Немного развалится, если вы загружаете несколько частей данных. Как узнать, все ли загружено?
  • 0
    С тех пор, как я добавил этот ответ в феврале, дела пошли еще дальше, и на этой странице было гораздо больше активности. Похоже, что в Angular лучшая поддержка для решения этой проблемы сейчас, чем то, что предлагается здесь. Ура,
Показать ещё 1 комментарий
1

Мне понравились ответы выше и многому научились у них, но в большинстве приведенных выше ответов есть что-то, что отсутствует.

Я застрял в аналогичном сценарии, когда я исправлял 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);
   });
}]);

Таким образом, мы можем справиться с обещанием сбой во время фазы конфигурации.

0

У меня был сложный многоуровневый интерфейс с панелью с отключенным слоем экрана. Создание директивы об отключении экранного слоя, который создавал бы событие 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;


    });
});
-1

Одним из возможных решений может быть использование директивы ng-cloak с элементом, в котором мы используем модели, например.

<div ng-cloak="">
  Value in  myModel is: {{myModel}}
</div>

Я думаю, что это требует наименьших усилий.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню