Custom hierarchical AngularJS directives with bi-directional binding - angularjs

I'm trying to build a CRUD form using two custom directives. The first one (crudForm) is the main form directive, that holds all of the controls applied to this form (textboxes, textareas, checkboxes, etc.), the second contained (one or many) inside is a directive for custom controls to be included in the form. I want to bind a single object to the main directive (crudForm), and bind each one of the object's fields to one of the child directives inside crudForm. For example, I have an object defined in my $scope as $scope.obj = { "order_id":20, "total": 44.50, "info": "..." }, and to have a form to edit it like
<crud-form key-field="order_id" entity="obj">
<control type="money" field-name="total" field-title="Total"></control>
<control type="textarea" field-name="info" field-title="Information"></control>
</crud-form>
I have a full example here.
The thing is that I want to automatically bind the object in the main controller, first to the form, and then each field to the controls, so that when there is a change in the input, the object bound will be changed as well. I can't do this, because as far as I have seen in the console log, the control's link function executes before the form's link function, so at the time the control's link function is being executed, the object bound to the form's link function isn't instanciated.
Any ideas?

Here is a modified Plunkr in which (only) the input of total is working and binded to obj: http://plnkr.co/edit/OjEzjZeUC2yTKkaoFEoP
Removed this code from form.js (it was throwing errors anyway because you can not access entity like this):
element.find('[rel=input]').on('change', function() {
$scope.$parent.entity[$scope.field.name] = $(this).val();
}).val($scope.$parent.entity[$scope.field.name]);
Added this to the controller code of crudForm:
this.getEntity = function() { return $scope.entity; };
In directive control changed the link function declaration from
post: function($scope, element, $attrs) {
to this, thus adding a reference to the parent controller.
post: function($scope, element, $attrs, parentCtrl) {
By the way: Write $scope as scope and $attrs as attrs. These arguments are position-fixed and not magically working by name. So avoid confusion.
Then I added this code to the link function in control:
$scope.entity = parentCtrl.getEntity();
Then I fixed script.js (you are setting different keys than you are referencing in index.html!) and as an example changed the code from money.tpl.html to this:
<div class="input-prepend">
<span class="add-on">{{ field.options.symbol }}</span>
<input type="text" id="field-{{ field.name }}" class="input-small" rel="input" ng-model="entity[field.name]"/>
</div>
The change here is ng-model="entity[field.name]". So as you can see, with the changes before you can just use entity in your child directive and directly bind to the referenced attribute / object. No actual need to copy any values.

Related

Why is binding lost when nesting directive in directive?

The Issue
Using my directive within another directive causes certain bindings to be lost, specifically in the ng-repeat usage.
The directive is used in many areas of my application without issue. It renders a list of inputs which is passed to it's scope from a parent template, like so:
<filter-header filters="filters"></filter-header>
Working Scenario
I have used the following scenario throughout the application and not come across an issue to date.
$routeProvider resolves filters list with WebAPI call for a controller
controller assigns list to its own scope, like so: $scope.filters = filters
template uses filter-header element and passes filters from it's scope to the directive, like so: <filter-header filters="filters"></filter-header>
The filter-header directive then renders the filters using an ng-repeat without issue. $$hashKey is present in each item of the filters collection, indicating the binding's existence.
Failing Scenario
In the following scenario, the binding seems to be lost and the ng-repeat fails to render anything.
$routeProvider resolves filters list with WebAPI call for a controller
controller assigns list to its own scope, like so: $scope.filters = filters
template uses a new element directive, assigns filters from it's scope to the new directive's via an attribute.
directive's template uses filter-header element and passes filters from it's scope to the directive, like so: <filter-header filters="filters"></filter-header>
The filter-header directive then FAILS TO render the filters using an ng-repeat. $$hashKey is NOT present in any item of the filters collection.
Annoyingly, I cannot replicate this in Plunker...
Oddity
The directive has another collection of item's passed to it, columns="columns" (can be seen in the code below). Columns binds correctly and is rendered in it's own ng-repeat. I cannot see how Columns is different from Filters as both are used almost exactly the same way.
Looking Deeper...
I have debugged the process all the way. The filters object is getting all the way to the end scope successfully. If I output the contents of filters to the screen within the final directive, using {{ filters }} I can see all of the filters as expected. However, in the very next line where my ng-repeat begins, no filters are iterated through.
To be certain it is not my list causing issues, I used a list that already works using the working scenario mentioned above and the ng-repeat does not render here.
To be certain it is not my directive's code causing issues, I converted it to a controller and routed directly to it (skipping the nested directive) as in the working scenario mentioned above and the ng-repeat now works.
Using $log to inspect the list, I notice one difference. In the working scenario, all lists contain a $$hashKey property for each item in the list. In the failing scenario, the $$hashKey is missing on all items in the list. This seems to indicate that the binding is being lost for some reason.
Can someone tell me the error in my ways? The only real difference I can see in my usage is that I pass the object to an middle-man directive before before passing it on the the directive where it is used. Strangely, in the very same directive, another list is used in a very similar way and it renders without issue within it's ng-repeat, and it's item's all have the $$hashKey property appended.
Code
There's a lot of code involved, so I'll try and pick out the relevant parts.
RouteProvider
$routeProvider.when('/Pride/Admin/AuditForms/:id', {
templateUrl: '/Templates/Admin/editAuditForm.html',
controller: 'editAuditFormController',
resolve: {
sectionFilters: function (auditFormSectionRepository) {
return auditFormSectionRepository.getFilters().$promise;
},
sectionColumns: function (auditFormSectionRepository) {
return auditFormSectionRepository.getColumns().$promise;
}
}
});
EditAuditForm Controller
prideModule.controller("editAuditFormController", function ($scope, sectionFilters, sectionColumns) {
$scope.sectionFilters = sectionFilters;
$scope.sectionColumns = sectionColumns;
});
EditAuditForm Template
<audit-admin-sections audit-form="auditForm" section-filters="sectionFilters" section-columns="sectionColumns" show-deleted="false"></audit-admin-sections>
AuditAdminSections Directive
prideModule.directive('auditAdminSections', function ($log) {
return {
restrict: 'E',
templateUrl: 'templates/admin/auditFormSections.html',
scope: {
sectionFilters: '=',
sectionColumns: '='
},
controller: function ($scope, $route, $timeout, $location, filterLogic, auditFormSectionRepository) {
// do stuff
}
});
AuditFormSections Template
<filter-header filters="sectionFilters" columns="sectionColumns"></filter-header>
FilterHeader Directive
prideModule.directive('filterHeader', function($log) {
return {
restrict: 'E',
templateUrl: 'templates/common/filterHeader.html',
scope: {
filters: '=',
columns: '='
},
controller: function ($scope, filterItemsRepository) {
$log.info("$scope.filters");
$log.info($scope.filters);
// This will log the filters as expected, however the $$hashKey property is missing from the items
}
});
FilterHeader template
<!-- at this point, {{ filters }} produces the list of filters -->
<form class="form-horizontal" ng-repeat="filter in filters">
<!-- at this point, nothing renders -->
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</form>
Update 1
I pulled the code out of the directive and into a new controller to mimic the Working Scenario mentioned above. The ng-repeater now functions as expected and the $$hashKey is present again. So something is definitely related to the difference between route->controller->directive->directive vs route->controller->directive.
Worth mentioning, on top of the code above, there are watches on the filters among other usages.
Update 2: Offender Uncovered
I've nailed it. But it makes no sense yet. It seems as though the form element is to blame. Changing to a div resolves the issue. I'm thinking this may be an angular bug as I'm struggling to see why this could work in one scenario and not the other.
Have discovered two fixes so far, but they are more hacks than fixes as I cannot understand the cause of the original problem. I'm still interested if anyone can point out the real issue, but until then this is the best I have:
Solution 1
After much researching, debugging, head scratching, rewriting, I by luck came across a solution. I'm not happy with it, as it doesn't make sense (unless someone can elaborate for me).
The problem seems to be with the form element and using the ng-repeat attribute on it while being nested in angular directives...!!! This has to be a bug, right?
Solution was as simple as changing this:
<form class="form-horizontal" ng-repeat="filter in filters">
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</form>
to this:
<div class="form-horizontal" ng-repeat="filter in filters">
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</div>
Solution 2
It seems that the $scope variables were also playing a part in the issue. If I rename each directive's $scope variable name for this particular variable (ie, filters) so that it is unique for each directive, the form ng-repeat works. This makes it seem like there is some sort of conflict within the directives' isolated scopes, however why this is only an issue for form ng-repeat baffles me. As such, it still doesn't explain to me what is the root cause of this behavior.

What does the ngModel directive documentation mean exactly?

I'm beginner to AngularJs but now I have used and understand how AngularJs works quite a bit.
The thing which I want to ask about is the few lines in the ngModel directive documentation.
These lines are:
Note: ngModel will try to bind to the property given by evaluating the
expression on the current scope. If the property doesn't already exist
on this scope, it will be created implicitly and added to the scope.
I don't understand what they are trying to say. I do know that the ngModel directive binds a property to the input, select and textarea controls. Just like a very simple code below:
Name: <input type="text" ng-model="myName">
{{myName}}
So, can anybody come up with any other precise example which helps me to understand those lines?
ngModel is a standard way that Angular binds a scope property (usually declared in a controller) to the UI.
So, typically, a controller is created with the property declared inside:
angular.controller('Ctrl', function($scope) {
$scope.myProperty = '';
});
And you would bind it to the UI like this:
<div ng-controller="Ctrl">
<input type="text" ng-model="myProperty"/>
</div>
In this example $scope is scoped in the UI on the div element because the ng-controller attribute binds the Ctrl to that element.
However, Angular allows you to be a bit lazy as well and not bother defining the myProperty in the controller:
angular.controller('Ctrl', function($scope) {
});
And still using it in the UI:
<div ng-controller="Ctrl">
<input type="text" ng-model="myProperty"/>
</div>
In this case Angular will implicitly (dynamically) create a myProperty on $scope. That can be used in the UI inside the ng-controller's scope.
This can be used for UI only properties that you don't want to bother the controller about. An example would be when you want to hide/show something based on a user's interaction.
Hope this helps.
You have a controller with a $scope.
You can init your $scope with values(e.g.):
$scope.myName= 'test';
And then you input will have a default value of test.
Name: <input type="text" ng-model="myName">
{{myName}}
You can choose not to init your scope with the value, and then $scope.myName will be created behind the scenes and bound with no default value.
Let's break this down:
Note: ngModel will try to bind to the property given by evaluating the expression
on the current scope. If the property doesn't already exist on this scope, it
will be created implicitly and added to the scope.
1) ngModel will try to bind to the property given by evaluating the expression on the current scope.
So if you have the element:
<input ng-model="theProperty">
This element will have a scope:
var theScope = element.scope()
Most of the times this scope is accessed in a controller constructor... now, let's go back to the statement:
will try to bind to the property given... on the current scope.
Note that it says "try" because it is possible that the current scope contains the property or not. Because angular will do something like this:
theScope['theProperty'] = the_inputs_value;
When the property did not exist, the property gets created. So this explains the next statement.
If the property doesn't already exist on this scope,
it will be created implicitly and added to the scope.

Use ngModel with plain ngController instead of directive?

In my application I would like to preserve the option of using plain controllers for certain sections of code - as opposed to creating directives for one-off things that will never be re-used.
In these cases I often want to publish some data from the controller to be used in the contained section. Now, I am aware that I could simply bind items in the controller's scope, however I'd like to specify the "model" location explicitly just to make the code more maintainable and easier to read. What I'd like to use is ng-model as it would be used on a custom directive, but just along side my plain controller:
<div ng-controller="AppController" ng-model='fooModel'>
{{fooModel}}
</div>
However I can see no way to get a reference to the generated ngModelController without using a directive and the 'require' injection.
I am aware that I could make my own attribute fairly easily by injecting the $attr into my controller and do something like:
<div ng-controller="AppController" my-model='fooModel'>
{{fooModel}}
</div>
In which case I just manually take or parse the myModel value and stick my model into the $scope under that name. However that feels wrong in this case - I really only need one "model" for a controller and I'd prefer not to have to add this boilerplate to every controller when ngModel exists. (It's the principle of the thing!)
My questions are:
1) Is there some way to use ngModel along with a plain controller to get the effect above?
2) I have been trying to figure out where ngModelControllers are stored so that I could look at the situation in the debugger but have not been able to find them. When using an ngModel directive should I see these in the scope or parent scope? (Where do they live?!?)
UPDATE: As suggested in answers below $element.controller() can be used to fetch the controller. This works (http://plnkr.co/edit/bZzdLpacmAyKy239tNAO?p=preview) However it's a bit unsatisfying as it requires using $evalAsync.
2) I have been trying to figure out where ngModelControllers are stored so that I could look at the situation in the debugger but have not been able to find them. When using an ngModel directive should I see these in the scope or parent scope? (Where do they live?!?)
The answer depends slightly on where you want to access the controller from.
From outside the element with ng-model
It requires "name" attributes on both the element with the ng-model attribute, and a parent form (or ngForm). So say you have the form with name myForm and the element with ng-model attribute with name myInput, then you can access the ngModelController for myFoo from the parent scope as myForm.myInput. For example, for debugging purposes:
<p>myFoo: {{myForm.myInput.$modelValue}}<p>
<form name="myForm">
<div ng-controller="InnerController" name="myInput" ng-model="model.foo"></div>
</form>
as can be seen at http://plnkr.co/edit/IVTtvIXlBWXGytOEHYbn?p=preview
From inside the element with ng-model
Similar to the answer from #pixelbits, using $evalAsync is needed due to the order of controller creation, but you can alternatively use angular.element.controller function to retrieve it:
app.controller('InnerController', function($scope, $element) {
$scope.$evalAsync(function() {
$scope.myModelController = $element.controller('ngModel');
});
});
Used, inside the controller to view it, for debugging purposes, as:
<div ng-controller="InnerController" ng-model="model.foo">
<p>myFoo: {{myModelController.$modelValue}}<p>
</div>
As can be seen at http://plnkr.co/edit/C7ykMHmd8Be1N1Gl1Auc?p=preview .
1) Is there some way to use ngModel along with a plain controller to get the effect above?
Once you have the ngModelController inside the directive, you can change its value just as you would were you using a custom directive accessing the ngModelController, using the $setViewValue function:
myModelController.$setViewValue('my-new-model-value');
You can do this, for example, in response to a user action that triggers an ngChange handler.
app.controller('InnerController', function($scope, $element) {
$scope.$evalAsync(function() {
$scope.myModelController = $element.controller('ngModel');
});
$scope.$watch('myModelController.$modelValue', function(externalModel) {
$scope.localModel = externalModel;
});
$scope.changed = function() {
$scope.myModelController.$setViewValue($scope.localModel);
};
});
Note the extra watcher on $modelValue to get the initial value of the model, as well as to react to any later changes.
It can be used with a template like:
{{model.foo}}
<div ng-controller="InnerController" ng-model="model.foo">
<p><input type="text" ng-model="localModel" ng-change="changed()"></p>
</div>
Note that this uses ngChange rather than a watcher on localModel. This is deliberate so that $setViewValue is only called when the user has interacted with the element, and not in response to changes to the model from the parent scope.
This can be seen at http://plnkr.co/edit/uknixs6RhXtrqK4ZWLuC?p=preview
Edit: If you would like to avoid $evalAsync, you can use a watcher instead.
$scope.$watch(function() {
return $element.controller('ngModel');
}, function(ngModelController) {
$scope.myModelController = ngModelController;
});
as seen at http://plnkr.co/edit/gJonpzLoVsgc8zB6tsZ1?p=preview
As a side-note, so far I seem to have avoided nesting plain controllers like this. I think if a certain part of the template's role is to control a variable by ngModel, it is a prime candidate for writing a small directive, often with an isolated scope to ensure there are no unexpected effects due to scope inheritance, that has a clear API, and uses require to access the ngModelController. Yes, it might not be reused, but it does help enforce a separation of responsibilities between parts of the code.
When you declare directives on an element:
<div ng-controller="AppController" ng-model='fooModel'>
{{fooModel}}
</div>
You can retrieve the controller instance for any directive by calling jQlite/jQuery $element.data(nameOfController), where nameOfController is the normalized name of the directive with a $ prefix, and a Controller suffix.
For example, to retrieve the controller instance for the ngModel directive you can do:
var ngModelController = $element.data('$ngModelController');
This works as long as the ngModel directive has already been registered.
Unfortunately, ngController executes with the same priority as ngModel, and for reasons that are implementation specific, ngModel is not registered by the time that the ngController function executes. For this reason, the following does not work:
app.controller('ctrl', function ($scope, $element) {
var ngModelController = $element.data('$ngModelController');
// this alerts undefined because ngModel has not been registered yet
alert(ngModelController);
});
To fix this, you can wrap the code within $scope.$evalAsync, which guarantees that the directives have been registered before the callback function is executed:
app.controller('ctrl', function ($scope, $element) {
$scope.$evalAsync(function() {
var ngModelController = $element.data('$ngModelController');
alert(ngModelController);
});
});
Demo JSFiddle

