web-dev-qa-db-fra.com

Retarder le changement de route AngularJS jusqu'au chargement du modèle pour éviter le scintillement

Je me demande s’il existe un moyen (similaire à Gmail) pour AngularJS de retarder l’affichage d’un nouvel itinéraire jusqu’à ce que chaque modèle et ses données aient été récupérés en utilisant ses services respectifs.

Par exemple, s'il y avait une variable ProjectsController répertoriant tous les projets et project_index.html qui était le modèle qui présentait ces projets, Project.query() serait récupéré complètement avant d'afficher la nouvelle page.

Jusque-là, l'ancienne page continuerait de s'afficher (par exemple, si je parcourais une autre page et que je décidais ensuite d'afficher cet index du projet).

318
Misko Hevery

$ routeProviderrésolution la propriété permet de retarder le changement de route jusqu'à ce que les données soient chargées.

Commencez par définir une route avec l'attribut resolve comme ceci.

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'});
}]);

remarquez que la propriété resolve est définie sur route.

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;
  }
}

Notez que la définition du contrôleur contient un objet de résolution qui déclare des éléments qui devraient être disponibles pour le constructeur du contrôleur. Ici, la phones est injectée dans le contrôleur et définie dans la propriété resolve.

La fonction resolve.phones est chargée de renvoyer une promesse. Toutes les promesses sont collectées et le changement d'itinéraire est retardé jusqu'à ce que toutes les promesses soient résolues.

Démo de travail: http://mhevery.github.com/angular-phonecat/app/#/phones Source: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831

375
Misko Hevery

Voici un exemple de travail minimal qui fonctionne pour Angular 1.0.2

Modèle:

<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/

Version simplifiée:

Comme $ http () retourne déjà une promesse (autrement dit différée), nous n’avons pas besoin de créer la nôtre. Nous pouvons donc simplifier MyCtrl. résoudre à:

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

Le résultat de $ http () contient des données , status , en-têtes et config objets, nous devons donc changer le corps de MyCtrl en:

$scope.datasets = datasets.data;

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

51
mb21

Je vois des gens demander comment faire cela en utilisant la méthode angular.controller avec injection de dépendance adaptée à la minification. Depuis que je viens de travailler, je me suis senti obligé de revenir et d'aider. Voici ma solution (reprise de la question initiale et de la réponse 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';
}]);

Étant donné que ce code est dérivé de la question/réponse la plus populaire, il n'a pas été testé, mais il devrait vous renvoyer dans la bonne direction si vous avez déjà compris comment créer un code amical de minification angular. La seule partie que mon propre code ne nécessitait pas était une injection de "Téléphone" dans la fonction de résolution pour "téléphones", et je n'ai utilisé aucun objet "délai" du tout.

Je recommande également cette vidéo sur youtube http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7FQNUQQOUOCrFw&index=4&feature=plcp , ce qui m'a aidé un peu

Si cela vous intéresse, j'ai décidé de coller également mon propre code (Écrit dans coffeescript) pour que vous puissiez voir comment je l'ai fait fonctionner.

Pour info, j’utilise d’avance un contrôleur générique qui m’aide à faire du CRUD sur plusieurs modèles:

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 ....
    ]
32
bitwit

This commit , qui fait partie de la version 1.1.5 et ultérieure, expose l'objet $promise de $resource. Les versions de ngResource comprenant ce commit permettent de résoudre les problèmes de la manière suivante:

$ routeProvider

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

contrôleur

app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {

    $scope.data = data;

}]);
18

Cet extrait est injection de dépendance convivial (je l’utilise même en combinaison de ngmin et de uglify ) et c'est une solution plus élégante pilotée par le domaine.

L'exemple ci-dessous enregistre un Phone ressource et un constant ​​ phoneRoutes , qui contient toutes les informations de routage pour ce domaine (téléphone). Quelque chose que je n'aimais pas dans la réponse fournie était l'emplacement de la logique résoudre - le module principal ne devrait pas savoir ou être dérangé sur la façon dont les arguments de ressources sont fournis au contrôleur. De cette façon, la logique reste dans le même domaine.

Remarque: si vous utilisez ngmin (et si vous ne l'êtes pas: vous devriez le faire), il vous suffit d'écrire les fonctions de résolution avec la convention du tableau 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;
        }]
      }
    }
  });

La prochaine étape consiste à injecter les données de routage lorsque le module est à l'état de configuration et à les appliquer au $ 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: '/' });

});

Tester la configuration de la route avec cette configuration est également assez facile:

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);
      });
    });
  });
});

Vous pouvez le voir en pleine gloire dans ma dernière expérience (à venir) . Bien que cette méthode fonctionne bien pour moi, je me demande vraiment pourquoi l’injecteur $ ne retarde pas la construction de n'importe quoi quand il détecte l’injection de n'importe quoi qui est un promesse objet; cela rendrait les choses sooooooooooooooooo beaucoup plus faciles.

