I have the following controller in Angular that reads some records from DB and then outputs them into a calendar. The problem is that the events array comes back as empty. I have tried using $rootScope.events as a substitute, but that gives an error "concat is not a function of undefined." What am I doing wrong? Is there some trickery with nested scopes?
I also just realized that eachActivity variable is undefined in the inner callback, as well. I assume this is a part of a general knowledge I am lacking.
app.controller('Calendar', ['$scope','$rootScope','$resource','moment', function($scope, $rootScope, $resource ,moment) {
var Activities = $resource('/api/activities');
Activities.query(function(activities){
$rootScope.activities = activities;
//console.log($rootScope.activities);
});
//console.log($rootScope.activities);
var vm = this;
var events = [];
//define the calendar on rootScope, so it has access to the Events data from the other controllers
$rootScope.calendar = new moment();
angular.forEach($rootScope.activities, function(eachActivity){
//console.log(eachActivity.events);
if (eachActivity.events.length > 0){
angular.forEach(eachActivity.events, function(eachEvent, eachActivity){
console.log(eachEvent);
var entry = {
title: eachActivity.title,
type: "warning",
startsAt: eachEvent.startDate,
endsAt: eachEvent.endDate,
incrementBadgeTotal: true
}
events.concat(entry);
});
}
});
vm.events = events;
console.log(vm.events);
vm.calendarView = 'month';
vm.viewDate = moment().startOf('month').toDate();
vm.isCellOpen = true;
}]);
To solve your immediate problem, change
angular.forEach(eachActivity.events, function(eachEvent, eachActivity)
to
angular.forEach(eachActivity.events, function(eachEvent)
The second argument is not necessary as eachActivity has already been defined in the outer loop.
Also change events.concat(entry); to events.push(entry);
In addition, instead of defining calendar on $rootScope, create a calendar factory instead and inject that into the controllers where you need to access calendar data. There are a number of reasons why this is better but the simplest is that $scopes are not meant for sharing data between controllers. They are primarily meant to act as a view model to bind data between your views and controllers.
EDIT (more details on creating a factory)
You can define a factory like so
app.factory('CalendarService', [function(){
var calendarEvents = []; // or {} or even null depending on how you want to use the variable
return {
calendarEvents : calendarEvents
};
}]);
In your controller,
app.controller('Calendar', ['$scope','$rootScope','$resource','moment', 'CalendarService', function($scope, $rootScope, $resource ,moment, CalendarService) {
...
// instead of $rootScope.calendar = new moment (and I don't understand why you are assigning this to a moment but that's another conversation
CalendarService.calendarEvents = events;
}]);
All you need to do is inject CalendarService into every controller where you need to use the events data and it will be available in the calendarEvents field.
There are a few things going on here:
1) The concat() method returns a new array comprised of the array on which it is called joined with the array(s) and/or value(s) provided as arguments. You'll need to assign the vaues: events = events.concat(entry) for them to persist for the next iteration.
2) You have nested Angular Loops. Often necessary, but be careful with your naming conventions.
angular.forEach($rootScope.activities, function(eachActivity){
angular.forEach(eachActivity.events, function(eachEvent, eachActivity)
Here your loops share identical argument names. This is highly discouraged because it can really create headaches for the developer trying to understand the scope of the object being iterated over. I'd suggest ensuring that your names are always unique and explicit to the scope.
3) Because you are overriding your argument names, your call to title: eachActivity.title is going to be looking at the inner loops second argument, which in this case is the KEY of the event within the forEach loop eachActivity.events object. Keys do not have properties, they are always strings - because of this your eachActivity variable is defined, yet it has no properties.
I'd suggest altering these few things and then editing your post with any progress made.
Related
I followed a AngularJS tutorial on http://www.tutorialspoint.com/angularjs/angularjs_services.htm
The method passed to CalcService service got me confused. Is Angular using revealing prototype or a different one. I was confused because that inner function declared in this.square should be private and not visible outside the context of the object. How Angular is able to access square.
mainApp.service('CalcService', function(MathService){
this.square = function(a) {
return MathService.multiply(a,a);
}
});
An AngularJS service is a very distinct thing.
When it's initialized, it gets newed. Take this as an example:
function CalcService() {
this.square = function() {
// square some stuff
};
}
// then in the controller, directive, or wherever,
// it gets initialized behind the scenes like this
new CalcService();
However, it gets initialized as singleton, meaning that there's only ever one reference to the object, even if the component where you register it attempts to re-initialize it (see my recent answer on singletons in AngularJS).
Not sure what you mean when you mention a "revealing prototype pattern", but the this , in the case of an AngularJS service, is simply implementing a non-prototypal method on a new, regular JavaScript object.
Keeping with the same example above, in "normal" JavaScript, you could call new CalcService().square(). JavaScript doesn't have any native notion of private methods (though there are ways of implementing "class" methods that appear to be private.)
var service = new CalcService();
service.square();
There's nothing "private" about that method, just like there's nothing "private" about methods that are attached to AngularJS service objects... The only thing remotely "private" about it is that it happens to belong only to that specific object by virtue of the this keyword.
In your example, you are passing a constructor function into the angular service DI method.
In the constructor function you assign a method to this.square .
Just try this without angular and you will see you it behaves thr same.
function Calc() {
this.square = function() {
console.log('we get here');
}
}
var calc = new Calc();
calc.square();
This is the main feature of Javascript's prototype object oriented model. This is plain old OO javascript.
Above answers does good explanation how service work but they don't explained how this which is newly created object is exposed.
Whenever you create a service angular create a new object of that function for you, and that's get return whenever its get inject in controller, directive, service, etc. Internally method uses prototype of function to create an this which is context of function. Lets look at below code how it work internally.
function CalcService(){
//The line below this creates an obj object.
//obj = Object.create(CalcService.prototype)
//this = obj;
//`this` is nothing but an instance of function/CalcService.prototype which is giving access to its property attached to this
var privateVariableExample = 'test'; //this is private variable of service.
this.square = function(a) {
//return multiplacation result from here
}
//return this;
}
var objectOfCalcService = new CalcService();
objectOfCalcService.square(1);
I'm not sure if i have completely wrapped my head around this idea - but I'll try my best to clearly describe what I am trying to do here.
I have a factory that changes and parses a URL for me, so I can pass params into a controller for use (that were stored in the url). This is sort of so I can save a state for the user and they can share it via copy'ing of a URL (send it to their friends or bookmark it or w/e).
I am trying to set up a factory (or service) that listens for locationChangeSuccess - so that if the user mofies the url and presses enter, it will refresh the scopes in the controllers. So here is what I have:
.factory("urlFactory", function($location, freshUrl, StateString){
//request to change new url
requestObj.requestState = function(moduleName, stateName, startVar){
}
//request item from url, via your module and state name
requestObj.parseState = function(moduleName, stateName){
}
I dropped the center out (if it is needed im happy to link), but those just get and set the url for me.
So in the controllers I do something like
$scope.mod2m3 = urlFactory.parseState("module2", "mod3");
$scope.mod2m4 = urlFactory.parseState("module2", "mod4");
So when they land on the page, they pull their state. This works great. However, now i'm trying to solve some edge case scenarios where maybe the user modifies the url.
So I can latch onto that even pretty easily with
.factory("urlWatcher", function($location, $scope){
var urlWatcher = {};
$scope.$on('$locationChangeSuccess', function(event) {
console.log("Asdsa");
});
return urlWatcher
});
However, where I am struggling is trying to determine a way where when this fires, it would connect the new value to the scope in the controller. It was suggested to me that a callback of some sort in the parse (set) function, but I am struggling with how to approach that. It would be super cool if I could set a way for this factory/service to re send the new value when it changes to the right place. Callback sounds good, however I don't know how to config this correct.
The easiest route would be to just do an
$scope.$on('$locationChangeSuccess', function(event) {
console.log("Asdsa");
});
In each controller and manually bind to each scope, but I am trying to make this as modular as possible (and thats also a ton of watchers on the locationchangesuccess). would be fantastic if I could figuire out a clean way to set the service/factory to listen once, and on change find the right module/controller and change the value.
I can't seem to think a clear route, so I would be very greatful for any insight to this issue. Thank you very much for reading!
If what you want is a publish/subscribe architecture, where publications are global and subscriptions have the same lifecycles as Angular scopes... then Angular events are what you're looking for. There's no point setting up an ad hoc communication system with callbacks and whatnut, that would just be partially reinventing events.
However, if you want to make the semantics more obvious / add flexibility, you can listen once to $locationChangeSuccess in a service and broadcast a custom event.
$rootScope.$on("$locationChangeSuccess", function (event) {
$rootScope.$broadcast('myCustomeEvent', {message: "Guys, time to refresh!"});
});
Then listen to this event in each of the scopes where it is relevant.
$scope.$on('myCustomeEvent', function (event) {
console.log("Asdsa");
});
If setting up the listening gets repetitive, by all means, factor it out in a function, which you can for example put in a service:
myApp.factory('someFactory', [function () {
return {
listenToLogAsdsa: function (scope) {
scope.$on('myCustomeEvent', function (event) {
console.log("Asdsa");
});
}
};
}]);
Then all you have to write in your controller is:
someFactory.listenToLogAsdsa($scope);
You can assign a variable in the scope to an object in the factory, that way it's bound to a reference instead of a value. Then, in your HTML you bind the reference to the DOM. urlFactory.parseState() should then save the result to said object, and return the key where it was saved.
For example:
In urlFactory:
requestObj.parseState = function(moduleName, stateName){
var key = moduleName+stateName;
this.urlContainer[key] = "www.example.com";
return key;
}
In the controller:
$scope.urls = urlFactory.urlContainer;
$scope.mod2m3 = urlFactory.parseState("module2", "mod3");
In your HTML:
{{urls[mod2m3]}}
This way, "urls" is bound to a reference, which angular watches for changes, and whenever you change urls[mod2m3], it will affect the DOM.
You can also just react to changes in the scope variables by watching them:
$scope.$watch('urls', function() {
//do something
});
NOTE: Since this is an object, you might need to use $watchCollection instead of $watch.
I am in doubt if the following design pattern would cause a memory leak.
I have been using it for some time with success, but I haven't seen this pattern used by others, so I'd like some confirmation if you see something wrong with it.
As from next month I have to start working on a large project, and I want to know for sure that I can use this without problems, or if I should use another strategy.
controller.js:
var Controller = function(options){
};
Controller.prototype.makeView = function(options){
options.controller = this;
options.otheroption = options.otheroption;
var view = new View(options);
};
Controller.prototype.getModel = function(options){
//--- Get model ---
var model = new Model();
var promise = model.fetch();
return promise;
});
view.js:
var View = Backbone.View.extend({
initialize: function(options){
this.controller = options.controller;
this.otheroption = options.otheroption;
},
getModel: function(){
var promise = this.controller.getModel();
promise.done(_.bind(function(model){
//Do something with the returned model instance
}, this));
};
});
Instantiate controller, eg. from the router, or another controller:
//--- Instantiate the controller and build the view ---//
var controller = new Controller();
controller.makeView(options)
To me, this doesn't look like a circular reference, because both the controller and view are declared as a local variable.
Yet the instantiated view can access the controller functions, which allows me to isolate the RESTful server interactions via models / collections that the view uses.
For me it would seem as if the only reference remaining would be the view that keeps a reference to the controller object.
What I do afterwards is clean up the view (I destroy the instance and its references when I don't need it anymore.
Your opinion on this pattern is highly appreciated.
My purpose is to isolate creation of views / server interactions in separate controller files: if you see holes in my method and have a better way of doing it, please share.
Thanks.
Short answer: There is no memory leak problem in the code you have posted. The view holds a reference to the controller, but not vice versa. So as long as the controller lives longer than the view, that reference does not keep your objects from being garbage-collected. I don't see a circular reference anywhere in your code.
Longer answer: The pitfalls would be in the code you haven't posted. In particular, any event handlers in your view must be cleaned up properly, otherwise your views never fade into oblivion. But you have said in your question that you clean up your view, so I guess you are aware of that sort of problem.
What controller doing is here looks like a utility to me. Could have been easily managed by a global level singleton. I see some issues in first glance.
Code repetition, assuming you would creating separate Controller for different types of Models and Views, makeView and getModel code needs to be repeated for each controller. If you extending from a BaseController, then you need to pass View and Model Class to getModel and makeView functions.
How do you handle a use-case where you have to use same model in different Views?
makeView and getModel is designed assuming for each makeView there would be a getModel call, in assumed order
I would rather write a utility function which can create and deploy views for me.
var deployView = function(view, config){
//do the view rendering
view.render();
view.$el.appendTo(config.el);
}
var createView = function(config) {
var view;
var viewType = 'model';
if (config.collection || config.Collection) {
viewType = 'collection';
}
if (viewType === 'model') {
if (config.Model) {
config.model = new config.Model(config.modelAttributes);
//fetch if needed
}
} else {
if (config.Collection) {
config.collection = new config.Collection(config.items);
//fetch if needed
}
}
var filteredConfig = _.omit(config, 'Collection', 'Model', 'View');
view = new config.View(filteredConfig);
deployView(view, filteredConfig)
}
JavaScript implementations haven't had a problem with circular references for a long time. (IE6 did have a memory leak from circular references if I recall correctly, which wasn't shared by any other major browser from that period.)
Modern JavaScript implementations perform garbage collection through a "mark and sweep" algorithm. First they scan through your web app's entire memory structure starting from the global object, and mark everything they find. Then they sweep through every object stored in memory and garbage collect anything that wasn't marked. As long as there isn't a reference to your object from the global object or any stored function, it can be garbage collected.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#Mark-and-sweep_algorithm
You're probably thinking of a reference counting-based implementation, which does have issues with memory leaks from circular references. In that implementation as long as one object contained a reference to another, that second object can't be garbage collected. That method was once used in web browsers but not anymore.
Nowadays, most memory leaks are from globally-accessible objects you forget to clean up and accidentally retaining data in function closures (a function that creates another function and passes/saves it somewhere). Since the closure's local variables can be accessed by the function created inside of them, they have to be retained as long as that function exists.
So go ahead and add all the circular references you want. Unless you need to target IE6, your code's fine.
I'm trying to add a new object to my firebase model. Below is my Angularfire setup:
angular.module('angularApp', ['firebase'])
.controller('MyCtrl', ['$scope', 'angularFire',
function MyCtrl($scope, angularFire) {
$scope.shapes = [];
var ref = new Firebase('https://angular-starter.firebaseio.com/shapes');
angularFire(ref, $scope, 'shapes');
$scope.addShape = function () {
console.log(typeof $scope.shapes);
var newShape = {color: "#536993", height: 0, id: 1, left: 0, position: "absolute"};
$scope.shapes.push(newShape);
};
}
])
I understand that object don't have a push method but I'm having trouble figuring out how to define the model as array object. I thought $scope.shapes = []; should do the job?
When you bind Firebase to a scope variable, using angularFire, you can't arbitrarily pick a data type. angularFire always works with models (objects), and stores the data as an object.
If your remote data has entirely numeric, and sequential keys, you can simulate array like behavior (although Firebase always stores data as objects, even in this case) by using angularFireCollection, which utilizes arrays for storing values.
Note that even in the case of angularFireCollection, you may not call push(), but instead need to use the built-in add() method.
Also note that angularFire 0.5 is imminent (next week perhaps) and will refine a lot of these confusions.
UPDATE
AngularFire 0.5 was released, so the appropriate method is now $add instead of add and there is no longer a distinction between angularFire and angularFireCollection.
Looks like Firebase reset the $scope.shapes to an object. You can use object method like
$scope.shapes[index] = newShape;
to store the data rather than use push method.
I want to add views and stores in controller dynamically. So, I've had this:
Ext.define('App.controller.MyController', {
extend: 'Ext.app.Controller',
stores: ['App.store.Users'],
views: ['App.view.Users.Index'],
I'm creating this controller dynamically with:
var controller = this.getController("Users");
How can I add store and views dynamically, something like:
var controller = this.getController(moduleID);
controller.stores = [];
controller.views = [];
controller.stores.push('App.store.Users');
controller.views.push('App.view.Users.Index');
But when I do that, it's not working. Console is telling me that Ext can't get "buffered from undefined" so I'm thinking that I have to do this with Ext.apply() or Ext.merge() or something like that to get getters and setters for stores.
What do you think?
EDIT for #asgoth:
When you use this.getController("nameOfController"); and if the controller doesn't exists, Ext-JS creates one for you. That's working 100% because when I console.log(controller); I'm getting data (and docs says that too). :)
You do not have that much choices, because you will need to have the arrays ready when you are instantiating the controller. By default this happens only once cause it should be managed by the Ext.app.Application Controller (instance).
First point is that you cannot use the getController method here because it does not accept any additional configuration. So the easiest solution would be the implementation of your own getController method, slightly renamed to avoid overriding.
here is a example one:
getControllerInstance: function(name, cfg) {
var me = this.application,
controllers = me.controllers,
controller = controllers.get(name);
if (!controller) {
controller = Ext.create(me.getModuleClassName(name, 'controller'), Ext.ApplyIf({
application: me,
id: name
},cfg);
controllers.add(controller);
if (me._initialized) {
controller.doInit(me);
}
}
return controller;
}
Please note that this variant does not add values to any array param instead it will override any any existing param!
Also note that all your controller will need to inherit from the controller that has this method.