Access to form controller hidden due to angular ui tab isolated/inherited scope

I have a simple case:
<div ng-controller="myController">
<tabset>
<tab>
<form name="myForm"></form>
</tab>
</tabset>
</div>
and now, in myController method, I would like to access myForm to call $setPristine:
$scope.myForm.$setPristine()
but I can not. tabset / tab creates isolated/inherited scope. It's just a sample, but I run into this problems when using angular js directives that create isolated scopes many times.
How can I get over this issue? In the past I did something like this (with ng-table that also creates new scope):
ng-init="$parent.saataaTable = this"
but it's far from perfect.
This was one of the most difficult concepts for me to get around and my solution is simple but kind of difficult to explain so bear with me.
Solution 1: Isolate Scopes
When you are only dealing with only isolate scopes (scope: {...}) or no scope (scope: false), you're in luck because the myForm will eventually be there. You just have to watch for it.
$scope.$watch('myForm', function(val) {
if (myForm) {
// now I can call $setPristine
}
});
Solution 2: Child Scopes
This is when you set scope: true or transclude: true. Unless you perform a custom/manual transclusion you will not get myForm on the controller's scope.
The trick is to access the form's controller directly from the form element. This can be done by the following:
// at the form element
element.data('$formController');
// or at the control (input, select, etc.)
element.inheritedData('$formController');
// where 'element' is a jqLite element (form or ng-form)
This sets you up for a new issue: how do we know when and how we can get that element and it's data.
A quick answer is that you need to set up a dummy $watch on your controller's scope to look for (in your case) myForm. When this watch is processed you will then be able to attempt to locate the form. This is necessary due to the fact that typically when your controller first executes the FormController won't yet be on the element's data object.
A quick and simple way to find the form is to simply get all of the forms. NOTE: if there are multiple forms within the element you'll have to add some logic to find the right one. In this case our form is a form element and it's the only one. So, locating it is fairly easy:
// assuming you have inject $element into your controller
$element.find('form').data('$formController');
// where $element is the root element the controller is attached to
// it is injected just like '$scope'
Once you have the controller you can access everything you would normally. It is also important to note that Solution 2 will always work once that FormController is on the element.
I have set up a Plunk to demonstrate the code here, but please note that is a demonstration so not all best practices were kept in mind.
EDIT
I found it important to note that if you don't want to worry about the scopes of the nested directives you can just watch the form name on the scope and handle things there.
$scope.$watch('myForm', function(val) {
if (angular.isDefined(val)) {
// now I have access
} else {
// see if i can `find` the form whose name is 'myForm'
// (this is easy if it is a form element and there's only one)
// then get the FormController for access
}
}
I could not make it work using the answer above, but I found a work-around.
In the form, I created a hidden input field with a ng-model and ng-init that set its value to the form. Then in my submit function in the controller I can access the formController via this ng-model
So, in the HTML, I create a hidden field inside the form:
<input id="test" ng-model="data.myForm" ng-init="data.myForm=myForm" hidden>
And in the Controller I can get hold of the formController via data.myForm
$scope.data.myForm.$setPristine();
It is probably not very good, so I will instead avoid to rely on the $pristine and $dirty properties of the formController and find another way to detect if the form has changed (using a master copy of the object, like they do in the sample in the documentation)

changing collection name in ng-repeat in $compile phase

I have a directive that makes things repeatable. For example, <div repeat-this><p>hey</p></div> will repeat <p>hey</p> , say, 5 times.
In the $compile function of repeat-this I wrap all the contents in a
<div class="repeatable" ng-repeat="item in collection">contents go here<div>
so that AngularJS takes care of replicating elements, the scope, etc for me.
However, the name of this collection is declared with a directive myCollectionName and can change depending on where the directive is (there are some ng-includes that can include more myCollectionName but consumers of this name should only use the closest one in the hierarchy). I can get the name of the collection using a directive controller in a parent element but then it is only available in the postLink function of repeat-this and I can't modify the item in collection part to make it like item in students or item in tickets. What would be a good way of doing this?
When accessing properties from scope within a directive you should never access the property directly.
For example (within a directive):
scope.myProperty = 'myval';
You should instead pass in the name of the property as a parameter to the directive:
<div myDirective="myProperty">
Inside your directive you can then use the passed in parameter to access the property:
link: function postLink(scope, element, attrs) {
var propertyAccessor = $parse(attrs.myDirective);
propertyAccessor.assign(scope, 'myval');
}
Using $parse is important because it allows you to use nested properties for instance myDirective="item.myProperty"

Resources