Stop AngularJs from creating new controller/$scope cache and use cached one - javascript

Using $routeProvider every time user clicks on a link, a new $scope is being generated. That means all the data is lost. How can i make Angular use the same controller/$scope?
Explanation:
http://jsfiddle.net/mpKBh/1/
(click on links)
<a href='#'>First controller</a>
<a href='#/view'>Second controller</a>
$routeProvider.
when('/', { template:"{{$id}}",controller: ContentListCtrl}).
when('/view', {template:"{{$id}}",controller: ContentDetailCtrl}).
P.s. is it possible to know which controller is currently active?

In AngularJS, $scope is not meant to hold data that persists across your application. For that, you want to use a service that is injected into both controllers. If you provide more detail on what data you're missing across routes, I would be happy to revise this answer to include something a little more actionable.
In re your PS: You can inject the $route service to get information about the current route; the $route.current.controller property will give you the constructor function of the current route.

For those researching how to "unbind" in AngularJS, he is a bit of info (related to the OP's last comment above)
When a view is destroyed, it's basically marked for garbage collection - but it's still there. That is why you are getting multiple requests when a scroll happens - because it's still listening for events.
So the easiest way to deal with this (that I have found, though I'd like to learn of other ways) is to listen for the $destroy event and react on it.
You can "unbind/unlisten" for an event by keeping a reference to what is returned by the $on method. Here is an example taken from a controller:
$scope.systemListener = $rootScope.$on("someEventYouListenTo", function (event, data) {
console.log('Event received by ' + $scope.name);
});
$scope.$on('$destroy', function () {
// Remove the listener
$scope.systemListener();
});
Now those old scopes/views won't react to events anymore.
Hope that helps someone!

Related

Heavy controller communication in AngularJS

I have implemented a single page application with AngularJS. The page consists of a content area in the middle and sections assembled around the center that show additional info and provide means to manipulate the center.
Each section (called Side Info) and the content area have a separate AngularJS controller assigned to them. Currently, I communicate via $rootScope.$broadcast and $scope.$on(), e.g.
app.controller('PropertiesController', function ($scope, $rootScope) {
$scope.$on('somethingHappened', function(event, data){
// react
});
});
I then call to communicate with other controllers:
$rootScope.$broadcast('somethingHappened', data);
I have quite a lot of communication happening between the Controllers. Especially if something is going on in the content area, several side info elements have to adopt. The other way around is also frequent: a user submits a form (located in a side info) and the content area and other side info elements have to adopt.
My question:
Is there a better way to handle SPA with heavy controller communication?
The code works fine but it is already getting a bit messy (e.g. it is hard to find which events are handled where etc.). Since the application is likely to grow a lot in the next weeks, I'd like to make those changes (if there are any better solutions) asap.
This is really interesting. Pub/Sub should be a right solution here.
You could add extra order to your project by using Angular services as your MVC's model, and update this model for each change. The issue here is that you should implement an observable pattern inside your service and register to them, in order for this to be live synced. So - we're back to Pub/Sub (or other Observable solution that you could think about...).
But, the project will be better organised that way.
For example - SideInfo1Service will be a service/model. Each property change will trigger an observable change which will change all listeners:
myApp.factory('SideInfo1Service', function($scope){
var _prop1;
return {
setProp1: function(value){
$scope.$broadcast('prop1Changed', value);
_prop1 = value;
},
getProp1: function(){
return _prop1;
}
}
});
You could find those really interesting blog posts about using Angular Services as your MVC's model:
http://toddmotto.com/rethinking-angular-js-controllers/
http://jonathancreamer.com/the-state-of-angularjs-controllers/
And, this post is about observable pattern in Angularjs:
https://stackoverflow.com/a/25613550/916450
Hope this could be helpful (:
You have multiple options in order to avoid broadcasts calls:
Share data between controllers using services like it was mentioned in the comments. You can see how to this at: https://thinkster.io/egghead/sharing-data-between-controllers
Create a main controller for the whole page and child controllers for each section (Content Area and Side Info). Use scope prototype inheritance. For example:
if in main controller you have:
$scope.myObject = someValue;
in child Controllers you can set:
$scope.myObject.myProperty = someOtherValue;
you can access myObject.myProperty from your Main Controller
You can use
$rootScope.$emit('some:event') ;
because it goes upwards and rootscope ist the top level
use
var myListener = $rootScope.$on('some:event', function (event, data) { });
$scope.$on('$destroy', myListener);
to catch the event
Then you have a communication on the same level the rootscope without bubbling
Here is my implemented eventbus service
http://jsfiddle.net/navqtaoj/2/
Edit: you can use a namespace like some:event to group and organize your event names better and add log outputs when the event is fired and when the event is catch so that you easy can figure out if fireing or catching the wrong eventname.
Very important question and very good answers.
I got inspired and created three plunks showing each technique:
Broadcasting: http://embed.plnkr.co/lwSNDCsw4gjLHXDhUs2R/preview
Sharing Service: http://embed.plnkr.co/GptJf2cchAYmoOb2wjRx/preview
Nested Scopes: http://embed.plnkr.co/Bct0Qwz9EziQkHemYACk/preview
Check out the plunks, hope this helps.

AngularJS Router invoke function when certain URL is loaded initially

I am using angular router` to track the state of my web app like this:
when('/', {
controller: "AController",
templateUrl: "APanel.html"
}).
when('/subpage/:id', {
controller: "BController",
templateUrl: "BPanel.html"
}).
And I am using Angular Service to track some shared values:
app.service('stateService', function() {
this.someSwitch = false;
this.someLongDataArray = [x, y, z];
});
Currently, before changing path to \subpage\:id url from AController, I will assign new values to members of the service, so they can be referenced in subpages.
Now the question is, if user directly launching the subpage url \subpage\:id, or hit the refresh button on browser on subpage, BController will be invoked, and I will lost the values in the service which are supposed to be prepared by AController.
I am wondering what I should do in this case. is there any way I can get called when user launch the subpage directly, so I have a chance to prepare the data? (Maybe I can watch for html onload event, but not sure that's the best answer).
Thanks
It appears, BController is dependent on AController.
Ideally, Controller should not contain any data/dom manipulaton, state maintenance. It is simply a glue between view and the $scope model.
Being said so, you need not create any such dependency between controllers. The service can be invoked from both controllers.
OR
If indeed there is a requirement that APanel.html must be loaded/initialized before BPanel.html is loaded, then you must check for some flag in BContoller and redirect user to APanel.html.
like
if(<check some flag>){
$location.path('/');
}
But then you have to find the way to redirect the user again to BPanel.html. I guess this is not a recommended approach.
I am not sure I get your question completely. But if there is a possibility that the user might hit BPanel.html directly then you should do something like this.
if(serviceExists()){
//Do your usual Bcontroller stuff here if the services was initialized
}
else{
//Show a warning/error like "Oops, something is wrong go back to '/'" OR
// Initialize services in BController
}
This should be in your BController if initializing your service before BController is that important. You basically force people to stay on AController.

Creating AngularJS object that calls method on app load

I am wondering if there is a convention within AngularJS for creating an object that lives within the app module, but is not attached directly to the view in any way, but is called when the view has loaded and the app starts up. In particular, I am trying to write an object that dispatches messages to listening controllers when they come in from the server.
Currently, I have implemented this by creating a "Controller" that attaches to the view. It has a monitor() function that is called when the page loads, and then listens in a loop for any incoming messages. I call the monitor() function from within the loaded view, by setting the ng-controller like so:
<div ng-controller="MyController">
{{ monitor() }}
</div>
This doesn't feel like the right thing to do. This "Controller" isn't interacting with the view in any way, so my gut tells me I am violating principles of AngularJS. But I haven't been able to turn up an easy solution that is endorsed by the AngularJS doc.
I am looking for a way to create an object that lives within the AngularJS world (in other words, it can use dependency injection to get access to services, and it can use $scope.$broadcast to send messages to other listening controllers), but that doesn't need to attach itself to the view in any way.
Ideally, I am looking for a way to say, "Here Angular, on startup, create this object, and run this method on it." Is there a way to do this?
You may use this as a starting point:
declaration of your object.
AngularJS: Service vs provider vs factory
myApp.factory('MessageBus', function() {
return {
listeners: [],
init: function() {
// do whatever you need at startup
},
pushMessage: function(msg) {
angular.forEach(this.listeners, function(listener) {
listener(msg);
});
},
subscribe: function(onMessageCallback) {
this.listeners.push(onMessageCallback);
}
};
});
calling a method on angular appilcation start
https://docs.angularjs.org/api/ng/type/angular.Module#run
myApp.run(function(MessageBus) {
MessageBus.init();
});
using this object within controllers
https://docs.angularjs.org/guide/di
myApp.controller('MessageCtrl', function($scope, MessageBus) {
$scope.messagesToShow = [];
MessageBus.subscribe(function(message) {
$scope.messagesToShow.push(message);
});
$scope.submitMessage = function(id, text) {
MessageBus.pushMessage({
type: 'TEXTMESSAGE',
id: id,
payload: text
});
};
});
Note that this is something to start with and nothing for any production code. For example the controller doesn't unsubscribe after being destroyed - if the page changes - and so you leak memory.
Don't use $broadcast-events for this
1: they are slow
2: if this MessageBus has a specific concern, than in should be an own object with a meaningfull name and api. Otherwise your $rootScope will be flooded with thousends of different events for different concerns when your application grows. A service is always easier to document and you have a clean dependency on that specific service. Only using events on the $rootScope hides this dependency from every developer reading and hopefully understanding your codebase,
Yeah you approach is really smelly. This function will be called every time a $apply/$digest invokes.
Maybe move the function into the run callback on the module.
var app = angular.module("YourApp", [//dependencies]);
app.run(function($YourUIService){
$YourUIService.monitor();
});
The run will be invoked, when your angularjs-module has loaded every dependency and is ready to run.
Didn't find the doc for this :/

Ember.js - How to trigger view method from controller?

I'm trying to call view method from controller, but no idea how to do this. From view I can easily call controller method like this.get('controller').send('method');
How to do something like that from controller this.get('view').send('method');?
To give you better overview what I'm trying to do.
I have application controller Ember.Controller.extend({}) I have application view Ember.View.extend({}) and application template.
In application template is login form, when user submit it controller method is executed. In this method if login credentials are incorrect I need to call view method which is executing jQueryUI method on login form (shake method to be exact and showing some text).
This sounds like a good use for Ember.Evented. By using event subscription and dispatching you can avoid coupling your view and controller.
Simply mixin Ember.Evented:
Controller = Ember.Controller.extend(Ember.Evented)
Now you can call on and trigger methods on your controller, to subscribe to an event and then to kick off the event. So, in your view you might do:
didInsertElement: function () {
this.get('controller').on('loginDidFail', this, this.loginFail);
}
And then in your controller call this.trigger('loginDidFail') to kick off your loginFail view method.
Remember to remove the handler after the view is dismissed... see the answer below.
Just wanted to answer on this question to address the issue with properly removing the listener if the view is cleared (when the route changes). It's also not necessary to use a jquery proxy, since the on/off methods support a target, which is good because unsubscribing a proxy is definitely more complicated. Revising what Christopher provided:
didInsertElement: function()
{
this.get('controller').on('loginDidFail', this, this.loginFail);
},
willClearRender: function()
{
this.get('controller').off('loginDidFail', this, this.loginFail);
}
Without removing the subscription any subsequent visits to the login route (without reloading the page) will add additional listeners; i.e. memory leaks, errors, and unexpected behavior.

event.preventDefault() not working for routeChangeStart in angularjs app

Hope any angularjs gurus can help me with this.Here is my angularjs code
$scope.$on('$routeChangeStart', function(event, next, current) {
if ($scope.myForm.$dirty) {
if(!confirm("Unsaved, do u want to continue?")) {
event.preventDefault();
}
}
});
It alerts in browser back button click when data is dirty, but on clicking cancel or ok it still completes the route change.Seems like event.preventDefault() is not working.
Can any one point out what may be wrong
I had lots of trouble finding this one, but instead of the "$routeChangeStart" event, you can listen to the "$locationChangeStart" event, for which you can prevent default:
$scope.$on("$locationChangeStart", function(event, next, current) {
if (!confirm("You have unsaved changes, continue navigating to " + next + " ?")) {
event.preventDefault();
}
});
You could also always prevent default, store "next", and display a clean JS modal and decide asynchronously.
$locationChangeStart is currently undocumented but referenced here : https://github.com/angular/angular.js/issues/2109
Fixed exactly after Angular 1.3.7
https://code.angularjs.org/1.3.7/docs/api/ngRoute/service/$route
$routeChangeStart
Broadcasted before a route change. At this point the route services starts resolving all of the dependencies needed for the route change to occur. Typically this involves fetching the view template as well as any dependencies defined in resolve route property. Once all of the dependencies are resolved $routeChangeSuccess is fired.
The route change (and the $location change that triggered it) can be prevented by calling preventDefault method of the event. See $rootScope.Scope for more details about event object.
According to the AngularJS docs (see at $broadcast) you simply cannot cancel an event of type broadcast ($routeChangeStart is of that type):
The event life cycle starts at the scope on which $broadcast was
called. All listeners listening for name event on this scope get
notified. Afterwards, the event propagates to all direct and indirect
scopes of the current scope and calls all registered listeners along
the way. The event cannot be canceled.
This problem was fixed in the newest versions ( >= 1.3.8 ).
Since the arguments supplied to $routeChangeStart are more detailed (and often more useful), if possible, try to update your angular version ...
The problem might persist if you are using a $stateProvider.
In this case use:
$scope.$on('$stateChangeStart', function( event){
.....
event.preventDefault();
});

Categories

Resources