How to broadcast path changes to all views in an AngularJS app? - javascript

I have an AngularJS single-page app displaying 3 views (which are actually 3 directives). To illustrate my question, let's assume my UI is identical to that of GMail and that my 3 views are:
Navigation Pane (left) -- where GMail displays folders like "Inbox", "Drafts"...
Toolbar (top right) -- where GMail displays buttons
Content Pane (bottom right) -- where GMail displays messages
These 3 views need to update themselves whenever the path changes. For example:
The Navigation Pane needs to highlight a specific item.
The Toolbar needs to show/hide certain buttons.
The Content Pane needs to load and display specific data from the server.
What's the best way to do this in AngularJS?
So far, I have:
Ruled out the use of $routeProvider.when() because I need to update three views, and $routeProvider only supports one ngView.
Created a SERVICE that watches the current path and uses $rootScope.$broadcast() to warn the controllers that the path changed.
(OR, IN LIEU OF #2) Created a CONTROLLER that does the same as #2.
Caught the event broadcasted by #2 or #3 with $scope.$on() (I do this in a view controller).
This kind of works, but I have several issues:
The "path change" event is often broadcasted BEFORE my event listeners are in place, especially at initial page load. This is probably due to the fact that my view templates are loaded from the server and the listeners can't be set up before the templates have finished loading.
Isn't there a more efficient/automated way to watch for path changes in AngularJS? (See my code below, it seems pretty "manual".)
Does the code that watches for path changes belong in a SERVICE or in CONTROLLER? (I'd lean towards a service, since a controller is more to add behavior to a view.)
How to guarantee that my views will catch the "path changed" event? Should I use an event at all? (maybe I could store the path in a service, and have my views watch the service instead?)
My code to watch for path changes (I put this in a service or in a controller):
var watchExpression = function() { return $location.path(); };
var listener = function(newPath, oldPath) {
// Broadcast event on $rootScope
$rootScope.$broadcast('PathChanged', newPath);
};
$rootScope.$watch(watchExpression, listener);
Then, in a view controller, I catch the event:
$scope.$on('PathChanged', function(event, path) {
// Update view based on new path.
});
Any help appreciated. Thanks.

Your final version seems fine with the locationChangeSuccess, but for others reading this, I think you ruled out the $routeProvider too quickly. You can have one ng-view for the main content pane that changes with the path, and then other independent ("static") controllers/templates for the navigation pane and toolbar.
Now to listen to route changes in these other 2 controllers:
$rootScope.$on('$routeChangeSuccess', function(evt, cur, prev) {
...do what you have to do here, maybe set a $rootScope.path as you did
})
All using native Angular functionality. I actually do this in http://provok.in, a website I built using Angular, and I have a similar layout (well, not exactly, but I have "static" sections and an ng-view for the main content, dynamically updated based on the path, by the routeProvider).

Related

Force a directive to always load prior to a controller

I am working on a UI issue. There is a misfire in load order that happens 1 out of every 50 times or so. The Angular JS app has a shared directive as a navigation button panel and that part is used by most of the other controllers.
Normal firing order
ButtonController.init() <--Set all buttons to hidden
EditGroupController.init() <--Set all buttons based on service call data
When the problem happens firing order
EditGroupController.init() <--Set all buttons based on service call data
ButtonController.init() <--Set all buttons to hidden
In scenario 2 above the EditGroupController tries to invoke an event on the ButtonController via:
$rootScope.$broadcast('setHeadersButtons', buttons);
However, when I trace into the angular internal code there is a logic branch where it looks for a registered listener with that name and it does not find one to fire because the module has not been loaded.
I remember in an Angular 7 app I used the APP_INITIALIZER to force a core piece to load first. Sorry if my wording is not precise front end / angular is not my specialty and this somewhat fell into my lap. Is there a way to ensure a directive in Angular JS is loaded before any other pieces?
There is an app.route.js, can I force the directive to load there?

How to correctly interact with a view which is managed from another controller?

The question might seem too vague but I could not think of a better way to describe the idea, so I'll try to explain it in details.
I have MasterController attached to <html> tag of my SPA application. This MasterController contains all the logic and models for controlling the following UI elements:
page title (<title> tag)
subheader which displays the title of current page (like Customers, Orders, Settings etc.)
name of the currently logged-in user
some commonly used action buttons which will be used for all pages in the system. To be specific, these buttons are Show filters, Export data to Excel and Add new record.
While the first two items on this list can be managed through detection of current ui-router state (through its $stateChangeSuccess event), the last two (username and buttons) are somewhat problematic, especially the buttons.
I can manage the button actions using $broadcast, so every controller can be notified about clicks on any button. But the tricky part here is that the buttons might be needed in different combinations - one page might need all of them, and another one might need none.
Let's say, ui-router loads some CustomersController. At that point MasterController receives $stateChangeSuccess event and by default hides all the buttons.
But now, how does CustomersController tell to MasterController that CustomersController will need two specific buttons from the very beginning?
Theoretically, I could use $emit from CustomersController to send an event to MasterController, but it somehow feels ugly. Events are meant for, well, events and not for sending requests like "hey, MasterController, if you are somewhere up the scope, can you please show the following buttons?".
Of course, I might be wrong and maybe there is some way to use Angular event system to manage this scenario in clean way.
What came to my mind is that maybe in the $stateChangeSuccess event I could somehow detect if there are currently any listeners for my button click events and then I could hide buttons which do not have any listeners attached, but I'm not sure how to do it, and I'm not sure whether it will work as expected - whether old listeners will be detached when ui-router recreates the view with another controller.
If you are just nesting controllers, their corresponding scopes actually make use of prototypical inheritance. So you could just define a function $scope.configureButtons in your MasterController and call this function from the $scope in your nested CustomerController.
If Controllers are not nested you would probably need to resort to $rootScope.$broadcast for setting up your buttons.
Why not just simply using diferent controllers for each view? Maybe generalize a bit the CustomerController and extend it (specialize it) for every combination of buttons you need. Using the $stateChangeSuccess feels like avoiding polymorphism to me.
Today I got a tricky idea based on #Diego Castaño Chillarón 's answer. I thought - but is it possible to use ui-router to swap controller of existing view and will it rebind also the $scope? And will I still be able to replace inner parts of the loaded view?
It turned out that it is doable! Now I don't have to control the common view fragments from the master control, and I don't need also to inherit or duplicate them - I just switch the controller to the required one through ui-router.
Like this:
$stateProvider
.state("customers", {
url: "^/customers",
views: {
"controller": {
controller: "CustomerController as cntrlr"
},
"page#customers": // <- this is important, absolute name required for ui-router to find nested view
{
templateUrl: "customers"
}
}
}) // other routes follow in the same manner
And my HTML looks like this:
<div id="routes-root" ui-view="controller">
<div id="content-header-buttons">
<button type="button" ng-click="master.toggleFilter()">Filter data</button>
<button type="button" ng-click="cntrlr.exportClicked()">Export</button>
<button type="button" ng-click="cntrlr.createNewClicked()">Create</button>
</div>
<div id="view-content" ui-view="page"></div>
</div>
As you see, I left master controller to control only visibility for filters block, which won't change.
But controller itself is attached to #routes-root element, preserving inner content, and ui-router (or Angular) is smart enough to attach $scope and cntrlr variable to the loaded controller. And then I load inner view into #view-content, which also gets attached to the already loaded controller.

Ember.js: Howto create routes that only execute code

My app has a main view with some hidden elements that can be activated by the user. Like a typical sidebar on mobiles that slides in from the left. Or a cart that reveals the details when you tap on it.
The information in those initially hidden elements is always up to date since their content is mapped in the main app template. So my routes would not need to render any DOM.
The transitions are JS based. But now I want those states to be reflected in the URL, in order to get a consistent back button behavior.
How can I achieve that using the ember framework?
Update to make it more clear, what I am talking about:
To my understanding triggering routes in ember has mainly two side-effects:
Represent the new state in the URL, enabling consistent browser history support.
Render templates that manipulate the DOM based on some data
In my case, when for instance a user taps on the minimized cart I need:
Represent the new state in the URL, enabling consistent browser history support.
Execute my showCart() JS Function (no DOM changes, no template rendering)
When the user now taps on the browser back button, closeCart() should be executed (based on the fact that the state in the URL carries the information that the cart is open).
The problem is, where can they access these slide in windows? Can they only be accessed from a single location in your route map? Or can the user click on it regardless of whatever view they are in? And if they click it from different views, do you move them back to a different route just to pop out some slide in window?
If they can only click it from one place, then you could just add code in your setupController of that specific route to fire the js to slide out the window.
setupController: function(){
Ember.run.scheduleOnce('afterRender', this, function(){
//run some js to slide out the window
});
}
Honestly if they can click a button anywhere and have the slide out appear, I wouldn't try putting it into the url. Just my opinion though.
You can use the activate and deactivate methods from your show route, to know when is entered or exited from your route:
App.ShowRoute = Ember.Route.extend({
activate: function() {
alert('Entering in show');
// here would be your showCart
},
deactivate: function() {
alert('Exiting from show');
// and here the closeCart
}
});
I created a live demo here, to you see this working.

Backbone.js: navigating between views - destroying and recreating

I'm working on a Backbone.js web application with a few different views. Let's say we have the AudioPlayer view at the top of the page that should be persistent and play audio while the rest of the page content changes. The rest of the page content should be able to be switched on demand (with the Router updating the url via navigate).
I'm looking for the correct way to hide/remove FirstView, insert a SecondView, and then hide/remove SecondView and insert/show FirstView again when the user clicks the "back" button.
I've been told that views should be removed when they are not shown to avoid memory leaks. If this is true, what's the proper way to re-create the view again, as its associated view.el has been destroyed during the remove process? Or is there a more logical way to do this?
This is the way I do it:
extend Backbone.View with a method called open that will append the view to the DOM as well as set a flag on the view that is now open, and a similar close property
make all views properties on a common views property of your app
create a method called clearViews on the app that will close all open views except the view names passed in
Here is a gist (in coffeescript) of what I've been using. Feel free to copy.
https://gist.github.com/4597528
So after extending Backbone in this way, lets suppose you want to create and open a new view in a Backbone route after closing all open views except the top nav bar, which app.views.topNav points to. You can say:
app.clearViews('topNav');
app.views.myNewView = new MyView;
app.render().open('body'); // or some other container
There are some great view and layout managers out there for larger projects like Marionette by Derick Baily and LayoutManager by Tim Branyen, but they seemed like overkill for my smaller projects.
I currently do this in several of my apps, and it is accomplished with a tabbed interface. You can see an example of those interface here:
Twitter Bootstrap
Zurb Foundation
jQueryUI
I use a backbone router to watch the url. This let's me keep deep-linking/bookmarking for users, but I usually have the view events trigger tab changes.
For my purpose I have a mult-tabbed app which has a chat window, image gallery window, and collaborative editing windows.

How to disable/enable controllers in JMVC?

Imagine you have one page app using Javascript MVC, which should switch between sub-apps and still stay on the same page. You can load additional controllers and views into the page, but to avoid the conflicts between different controllers, you need to enable the current controller and disable others. You can even destroy the controller and add the current sub-app controllers if needed.
Is it possible? if so, how?
Thanks.
Usually you have some kind of hierarchy on your page (e.g. a page controller, some for navigation etc.) and you only replace the controllers of the part of your page that actually changes.
For a central content element that means either destroying the controllers (by calling destroy on it) or replacing the element that will change (which will remove the controller attached to it as well). So you don't really need to disable anything because you are actually removing the whole thing.

Categories

Resources