I've assembled a modestly sized application and I am in the process of factoring code to reduce the overall number of maintained lines, as well as performance tuning. The use case that has me posting this question is that I have a button embedded in a menu that invokes (or needs to invoke) a method on a controller that displays a form. This currently is implemented using a direct reference to the specific button control, creating a panel, and putting that panel inside of my viewport.
The question at: ExtJS 4.1 Call One Controller From Another begins to address the issue of best-practices near the end of responses, but doesn't really settle on a base-case that can be reproduced or extended to cover more complex implementations (which is the aim of my question.)
Given the two controllers:
A "main menu" controller.
// controller/Menu.js
Ext.define("App.controller.Menu", {
extend: "Ext.app.Controller",
init: function () {
this.control({
"viewport > mainmenu > button": function (ctl, evt) {
}
});
}
});
A user account controller
// controller/User.js
Ext.define("App.controller.User", {
extend: "Ext.app.Controller",
stores: ["User"],
views: ["user.Edit", "user.List"],
init: function () {
}
});
The Question
What would be the (best) way to implement a crosswise connection between the two controllers to properly delegate the responsibility of responding to a click event on the menu button for "Create a New Account?"
One Possible Solution
Using componentquery I can easily narrow down the focus of the button in the main menu view using a tag property such that the User controller is responding directly to the event:
// controller/User.js
"viewport > mainmenu > button [tag=user.create]": function () {}
An unknown alternative
Or I could possible winnow my way through the object graph to find the reference to the User controller from the Menu controller and invoke it that way.
// controller/Menu.js
// switch on tag case "user.create"
App.controller.User.createUserForm()
The real question
The question that results from all of this is what the "most acceptable" solution is here. One could imagine the use of a third, mediating controller, to "route" requests from controls to controllers but I think that goes against what the remainder of the framework is attempting to do. Using a variation of either of these methods currently works however neither feels completely clean and reliable; or ultimately maintainable long-term (as code gets spread out rather quickly.) Additionally the thought had occurred to us to drop into raw events but we run into the same kind of maintainability issues there.
Some short lines:
A thing that I don't understand is that Sencha Touch has routing but no eventbus where ExtJS has a event bus but no routing... (and there are more points where the MVC implementation differ) Whatsoever, because I am using ExtJS most of the time I created a custom routing to fill this gap for me. Maybe sencha will add this in version 5.
The easiest and quickest solution: use the getController() of the Ext.app.Application controller to invoke the responsible controller from your menu controller.
The (imo) best solution: write yourself a router where each controller register it's routes to and use both; routing and eventbus. This gets really handy if your app have shared components that are used by more than one dev team.
Related
I have a modal window with forms/select inside and when i click on some select inputs, some forms appears, some disappears. Everything works fine, but the only problem for me is the 'cancel' button.
Indeed, i would love to rollback changes made on the models. i am fully aware of the rollback method, but the forms manipulations involve more than properties modifications, it also involves deletion of models.
Therefore, the global idea would be to make a copy of my model everytime i am entering the modal window. Then, once the user 'commit' theses changes, i merge the data to my existing models.
Do you know a more elegant solution ?
Thanks you very much !
It sounds like you're using Ember Data and that you're aware of using modelOfForm.rollback();.
If your problem is that you are doing deletes of
related models, that run behind your form fields (e.g. belongsToModelOfFormField.deleteRecord();). Then it might be more elegant and less coding to manually do the Rollbacks on your model and the related models from within your Route's
{ actions: {
cancelForm : function(e) {
...
belongsToModelOfFormField1.rollback();
belongsToModelOfFormField2.rollback();
// etc.
modelOfForm.rollback();`
}
}
(Assuming your handlebar button is something like <button type="button" {{action cancelForm}} >cancel</button>.)
A duplicate cache of all your form model for safe keeping and doing your own resorting of the info sounds like an extra headache, but as devs we all know sometimes you're reduced to that.
Another example would be to have async on the child models, and to do modelOfForm.get('content').rollback(); as Toran explains here, performing rollback on model with hasMany relation . Hope that leads you to your answer and less coding :).
I need to use two states in parallel, one for my page and an other for a modal with several sub states.
Right now calling the modal state will wipe out my page since the page state changed.
Create a child state child of my page wouldn't be a solution since the modal will be used on several pages.
Example:
$stateProvider
.state('user', {}) // page
.state('bookshelf', {}) // page
.state('books', {}) // modal
.state('books.read', {}) // sub state of modal
So if I'm on user and open my modal then the state would change to books, my modal would then have the content but the page content will be wiped out.
How do I fix it?
I believe the way you're looking to do this is not possible with UI.Router currently. What you're describing is a modal component (which would ideally be written as a directive), which tracks it's state independently from the main state.
The way to think about it, is that UI.Router works by creating a state tree. At any given time you can only be looking at one branch of the tree. You can go deeper down a branch (ie: book, book.open, book.open.checked), but you can't be in two places at once.
Another issue with the problem above is how do you serialize the state of the two different trees into one url? It's not to say it can't be done, it's just a hard problem to solve.
Checkout these issues:
https://github.com/angular-ui/ui-router/issues/119
https://github.com/angular-ui/ui-router/issues/384
https://github.com/angular-ui/ui-router/issues/475
Also checkout these repos, they might be further along the lines of solving the problem.
https://github.com/afterglowtech/angular-detour
https://github.com/stu-salsbury/angular-couch-potato
As far as solving your immediate problem, I think the 'easiest' way would be to ditch controlling the state of the modal inside your state config.
Instead, I would add some sort of root or abstract state, and then track whether the modal is open there. Then, you can communicate between controllers using events as shown here. Note: There are performance implications with listening to $rootScope, so be sure to research those. However (someone feel free to correct me), the implementation here doesn't have those problems, because the AppCtrl is never destroyed.
Jan 15, 2015 Edit
Turns out this is a pretty popular use case, and one of the core contributors to UI Router maintains a plugin/addition called UI Router Extras
It also includes utilities for lazy loading, called "Future States" which are very helpful.
That being said, one feature I'm hoping to get time to work on is maintaining all state within the URL (or perhaps, local storage) and allowing for reusable state "components". The latter is in the UI Router roadmap as well.
So I have this code example that uses Angular 1.2 RC2 and everything works fine, you click on the handle to toggle the display of the content and the controller and directive have seperate scopes as intended:
http://plnkr.co/edit/e3XAZuhSMAxmkWzKKM39?p=preview
Now I upgraded to Angular RC3 yesterday and now the functionality does not work as it stands in the plunker, I get the error the specific requires generic which is can't find. Going through the change log, I though this might have to do with this breaking change:
$compile: due to 31f190d4, the order of postLink fn is now mirror opposite of the order in which corresponding preLinking and compile functions execute
To fix this they either suggest converting the post linking to a pre linking (which I can do since my post linking needs access to the scope which is not available in the pre linking) or to decrease the priority of the directive. So this plunker does that and functionality does work:
http://plnkr.co/edit/arP3aruH8HEdiwFg6mWp?p=preview
However there is a major issue and that is now because specific has a higher priority, the isolate scope that generic needs is no longer being created so now contentVisible is on the controller scope which is not good.
Now I could just move the scope: {} from the generic directive to the specific directive however it should be possible to use the generic directive by itself and if I did it would attached to whatever scope it is part of and not its own (which would make it impossible to have multiple instance of this directive, which is way it needs its own scope).
The only thing I can think of is to add a directive, called something like isoScope, make sure it has a very high priority, and have it define scope: {}. Then if I need to use generic by itself, I just have to make sure to also add the isoScope directive to make sure it has an isolate scope. Like this:
http://plnkr.co/edit/1NYHpUcPFWEbAzvkCeRH?p=preview
Now I am hoping there is a better way to accomplish what I am looking for without the isolateScope directive. Am I missing a way of that this without that?
UPDATED EXAMPLE
So here is another plunker that includes hopefully better examples of what I am trying to convey (still has virtually no styles but should not be needed to get the point across):
http://plnkr.co/edit/KtRMa1c9giDrhi1Rqyho?p=preview
I have 3 directives:
expander
notification
isolateScope
The expander directive only adds functionality to be able expander and collapse content, nothing more. This functionality is something that should be able to be used alone or with another directive (which is why it has a controller).
The notification directive is used to display notification however since we don't want to display the notifications all the time, we use it with the expander directive so that the user can toggle the display of the actually notifications (similar to how stackoverflow.com does it in the top left).
While I imagine the expander would most likely be used with another directive it should be possible to use alone and that is where the isolateScope directive comes into play. Since the expander directive adds data to the scope and you may want to have multiple expanders on the same page, it needs to have an isolate scope in order to work. Now on a users profile page you have have data like developer key and address that you don't really need to display all the time so lets have the user control that. I have the isolate scope to be able to control both of those independently because without the isolate scope, both of them would be on the same scope and be controlled by the same instance on contentVisible.
I just don't see anyway with how directives now run in 1.2.0 RC3 to be able to accomplish this without that isolateScope directive (though I would be happy to be proven wrong).
I have updated your code so that it does what I think you want (at a minimum this works the way your old code does, but under rc3 as you wanted): http://plnkr.co/edit/nsq4BGAih3lfNmS2mLP7?p=preview
But I made quite a few changes and a significant architectural change so let me know if this moves away from the spirit of what you're trying to achieve.
I think the gist of the issue was that your two directives (generic and specific) were tightly coupled around contentVisible which created a complex dependency that resulted in you having to very carefully manage invocation timing. My approach was to decouple the two directives- encapsulating contentVisible within generic. This allows generic and specific to instantiate fully independently. So you're not dependent on any invocation timing. And thus the directive priority change no longer has any impact on this code. So, one big win with the solution I propose is it should be robust against further changes by the Angular team.
Specifically, I moved the template in to the same directive (generic) as the controller which manages contentVisible . This way the controller that changes contentVisible lives on the same scope as the template which uses it.
Now specific just calls over to the required: generic controller to toggle visibility (effectively as a setter function).
I also moved the ng-class assignment into the template in order to encapsulate that change within one place (the template) and so you don't need jquery or a link:/compile: on generic.
This is a regression. A fix is in the works: https://github.com/angular/angular.js/issues/4431
I have problem, looks very close to your. So if anything will change want to be notified.
My task: I have contact, that could be shown in defferent ways (very common task), but difference between views is in templates, whereas help functions and preparations are same, so I need generic directive for all views.
What I found:
1. in rc2 it works fine in rc3 unstable
2. in rc3 it could work same only when template is inline, but not when it is templateUrl (even if cached)
So I created two planks rc2 version and rc3 version.
Hope this will help.
I would like to replicate Google's home page functionality in Angular, and it's causing me grief. I've gone through the Egghead videos and read the entire API, but there's no particular example for that exact behavior. What I'd like it to do is the following:
user comes to home page, main search bar is present and generic black header bar
user searches for something, and only when he presses "search" does the main search bar disappear, the url changes to mysite.com/q/searchTerm and a new sub-header appears under the black header bar much like with Google's home page, where the main search field is removed and placed in a grayish bar under the main header bar (if you have instant-search off)
the results of the search appear in place of the now gone main search bar, just like with Google, but this part I can handle with routes and views. The layout switch between two identical controllers is what bothers me.
So far what I've tried was:
make a parent controller for both MainCtrl sub controllers, and set its scope.data = {searchHeaderDisplay: false}
have both sub controllers share the same name (MainCtrl) because they share the exact same functionality
make the one in the header bar ng-show="data.searchHeaderDisplay" and the main one ng-hide="data.searchHeaderDisplay" and then try switching the data.searchHeaderDisplay on ng-click of Search Button. This didn't work - no effect was produced.
I'm still coming to terms with AngularJS, so I'm sure it's quite simple, I just need a practical example or two to learn from.
Edit: would it be better to shove the secondary header (with the smaller search field) into a separate view template along with the search results, and just have the root view be the main search field? The documentation is very lax on best practices regarding views and routes, especially routes that will have multiple controllers doing something.
If I understand you correctly, you set scope.data = {searchHeaderDisplay: false} on the parent controller of the two MainCtrl controllers, with the intention to enable the MainCtrl controllers to share the same model data. That's all fine.
Without seeing your code, my guess is that the problem lies in how you switch data.searchHeaderDisplay on ng-click. You might have set data.searchHeaderDisplay at the child scope level (ie. the scopes that corresponds the MainCtrl controllers) when you should have assign the value to their parent scope level. Let me know if you need me to elaborate.
UPDATE:
After taking a look of the provided code, the problem is indeed as what I suspected earlier (above).
mainProductSearch() is what needs to be changed. Instead of scope.data.searchHeaderVisible = true;, you need scope.$parent.data.searchHeaderVisible = true for the reason briefly explained earlier. If the rationale behind is still not clear to you, then you probably need to familiarize yourself with prototypal inheritance chain of AngularJS scope (and/or Javascript object in general). Scope prototypal inheritance an essential part of AngularJS. Here is a great article on the topic.
Instead of an AppCtrl (which is essentially acting like $rootScope), and using $parent (which is a fragile solution because changing the HTML structure could cause this to break -- e.g., you might find you need to use $parent.$parent... if you add an intermediate ng-controller), I suggest a service for storing model data related to your header. Let's call it searchService.
Controllers that need to affect this model can inject the service. This has the additional advantage that the dependencies are clear (vs controller $scope inheritance, where the dependencies are not clear). E.g., when the user presses "search", the controller can call a notification method defined on the service: searchService.newSearchTerm(searchTerm). Now, all views (like the header view) that are watching for changes in the model will notice the change and can update accordingly.
You might consider using ng-view for the main content area of your page.
See also https://stackoverflow.com/a/14619122/215945, where a very similar layout is discussed. In that SO post, a shopping basket with an item count is in the header. The item count needed to be updated by multiple controllers, so we put it into a service.
I have a backbone.js app, whose views have multiple states, which differ substantially from each other ("View","Edit", etc). There are at least 2 different templates for every view. This is OK. My problem is with the JS view managing code.
I rely on an initalize-thin-render-thick approach (which, I think is pretty bad), where the render method is where 80%-90% of the logic occurs. When I want to change the state, I simply call the render method with a specific parameter ("view","edit"). On the basis of that, the view decides what to show and what not, to which events to bind, etc.
I think this is bad, because, on one side it puts bottlenecks on the rendering process, on another, it is not proper state machine, which means that I am not carrying about possible callbacks that might have been bound previously. When I receive the view, I simply clean the view and that's it.
I also observed, that I am not using the delegated event system, provided by backbone, which I think is another minus, because I think, it is very well implemented (BTW, does it make sure to unbind callbacks, when a certain DOM element is removed?)
I think I need some serious refactoring. Please, help with some advice, as to what the best approach for a multi-state Backone view would be.
What I tend to do for these cases is to make a toplevel view that manages a subview for each individual state (index, show, edit, etc.). When a user action is invoked, e.g. "edit this user", "delete this user", "save my changes", the active state view signals the router (directly, or through a hyperlink), and the router will tell the toplevel view to update its state.
Continuing the user editor example, let's say that I have a top level view called UserEditorView. It renders a basic container for the user editor (title bars, etc.) and then, by default, instantiates and renders Users.IndexView inside that container.
Users.IndexView renders the list of users. Next to each user is an edit icon, which is a link to "#users/555/edit". So, when the user clicks it, that event goes to the router, which tells UserEditorView, "hey, I want to edit user #555". And then UserEditorView will remove the IndexView (by calling its .remove() method), instantiate Users.EditView for the appropriate user model, and put the EditView into the container.
When the user is done editing the user, she clicks on "Save", and then EditView saves the model. Now we need to get back to the IndexView. EditView calls window.router.navigate('users', { trigger: true }), so the URL gets updated and the router gets invoked. The router then calls .showIndex() on the UserEditorView, and the UserEditorView does the swap back to IndexView from EditView.
On a simple way to manage unloading of events, I've found this article on zombie views quite useful.
Basically, I don't have a toplevel view, but I render all the views using a view handler that takes care of the views for a given container.
To make your renderer thinner, I would recommend using routes. They are easy to setup, and you can have different views for each route. Or, what I'm used to do is just to have different templates. Using a general Backbone.View overwrite:
Backbone.View = Backbone.View.extend({
initialize: function(attrs) {
attrs = attrs || {}
if(!_.isUndefined(attrs.template)) {
this.template = attrs.template;
}
}
});
I've noticed that I reuse views in two ways:
1. edit views differ only in the underlying model and template, but not the associated logic (clicking the submit validates and saves the model)
2. the same view can be reused in several places with different templates (like a list of users as a ranking or you accounts)
With the above extension, I can pass {template: '/my/current/template/} to the view, and it will be rendered as I want. Together with routes, I finally got a flexible, easy to understand and thin setup.