Currently I'm loading my firebase data right in my controller like this:
app.controller("MyCtrl", function ($scope, $firebaseObject) {
$scope.users = {};
var myDB = new Firebase('https://mydb.firebaseio.com/');
$firebaseObject(myDB.child("users")).$bindTo($scope, "user");
});
This way after the page is loaded, there is a delay during which the data is retrieved from firebase, therefore at first the page appears to be empty until the data arrives. This is unconvinient.
In order to prevent this flashy behavior I wanted to resolve the required data in my $routeProvider but wasn't able to get a promise.
How to properly get rid of the content flashing?
Is there a way to get a promise so one can resolve the $firebaseObject before the view is shown?
Please do not offer quick-and-dirty-solutions containing ng-cloak, ng-hide, ng-show etc.
You can use the $loaded() promise in your resolve function.
app.config(["$routeProvider", function($routeProvider) {
$routeProvider.when("/home", {
// the rest is the same for ui-router and ngRoute...
controller: "HomeCtrl",
templateUrl: "views/home.html",
resolve: {
"myObject": ["$firebaseObject", function($firebaseObject) {
var ref = new Firebase("...");
return $firebaseObject(ref).$loaded();
}]
}
})
https://www.firebase.com/docs/web/libraries/angular/guide/synchronized-objects.html
Check out the AngularFire docs on routing in authentication for better guidance on handling routes with promises in general.
https://www.firebase.com/docs/web/libraries/angular/guide/user-auth.html
Related
Given the following state in ui-router:
.state('some.state', {
url: '/some/:viewType',
templateUrl: 'myTemplate.html',
controller: 'SomeStateController',
controllerAs: 'vm',
data: {
authorizedFor: [SOME_ROLE]
}
}
I'm trying to use the "data" object for a state to help control access to authorized states. Separately, I handle the $stateChangeStart event to look at data.authorizedFor and act accordingly.
The problem, though, is that the list of authorized roles might change based on the value of :viewType. I thought I could let data:{} be a function, inject $stateParams, and handle the logic there...but that won't do.
So, I tried using the params object instead, but at the $stateChangeStart time, the :viewType is not yet accessible from $state.params or $stateParams.
Stepping through in dev tools, I noticed that $state.transitionTo.arguments is populated, but it seems awfully hacky to go that route.
params: {
authorizedFor: function($state) {
console.log($state.transitionTo.arguments[1].viewType); // has value I need
}
}
Any suggestions?
My suggestion is to use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.
for example:
$stateProvider
.state('profile', {
url: '/profile',
templateUrl: 'profile.html',
resolve:{
'ProfileService': function(ProfileService){
return ProfileService.promise_skillRecommendation_mock;
}
}
})
The profileService code:
var app = angular.module('app').service("ProfileService", function($http){
var myData = null;
var promise_skillRecommendation_mock =
$http.get('Mock/skillRecommendation-mock.json')
.success(function(data){
myData = data;
});
return{
promise_skillRecommendation_mock: promise_skillRecommendation_mock,
get_skillRecommendation: function(){
return myData;
}
};
});
and the controller code which will use this service is:
angular.module('app').controller('ProfileController', function($scope, $http, ProfileService){
$scope.skillRecommendation = ProfileService.get_skillRecommendation();
The object in resolve below must be resolved (via deferred.resolve() if they are a promise) before the controller is instantiated. Notice how each resolve object is injected as a parameter into the controller.
by using this code, the page will be displayed only after that the promise will be resolved.
for more info please view this page: https://github.com/angular-ui/ui-router/wiki
I'm in the process of learning AngularJS, working on a more in-depth ToDo app. I'm having an issue with trying to limit access to a url or "route" using angular.
When you hit my dev url on my machine (todo.ang) it brings you to todo.ang/#/home, on this view you see the categories which have todos associated to each. EG (category = cat, cat has a todo of "feed", and "play"), when you click a category I'm calling the $scope.goToCategory function (seen in my JS fiddle) which sets a variable for my firebase ref then redirects you too /#/todo. This is working correctly.
My problem is, I don't want the user to be able to access /#/todo if the todoRef variable is still undefined. But it seems like even after $scope.goToCategory is called and todoRef is set to a firebase URL, the routerprovider never gets recalled to know that todoRef has been set to a different value so it always forces you back to /#/home.
code:
var todoRef = undefined;
if (todoRef !== undefined) {
$routeProvider.when('/todo', {
templateUrl: 'views/todo.html',
controller: 'TodoCtrl'
});
}
$scope.goToCategory = function(catId) {
test = catId;
todoRef = new Firebase("URL HERE");
$location.path('/todo');
}
I didn't include the entire file of code but if thats necessary, I can do that as well.
JSFiddle
All routes are only being set during the config phase.
what happens in your code is that 'todo' route is ignored during the initiation of ngRoute.
What you should do is to setup the route but have a resolve like so:
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/todo', {
templateUrl: 'views/todo.html',
controller: 'TodoCtrl',
resolve: {
todoRef: ['$q', function($q) {
return todoRef ? todoRef : $q.reject('no ref');
}]
}
});
}]);
If 'todoRef' is undefined the route is rejected.
Also you should consider moving 'todoRef' into a service and not on global scope.
You can also listen for route errors and for example redirect to home route:
app.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.$on('$routeChangeError', function() {
$location.path('/home');
});
}]);
This might be a beginner question, but I am retrieving data via http calls in AngularJS and setting them as properties in the $scope variable. However, since http calls take a while, my page tries to load AngularJS more than once in order to render different parts of the page as more the data is retrieved. Is there a way around this? (to hold off on loading the page before all data has been retrieved)
What you could do is to use ng-hide or ng-cloak, so that whatever should not be displayed until the http call fully loaded the data would remain hidden.
take a look at the resolve property in the route settings. If you set something to be resolved the router will resolve this before going to the controller.
app.config(function ($routeProvider) {
$routeProvider
.when('/',
{
templateUrl: "app.html",
controller: "AppCtrl"
resolve: {
app: function ($q, $timeout) {
YourFactory.getData({});
}
}
}
)
});
then create a Factory that will get the data you need
app.factory('YourFactory', ['$http', '$q',
function($http, $q) {
var url = '/api2.php/api';
var YourFactory = {};
var factory_data = [];
var messages = [];
YourFactory.getData = function(params) {
console.log("into GET data");
var deferred = $q.defer();
$http.get(url).success(function(response) {
angular.copy(factory_data, response.data);
deferred.resolve();
}).error(function(response) {
//couldn't resolve therefore it's rejected
deferred.reject();
});
//returns a promise that indicates that something is being resolved and will be returned for the app to continue
return deferred.promise;
};
YourFactory.data = function() {
return factory_data;
};
return YourFactory;
}
]);
then in your controller you need to input the factory and set the scope data from the Factory. Remember that Angular used the Factory to get data before the controller using the resolve property.
app.controller("AppCtrl", ['$scope','YourFactory',
function($scope, YourFactory) {
$scope.data = YourFactory.data();
});
(I haven't tested the code, I simply wrote an example based on an app that I'am doing and in which I passed through the same things as you)
Look at this links if you have any doubt.
https://egghead.io/lessons/angularjs-resolve
http://www.javierlerones.com/2013/07/preloading-data-using-deferred-promises-in-angular-js.html
I am relatively new to Angular but I am quite an experienced developer. So far I have made quite some progress in building my application to work with a CMS. I am a bit lost however on what the 'correct' approach would be to handle data in my model.
This is best described with an example:
Because I am hooking up my angular frontend with a CMS, the routing (pages) exist only in the CMS context. This means that the routing should be dynamic as well. I have managed to get the dynamic routes thing to work, but when I try to do things the right way (actually getting data from a server) I run into some issues...
app.config(function($provide, $routeProvider) {
$provide.factory("$routeProvider", function() {
return $routeProvider;
});
});
// Load the dynamic routes from the API...
app.run(function($routeProvider, $http, $scope, logger, siteRoutes) {
$routeProvider.when('/', { templateUrl: '__views/', controller: 'ContentPageController' });
$routeProvider.otherwise({redirectTo: '/'});
});
In other words, I inject a service into my app.run method (siteRoutes) and this one should connect to the API.
So my siteRoutes is a service:
cmsModule.service('siteRoutes', function siteRouteFactory(apiConnection, logger)
// SNIP
And in this service I inject my generic apiConnection service:
cmsModule.factory('apiConnection', ['$q', '$http', '$timeout', 'logger', function apiConnectionService($q, $http, $timeout, logger)
What I want is this:
I would like the siteRoutes service to load the data once and not execute the connection every time. I did this in the following way:
bla.service('example', function() {
var service = {
get: function(apiStuff) { // DO API CONNECT WITH .THEN HERE },
data: {}
}
service.get();
return service;
}
I would like one entry point towards the Api that handles all the $q stuff (my factory) I assumed I need to handle all the .then() stuff in my siteRoutes object, which is what I did.
Now, what happens in my app.run method is that I don't get the siteRoutes object with any data. So I recon I need to do a .then there as well?
But that made me question the entire design of putting all logic in a separate factory for the connection, because I basically like my app to just use the data and have my library deal with the async stuff (if you get what I am saying)...
Hope this is clear.
TL;DR -> How to make your services / factories handle async stuff without making your 'app' deal with it?
The templateUrl property can also be a function that takes the url parametes as input.
In the example below all routes will load a template with same name.
Eg. domain.com/#/blabla.html will load the view blabla.html from the server.
myApp.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/:templateName',
{
templateUrl: function (params) {
return params.templateName + ".html";
}
}
)
.otherwise({ redirectTo: '/main' });
}]);
I've been experimenting a little with Angular.js lately. As part of this I created a very simple set of controllers with an ng-view and templates to trigger depending on the route requested. I'm using angularFireCollection just to grab an array from Firebase. This works fine in the thumbnailController which does not form part of the ng-view.
My problem is that in addition to the data flowing into the thumbnailController, I also need the two other controllers to be able to access the data. I initially simply set the data to either be part of $rootScope or to have the ng-view as a child of the div in which the thumbnailController is set.
However, the issue from that perspective is that each sub-controller presumably attempts to set the data in the template before it is actually available from Firebase.
The solution appears to be using resolve as per the answer to this question angularFire route resolution. However, using the below code (and also referencing angularFireCollection) I get an error message of angularFire being an unknown provider. My understanding is the code below should be sufficient, and I would also add that the usage of angularFireCollection in thumbnailController works fine as I say.
I also experimented with injecting angularFire/aFCollection directly into the controllers using .$inject however a similar issue arose in terms of it being considered an unknown provider.
If possible could someone advise on what the issue may be here?
var galleryModule = angular.module('galleryModule', ['firebase']);
galleryModule.config(['$routeProvider', 'angularFire', function($routeProvider, angularFire){
$routeProvider.
when('/', {
controller: initialController,
templateUrl: 'largeimagetemplate.html',
resolve: {images: angularFire('https://mbg.firebaseio.com/images')}
}).
when('/view/:id', {
controller: mainimageController,
templateUrl: 'largeimagetemplate.html',
resolve: {images: angularFire('https://mbg.firebaseio.com/images')}
}).
otherwise({
redirectTo: '/'
});
}]);
galleryModule.controller('thumbnailController', ['$scope', 'angularFireCollection', function($scope, angularFireCollection){
var url = 'https://mbg.firebaseio.com/images';
$scope.images = angularFireCollection(url);
}]);
function initialController($scope,images){
$scope.largeurl = images[0].largeurl;
}
function mainimageController($scope, images, $routeParams){
$scope.largeurl = images[$routeParams.id].largeurl;
}
I got the chance to dig into this a little bit - it seems like regular services cannot be used in .config sections. I'd instantiate angularFire in the controller instead of using resolve, for example:
galleryModule
.value("url", "https://mbg.firebaseio.com/images")
.controller('thumbnailController', ['$scope', 'angularFireCollection', 'url',
function($scope, angularFireCollection, url) {
$scope.images = angularFireCollection(url);
}])
.controller('initialController', ['$scope', 'angularFire', 'url',
function($scope, angularFire, url) {
angularFire(url, $scope, 'images').then(function() {
$scope.largeurl = $scope.images[0].largeurl;
});
}])
.controller('mainimageController', ['$scope', 'angularFire', '$routeParams', 'url',
function($scope, angularFire, $routeParams, url){
angularFire(url, $scope, 'images').then(function() {
$scope.largeurl = $scope.images[$routeParams.id].largeurl;
});
}]);
This is not ineffecient, since the data is only loaded once from the URL by Firebase, and all subsequent promises will be resolved almost immediately with data already at hand.
I would like to see angularFire work with resolve in the $routeProvider, however. You can use this method as a workaround until we figure out a more elegant solution.