changing collection name in ng-repeat in $compile phase - angularjs

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"

Related

AngularJs apply custom directive to HTML conditionally

Is there a right way to apply custom directive to HTML template based on some condition
Eg: <li my-custom-directive="{{item}}">
I need to apply "my-custom-directive" only if {{item}} is defined.
This feels like a design problem rather than a technical one.
Rather than apply the custom directive conditionally, simply figure out what to do inside the directive. Semantically, this makes more sense.
For instance, if item is undefined in this case, simply don't do something inside the directive.
Use ng-if, DOM is not inserted until condition is met.
AngularJS leaves a comment within the DOM for its reference,
so <li my-custom-directive="{{item}}"> would not be within the DOM at all until {{item}} is defined.
If you need to add directives dynamically to the DOM from a variable, use $compile provider. I've created myself a directive for such things
angular.module('test', []).directive('directiveName', ['$compile', function($scope) {
return {
link: function($scope, element, attrs, ctrl) {
element.replaceWith($compile(attrs.direciveName)($scope))
}
}
}]);
And you can use it as such:
<div directive-name="{{customDirectiveName}}"></div>
{{customDirectiveName}} being a $scope variable from somewhere else. From this point you could ng-repeat on JSON objects recieved from server, ect.
It depends on your requirement , if you use it has as element instead of attribute you can achieve using ng-if.
for ex in the below code li wouldnt appear in the dom as and when item is undefined,
<my-custom-directive="{{item}}" ng-if="item">
<li>{{item}}</li>
</my-custom-directive>

Find other element with same directive within same scope?

Is there any way to find other element with same directive applied within same scope?
I want to use this info to create matching input values directive.
e.g. I might have a template with a few inputs which all using same directive:
<div ng-controller="MainCtrl">
<input type="text" same-value>
<input type="text" same-value>
</div>
Primarily I would use this for comparing password & password repeat, but I wanted to make this more general directive.
You can create a parent directive and communicate between children and parent via the controller or you can create a service that is shared across instances.
Another option is to provide shared data and functionality when the directive is declared, as in:
angular.module("whatever").directive("sameValue", function() {
var sameValueInstances = [];
return {
link: function(scope, elem, attr) {
sameValueInstances.push(scope);
// register listener to remove from array when destroyed..
// do something with the instance array
sameValueInstances.forEach(...);
}
};
});
There is more than one instance of the directive, but there is only one instance of the declaration of the directive (the function itself). The declaration is shared and therefore sameValueInstances are shared. When angular instantiates a directive, it will create a new scope and cal the link function. Each instance gets its own scope, which is why we put the scopes on the instances. We then use whatever data we add to the scope to identify instances and can use functions on scope to communicate.

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

How can I pass an ng-repeated object to my AngularJS directive?

Take a look at my code here...
Controller ("tasks" is an array of JSON objects resolved in my Routes.js):
app.controller('testCtrl', function(tasks){
$scope.tasks = tasks.data;
};
HTML:
<div class="client-wrapper">
<div class="task-wrapper" ng-repeat="taskData in tasks">
<div task="taskData">
</div>
</div>
</div>
Directive:
app.directive('task', function(){
return {
restrict: 'A',
scope: {taskData: '=taskData'},
templateUrl: "/templates/directives/task.html",
link: function(scope, element, attribute) {
console.log(scope.taskData);
}
};
});
For some reason, I seem incapable of figuring out how to pass the current object being looped through in the tasks array to this directive so that I can manipulate it therein.
I've tried numerous solutions, as seen below:
how to pass a json as a string param to a directive <--- which tells me to output {{ object }} inside if an HTML attribute, and then $eval that back to JSON in the directive...
That's a very gross way of doing it, and I definitely don't want to do it that way (nor do I think this will allow it to two-way-bind back to the actual object in the tasks array in the controllers scope. This method just converts the JSON to string --> evals back to JSON + makes a copy of that string inside the directives scope).
https://groups.google.com/forum/#!msg/angular/RyywTP64sNs/Y_KQLbbwzzIJ <-- Same as above, they're saying to output the JSON as a string in an attribute, and then $eval it back to JSON... won't work for me for the same reasons as the first link.
http://jsfiddle.net/joshdmiller/FHVD9/ <-- Copying his exact syntax isn't possible because the data I want to pass to my directive is the current index of the tasks array while being ng-repeated... This is close, but doesn't work within the constraints of ng-repeat apparently?
Angularjs pass multiple arguments to directive ng-repeat <-- This syntax isn't working for me -- if I attempt to pass the taskData object (the current representation object in the array being looped through) in a parameter, it passes the literal string "taskData" and not the object itself? At this point I'm really scratching my head.
What I'm trying to accomplish (since I might be going about this in the wrong way entirely and feel I should explain the problem as a whole):
I have a bunch of data called tasks. They have a few properties, such as:
completed: true || false
description: a string
tags: an array of strings
Etc, etc.
I am outputting a big table of rows for all of these tasks. Each row will be a directive, with some buttons you can press on that row in order to change the data pertaining to the task on this row.
I want to have the functions to manipulate the data of each individual task inside the link function of the directive. So like markAsCompleted() would be a function within the directive, that would update the completed boolean of whichever task was being clicked on.
I am doing it this way, because as I see it I have two options:
A function in the controller where I pass in the task to modify as a parameter, and then perform the data manipulation
Or a function in the angular directive that just manipulates the data of the object attached to this particular directive (and my current issue is my apparent inability to two-way bind an object to this particular directive)
I want to be able to accomplish the second option in order to make my directive modular and stand-alone.
So yeah. I'm confused as to how to go about doing this and would greatly appreciate some insight as to where I'm going wrong :)
scope: {taskData: '=taskData'} means Angular expects an attribute called task-data with the value to be passed in. So give this a try...
<div class="client-wrapper">
<div class="task-wrapper" ng-repeat="taskData in tasks">
<div task task-data="taskData">
</div>
</div>
</div>
In your current attempt your directive is expecting an attribute called task-data.
This should fix your problem:
app.directive('task', function(){
return {
restrict: 'A',
scope: {task: '='},
templateUrl: "/templates/directives/task.html",
link: function(scope, element, attribute) {
console.log(scope.taskData);
}
};
});
Notice i changed the name of your property in the directive's scope from task-data to task

Custom hierarchical AngularJS directives with bi-directional binding

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.

Resources