Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top