For the sake of understanding suppose I have an AngularJS application that has similar data as Stackoverflow so that it:
is using the usual ngRoute/$routeProvider
has a userService that returns favourite and ignore tag lists of the logged in user - both lists are fetched at the same time and request for them is a promise that when resolved caches these lists
has a view that displays a list of questions with a QuestionsController that provides its model (similar to Stackoverflow)
QuestionsController makes a request for questions and then depends on cached tag lists to mark them appropriately
As the recommended guideline when controllers rely on other async data we should offload those to route resolve part so when controllers are being instantiated those promises are already resolved. Therefore I offload tag list fetching to it so both lists are ready and injected into the controller. This all works as expected.
The additional feature of my questions list view is that when a user clicks a tag displayed on questions it automatically adds this tag to favourite list (or off of it when that tag is already part of favourite list).
Route configuration
...
.when({
templateUrl: "...",
controller: "QuestionsController as context",
resolve: {
tags: ["userService", function(userService) {
return userService.getMyTags();
}]
}
})
.when(...)
...
Controller pseudo code
QuestionsController.prototype.markQuestions = function() {
this.model.questions.forEach(function(q, idx) {
// "myTags" is resolve injected dependency
q.isFavourite = q.tags.any(myTags.favourite);
q.isIgnored = q.tags.any(myTags.ignored);
});
};
QuestionsController.prototype.toggleTag = function(tag) {
var self = this;
// change tag subscription
tagService
.toggleFavourite(tag)
.then(function() {
// re-mark questions based on the new set of tags
self.markQuestions();
});
};
The problem
When the view displays, all questions are loaded and correctly marked as per provided tag lists. Now when a user clicks on a specific tag and that tag's favourite status gets changes my controller's dependency should get automatically updated.
How can I do that since my controller is already instantiated and had tag lists injected during instantiation?
I would like to avoid loading those lists manually within my controller because in that case I should do the same during instantiation and reuse the same functionality and not have it in two places (route resolve and inside controller).
So long as your "resolved" variable is referring to the same object used elsewhere, they are one and the same.
So, if your userService.getMyTags is conceptually like the following:
.factory("userService", function($timeout){
var tags = [/*...*/];
return {
getMyTags: function(){
return $timeout(function(){ return tags; }, 500);
},
addTag: function(newTag){
tags.push(newTag);
}
}
});
Then any reference to tags anywhere would get the changes:
.controller("ViewCtrl", function($scope, tags){
$scope.tags = tags; // tags is "resolved" with userService.getMyTags()
})
.controller("AddTagCtrl", function($scope, userService){
$scope.addTag = function(newTag){
userService.addTag(newTag); // changes will be reflected in ViewCtrl
}
}
plunker, to illustrate
Related
I want to display view once the list of content is retrieved from the database display it in the view.
On a high level what I am doing now works more or less but upon first access the previous results appears to be cached or still saved in my storage service.
This is what the service looks like.
function StorageService() {
var opportunities;
var getOpportunities = function() {
return opportunities;
}
var setOpportunities = function(data) {
opportunities = data;
}
return {
getOpportunities: getOpportunities,
setOpportunities: setOpportunities
}
}
So when I click on the tab to getOpportunities I go direct to the view first and the load the data.
$scope.getAllOpportunities = function() {
$state.go("app.opportunities");
communicationService.getOpportunitiesAll().then(function(data) {
StorageService.setOpportunities(data);
if (!data.length) {
$("#noShow").html("There are no opportunities");
}
}, function(error) {})
}
once the view is rendered I retrieve the results and bind it to the view.
controller('OpportunitiesController', function($scope) {
$scope.$on("$ionicView.afterEnter", function(scopes, states) {
$scope.opportunities = StorageService.getOpportunities();
});
What I am doing here also feels a bit wrong.
Is there a better way or a way that I can improve on the existing.
I want to load the view and the replace the loader with the data once the data is ready.
Thanks in advance
You should resolve the promise in the route, using the resolve property. That way, the data will always be available when the controller is instantiated.
https://toddmotto.com/resolve-promises-in-angular-routes/
Unless the resource is huge and you want to show som loading animation while getting the data. Then it would probably be more proper to just get the data in the controller.
controller('OpportunitiesController', function($scope) {
communicationService.getOpportunitiesAll().then(function(response){
$scope.opportunities = response;
})
});
html:
<span ng-if="!opportunities">Getting stuff</span>
<span ng-if="opportunities">Stuff fetched</span>
Also, there is no use to have getter and setters in the service. Javascript objects are passed by reference so you can just expose the property directly.
I have a main controller for my profile edit page, from where users can add their education, in a modal window. The modal has it's own controller. The user can add as many education items they want and they can modify the existing ones.
Now for my issue. I want the modal controller to take care of both adding a new item and updating an existing one. The difference is that on update, the controller should receive a fully populated education object, which it should update. I don't know how to pass this object from the main controller to the modal controller. I've seen some ways of passing data between controllers via services, but that seems too much of a hassle, and it doesn't make sense to me to use a service in this particular case.
My main controller creates modals like so:
vm.openEducation = function(){
$modal.open({
templateUrl: "some.html",
controller: "SomeController",
controllerAs: "vm",
size: 'lg'
}).result.then(function (education) {
vm.educations.push(education);
});
}
And the child controller (which currently only supports insert, hence the empty init of vm.education):
function SomeController($scope){
var vm = this;
vm.education = {}; // or get from parent controller
vm.save = function () {
$scope.$close(vm.education);
};
vm.close = function () {
$scope.$dismiss();
}
return vm;
}
You can use $rootScope.$broadcast to send data from the modal and then $scope.$on to receive that data anywhere else.
For example:
// in your modal
$rootScope.$broadcast('education:updated', yourDataObj)
// in your controller
$scope.$on('education:updated', function(e, data) {
console.log(data)
})
Note that the $broadcast will be picked up by anything that's listening for it, so if you have multiple controller instances you will get multiple console.log messages
EDIT: As asked, I'll explain a bit more efficiently !
I've been sitting in front of an annoying problem recently, which is that whenever I update a value inside a directive, the controllers I'm not currently "in" are the only ones to be updated properly.
Scenario example: Profile page is made of two controllers. Navbar_controller which is just currently displaying the user name :
<div ng-if="Auth.isAuthenticated">Hello, {{Auth.getCurrentUser().name}}</div>
The second controller , Profile_controller is here to update user values. This is a simple function in the angular first controller, which updates CurrentUser:
$scope.updateUser = function (type, form) {
if (!$scope.modif)
return ;
$http.put('/api/users/' + Auth.getCurrentUser()._id + '/update', {type:type, modif:$scope.modif})
.success(function (data, status) {
$scope.user = Auth.setNewUser(data);
})
.error(function () {
console.log("error");
});
};
When I update, for example, the name. I can see that the database has been modified properly. And indeed, navbar_controller got the update because a new name is printed in the div. However, Profile_controller doesn't get the update: the name printed in the profile page didn't change.
Here are the two basic functions in Auth.service.js :
getCurrentUser: function() {
return currentUser;
},
// 'user' is the data retrieved in http put request dot success
setNewUser: function(user) {
currentUser = user;
$rootScope.$broadcast(); // Navbar_controller is updated with or without this line
return currentUser;
}
Anyway, if I look at the navbar and its controller, which is calling Auth.getCurrentUser() method, the user values are instantly modified. I'e been using an ugly method consisting in modifying the controller values manually or by refreshing the page... But this isn't the way to go, right ?
There must be something with "$rootScope.$broadcast();", but I'm really new to Angular and other questions on stackoverflow are too specific to help me understand properly.
Thank you !
Your question was a little difficult to understand, but I think the problem is that you are reference a changing object in your various controllers. Here is an example to explain:
Service:
var myObject = { ... };
return {
getObject() { return myObject; }
setObject(obj) { myObject = obj; }
};
Controller 1:
$scope.myObjA = Service.getObject();
Controller 2:
$scope.myObjB = Service.getObject();
Now on initialisation both controllers will be referencing the same object, so if you changed a property inside either controller (eg. $scope.myObjB.name = 'bob';), then the other controller would also see the name.
However if you changed the object itself in a controller (eg. Service.setObject(newObj);), then the controller will be referencing the new object, while the other controller will still be referencing the old one.
You can fix this by wrapping your service object in a container:
var cont = {
user: ...
};
function getContainer() { return cont; }
function setNewUser(user) { cont.user = user; }
Then inside your controllers, get the container (not the user):
$scope.cont = Service.getContainer();
And inside your html:
<div>{{cont.user.name}}</div>
Now when you update the user, all attached controllers will be updated.
Well I'd try to change and store the user information in $rootScope, for your scenario could be a good fit.
getCurrentUser: function() {
$rootScope.currentUser===undefined ? 'no User': $rootScope.currentUser;
},
setNewUser: function(user) {
$rootScope.currentUser = user;
//$rootScope.$broadcast(); no need to broadcast
return getCurrentUser();
}
in that way currentUser will be updated in different scopes as needed!
I'll quote AnuglarJs FAQ regarding to $rootscope:
$rootScope exists, but it can be used for evil
Occasionally there are pieces of data that you want to make global to
the whole app. For these, you can inject $rootScope and set values on
it like any other scope. Since the scopes inherit from the root scope,
these values will be available to the expressions attached to
directives like ng-show just like values on your local $scope.
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.
I have an app that has items, and you can do things like add new items, update the text of an item, move the item to a different folder, etc.
I have an items factory that holds all the items as plain objects inside an array, and the factory returns a singleton that has various methods, like get(), set(), etc.
To add some context to the question, I'm working with Node.js and MongoDB as well.
Anyway, due to all the various factories I have, like items, folders, and all the various controllers for different views, I am relying heavily on events. To give some examples:
// items factory
update: function(params) {
// add to database, then...
.then(function() {
$rootScope.$emit('itemCreated');
});
}
// items controller
// I need to refresh the items list in the scope
$rootScope.$on('itemCreated', function() { // when an item is added to the database
$scope.items = items.getAll(); // retrieve all items from the items factory
});
These are their own kind of "subset" of events, in that they all pertain to "CRUD" operations on items.
But, I also have other events that I use. For example, I have an interceptor that listens to any requests and responses. I have a loading widget (an image of a spinning wheel) that uses a directive. This directive will show the loading widget when a request begins, and hide the loading widget when a request ends. This is also event based.
// on request
$rootScope.$emit(_START_REQUEST_);
// on any response
$rootScope.$emit(_END_REQUEST_);
I attempted to "modularize" these request and response events by simply making them constants.
.constant('_START_REQUEST_', '_START_REQUEST_');
I am trying to find a solution in order to "modularize" all my other events, like the events emitted on CRUD operations for items. One idea I've had is to define all of the item CRUD events inside the items factory:
events: {
update: 'itemUpdate',
create: 'itemCreated'
// etc.
}
Then, I can simply inject my items factory into a controller, and reference events like so:
$rootScope.$on(items.events.update, function() {});
I also considered simply defining all events, regardless of whether they are interceptor events or item events, as constants in my app. However, it seemed like this solution directly coupled item events to the module itself, rather than to the items factory, which is where I feel they "belong".
Basically, the issue is that right now all my events definitions seem to be scattered around. My question is: what pattern or best practice would you recommend for modularizing and defining events in AngularJS?
I agree that these item events should belong to the event source. You could implement a observer pattern in the item factory that hides the dependency on $rootScope for event listeners. This way the event key itself is a private detail of the item factory, and the subscription to the event is made explicit by calling a dedicated function for it. This approach makes your code more independent of $rootScope and easier to maintain than an event name convention (thinking about usages search for the specific event subscription method vs. usages of $rootScope.$emit / $on):
angular.module('events', [])
.service('items', ['$rootScope', function($rootScope) {
var createdEventKey = 'item.created';
return {
create: function () {
$rootScope.$emit(createdEventKey, {"name": "aItemName"});
},
onCreated: function(callback, scope) {
var unsubscribeFunction = $rootScope.$on(createdEventKey, function(event, payload) {
callback(payload);
});
// allow to unsubscribe automatically on scope destroy to prevent memory leaks
if (scope) {
scope.$on("$destroy", unsubscribeFunction);
}
return unsubscribeFunction;
}
}
}])
.controller('TestController', function($scope, items) {
items.onCreated(function (item) {
console.log("Created: " + item.name);
}, $scope);
});
complete example: http://jsfiddle.net/8LtyB/32/
If all you want is a way to create a separate object for containing the names of events, why not use a service?
myApp.service('itemEvents', function () {
var events = {
update: 'itemupdate',
create: 'itemcreate',
...
};
return events;
});
This is essentially what you had before when you were suggesting using a factory to contain the event definitions, except that a service is a single object instance, and is instantiated at module start-up. In contrast, a factory creates a new instance when injected into a controller. (Here's a good SO post on the difference between services and factories)
You can inject this service into your controllers or directives:
myApp.controller('ItemController', function ($scope, itemEvents) {
$scope.on(itemEvents.update, function () { /* something interesting */ });
});
This gives you a nice place to centralize your event name definitions. As a side note, some people hold to the convention of using all lowercase when defining event names (so itemupdate instead of itemUpdate). Hope this helps!
You can use the following:
app.config(function($provide) {
$provide.decorator("$rootScope", function($delegate) {
var Scope = $delegate.constructor;
var origBroadcast = Scope.prototype.$broadcast;
var origEmit = Scope.prototype.$emit;
Scope.prototype.$broadcast = function() {
console.log("$broadcast was called on $scope " + Scope.$id + " with arguments:",
arguments);
return origBroadcast.apply(this, arguments);
};
Scope.prototype.$emit = function() {
console.log("$emit was called on $scope " + Scope.$id + " with arguments:",
arguments);
return origEmit.apply(this, arguments);
};
return $delegate;
});
})
example: http://plnkr.co/edit/cn3MZynbpTYIcKUWmsBi?p=preview
src: https://github.com/angular/angular.js/issues/6043
assuming these $scope.$emit works like jquery events I would suggest you name your emits to be generic for example in you database update simply do this:
$rootScope.$emit('Created')
then in your items controller do this :
$rootScope.$on('Created.item', function() { // when an item is added to the database
$scope.items = items.getAll(); // retrieve all items from the items factory
});
then you can wire to the created event in any of your controllers and its name is generic. The .item should add a namespace. if you make all of your events in your items controller have the .item name space you should be able to do a
$rootScope.$off('item')
This will clear up memory leaks
I have a link that is used to load data, and then I want the CSS styling to change. I want both the data to be loaded and the CSS change to happen from one click event.
<section ng-controller="OffrdHeadCntrl">
<section id="menuOff" ng-class="{true: 'OpenMenu', false: 'ZeroWide'}[MenuStatus]">
<b>Menu</b><br>
<nav>
<ul id='OffrdCat'>
<li>Antiques</li>
<li>Appliances</li>
</ul>
</nav>
</section>
.... More HTML in another section here.
</section>
This is the routeProvider:
var ysshApp = angular.module('ysshApp', [
'ngRoute',
'ysshControllers',
'firebase'
]);
// 'ysshServices'
ysshApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/Antiques', {
templateUrl: 'Client_Pages/Offered_Menu.html',
controller: 'CommonController',
customInput: 'zeg0yv7nxle6a5sr2xl-ezs'
}).
when('/Appliances', {
templateUrl: 'Client_Pages/Offered_Menu.html',
controller: 'CommonController',
customInput: 'zh86tu488b8g6maw9wrfk'
})
.otherwise({ redirectTo:'/Tab_Home_1' });
}]);
When the user clicks 'Antiques', a bunch of data loads. The link triggers the URL to change, and that causes the $routeProvider to use a controller that then loads the data. I also have a function that changes the CSS styling:
This is the controller that loads the data:
var ysshControllers = angular.module('ysshControllers', []);
ysshControllers.controller('CommonController',
function($scope, $http, $route) {
$scope.dbKey = $route.current.customInput;
$scope.urlToDb = 'https://' + $scope.dbKey + '.firebaseio.com/.json';
$http.get($scope.urlToDb).success(function(data) {
var values = [];
for (var name in data) {
values.push(data[name]);
}
$scope.UsedItems = values;
});
// Initially order by date and time
$scope.orderProp = 'Time';
}
);
There is the function/controller? that changes the CSS: (I stripped some stuff out)
function OffrdHeadCntrl($scope) {
$scope.OpenCloseMenu = function() {
if ($scope.MenuStatus === true) {
$scope.OutputStatus = 'MuchWide';}
else
{$scope.OutputStatus = 'FullWideVal';}
};
}
I can get either the CSS to change, or the data to load, but not both. I've tried putting the ng-click="OpenCloseMenu()" in all kinds of places, but either the routeProvider will work, or the CSS change will work, but not both.
I've tried adding lines of code to the Common Controller, but that doesn't work. Can I add an event that runs after the data is done loading? I've seen examples that create a service that relates multiple controllers, but it's for binding data, not running a button click event.
I'm trying to change the CSS in order to hide the menu part the page before the data is loaded. I can use the Resolve object property to cause something to be resolved first, before the data is loaded. That would work, but I'm having trouble configuring the controller so it is available. I can get Resolve to work, but I can't get the function to fire. If I add this to the routeProvider, an alert msg will pop up, then once I click OK, the data loads.
resolve: {
// Run code in resolve first
whatever: function() {
alert("it ran");
}
}
This will work for me if I can get the function I need to run, but I haven't figured that out yet.
I successfully got a $routeChangeSuccess to run, but it runs BEFORE the data is loaded.
ysshControllers.controller('OffrdCatChg', function($scope) {
$scope.$on('$routeChangeSuccess', function(){
alert("it ran the success?");
});
});
So, I don't think that is going to help me. I created a new controller, and put the name of the controller into an HTML tag in that page. It runs when the route changes, but it runs before the data is loaded. That would be okay, but some of my other code runs afterwards, and sets the CSS back to the original. There must be a way to test for a particular controller having been run.
Since the data loaded is accompanied by url change, so link may be directly visited, not only from the clicking previous ones, so you should put data-loading in the first place.
As for the CSS changing, it's a state reflection thing, closely related to url changing(If I understand your description right), so you should listen to url change event, in something like a rootController( aside from ng-view), such as:
$scope.$on '$routeChangeSuccess', (e, current, previous) ->
//getting url segments, and updates css as it should be
return
The logic for me is:
click the link ---> router change ---> new controller(data load)
|
|
|--------->routeChangeSuccess event ---> update the css
I was able to find a solution. First of all, I think I had my controller defined wrong. I had a function defined, and it was working, but I don't think it was set up right. I had a function written like this:
function CntrlMyCSS($scope) {
$scope.OpenCloseMenu = function() {
if ($scope.MenuStatus === true) {
$scope.OutputStatus = 'MuchWide';}
else
{$scope.OutputStatus = 'FullWideVal';}
};
}
I didn't have it tied to a module, or defined as a controller. This is the new change.
ysshControllers.controller('HideShowInOffered',
ysshControllers.controller('HideShowInOffered',
function CntrlMyCSS($scope) {
$scope.OpenCloseMenu = function() {
if ($scope.MenuStatus === true) {
$scope.OutputStatus = 'MuchWide';}
else
{$scope.OutputStatus = 'FullWideVal';}
};
}
);
So, I guess that even though I added
ng-controller="CntrlMyCSS"
to a HTML tag, and it worked, it wasn't really a controller? idk. I was using the name of a function as the controller. I know that defining it as a controller seemed to make the stuff inside that controller available to my other controller. I still don't really understand, but basically I just added a line at the top of the function to tie it to the module, and define it as a controller, and change the HTML tag to the name of the controller instead of the name of the function.
The other thing that helped was being able to debug the code by setting break points, stepping through lines, and looking at assigned values. If it wasn't for that, I'd probably still be going down roads to nowhere.
The fix was basically just tweaking what I had set up. I tried $routeChangeSuccess, but by stepping through the code, I saw that it was firing before the data loaded. That didn't work for me, because the controller was setting default CSS values for when the page originally loads. So it would change the CSS, and then set it back to original. Which happens so fast, I wouldn't have known that without the debugger.
$scope.$on('$routeChangeSuccess', function(){
alert("it ran the success?");
});
Trying to get a fix through the routeProvider didn't seem to be of any use either. It was basically an issue of the code being available to be called, and learning the timing the program flow through debugging.
The lesson to the story? Take the time to make sure you have a way to step through your code, and determine what the program flow is. There are probably infinite numbers of bad guesses, and maybe only one solution. The odds are against you unless you can debug.
This is the controller that loads the data, then formats the data, then changes the CSS to close the menu.
ysshControllers.controller('CommonController',
function($scope, $http, $route) {
$scope.dbKey = $route.current.customInput;
$scope.urlToDb = 'https://' + $scope.dbKey + '.firebaseio.com/.json';
$http.get($scope.urlToDb).success(function(data) {
var values = [];
for (var name in data) {
values.push(data[name]);
}
$scope.UsedItems = values;
});
// Initially order by date and time
$scope.orderProp = 'Time';
$scope.MenuStatus = false;
$scope.OutputStatus = 'FullWideVal';
}
);