I'm currently having an issue with keeping my scopes updated using a factory service and AngularFire. I think the code speaks for itself:
//Creates firebase object based on URL ($stateParams as an ID)
jobControllers.factory('Job', getJobService);
function getJobService($firebaseObject, $stateParams) {
var ref = new Firebase("<my firebase url>" + $stateParams.jobId);
return $firebaseObject(ref);
}
//Sets Scope to firebase object in getJobService
jobControllers.controller('JobCtrl', function ($scope, $location, Job) {
$scope.job = Job;
});
This works fine, when you click a link like /jobs/:jobId/ it loads the proper data. The problem is once you go back to /jobs/ and click another :jobId the data remains the same. I am fairly certain the issue is that my firebase URL is staying the same and $stateParams isn't changing on controller load so the data uses the URL For the first item clicked. If this is the case, how can I change my factory to take a parameter so I can pass the correct updated $stateParams to it in the controller?
I previously did not have this problem when I had called my firebase objects inside the controller as such:
jobControllers.controller('JobCtrl', function ($scope, $location, $stateParams, $firebaseObject) {
var ref = new Firebase("<my firebase url>" + $stateParams.jobId);
$scope.job = $firebaseObject(ref);
});
My reasoning for using a factory is because I did not like how I was defining the firebase URLs in every different controller, so I wanted to create a reusable service for such. If there is a better way - please advise.
edit: Figured it out thanks to charlieftl
function getFireBaseService($firebaseArray, $firebaseObject) {
return {
getArray: function(url) {
var ref = new Firebase("https://<my-app>.firebaseio.com" + url);
return $firebaseArray(ref);
},
getObject: function(url) {
var ref = new Firebase("https://<my-app>.firebaseio.com" + url);
return $firebaseObject(ref);
}
}
}
The problem is the factory is a singleton and only initializes once. Your approach will only use whatever value $stateParams.jobId is when it does initialize
Use a function to return the firebase object so that it will be run each time it is called upon
function getJobService($firebaseObject, $stateParams) {
function getJob() {
var ref = new Firebase("<my firebase url>" + $stateParams.jobId);
return $firebaseObject(ref);
}
//return object from factory
return {
getJob: getJob
}
}
Then in controller
$scope.job = Job.getJob();
Related
I would like to do something like this:
app.config(function($routeProvider){
$routeProvider.when('products/list', {
controller: 'ProductListCtrl',
templateUrl : 'products/list/view.html',
resolve : { data : function(){
...
},
loadingTemplateUrl : 'general/loader.html'
}
});
I would like to have the loading page in a different view.
This would make the code in the view and controller of every page cleaner, (no <...ng-include ng-show="loading"...>). This would also mean that I don't have to $scope.$watch the data for changes. Is there a clean solution to do something similar (not necessarily in the .config method) or an alternative library to do this?
Assuming you want to show some general template for all state transitions while the data is resolved, my suggestion is to listen to the events fired by the routing library. This allows to use one central point to handle all state transitions instead of polluting the routing config (which I think will not be that easy to do).
Please see the docs for $routeChangeStart, $routeChangeSuccess and of course $routeChangeError at the angular router docs
Maybe someone could be interested in what I did: I created a new service and a new view directive. It could seem like a lot of work, but doing this was much easier than I had expected. The new service enables me to separate the main view from the loading view, that I could reuse in all pages of the application. I also provided the possibility to configure an error template url and error controller, for when the loading failed.
The Angular $injector, $templateRequest and $controller services do most of the work. I just had to connect a directive, that depends on these services, to the right event ($locationChangeSuccess), and to the promise, retrieved (using $q.all) from the resolve object's functions. This connection was done in the route service. The service selects the right template url and comtroller, and passes it on for the directive to handle.
A shortened version (with the getCurrentConfig method left out):
RouteService:
(function () {
'use strict';
// provider:
angular.module('pikcachu')
.provider('pikaRouteService', [function () {
var routeConfigArray;
var otherwiseRouteConfig;
//configuration methods
this.when = function (url, routeConfig){
routeConfigArray.push({url: url, routeConfig: routeConfig});
return this;
}
this.otherwise = function(routeConfig){
otherwiseRouteConfig = routeConfig;
return this;
}
// service factory:
this.$get = ['$rootScope', '$location', '$q', '$injector', '$templateRequest',
function ($rootScope, $location, $q, $injector, $templateRequest) {
function RouteService() {
this.setViewDirectiveUpdateFn = function(){ /*...*/ }
function init(){
$rootScope.$on('$locationChangeSuccess', onLocationChangeSuccess);
}
function onLocationChangeSuccess(){
// get the configuration based on the current url
// getCurrentConfig is a long function, because it involves parsing the templateUrl string parameters, so it's left out for brevity
var currentConfig = getCurrentConfig($location.url());
if(currentConfig.resolve !== undefined){
// update view directive to display loading view
viewDirectiveUpdateFn(currentConfig.loadingTemplateUrl, currentConfig.loadingController);
// resolve
var promises = [];
var resolveKeys = [];
for(var resolveKey in currentConfig.resolve){
resolveKeys.push(resolveKey);
promises.push($injector.invoke(resolve[resolveKey]));
}
$q.all(promises).then(resolveSuccess, resolveError);
function resolveSucces(resolutionArray){
// put resolve results in an object
var resolutionObject = {};
for(var i = 0; i< promises.length;++i){
resolved[resolveKeys[i]] = resolutionArray[i];
}
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
function resolveError(){
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
}
}
init();
}
return new RouteService();
}]
})();
View directive
(function () {
'use strict';
angular.module('pikachu')
.directive('pikaView', ['$templateRequest', '$compile', '$controller', 'pikaRouteService', function ($templateRequest, $compile, $controller, pikaRouteService) {
return function (scope, jQdirective, attrs) {
var viewScope;
function init() {
pikaRouteService.listen(updateView);
}
function updateView(templateUrl, controllerName, resolved) {
if(viewScope!== undefined){
viewScope.$destroy();
}
viewScope = scope.$new();
viewScope.resolved = resolved;
var controller = $controller(controllerName, { $scope: viewScope });
$templateRequest(templateUrl).then(onTemplateLoaded);
function onTemplateLoaded(template, newScope) {
jQdirective.empty();
var compiledTemplate = $compile(template)(newScope);
jQdirective.append(compiledTemplate);
}
}
init();
};
}
]);
})();
I have a textarea in my HTML like this:
<textarea ng-model="commentBox"></textarea>
To access this i simply use "$scope.commentBox" in my controller. But my question is, how do i access the same commentBox within a different controller?
I do have a factory/service setup for this purpose, but i cant figure out how to get the commentBox value in there, for my other controller to use it.
In my factory i have an object var saved = {} and I want to add a property called "comment", with the value of whatever is inside the textarea. Like this saved.comment = commentbox And then access that value from the other controller.
I'm still new at Angular and tried to send the scope information in a parameter to the facory.
var saved = {};
factory.addComment = function (commentbox) {
saved.comment = commentbox
}
Then have my controller send the scope information on a button click,
$scope.testFunction = function () {
myFactory.addComment($scope.commentBox);
}
But yeah, that did not work out.
Note that i need this to work within the factory, and not by using another ng-controller in the HTML.
You need to return your saved var in your factory
var saved = {};
factory.addComment = function (commentbox) {
saved.comment = commentbox
}
return saved
Here there is an example using a service
app.service('fakeService', function() {
var savedData = {};
var addComment = function(newComment) {
savedData.commnet = newComment;
};
var getComment = function(){
return savedData.comment;
};
return {
addComment: addComment,
getComment: getComment
};
});
To inject a factory/service in your controller
app.controller('fakeController', ['$scope','yourFactory',function ($scope, yourFactory) {
console.log(yourFactory.comment) // here you should log saved comment
}])
Then in your controller, you can inject the factory/service and access to saved data. But remember, is your refresh your page, data will be lost, so, to avoid that, you should persist data on your DB.
Over the time in using angular i have switched from one practive of saving state and data to another here are the two.
Please suggest me which one to continue saving and why?
angular.module('app')
.controller('personController', ['$scope', '$log',personsService, cntrl])
.factory('personsService', ['$http', '$q', service]);
//First
function cntrl($scope, $log, service){
$scope.state = {creating:false; updating:false;}
$scope.createPerson = createPerson;
$scope.person = null;
function createPerson(fn, ln, age){
$scope.state.creating = true;
service.createPerson({fn:fn, ln:ln, age:age})
.success(function(r){/*something*/})
.error(function(){/*something*/})
.finally(function(){
$scope.state.creating = true;
});
}
}
function service($http, $q){
var r = {
createPerson : createPerson
};
return r;
function createPerson(o){
return $http.post('/person/create', o);
}
}
//Second
function cntrl($scope, $log, service){
$scope.person = service.person;
$scope.state = service.state;
$scope.service = service;
}
function service($http, $q){
var state = {creating:false, updating:false};
var person = {};
var r = {
state : state,
person : person,
createPerson : createPerson
}
return r;
function createPerson(o){
state.creating = true;
var def = $q.defer();
$http.post('/person/create', o)
.success(function(dbPerson){
def.resolve(dbPerson);
angular.extend(dbPerson, person);
})
.error(function(e){
def.rreject(e);
angular.extend({}, person); //or any other logic
})
.finally(function(){
state.creating = false;
});
return def.promise;
}
}
As you can see in the
1. first example
The ajax state is maintained in the controller. and service only exposes functions that are required for that ajax state. The Person object is also maintained inside the controller so i dont have to maintain the reference to the same object as the object is directly attached to the cart. I can simpply do $scope.person = {};
2. Second Example
The ajax state is maintained by the service which now exposes the person object as well as the functions used to manipulate the object. Now i need to maintain the reference to object bby using functions sucn as angular.extend and angular.copy. Now the ajax state exposed by a controller is divided over multiple services. Advantages are modularity of the code and almost complete logic less controller.
I would suggest you to Go for the second Method.
An AJAX Call for data should always be separate from Controller . Controllers require data that should be shown in the UI.
Tomorrow you might require the same data from the same API to be used in a different way . In this way you are using the same service to get the data and modify according to the need . More over the Controller should not be worried about where the data is coming from. i.e it could come from An AJAX call , a web socket, local storage etc.
So in your case angular.extend makes more sense as you are working on a copy of the data and not the original data which might be used by some other controller over time. Re-usable and clean.
I would say the correct approach would be the mix of both the approaches.
#Parv mentioned the state object is being used at the UI, it should rest in teh controller. But the service function in approach 1 is at fault. The logic there should use the $q defer object and be something like what you are doing in your approach 2..
angular.module('app')
.controller('personController', ['$scope', '$log',personsService, cntrl])
.factory('personsService', ['$http', '$q', service]);
//First
function cntrl($scope, $log, service){
$scope.state = {creating:false; updating:false;}
$scope.createPerson = createPerson;
$scope.person = null;
function createPerson(fn, ln, age){
$scope.state.creating = true;
service.createPerson({fn:fn, ln:ln, age:age})
.then(function(r){/*assign $scope.person = r;*/}),function()
{/*something*/}).finally(function() {
$scope.state.creating = false;
});
}
}
function service($http, $q){
var r = {
createPerson : createPerson,
person: {}
};
return r;
function createPerson(o){
var def = $q.defer();
$http.post('/person/create', o)
.success(function(dbPerson){
def.resolve(dbPerson);
angular.extend(dbPerson, person);
})
.error(function(e){
def.reject(e);
angular.extend({}, person); //or any other logic
});
return def.promise;
}
}
This way we have segregated the post logic from the things that matter at the UI.
I'd like to do simple notifications in angular. Here is the code I've written.
http://pastebin.com/zYZtntu8
The question is:
Why if I add a new alert in hasAlerts() method it works, but if I add a new alert in NoteController it doesn't. I've tried something with $scope.$watch but it also doesn't work or I've done something wrong.
How can I do that?
Check out this plnkr I made a while back
http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=preview
I show a couple of ways controllers can use data from services, in particular the first two show how to do it without a watch which is generally a more efficient way to go:
// Code goes here
angular.module("myApp", []).service("MyService", function($q) {
var serviceDef = {};
//It's important that you use an object or an array here a string or other
//primitive type can't be updated with angular.copy and changes to those
//primitives can't be watched.
serviceDef.someServiceData = {
label: 'aValue'
};
serviceDef.doSomething = function() {
var deferred = $q.defer();
angular.copy({
label: 'an updated value'
}, serviceDef.someServiceData);
deferred.resolve(serviceDef.someServiceData);
return deferred.promise;
}
return serviceDef;
}).controller("MyCtrl", function($scope, MyService) {
//Using a data object from the service that has it's properties updated async
$scope.sharedData = MyService.someServiceData;
}).controller("MyCtrl2", function($scope, MyService) {
//Same as above just has a function to modify the value as well
$scope.sharedData = MyService.someServiceData;
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl3", function($scope, MyService) {
//Shows using a watch to see if the service data has changed during a digest
//if so updates the local scope
$scope.$watch(function(){ return MyService.someServiceData }, function(newVal){
$scope.sharedData = newVal;
})
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl4", function($scope, MyService) {
//This option relies on the promise returned from the service to update the local
//scope, also since the properties of the object are being updated not the object
//itself this still stays "in sync" with the other controllers and service since
//really they are all referring to the same object.
MyService.doSomething().then(function(newVal) {
$scope.sharedData = newVal;
});
});
The notable thing here I guess is that I use angular.copy to re-use the same object that's created in the service instead of assigning a new object or array to that property. Since it's the same object if you reference that object from your controllers and use it in any data-binding situation (watches or {{}} interpolation in the view) will see the changes to the object.
I have 2 services and would like to update a variable in the 1st service from the 2nd service.
In a controller, I am setting a scope variable to the getter of the 1st service.
The problem is, the view attached to the controller doesn't update when the service variable changes UNLESS I use angular.extend/copy. It seems like I should just be able to set selectedBuilding below without having to use extend/copy. Am I doing something wrong, or is this how you have to do it?
controller
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
$scope.building = BuildingsService.getSelectedBuilding();
});
service 1
app.factory('BuildingsService', function() {
var buildingsList = [];
var selectedBuilding = {};
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
angular.extend(selectedBuilding, _.find(
buildingsList, {'building_id': buildingId})
);
};
var getSelectedBuilding = function() {
return selectedBuilding;
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
service 2
app.factory('AnotherService', function(BuildingsService) {
...
// something happens, gives me a building id
BuildingsService.setSelectedBuilding(building_id);
...
});
Thanks in advance!
When you execute this code:
$scope.building = BuildingsService.getSelectedBuilding();
$scope.building is copied a reference to the same object in memory as your service's selectedBuilding. When you assign another object to selectedBuilding, the $scope.building still references to the old object. That's why the view is not updated and you have to use angular.copy/extend.
You could try the following solution to avoid this problem if you need to assign new objects to your selectedBuilding:
app.factory('BuildingsService', function() {
var buildingsList = [];
var building = { //create another object to **hang** the reference
selectedBuilding : {}
}
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
//just assign a new object to building.selectedBuilding
};
var getSelectedBuilding = function() {
return building; //return the building instead of selectedBuilding
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
With this solution, you have to update your views to replace $scope.building bindings to $scope.building.selectedBuilding.
In my opinion, I will stick to angular.copy/extend to avoid this unnecessary complexity.
I dont believe you need an extend in your service. You should be able to watch the service directly and respond to the changes:
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
// first function is evaluated on every $digest cycle
$scope.$watch(function(scope){
return BuildingsService.getSelectedBuilding();
// second function is a callback that provides the changes
}, function(newVal, oldVal, scope) {
scope.building = newVal;
}
});
More on $watch: https://code.angularjs.org/1.2.16/docs/api/ng/type/$rootScope.Scope