Éditer: utilisé Angular v1.2 (rc2)

16
null

Retarder l'affichage de l'itinéraire entraînera certainement un enchevêtrement asynchrone ... pourquoi ne pas simplement suivre l'état de chargement de votre entité principale et l'utiliser dans la vue. Par exemple, dans votre contrôleur, vous pouvez utiliser les rappels de succès et d’erreur sur ngResource:

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

Ensuite, dans la vue, vous pouvez faire n'importe quoi:

<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>
11
jpsimons

J'ai travaillé à partir du code de Misko ci-dessus et c'est ce que j'ai fait avec. C'est une solution plus courante puisque $defer a été remplacé par $timeout. Cependant, substituer $timeout attendra le délai (dans le code de Misko, 1 seconde), puis renverra les données en espérant qu'elles seront résolues à temps. De cette façon, il retourne au plus vite.

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
Justen

Utiliser AngularJS 1.1.5

Mise à jour de la fonction 'phones' dans la réponse de Justen en utilisant la syntaxe AngularJS 1.1.5.

Original:

phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
}

Mis à jour:

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

Beaucoup plus court grâce à l'équipe Angular et aux contributeurs. :)

C'est aussi la réponse de Maximilian Hoffmann. Apparemment, ce commit est entré dans la 1.1.5.

7
OJ Raqueño

Vous pouvez utiliser la propriété $ routeProvider resol = pour retarder le changement de route jusqu'au chargement des données.

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'});
}]);

Notez que la propriété resolve est définie sur route.

EntitiesCtrlResolve et EntityCtrlResolve est constante objets définis dans le même fichier que EntitiesCtrl et EntityCtrl contrôleurs.

// 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..
});
5
Bohdan Lyzanets

J'aime l'idée de darkporter, car il sera facile à comprendre pour une équipe de développeurs novice chez AngularJS et de travailler immédiatement.

J'ai créé cette adaptation qui utilise 2 div, une pour la barre de chargement et une autre pour le contenu affiché après le chargement des données. La gestion des erreurs se ferait ailleurs.

Ajouter un drapeau 'prêt' à $ scope:

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

En vue 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>

Voir aussi: docs de la barre de progression de Boostrap

3
reggoodwin

J'ai aimé les réponses ci-dessus et j'ai beaucoup appris d'eux, mais il manque quelque chose dans la plupart des réponses ci-dessus.

J'étais coincé dans un scénario similaire dans lequel je résolvais l'URL avec des données extraites lors de la première requête du serveur. Le problème que j'ai rencontré était ce qui si la promesse est rejected.

J'utilisais un fournisseur personnalisé qui renvoyait un Promise qui était résolu par le resolve de $routeProvider au moment de la phase de configuration.

Ce que je veux souligner ici, c'est le concept de when il fait quelque chose comme ça.

Il voit l'URL dans la barre d'URL puis le bloc when respectif dans le contrôleur appelé et la vue est référée jusqu'à présent si bien.

Disons que j'ai le code de phase de configuration suivant.

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

Sur l'URL racine dans le navigateur, le premier bloc d'exécution est appelé, sinon otherwise est appelé.

Imaginons un scénario où je tape rootUrl dans la barre d'adresse AuthServicePrivider.auth() la fonction est appelée.

Disons que la promesse retournée est dans rejeter état quoi alors ???

Rien n'est rendu du tout.

Le bloc Otherwise ne sera pas exécuté comme pour toutes les URL non définies dans le bloc config et inconnues de la phase de configuration angularJs.

Nous devrons gérer l'événement qui est renvoyé lorsque cette promesse n'est pas résolue. En cas d'échec, $routeChangeErorr est déclenché sur $rootScope.

Il peut être capturé comme indiqué dans le code ci-dessous.

$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);
});

OMI C'est généralement une bonne idée de mettre le code de suivi des événements dans un bloc d'exécution d'application. Ce code est exécuté juste après la phase de configuration de l'application.

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);
   });
}]);

De cette façon, nous pouvons gérer l’échec de la promesse au moment de la phase de configuration.

1
Ashish Singh

J'ai eu une interface de panneau coulissant multi-niveaux complexe, avec couche d'écran désactivée. Création d'une directive sur la couche d'écran désactivée qui créerait un événement clic pour exécuter l'état tel que

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

produisaient un effet de chiquenaude. history.back () au lieu de cela a fonctionné, mais ce n'est pas toujours dans l'histoire dans mon cas. SO ce que je découvre, c'est que si je crée simplement l'attribut href sur mon écran de désactivation au lieu de state.go, fonctionne comme un charme.

<a class="disable-screen" back></a>

Directive 'retour'

app.directive('back', [ '$rootScope', function($rootScope) {

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

} ]);

app.js je viens de sauvegarder l'état précédent

app.run(function($rootScope, $state) {      

    $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {         

        $rootScope.previousState = fromState.name;
        $rootScope.currentState = toState.name;


    });
});
0
Dima