I want to change the color of font in an input field depending on the value coming from firebase.
In jquery I use $(document).ready() but my code fires before the firebase data is loaded into the dom.
I have reverted to using using setTimeout() to give the dom enough time to load which is not really the way to do it.
There must be an event the fires after the data is attched to the DOM?
app.controller('myCtrl', function($scope, $stateParams, $firebaseObject) {
var ref = new Firebase('xxxxxxxxx');
$scope.Details = $firebaseObject(ref);
//what I really need is, "tell me when the firebase object is loaded to the DOM
//so I can do my stuff"
setTimeout(function(){
if($("#idInput").val() ==='foo'){
$("#idInput").css("color", "red");
}
}, 500);
});
Use $loaded(), if you need to use the data from the $firebaseObject.
$scope.Details = $firebaseObject(ref);
$scope.Details.then(function(data) {
// loaded data here
});
Otherwise though, the $firebaseObject informs the $digest loops when the data has loaded.
Another tactic is to use resolve in the router to load the data into the controller. This is much cleaner, because you don't need to unwrap the promise.
.config(function($stateProvider) {
$stateProvider.state('home', {
controller: 'myCtrl',
template: 'myTemplate.html',
resolve: {
details: function($firebaseObject, $stateParams) {
var ref = new Firebase('xxxxxxxxx');
var childRef = ref.child($stateParams.id);
return $firebaseObject(childRef).$loaded();
}
}
});
})
Then in your controller the data will be resolved:
.controller('myCtrl', function($scope, details) {
$scope.Details = details; // totally available to use
})
Read the docs for more information on resolving data with routing and AngularFire.
Don't seems that you need to use setTimeout here, you could use ng-class directive to apply CSS on change of input value.
HTML
<input id="idInput" ng-model="idInput" ng-class="{ 'red': idInput == 'foo' }"/>
CSS
.red {
color: red;
}
If you are really interested to do something after data gets loaded through $firebaseObject(ref) then you should use $loaded method over $firebaseObject which will get called once data loaded. Will prefer to use $timeout instead of setTimeout to make sync scope variable binding with html by running digest cycle. Additionally don't do DOM manipulation from controller its considered as antipattern.
Code
$scope.Details = $firebaseObject(ref);
$scope.Details.$loaded()
.then(function(data) {
//do some code
$timeout(function(){
//you shouldn't use DOM manipulation from angular controller.
//if($("#idInput").val() ==='foo'){
//$("#idInput").css("color", "red");
}
}, 500);
})
Related
I'm trying to get some JSON data to pass between controllers. I get some JSON with $http, set my callback with a $q defer and assign the result to my $rootScope.productList.
Everything is working, but when I add a $watch on $rootScope.productList, it returns me undefined. Do you have any solution about this?
My $watch inside a controller:
$rootScope.watch('productList', function(newVal, oldVal) {
$scope.filters = $rootScope.productList;
console.log($rootScope.productList);
});
and inside another controller, I get my data. I replaced the $http by a timeout to reproduce the behaviour.
$timeout(function() {
$rootScope.productList = $scope.productList;
console.log($rootScope.productList);
}, 500);
http://plnkr.co/edit/7wWxXgq5BARYgm0tYVgf
I tried with a $watch, but I'd take any workaround.
The problem here is that watch() is not a recognized function - what you probably want is $watch().
In other words, you were just missing the $ next to 'watch' in your original code.
Try this:
$rootScope.$watch('productList', function(newVal, oldVal) {
$scope.filters = $rootScope.productList;
console.log($rootScope.productList);
})
If you need to be able to transfer data between controllers, you should be implementing a service in angular. The $rootScope wasn't created for you to set your data.
What is the better solution to hide template while loading data from server?
My solution is using $scope with boolean variable isLoading and using directive ng-hide, ex: <div ng-hide='isLoading'></div>
Does angular has another way to make it?
You can try an use the ngCloak directive.
Checkout this link http://docs.angularjs.org/api/ng.directive:ngCloak
The way you do it is perfectly fine (I prefer using state='loading' and keep things a little bit more flexible.)
Another way of approaching this problem are promises and $routeProvider resolve property.
Using it delays controller execution until a set of specified promises is resolved, eg. data loaded via resource services is ready and correct. Tabs in Gmail work in a similar way, ie. you're not redirected to a new view unless data has been fetched from the server successfully. In case of errors, you stay in the same view or are redirected to an error page, not the view, you were trying to load and failed.
You could configure routes like this:
angular.module('app', [])
.config([
'$routeProvider',
function($routeProvider){
$routeProvider.when('/test',{
templateUrl: 'partials/test.html'
controller: TestCtrl,
resolve: TestCtrl.resolve
})
}
])
And your controller like this:
TestCtrl = function ($scope, data) {
$scope.data = data; // returned from resolve
}
TestCtrl.resolve = {
data: function ($q, DataService){
var d = $q.defer();
var onOK = function(res){
d.resolve(res);
};
var onError = function(res){
d.reject()
};
DataService.query(onOK, onError);
return d.promise;
}
}
Links:
Resolve
Aaa! Just found an excellent (yet surprisingly similar) explanation of the problem on SO HERE
That's how I do:
$scope.dodgson= DodgsonSvc.get();
In the html:
<div x-ng-show="dodgson.$resolved">We've got Dodgson here: {{dodgson}}. Nice hat</div>
<div x-ng-hide="dodgson.$resolved">Latina music (loading)</div>
I have a look up service which connects with the API service to bind the dropdown lists.
var Lookup = angular.module('Lookup', [])
.run(function ($window, $rootScope, DropDownLookUp) {
debugger;
$rootScope.MaritalStatusList = DropDownLookUp.maritalStatusList();
$rootScope.ProvinceList = DropDownLookUp.provinceList();
$rootScope.GenderList = DropDownLookUp.genderList();
$rootScope.ProvinceOfEmploymentList = DropDownLookUp.provinceOfEmploymentList();
});
I am using $Http.Get method to fetch the data.
var maritalStatusList = function () {
var keyName = "dropdown-maritalstatus-list";
// debugger;
var data = StoreData.retrieveStaticData(keyName);
if (data == null) {
HttpService.Get(config.apiUrl + "HomeAPI/MaritalStatusLookUp", "maritalStatusList", "maritalStatusList").then(function (results) {
StoreData.saveStaticData(JSON.stringify(results), keyName);
data = results;
return data;
});
}
else {
return data;
}
};
This look up module is being called when my default App module loads.
Service is getting fired correctly. But my page is getting loaded before the above calls completed. Hence no data displayed in the dropdown.
How do I can delay the page load, until I have all the necessary data?
Easy method - Use $routeProvider resolve method. - Delays navigation to a page until all promises are fulfilled
Other method - Don't render dom elements or secondary apps:
.
The gist of the answer below is to simply wrap any necessary dom elements in an ng-if, and set the evaluated expression to true within the success callback of your $http request. The example below is a bit overkill, in that it's using 2 apps on the page.
This is probably an ugly no-no solution, but it does work. In essence, I'm creating multiple apps on the page (which requires manual bootstrapping). The second app has a dependency on the first, and is rendered within the fake "success callback" of the first app's fake $http via ng-if.
Notice there is no ng-app reference, because the app is manually bootstrapped using the element's id:
<section ng-if="loaded" id="myapp" ng-controller="MyNameIsController">
My Name is {{FirstName}} {{LastName}}! {{loaded}}
</section>
I'm simulating an $http request with a $timeout in the first app:
HelloWorldApp.run(function($rootScope, $timeout){
$timeout(function(){
$rootScope.loaded = true;
},1500)
})
Here's the plunker I forked to get it going.
I have my angular controller setup like most of the examples shown in the docs such that it is a global function. I assume that the controller class is being called when the angular engine sees the controller tag in the html.
My issue is that i want to pass in a parameter to my controller and i don't know how to do that because I'm not initializing it. I see some answers suggesting the use of ng-init. But my parameter is not a trivial string - it is a complex object that is being loaded by another (non-angular) part of my js. It is also not available right on load but takes a while to come along.
So i need a way to pass this object, when it finally finishes loading, into the controller (or scope) so that the controller can interact with it.
Is this possible?
You can use a service or a factory for this, combined with promises:
You can setup a factory that returns a promise, and create a global function (accessible from 3rd-party JS) to resolve the promise.
Note the $rootScope.$apply() call. Angular won't call the then function of a promise until an $apply cycle. See the $q docs.
app.factory('fromoutside', function($window, $q, $rootScope) {
var deferred = $q.defer();
$window.injectIntoAngularWorld = function(obj) {
deferred.resolve(obj);
$rootScope.$apply();
};
return deferred.promise;
});
And then in your controller, you can ask for the fromoutside service and bind to the data when it arrives:
app.controller('main', function($scope, fromoutside) {
fromoutside.then(function(obj) {
$scope.data = obj;
});
});
And then somewhere outside of Angular:
setTimeout(function() {
window.injectIntoAngularWorld({
A: 1,
B: 2,
C: 3
});
}, 2000);
Here's a fiddle of this.
Personally, I feel this is a little bit cleaner than reaching into an Angular controller via the DOM.
EDIT: Another approach
Mark Rajcok asked in a comment if this could be modified to allow getting data more than once.
Now, getting data more than once could mean incremental updates, or changing the object itself, or other things. But the main things that need to happen are getting the data into the Angular world and then getting the right angular scopes to run their $digests.
In this fiddle, I've shown one way, when you might just be getting updates to an Array from outside of angular.
It uses a similar trick as the promise example above.
Here's the main factory:
app.factory('data', function($window) {
var items = [];
var scopes = [];
$window.addItem = function(item) {
items.push(item);
angular.forEach(scopes, function(scope) {
scope.$digest();
});
};
return {
items: items,
register: function(scope) { scopes.push(scope); }
};
Like the previous example, we attach a function to the $window service (exposing it globally). The new bit is exposing a register function, which controllers that want updates to data should use to register themselves.
When the external JS calls into angular, we just loop over all the registered scopes and run a digest on each to make sure they're updated.
In your non-angular JavaScript, you can get access to the scope associated with a DOM element as follows:
angular.element(someDomElement).scope().someControllerFunction(delayedData);
I assume you can find someDomElement with a jQuery selector or something.
My problem is that i need a service loaded before the controller get called and the template get rendered.
http://jsfiddle.net/g75XQ/2/
Html:
<div ng-app="app" ng-controller="root">
<h3>Do not render this before user has loaded</h3>
{{user}}
</div>
JavaScript:
angular.module('app', []).
factory('user',function($timeout,$q){
var user = {};
$timeout(function(){//Simulate a request
user.name = "Jossi";
},1000);
return user;
}).
controller('root',function($scope,user){
alert("Do not alert before user has loaded");
$scope.user = user;
});
You can defer init of angular app using manual initialization, instead of auto init with ng-app attribute.
// define some service that has `$window` injected and read your data from it
angular.service('myService', ['$window', ($window) =>({
getData() {
return $window.myData;
}
}))
const callService = (cb) => {
$.ajax(...).success((data)=>{
window.myData = data;
cb(data)
})
}
// init angular app
angular.element(document).ready(function() {
callService(function (data) {
doSomething(data);
angular.bootstrap(document);
});
});
where callService is your function performing AJAX call and accepting success callback, which will init angular app.
Also check ngCloak directive, since it maybe everything you need.
Alternatively, when using ngRoute you can use resolve property, for that you can see #honkskillet answer
even better than manually bootstrapping (which is not always a bad idea either).
angular.module('myApp', ['app.services'])
.run(function(myservice) {
//stuff here.
});
As I said in the comments, it would be a lot easier to handle an unloaded state in your controller, you can benefit from $q to make this very straightforward:
http://jsfiddle.net/g/g75XQ/4/
if you want to make something in the controller when user is loaded: http://jsfiddle.net/g/g75XQ/6/
EDIT: To delay the route change until some data is loaded, look at this answer: Delaying AngularJS route change until model loaded to prevent flicker
The correct way to achieve that is using resolve property on routes definition:
see http://docs.angularjs.org/api/ngRoute.$routeProvider
then create and return a promise using the $q service; also use $http to make the request and on response, resolve the promise.
That way, when route is resolved and controller is loaded, the result of the promise will be already available and not flickering will happen.
You can use resolve in the .config $routeProvider. If a promise is returned (as it is here), the route won't load until it is resolved or rejected. Also, the return value will be available to injected into the controller (in this case Somert).
angular.module('somertApp')
.config(function($routeProvider) {
$routeProvider
.when('/home/:userName', {
/**/
resolve: {
Somert: function($q, $location, Somert) {
var deferred = $q.defer();
Somert.get(function(somertVal) {
if (somertVal) {
deferred.resolve(somertVal);
} else {
deferred.resolve();
$location.path('/error/'); //or somehow handle not getting
}
});
return deferred.promise;
},
},
});
});
There are a few ways, some more advanced than others, but in your case ng-hide does the trick. See http://jsfiddle.net/roytruelove/g75XQ/3/