javascript - ng-if
Retrasar el cambio de ruta de AngularJS hasta que se cargue el modelo para evitar el parpadeo (13)
Utilizando AngularJS 1.1.5
Actualización de la función ''teléfonos'' en la respuesta de Justen utilizando la sintaxis AngularJS 1.1.5 .
Original:
phones: function($q, Phone) {
var deferred = $q.defer();
Phone.query(function(phones) {
deferred.resolve(phones);
});
return deferred.promise;
}
Actualizado:
phones: function(Phone) {
return Phone.query().$promise;
}
Mucho más corto gracias al equipo Angular y colaboradores. :)
Esta es también la respuesta de Maximilian Hoffmann. Al parecer ese compromiso lo hizo en 1.1.5.
Me pregunto si hay una forma (similar a Gmail) para que AngularJS demore la presentación de una nueva ruta hasta después de que cada modelo y sus datos hayan sido obtenidos utilizando sus respectivos servicios.
Por ejemplo, si hubiera un ProjectsController
que listaba todos los proyectos y project_index.html
que era la plantilla que mostraba estos proyectos, Project.query()
buscaría completamente antes de mostrar la nueva página.
Hasta entonces, la página anterior seguiría apareciendo (por ejemplo, si estuviera navegando en otra página y luego decidiera ver este índice del Proyecto).
Aquí hay un ejemplo de trabajo mínimo que funciona para Angular 1.0.2
Modelo:
<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
});
});
Versión simplificada:
Como $ http () ya devuelve una promesa (también conocida como diferida), en realidad no necesitamos crear la nuestra. Así que podemos simplificar MyCtrl. resolver:
MyCtrl.resolve = {
datasets : function($http) {
return $http({
method: ''GET'',
url: ''http://fiddle.jshell.net/''
});
}
};
El resultado de $ http () contiene datos , estado , encabezados y objetos de configuración , por lo que necesitamos cambiar el cuerpo de MyCtrl a:
$scope.datasets = datasets.data;
El retraso en mostrar la ruta es seguro que conducirá a un enredo asíncrono ... ¿por qué no simplemente rastrear el estado de carga de su entidad principal y usarlo en la vista? Por ejemplo, en su controlador puede usar las devoluciones de llamada de éxito y error en ngResource:
$scope.httpStatus = 0; // in progress
$scope.projects = $resource.query(''/projects'', function() {
$scope.httpStatus = 200;
}, function(response) {
$scope.httpStatus = response.status;
});
Entonces en la vista se podría hacer lo que sea:
<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>
Este fragmento es amigable con la inyección de dependencias (incluso lo uso en combinación con ngmin y uglify ) y es una solución basada en dominios más elegante.
El siguiente ejemplo registra un recurso de teléfono y un número constante de rutas telefónicas , que contiene toda la información de enrutamiento para ese dominio (teléfono). Algo que no me gustó en la respuesta proporcionada fue la ubicación de la lógica de resolución : el módulo principal no debe saber nada ni debe preocuparse por la forma en que se proporcionan los argumentos de recursos al controlador. De esta manera la lógica se mantiene en el mismo dominio.
Nota: si está usando ngmin (y si no lo está: debería) solo tiene que escribir las funciones de resolución con la convención de arreglos 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 siguiente pieza es inyectar los datos de enrutamiento cuando el módulo está en el estado de configuración y aplicarlo a $ 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: ''/'' });
});
Probar la configuración de la ruta con esta configuración también es bastante fácil:
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);
});
});
});
});
Puedes verlo en toda su gloria en mi último experimento (próximo) . Aunque este método funciona bien para mí, realmente me pregunto por qué el inyector $ no retrasa la construcción de nada cuando detecta la inyección de algo que es un objeto de promesa ; Haría las cosas tan increíblemente fáciles.
Edición: Angular v1.2 utilizado (rc2)
He tenido una compleja interfaz de panel deslizante de niveles múltiples, con la capa de pantalla deshabilitada. Creación de directivas en la capa de pantalla deshabilitada que crearía un evento de clic para ejecutar el estado como
$state.go(''account.stream.social.view'');
Estaban produciendo un efecto de chasquido. history.back () en lugar de que funcionó bien, sin embargo, no siempre ha vuelto a la historia en mi caso. Entonces, lo que descubro es que si simplemente creo el atributo href en mi pantalla de desactivación en lugar de state.go, funcionó como un encanto.
<a class="disable-screen" back></a>
Directiva ''atrás''
app.directive(''back'', [ ''$rootScope'', function($rootScope) {
return {
restrict : ''A'',
link : function(scope, element, attrs) {
element.attr(''href'', $rootScope.previousState.replace(//./gi, ''/''));
}
};
} ]);
app.js acabo de guardar el estado anterior
app.run(function($rootScope, $state) {
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
$rootScope.previousState = fromState.name;
$rootScope.currentState = toState.name;
});
});
Me gusta la idea de darkporter porque será fácil para un equipo de desarrollo nuevo en AngularJS entender y trabajar de inmediato.
Creé esta adaptación que usa 2 divs, uno para la barra del cargador y otro para el contenido real que se muestra después de cargar los datos. El manejo de errores se haría en otro lugar.
Agregue un indicador ''listo'' a $ alcance:
$http({method: ''GET'', url: ''...''}).
success(function(data, status, headers, config) {
$scope.dataForView = data;
$scope.ready = true; // <-- set true after loaded
})
});
En la vista 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>
Ver también: Documentos de la barra de progreso de Boostrap
Me gustaron las respuestas anteriores y aprendí mucho de ellas, pero hay algo que falta en la mayoría de las respuestas anteriores.
Me quedé atrapado en un escenario similar en el que estaba resolviendo la url con algunos datos que se recuperaron en la primera solicitud del servidor. El problema que enfrenté fue qué si la promesa es rejected
.
Estaba usando un proveedor personalizado que solía devolver una Promise
que se resolvió mediante la resolve
de $routeProvider
en el momento de la fase de configuración.
Lo que quiero subrayar aquí es el concepto de when
hace algo como esto.
Ve la url en la barra de url y luego la respectiva when
bloque en el controlador llamado y la vista se refieren hasta ahora tan bien.
Digamos que tengo siguiente código de fase de configuración.
App.when(''/'', {
templateUrl: ''/assets/campaigns/index.html'',
controller: ''CampaignListCtr'',
resolve : {
Auth : function(){
return AuthServiceProvider.auth(''campaign'');
}
}
})
// Default route
.otherwise({
redirectTo: ''/segments''
});
En la raíz del URL en el primer bloque de ejecución de get get call, de lo otherwise
se invoca.
Imaginemos un escenario al que llego a rootUrl en la barra de direcciones Se AuthServicePrivider.auth()
función AuthServicePrivider.auth()
.
Digamos que la promesa devuelta está en estado de rechazo , ¿entonces qué?
Nada se rinde en absoluto.
Otherwise
bloque no se ejecutará como lo es para cualquier url que no esté definido en el bloque de configuración y sea desconocido para la fase de configuración de angularJs.
Tendremos que manejar el evento que se activa cuando esta promesa no se resuelve. En caso de error, $routeChangeErorr
se $rootScope
en $rootScope
.
Se puede capturar como se muestra en el código a continuación.
$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 En general, es una buena idea colocar el código de seguimiento de eventos en el bloque de ejecución de la aplicación. Este código se ejecuta justo después de la fase de configuración de la aplicación.
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 esta manera podemos manejar la promesa de fallas en el momento de la fase de configuración.
Puede usar la propiedad de resolución $ routeProvider para retrasar el cambio de ruta hasta que se carguen los datos.
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''});
}]);
Observe que la propiedad de resolve
está definida en la ruta.
EntitiesCtrlResolve
y EntityCtrlResolve
son objetos constantes definidos en el mismo archivo que los controladores EntitiesCtrl
y 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..
});
Trabajé desde el código de Misko anterior y esto es lo que he hecho con él. Esta es una solución más actual ya que $defer
se ha cambiado a $timeout
. Sin embargo, la sustitución de $timeout
esperará el período de espera (en el código de Misko, 1 segundo), y luego devolverá los datos con la esperanza de que se resuelvan a tiempo. De esta manera, vuelve lo antes posible.
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;
}
}
Una posible solución podría ser usar la directiva ng-cloak con el elemento donde estamos usando los modelos, por ejemplo,
<div ng-cloak="">
Value in myModel is: {{myModel}}
</div>
Creo que esta toma menos esfuerzo.
Veo que algunas personas preguntan cómo hacer esto utilizando el método de controlador angular con inyección de dependencia amigable con minificación. Como acabo de trabajar, me siento obligado a regresar y ayudar. Aquí está mi solución (adoptada de la pregunta original y la respuesta 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'';
}]);
Dado que este código se deriva de la pregunta / respuesta más popular, no se ha probado, pero debería enviarlo en la dirección correcta si ya sabe cómo hacer un código angular que sea compatible con la minificación. La única parte que mi propio código no requirió fue una inyección de "Teléfono" en la función de resolución para "teléfonos", ni tampoco usé ningún objeto de "retardo".
También recomiendo este video de youtube http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp , que me ayudó un poco
Si le interesa, también he decidido pegar mi propio código (escrito en coffeescript) para que pueda ver cómo lo hice funcionar.
Para su información, por adelantado uso un controlador genérico que me ayuda a hacer CRUD en varios modelos:
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 ....
]
$routeProvider propiedad $routeProvider permite retrasar el cambio de ruta hasta que se carguen los datos.
Primero define una ruta con un atributo de resolve
como este.
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''});
}]);
Observe que la propiedad de resolve
está definida en la ruta.
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;
}
}
Observe que la definición del controlador contiene un objeto de resolución que declara cosas que deberían estar disponibles para el constructor del controlador. Aquí los phones
se inyectan en el controlador y se definen en la propiedad de resolve
.
La función resolve.phones
es responsable de devolver una promesa. Todas las promesas se recogen y el cambio de ruta se retrasa hasta después de que se hayan resuelto todas las promesas.
Demostración de trabajo: http://mhevery.github.com/angular-phonecat/app/#/phones Fuente: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831
Este compromiso , que forma parte de la versión 1.1.5 y superior, expone el objeto $promise
de $resource
. Las versiones de ngResource, incluida esta confirmación, permiten resolver recursos como este:
$ routeProvider
resolve: {
data: function(Resource) {
return Resource.get().$promise;
}
}
controlador
app.controller(''ResourceCtrl'', [''$scope'', ''data'', function($scope, data) {
$scope.data = data;
}]);