Find other element with same directive within same scope? - angularjs

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.

Related

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

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.

how do i bind a directive to an injected service instead of a parent or isolated scope?

related to this question: How do I create a dynamic nav which gets allowed menu items passed to it?
Basically, I have a nav outside of any views. The nav needs to display menu items which a user has access to.
I create the nav markup with a directive like so:
<tree family="treeFamily"></tree>
Where treeFamily is the data which will be used to build the navigation menu.
However, since my nav is outside of any views, it doesn't have a controller, so there is no scope variable called treeFamily. Which means the directive doesn't get any data to create a navigation.
I originally thought I could just inject a service with the data for the menu items, but then there is no way that I can see to tell an angular directive to use data taken from an injected service for binding.
The only other way that seems to be possible is to have a $rootScope variable called treeFamily and have the directive generated markup bind to that instead.
I still think you want to have a look at angular-ui router, as mentioned I in your previous question
https://github.com/angular-ui/ui-router
However, the way I'd do this without angular-ui-router is to create the service, then just inject the service in to the directive when you declare that, and use the data in there as per http://docs.angularjs.org/guide/directive.
For example:
angular.module('yourModule').service('yourService', function() {
// define your service
});
angular.module('yourModule').directive('yourDirective', function(yourService) {
return {
link: function postLink(scope, element, attrs) {
// you can now define your directive and access your yourService service
}
};
});
If you don't want to use a $rootScope variable here is a slightly hacky solution but you could get the scope by the element.
Example.
Say your data is applied to a test controller so you have a element like this
<div id="test" ng-controller="test">
You could do this example using jQuery (not required)
$('#test').scope().treeFamily
There it is you have access to the scope that you need to get your data from, demo in progress.
Demo: http://jsfiddle.net/hq26h/
In the demo the random directive is accessing the treeFamily data from the test controller when the directive is outside the controller.
If you wan't your service data to be bindable, you can do this
app.directive('something', function( $someNavDataService ) {
return function( $scope ) {
$scope.navData = $someNavDataService;
};
});

angular js: programmatically instantiate a scope?

Simple case: Ask a controller $scope to create a child scope. This new scope would be applied to a $compile's linking function -- i.e. programmatic directive instantiation.
My need falls just outside the scope: true declaration in the directive definition -- I do need a private directive scope but I don't want one created by the framework every time I instantiate the directive. Rather, I'd like to reapply the existing scope to a fresh linkage -- a fresh directive.
IOW, I want to teach a new dog old tricks.
I'm talking about the scope obtained from the compiled directive (see: Retrieving Scopes from the DOM).
Consider the scenario where the HTML representation of the directive (let's call it "half") may move in and out of and around the document. I merely want to save its state (let's call it "the other half") off and reapply it to a fresh half-compiled directive instance.
Scope hierarchy would be respected, i.e. this new directive instance would nestle itself in the same Angular-DOM area as it did in a former life so I don't think any worm holes would be opened nor anti-matter created.
A super-contrived plunk for your viewing pleasure.
If you want create private scope inside directive you can use scope.$new method like that:
app.directive('colorblock', function ($rootScope) {
link: function (scope, iElement, iAttrs, controller) {
var privateScope = $rootScope.$new(true);
}
...
});
It will create isolate scope.

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