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.
Related
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.
The more I read (and try to test) Angular apps, I'm seeing that it is bad practice for a controller to refer to the DOM. (e.g. this blog post).
I must be missing something big, because if the controller can't access the DOM (e.g. by "regular" javascript calls like document.getElementsByClassName), then I don't understand how to do a lot of things I'd consider very basic.
Here's a contrived, simple example that has some of the same problems that my app does:
I have a directive that is simply a red box (a div with some styles applied), and uses ng-transclude. So I'd use it in my html file like <red-box>Text that goes in the red box</red-box>
A button, when clicked, changes the color of all red boxes to blue. I would have something like this in my html file: <intput type="button" value="Make Them Blue" ng-click="makeThemBlue"/>
In the controller's makeThemBlue function, I'd (for example) find all of the divs by class name, and change the class to something else which makes them blue
Now consider that my real app is much more complicated - many "boxes" consisting of nested directives, that can be dragged around, and have their positions saved. The controller reads all of the saved settings, and lays everything out according to how the user saved it.
How would I do something like either of the above examples without having the controller access the DOM?
Here are my key rules:
Directives - For solid components and for DOM manipulation.
Services - For business logic and saving state. Directives, Controllers, Services etc. should use them.
Controllers - A views helper. No business logic should be executed inside. For complicated issues use a service.
In your case a box should be a directive.
You directive will use an observable service and register for the click event.
When the click event occurs, the observer will notify all the registered directive instances that it was clicked, and you should apply to that in your directive.
You should use Directives for this purpose.
See the documentation for Directive
Angular Directives
It gives you a built in jquery like functionality to Access Dom
the link function in Directive is amazing to Manipulate Dom with the same syntax(almost) as jquery.
Further You can Maintain Chunks of Functionality By Making Services , So you can separate each login plus you have Access to Dom and can manipulate them easily
Injecting services to controller function of directive will give you to maintain code reuseability.
By having the DOM access the controller (or natively from within the directive). This is what the declarative paradigm is all about.
If your box needs to change it's color, have it read that value, or a class, or whatever you need, from a value in the controller, or in the directive itself.
In a very basic sense:
<my-directive color="getColor()"></my-directive>
I have an app that has a navbar on everypage, so I factored it out into index.jade that inherits from my layout.jade file. This is fine for 99% of my app.
The only place it runs into issues is on the login screen as it just feels weird to have a navbar at the login screen, so I used angular to check a boolean against a $rootScope variable and hide the navbar if you are on the homepage.
This is okay but the navbar still loads for a split second, then it disappears. I know I could correct this by not factoring it out into a single file and just cramming it into every single jade file besides the login page, but this would be a maintenance nightmare.
Is there a way that I can keep my navbar factored out, hide it on the login screen using ng-hide, and not load at all until I login.
This is my code to give you an idea of what I did.
The controller that checks to see if you are at the root of the app ('/')
angular.module('app').controller('LoginCtrl', function($scope, $http, mvIdentity, mvNotifier, mvAuth, $location, $rootScope) {
$rootScope.currentPath = $location.path();
....
....
The jade file that shows how this variable is being used to hide the navbar
block main-content
section.content
nav.navbar.navbar-default(ng-hide='currentPath=="/"', role='navigation')
....
....
....
Then upon successful login, the currentPath variable gets set to false to bring the navbar back.
$rootScope.currentPath = false;
I would like to keep this set up because that means any changes to the navbar can be made in a single file and would be inherited across the app, cutting maintenance down tremendously, but I would like for the navbar to wait to load until login. As opposed to flickering from shown, then to hidden, and then to shown again.
Yes, it's possible to do this.
The reason you're seeing things flash up is because the page loads before angular has bootstrapped and applied ng-hide.
There are two approaches to solve this.
1. Put your script files in the head of the page
This is my preferred approach. By putting the script files in the head element, the page won't render until the scripts have loaded. This means angular will bootstrap the page first, and when it does render, the ng-hide will be correctly applied.
Some people prefer the page to start loading first, so won't use this method, but I personally don't think it matters.
2. Use ngCloak on the elements you need to hide
Basically, by applying the ngCloak directive to elements, and embedding the style provided in the link, you can hide certain elements until after the page is loaded. Essentially all ngCloak does is applies display:none to elements and removes it when angular finishes loading.
I'm making a game using JavaScript, currently I'm using window.location = "somepage.html" to perform navigation but I'm not sure if that is the correct way to do it. As I said in the title I've choosed Blank App Template so I do not have any navigator.js or something like.
Can you guys tell me the best way to do it?
Although you can use window.location to perform navigation, I'm sure you've already noticed a few of the downsides:
The transition between pages goes through a black screen, which is an artifact of how the underlying HTML rendering engine works.
You lose your script context between pages, e.g. you don't have any shared variables or namespaces, unless you use HTML5 session storage (or WinRT app data).
It's hard to wire up back buttons, e.g. you have to make sure each destination page knows what page navigated to it, and then maintain a back stack in session storage.
It's for these reasons that WinJS + navigator.js created a way to do "pages" via DOM replacement, which is the same strategy used by "single page web apps." That is, you have a div in default.html within which you load an unload DOM fragments to give the appearance of page navigation, while you don't actually ever leave the original script context of default.html. As a result, all of your in-memory variables persist across all page navigations.
The mechanics work like this: WinJS.Navigation provides an API to manage navigation and a backstack. By itself, however, all it really does is manage a backstack array and fire navigation-related events. To do the DOM replacement, something has to be listening to those events.
Those listeners are what navigator.js implements, so that's a piece of code that you can pull into any project for this purpose. Navigator.js also implements a custom control called the PageControlNavigator (usually Application.PageControlNavigator) is what implements the listeners.
That leave the mechanics of how you define your "pages." This is what the WinJS.UI.Pages API is for, and navigator.js assumes that you've defined your pages in this way. (Technically speaking, you can define your own page mechanisms for this, perhaps using the low-level WinJS.UI.Fragments API or even implementing your own from scratch. But WinJS.UI.Pages came about because everyone who approached this problem basically came up with the same solution, so the WinJS team provided one implementation that everyone can use.)
Put together then:
You define each page as an instance of WinJS.UI.Pages.PageControl, where each page is identified by its HTML file (which can load its own JS and CSS files). The JS file contains implementations of a page's methods like ready, in which you can do initialization work. You can then build out any other object structure you want.
In default.html, define a single div for the "host container" for the page rendering. This is an instance of the PageControlNavigator class that's defined in navigator.js. In its data-win-options you specify "{home: }" for the initial page that's loaded.
Whenever you want to switch to another page, call WinJS.Navigation.navigate with the identifier for the target page (namely the path to its .html file). In response, it will fire some navigating events.
In response, the PageControlNavigator's handlers for those events will load the target page's HTML into the DOM, within its div in default.html. It will then unload the previous page's DOM. When all of this gets rendered, you see a page transition--and a smooth one because we can animate the content in and out rather than go through a black screen.
In this process, the previous page control's unload method is called, and the init/load/processed/ready methods of the new page control are called.
It's not too hard to convert a blank app template into a nav template project--move your default.html/.css/.js content into a page control structure, add navigator.js to default.html (and your project), and put a PageControlNavigator into default.html. I suggest that you create a project from the nav app template for reference. Given the explanation above, you should be able to understand the structure.
For more details, refer to Chapter 3 of my free ebook, Programming Windows Store Apps with HTML, CSS, and JavaScript, Second Edition, where I talk about app anatomy and page navigation with plenty of code examples.
It's a bit complex so bear with me. I've got three layers of directives:
Top layer - a popup directive
Middle layer - a switch-pane directive *
Bottom layer - one of several views
The top layer is just some popup that represents a wizard in my app.
The middle layer is a directive I've made that acts as a stack of views - you can "push" and "pop" views. The "top" view is displayed and the rest are pushed aside and blurred.
The bottom layer is a bunch of views that are normally unrelated to each other, which are dynamically loaded and displayed in the switch-pane according to what the user does.
So far, this works, BUT: currently, the top layer's $scope has an array property that represents all the views the switch-pane should display, passed to the switch-pane directive as an attribute, and the switch-pane directives $watches it and updates itself.
This is OK but I don't think it's good enough - I'd like the switch-pane directive itself to manage it's stack of views, and only expose a push and pop API.
Here are a few ways I thought of:
Using $broadcast / $emit - the top layer will $broadcast a "push" event and the switch-pane will catch it and do whatever it needs
Using a service (to subscribe and fire the "push" event - this is just like using $broadcast but doesn't propagate throughout the scope tree
Using a service that allows the switch-pane directive to register an API of it's own. Using some way of identifying it such as an attribute or even element ID
Using angular.element().scope() to get access to the switch-pane's inner workings
Frankly, I don't like any of these methods much. Certainly I want to avoid being tied to the DOM, so the last 2 are worst.
Any other ways to do this? Which is the most Angular-ish way to expose a directive's API, considering we don't really have access to a certain instance of a directive except via DOM?
You could check how ng-form, ng-model and ng-input is implemented. Basically if the form has a name, for instance <form name="foo" ...> its controller is published to the current scope, in this case under $scope.foo variable. Once the controller is published you could use its API outside the ng-form directive.
You could also access this controller from the other directive if require: "^ngForm" option is specified.
Here is an example taken from my project. It's a sort of wrapper for jqGrid grid plugin written in jQuery: https://github.com/9ci/angle-grinder/blob/06856b0d940b572960025f06f470c2f40fdc0ceb/app/scripts/modules/gridz.coffee#L12