I'm writing a large scale marionette application, which is ran initially from a router / controller.
Here's my question -- is it good practice to include other functions in your controller that aren't meant for routes?
So say I have the following method:
index : function() {
alert('test!');
}
is it consistent with best practices to be able to declare other functions in the controller not called when routes are initialized? Like so:
noRouteAssociated: function() {
alert('test!');
}
index: function() {
this.noRouteAssociated();
}
Obviously this is a simplified example, but I am trying to avoid putting large amounts of information or function declarations inside of methods only because they're associated with routers.
The roles and responsibilities of controllers are best illustrated by #davidsulc in this SO post and better yet his new book.
Generally speaking, it's okay to include methods that aren't meant for routes, if they're controlling the workflow of your app. Event triggering is a good example, but if you want to change the appearance of something or retrieve data from a database, you should move these methods to a view or model, respectively. The block quote below is taken directly from Marionette's controller documentation.
The name Controller is bound to cause a bit of confusion, which is rather unfortunate. There was some discussion and debate about what to call this object, the idea that people would confuse this with an MVC style controller came up a number of times. In the end, we decided to call this a controller anyways, as the typical use case is to control the workflow and process of an application and / or module.
But the truth is, this is a very generic, multi-purpose object that can serve many different roles in many different scenarios. We are always open to suggestions, with good reason and discussion, on renaming objects to be more descriptive, less confusing, etc. If you would like to suggest a different name, please do so in either the mailing list or the github issues list.
Related
I am working on a project where in there are almost 90+ modules.
All modules has a set of input fields and on submit the data should be saved on the server.
At any given point in time only one module is active. But there can be open modules in the background.
Submit button is common to all modules, meaning there is only one Submit button throughout the application.
Below picture explains it more.
The prime motto is to keep the individual module changes to minimum and a way to handle certain things(validation, reload etc) in the module from a central place.
The current approach I am planning is,
Use a 'moduleInit' directive that all module should include in its
partial.
The directive takes the $scope of the module and pass it to a
common service/factory (pushConfigService)
The pushConfigService stores and keep this scope as long as the
module is open. Once the scope is destroyed the reference of the
same will be removed from the pushConfigService.
The footer panel is another directive with Submit button in it and
calls a save function in the pushConfigService which in turn calls
a $scope function in the module to get the form data.
pushConfigService talks to a bunch of other services like
dirtyChecker, apiGenerator and finally post data to the server.
Each module will have a set of scope methods defined with some standard names. Eg: _submit, _onSubmit, _cancel, _reload etc.
Another way to handle this, broadcast the submit event and each module listens to the same. There is possibility more actions will be added to the footer panel.
So I am little bit hesitant to use the broadcast approach.
My question, Is it a good idea to pass controller scope to a service? Any alternate suggestions?
Thanks in advance.
I believe your core concept is a nice way to handle this setup. Yet I'd suggest to split business logic from UI. I don't have a sample of your code so it is a little hard to build an exact example. Yet since you're using the $scope variable I'm going to assume you're not using a styleguide like or similar to John Papa's. His ways encourage you to not use the $scope and to stay close to actual JavaScript "classes".
How does this make a difference?
Instead of passing the whole scope, you'd be able to just pass the instance of your specific module. For one it is less confusing to you and colleagues to have a concrete interface to operate on instead of having to figure out the composition of given scope. In addition it prevents services from being able to alter the $scope.
The latter could be considered a good practice. Having just the controllers alter the scope make it easy to find the code which alters and manages the UI. From there on the controller could access services to do the actual logic.
Taking it one step further
So passing the class instance instead of scope should be an easy adjustment to the already proposed setup. But please consider the following setup as well.
It seems there are quite some different ways to handle and process the data provided by the module/end user. This logic is now implemented in the controller. One might think some of these modules share similar handling methods (big assumption there). You could move this logic to, so to speak, saving strategies, in services. On activation of a module, this module will set its preferred saving strategy in the service which handles the submit button click. Or more precisely, the save data method which should be called from the onClick handler in the controller.
Now these services/strategies might be shared among controllers, potentially setting up for a better workflow and less duplicated code.
I've created a BookService that is used for storing (add), getting (getAll) and removing (remove) books. Now I want to use this BookService inside my controller to publish some methods that I can later use in my view. My controller looks like the following:
app.controller('BookController', ['BookService', '$scope', function(BookService, $scope) {
$scope.addBook = function() { BookService.add(); }
$scope.getAllBooks = function() { BookService.getAll(); }
$scope.removeBook = function() { BookService.remove(); }
}]);
As you can see, the controller is just a proxy that forwards the method calls to the service. Is this in general bad practice? And is there a better way to solve that using Angular.js? Calling the service directly from the view without a controller seems, to me like a bigger problem.
Best Practice
From what I can see this is perfectly good practice, I'm assuming that you're using $scope here to bind these services to a button or something similar. The Idea of using a factory or a service is to create some well organised reusable code that you can then instantiate in your controller exactly like you have done.
Controllers can get quite complex so moving basic functionality to your service is the best thing to do. In your case you have a relatively simple controller so you may not see the benefit of coding in this way. But I assure you that as your code base grows you will be required to code like this just to keep things readable.
Note
Just as an extra note I've added a data process that you might take with an example of a database request
database > API > service/factory > controller > view
This would be the correct path to take when requesting something from a database.
What you have shown is not bad practice; it is in fact recommended that you abstract resource management logic (like adding, removing and finding resources of the same type) into a separate service, for organization and maintainability and reusability purposes.
I do understand from your example how the controller seems to be just a proxy to the service though, and why you would think it's repetitive and possibly redundant. Let me expand on your example to see if I can explain the reasons for abstracting the logic. In doing so, let's consider only the adding operation, even though the logic would apply to others as well.
Reason 1: adding a book (or any resource for that matter) in a web app is usually more complicated than just a bookService.add() call. It requires having some sort of form to enter the details, a submit button to handle the actual request, possible massaging of the entered data, adding new fields like time stamps and author ids, and persisting to some data storage. The first two or three of those steps would be applicable to the screen or view from which you are adding, so it makes sense to put that in a controller (the view model), whereas the rest of the steps should go into the generic service.
Now say you have another screen where you see a friend's list of books and you can add it to your list by selecting a book. In that case, the controller logic for that view's controller will differ from the previous example, but the service steps will still be identical, and thus do not need to be duplicated since the service can be injected into both controllers.
That's a big win.
Example 2: let's say you've already split the code between controller and service as describe in example 1 above. Then you decide that your data storage needs have changed and now you want to update your logic to save to a different database system. Now instead of having to change all the controllers, you just change your services and the everything works.
This is also a really big win.
Yes you'ved done this correctly. Just keep in mind that as you scaled up your app in complexity, you'll want to have isolated scopes, (and) decomposition into component directives. Each directive will have its own controller and template fragment.
I have a Bootstrap modal which corresponds to a ng-repeat list. I would like to access the ng-repeat scope data from its parent, which contains the modal. I do this so that when the modal button is clicked on the list, the corresponding data from the JSON appears in the modal.
I have found 2 ways of doing this, and I wonder which is the best alternative?
Method 1
View:
<li ng-init="myFunction(item,$parent)" ng-repeat="item in data.webapps_1.items>
Controller:
$scope.myFunction = function(item,parent){
parent.selected=item.counter-1;
};
Method 2 View:
<li ng-init="$parent.selected=item.counter-1" ng-repeat="item in data.webapps_1.items>
With nothing in the controller.
I have read in the Angular ngInit docs that
The only appropriate use of ngInit is for aliasing special properties
of ngRepeat, as seen in the demo below. Besides this case, you should
use controllers rather than ngInit to initialize values on a scope.
But the list of special properties of ngRepeat does not include $parent.
So, which is the better practice? Including the expression $parent.selected=item.counter-1in the controller or in ngInit directive?
Thanks in advance
Either of the two fine really, so long as you're consistent. Depends on the scale of the app though.
IMO if the app is going to be large you'll want to go the function way, to better adhere to the whole MVC philosophy of decoupling and separation of concerns (http://victorblog.com/2013/03/18/angularjs-separation-of-concerns/).
ng-init="myFunction(item,$parent)"
It's a better structure because you want to keep most of your business logic in the javascript controllers, not in the view.
Personally, I prefer the ng-init approach. Mainly for consistency of the timing of the call (i.e. more like an "onLoad" event). I understand the concerns regarding SoC and that makes sense as well. However, I have seen some specific scenarios (particularly pages with a lot of directives), when changing scope variables with the = binding in the (non ng-init) function, caused extra (premature) digest loops. Obviously it depends on what you're actually doing in the function as well as your scope bindings, but in my opinion its best to just avoid the situation and use the ng-init directive across the board. Your experience may obviously vary wildly. Like others said just pick an approach and stick with it and be aware of the impact of every call that you're making in the function itself. :)
EDIT: Regarding the accepted answer, the example was used passing values to the init function in ng-init. And hes correct that is a violation of the pattern. I typically use parameterless init functions and call it like:
ng-init="model.init()"
Any reference to $scope or what have you would happen inside the function itself. I rarely pass anything to init, unless its a static value. For example in several cases on an ASP.NET WebApi project, I wanted minimize round trips to the server so I would resolve a value from the MVC View Model, which is just rendered and passed as a string like so:
ng-init="model.init(#model.myValue)"
In those cases its usually best to keep it to simple value types (i.e. string, int, etc), but I have occasionally for small directives such as one that does nothing but display a dropdown, I have passed arrays to pre-populate the dropdown binding. Those cases are extremely situational and this one in particular would obviously only work when using server side rendering of the template.
Note: Sorry for the length of the post, but the reason I decided not to break it down in separate questions was because I find these issues hard to address without a complex problem like this. I am dazzled, and a bit afraid, that I'm trying to force Angular to do stuff for me which is not the 'Angular way'. Any advice would be highly appreciated, and would probably get me on the right track with Angular.
My problem is as follows:
I have a dynamically generated form, controlled by myFormCtrl. I want to go extremely modular: I want to use it whenever and wherever. This means, that sometimes I'll need to put it somewhere as-is, sometimes I need to nest forms dynamically (like when I change a form value, and other sub-form appears), or control two separate forms as one in a view by a parent controller, with one 'Save' button for both. The myFormCtrl uses $scope.type_id and $scope.rowid to know, which record should it display from the database. The records are then Ajax-fetched by a service, and saved under the myFromCtrl's $scope.formItems. When saved, the form sends back data to the server (via service) with the type_id and scope credentials, so the restful api knows where to put the record.
In theory that would be really easy to do in Angular.js.
It definitely would be in every object-orientated language: The parent class could call a public method of getFormValues() of myFormCtrl. Now that can't be done in Angular: parent can't read children's scope.
For me, it seems, that this is not a simple 'how to communicate between controllers' issue. I know that the answer to that question is services, events, scope inheritance.
Also, a number of other problems seem to emerge from each solution I found sofar.
So I have a myFormCtrlBase class, which does basic stuff, and other more advanced classes extend this one. I also have a formControls.html, and a formLayout.html partial. The first contains an ng-switch, and gives the appropriate input element based on $scope.formItem.controltype, the second has the html layout of a common form, ng-including formControls.html at the right places. It utilizes ng-repeat="formItem in formItems", so that's where formControls.html's $scope.formItem comes from.
When I want the form to have a different layout for example, I create a customFormLayout.html partial ng-controlled by the myFormCtrl class.
First question: what if my form layout can't be put in an ng-repeat?
Like when form elements need to be placed scattered across the page, or form layout is not something which could be fit in an ng-repeat loop. my formControls.html still expects a $scope.formItem to work with. Easy OO solution: parent puts formItem in child's scope.
My solution: I created a <formItemScopeChanger formItemScope="formItems[1]"> directive which gets formItems[1] as an attribute, and turns it to $scope.formItem variable. This solutions feels messy: directives are not meant to be used like this. Doesn't seem very Angulary. Is this really the best solution?
Second question: Is ng-init really that evil?
Say, form is not put in the view by $routeProvider, but in a custom partial: rent-a-car.html. Here, I want to have a form where the user can select a car, and an other form, where I get his contacts. The two forms work with different $scope.type_id's, so there need to be two different forms:
<h1>Rent a car!</h1>
<div ng-controller="myFormCtrl" ng-init="type_id='rentOrder'">
<div ng-include="'formLayout.html'"></div>
</div>
<h2>Your contact information</h2>
<div ng-controller="myFormCtrl" ng-init="type_id='User';rowid='{{userData.rowid}}'">
<div ng-include="'formLayout.html'"></div>
</div>
Angular docs sais, that the only appropriate use of ng-init is when aliasing ng-repeat values. I don't see what the problem is with the example above - it is still the cleanest solution, isn't it?
I use the same technique with nested forms - I put a controller in with a template, initialized from the html by ng-init, and shown/hidden with an ng-if condition.
BTW, this is the only real initialization technique I found beside writing a new controllers (extending myFormCtrlBase). In an OO language, parent would write into the child's scope and then initialize it.
Perhaps my approach is influenced by my previously used languages and programming techniques, and is absolutely wrong.
Some would say, 'get init values from parent scopes!', but I can't seem to understand how that would be safe and efficient. I'd need to do $scope.type_id=($scope.type_id || $routeParams.type_id) with every scope property, which is first: really not nice to look at, second: is risky. Maybe this is a single form in a simple template, but somewhere in the scope hierarchy, there is a possibility, that it will find a completely different type_id. Perhaps it will be a completely different controller's type_id.
I don't see how using '.'-s in my scope variables would help. I has the same risk as I see it.
Third question: how to handle rentACar.html submission?
When hitting a Save button on my rentACar.html page, the rentACarCtrl (the controller in charge of the model of the view) should somehow retrieve the values of the two forms, and handle the validation and submission. I can't seem to understand how the common mantra 'controllers communicate through services' would be applicable here. A service for only to these two forms?
Am I on the right track? Every one of these solutions seem quirky. I feel lost :)
+ 1 question: Even after all this hassle, I can't seem to find a good reason why Angular wouldn't let parents call children's public stuff. Is there a good reason? Most of the above problems would have an easy answer in every true OO js framework.
You need to think about how you would test the logic of each of these components. Ask yourself how each of these 'features' work in isolation.
A few tips to help get you back on track:
Try and say away from a 'base' controller, I have hit many dead ends with scope inheritance, the logic gets muddled and hard to follow. Also this affects testing, because you find yourself having to stand up more objects than should be necessary for a test
Favor a singleton (angular service) for shared state over scope inheritance (a parent controller)
Create a directive and bind to the shared services state before using ng-include (prefer interacting with a service over scope inheritance)
Use an event pattern when another service or controller needs to now about events triggered from directives. A shared service (state) can listen for those events
What your asking is quite complex and I would like to help, Try to focus on one feature at a time and provide some code, I can show you how to use a shared service and the event pattern once you provide some examples
Also, taking a test first approach will often reveal the best 'Angular Way' of doing things.
Thanks to Mark-Sullivan, and a lot of work, trial-and-error attempts, the whole thing has boiled down to this. I'd like to get feedback from Mark, and other Angular gurus about this. What do you think?
You don't do class/prototypical inheritance in Angular.js. It is hard to test, and thats a big problem. For those, who are looking for 'inheritance' in Angular, I recommend this:
Your base class is the controller. The controller is an abstract model anyways, so it is perfect for that purpose. Use a $scope.init() function in your controller, but don't call it from there!
If you want to 'extend' your controller's functionality, use directives. In you directive link() function, call the controller's $scope.init(). (when compiling, angular runs controllers first, and directive link functions after). If scope had a $scope.name='base', in the directive link you will be able to redefine $scope.name=child, and after that, run $scope.init().
But wait! But this only allows a single-level inheritance. - Yes, thats true. But if you are looking for multilevel inheritance, you should use Services.
Multilevel inheritance is nothing else, but sharing the same code in a hierarchical class structure. For this purpose, use Services, and throw in these services with the dependency injector into your directives. Soo easy. This should be easy to accomplish, easy to understand, and tests run smooth.
Directives are very powerful tools, because you can dynamically combine partials with controllers.
Lately (2.x / 3.x) I just used xtype & factory methods to receive a instance of a class which was as simply as fast. Now I have started 4.x and my first App with MVC. As described in the tutorial the MVC pattern requires me to extend a class for each view I wan't to use, even if I use it just one time. But the best practice written by Sencha itself says:
just extend for re-useability or adding of functionality
In my case I need to register a whole bunch of classes even if they could be created from one base class except of some params like title, width,...
Another point is that the Controller overwrites any StoreId by convention and also requires a strict typing, means the class-name must end with an s. But as far as I know I cannot spare neither the model nor the store within the the controller store/model-array so is there any other point for this convention cause it seems not to spare typing.
Next point is that after merging from 3.X to 4.X the application initial load time has extended which seems to be caused due to either the many new classes that need to get defined or due to the fact that all controllers get instantiated at startup due to the default behavior of the MVC pattern. Is there any way to not auto instantiate a controller and just doing it lazy, for example when I request it on the application controller?
Yes I know, that are a bunch of questions but I guess they all around the same topic.
EDIT
After some sourcecode-digging I am no longer sure about the
requirement of the s when naming a store. I thought I stumbled over
this while going through the MVC tutorial. Can anyone verify this?
EDIT 2
My conclusions
Lacy rendering is quite simple. First of all the Controller should not be mentioned in the ApplicationController controller array. To create a instance of such a controller use the ApplicationController.getController(pureClassName)
[Note that each controller contains a reference to the ApplicationController called application] Now you need to be aware of the fact that the init(application) method and the onLaunch(application) method get no longer invoked by the ApplicationController, you need to do this yourself. When calling getController() the ApplicationController first lookup if there is already a instance of this controller in the internal reference cache if not it creates a instance and inject the controllername as Id. So controllers are a sort of singleton which is perfectly fine.
The controller itself creates all the getter for the registered stores, models and views and, that's a guess, it instantiate them (at least the stores)
About naming restrictions for stores, there no restrictions about ending with an s.
These are very valid points.
First, it has to be mentioned that you don't have to use MVC with ExtJS 4. You can still use ExtJS 3 style in your code.
I assume that if you understand the advantages of MVC and decide to adapt it, then yes - you will have to extend classes and there is some overhead, but admittedly you will end up with a cleaner, more reusable code. It has to be said that while you need to extend top-level views, the items within them can still be coded old style. In addition to this, within the init() of the controller you can modify certain view configs (which allows less of class extension, but more controller code).
I have to admit that if you have experience with ExtJS 3 and you're migrating to MVC style of an app, you will eventually see that the benefits outweigh the work involved.
Personally it's the first time I've been made aware of this 's' business with stores. So I can't comment much on this.
Lastly, a properly-written ExtJs 4 app, one that makes use of dynamic loading should load faster than an ExtJS 3 app. You can also compile an Ext version that only includes code used in your app. And yes, you can instantiate controllers (and their views and stores) when you need them, which works like a charm:
loadPage: function(aControllerName)
{
// save recent page in a cookie
Ext.util.Cookies.set('RecentPage', aControllerName);
// Dynamically load the controller
var iController = this.getController(aControllerName);
// Manually initialise it
iController.init();
// Load the page (by getting the first view of the controller).
var iPage = this.getView(iController.views[0]).create();
// Add the page to the content panel.
var iContentPanel = this.getContentPanel();
iContentPanel.removeAll(true);
iContentPanel.add(iPage);
iContentPanel.doLayout();
}
1.Izhaki answered your question well about the lazy controller initialization.
2.I am not really following you on the gripe about Store names. There are no restrictions on store names. They are merely suggestions of naming conventions.
3.The Ext.define method is great to define your classes - similar to Java or other OO languages. This is NOT required however and you can simply use Ext.create method to create an instance of a framework component and pass it custom config object.
You can also use Ext.define to create you base class and then call Ext.create('MyBaseClass',{title:'mynew tile'}); to get a slightly modified version of your base class.
I encourage you to read through the Sencha guides on MVC, and Class system and also review their examples to get better understanding.