In a directive's onclick, invoke a function "pointer" - angularjs

Imagine these uses of a directive (in one HTML file):
<my-dir function="callMe()"/>
<my-dir function="someOtherFunction()"/>
Here's the important bit of the directive:
.directive('myDir', function () {
return {
scope: {
function: '&'
},
};
})
Here's the template HTML:
<div ng-click="function(5)">click here</div>
This does not work - While #function does indeed invoke #callMe and #someOtherFunction, there seems to be no way to pass the argument. How can I pass a method to the directive so that the directive can invoke the function and pass an argument?
It's pretty clear the HTML could simply refer to #callMe directly. But then the directive would not work for #someOtherFunction
EDIT - Here's a fiddle I was working on. I amended it with PSL's suggestions. It seems to work now!

Try this way:-
Specify the name of the argument in the directive arg:-
<my-dir callback="func1(arg)"></my-dir>
<my-dir callback="func2(arg)"></my-dir>
On your template provide the key value pair with the same key name as that of the function.
template:'<div ng-click="callback({arg:5})">click here</div>'
Demo
ng-onclick should be ng-click unless you are using some other angular component which provide an ng-onclick directive. Use closing tags for your directive elements.

Here's a plunk:
http://plnkr.co/edit/YQgijS?p=preview
The relevant bits:
I have no idea why this works, and I feel like it indicates there's something weird about & scope that I haven't learned yet.
When passing the variable in to the directive via an & scope, pass theFunction and not theFunction()
When calling the function in an ng-click, call fun()(args) instead of fun(args)
I know, weird, right? I'm sure there's something going on there. I asked a SO question about it a while ago, but got no response.

Related

AngularJS : Directive shell that allows user defined content with directive scope

Here's a plunker example you can see: http://plnkr.co/edit/NQT8oUv9iunz2hD2pf8H
I have a directive that I would like to turn into a web component. I've thought of several ways as to how I can achieve that with AngularJS but am having difficulty with a piece of it. I'm hoping someone can explain my misstep rather than tell me a different way to do it.
Imagine you have a directive component that sets up some shell with css classes maybe some sub components, etc.. but lets the user define the main content of the component. Something like the following:
<my-list items="ctrl.stuff">
<div>List Item: {{ item.name }}</div>
</my-list>
The HTML for the list directive could be something like the following (with OOCSS):
<ul class="mas pam bas border--color-2">
<li ng-repeat="items in item track by item.id" ng-transclude></li>
</ul>
Normally this can be solved in the link function by linking the directives scope to the new content. And it does work for other components. However introducing the ng-repeat seems to break that portion of the control. From what I can tell, the appropriate place might be the compile function but the documentation says the transcludeFn parameter will be deprecated so I'm not sure how to proceed.
I should also note that when using the beta AngularJS, there is either a bug or a new paradigm coming, because this is no longer a problem. It seems like the transcluded content always gets access to the directives scope as well as the outer controllers scope.
I really appreciate any enlightenment on this.
It's by design that content added via ng-transclude will bind with an outer controller scope, not a scope of the current element that ng-transclude is on.
You could solve the problem by copy the ng-transclude's code and modify it a bit to give a correct scope:
.directive('myTransclude', function () {
return {
restrict: 'EAC',
link: function(scope, element, attrs, controllers, transcludeFn) {
transcludeFn(scope, function(nodes) {
element.empty();
element.append(nodes);
});
}
};
});
And replace the ng-transclude with my-transclude in your directive template.
Example Plunker: http://plnkr.co/edit/i7ohGeRiO3m5kfxOehC1?p=preview

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

Why ng-hide doesn't work with custom directives?

I'm reading the directives section of the developers guide on angularjs.org to refresh my knowledge and gain some insights and I was trying to run one of the examples but the directive ng-hide is not working on a custom directive.
Here the jsfiddle: http://jsfiddle.net/D3Nsk/:
<my-dialog ng-hide="dialogIsHidden" on-close="hideDialog()">
Does Not Work Here!!!
</my-dialog>
<div ng-hide="dialogIsHidden">
It works Here.
</div>
Any idea on why this is happening?
Solution
Seems that the variable dialogIsHidden on the tag already make a reference
to a scope variable inside the directive and not to the variable in the controller; given
that the directive has it's own insolated scope, to make this work it's necesary to pass
by reference the variable dialogIsHidden of the controller to the directive.
Here the jsfiddle:
http://jsfiddle.net/h7xvA/
changes at:
<my-dialog
ng-hide="dialogIsHidden"
on-close="hideDialog()" dialog-is-hidden='dialogIsHidden'>
and:
scope: {
'close': '&onClose',
'dialogIsHidden': '='
},
You're creating an isolated scope inside your directive when asigning an object to scope. This is why $scope.dialogIsHidden is not passed through to the directive and thus the element is not being hided.
Kain's suggested adjustment for the fiddle with using $parent illustrates this.
your can change the
<my-dialog ng-hide="dialogIsHidden" on-close="hideDialog()">
to
<my-dialog ng-hide="$parent.dialogIsHidden" on-close="hideDialog()">

How to use ng-show on a directive that has an isolated scope

I have a directive that I use like this:
<dir model="data"></dir>
The directive has an isolated scope.
scope :{
model:'='
}
Now I'm trying to use ng-show on that directive using another attribute of my page's $scope, like this:
<dir ng-show="show" model="data"></dir>
But it's not working because the directive is trying to find the show attribute on its own scope.
I don't want the directive to know about the fact that its container might choose to hide it.
The workaround I found is to wrap the directive in a <div> and apply ng-show on that element, but I don't like the extra element this forces me to use:
<div ng-show="show" >
<dir model="data"></dir>
</div>
Is there a better way of doing this?
See this plunker: http://plnkr.co/edit/Q3MkWfl5mHssUeh3zXiR?p=preview
Update: This answer applies to Angular releases prior to 1.2. See #lex82's answer for Angular 1.2.
Because your dir directive creates an isolate scope, all directives defined on the same element (dir in this case) will use that isolate scope. This is why ng-show looks for property show on the isolate scope, rather than on the parent scope.
If your dir directive is truly a standalone/self-contained/reusable component, and therefore it should use an isolate scope, your wrapping solution is probably best (better than using $parent, IMO) because such directives should normally not be used with other directives on the same element (or you get exactly this kind of problem).
If your directive doesn't need an isolate scope, your problem goes away.
You could consider migrating to Angular 1.2 or higher. The isolate scope is now only exposed to directives with a scope property. This means the ng-show is not influenced by your directive anymore and you can write it exactly like you tried to do it in the first place:
<dir ng-show="show" model="data"></dir>
#Angular-Developers: Great work, guys!
Adding the following into the link function solves the problem. It's an extra step for the component creator, but makes the component more intuitive.
function link($scope, $element, attr, ctrl) {
if (attr.hasOwnProperty("ngShow")) {
function ngShow() {
if ($scope.ngShow === true) {
$element.show();
}
else if($scope.ngShow === false) {
$element.hide();
}
}
$scope.$watch("ngShow", ngShow);
setTimeout(ngShow, 0);
}
//... more in the link function
}
You'll also need to setup scope bindings for ngShow
scope: {
ngShow: "="
}
Simply use $parent for the parent scope like this:
<dir ng-show="$parent.show" model="data"></dir>
Disclaimer
I think that this is the precise answer to your question but I admit that it is not perfect from an aesthetical point of view. However, wrapping the <div> isn't very nice either. I think one can justify it because from the other parameter passed to the isolate scope, it can be seen that the directive actually has an isolate scope. On the other hand, I have to acknowledge that i regularly forget the $parent in the first place and then wonder why it is not working.
It would certainly be clearer to add an additional attribute is-visible="expression" and insert the ng-show internally. But you stated in your question that you tried to avoid this solution.
Update: Won't work in Angular 1.2 or higher.

Angular UI select2 with tags not working inside custom directive

Using the Angular UI Select2 directive, with tags defined on an input field. If the input is itself inside a custom directive, then it is not initialised correctly and the console gives an error:
query function not defined for Select2 tagging
I suspect this might be to do with the order in which the directives are compiled / linked vs when the select 2 function is called.
Maybe there is a simple workaround, perhaps using the compile function or a directive controller instead of a link function? Or maybe it is an issue with the Angular UI select2 directive.
I have made a plunker that displays the problem:
http://plnkr.co/edit/myE5wZ
So my question is - How do you get get select2 tags working from inside a custom Angular directive?
In the end I managed to find a solution I was happy with involving nesting two directives, that way the logic can be encapsulated inside the parent directive (not spilling out into the controller).
A Plunker of my solution is here for anyone who may stumble across the same issue:
http://plnkr.co/edit/ZxAPF5BzkgPtn9xddCRM
I just encountered this today and summarily realized the fix:
PostLinking functions are executed in reverse order (deepest grandchild to greatest grandparent).
Put your custom modal's code (or anything that sets $scope data for use in its children) inside a PreLinking function. PreLinking functions go from parent to child, and all PreLinking functions are performed before the PostLinking functions.
I had a similar issue. Your solution works but IMHO I think an even better solution is to use the controller function instead of the link function inside the directive. Doing this you do need nested directives.
By using the controller function instead of the link function in the directive it's working. Example:
function myFunction() {
var dir = {};
dir.scope = { myModel: '=' };
dir.restrict = 'E';
dir.templateUrl = 'myTemplate.html';
dir.replace = true;
dir.controller = function ($scope) {
$scope.myVar = ...;
};
return dir;
};
I have this error too. My short solution:
<input type="hidden"
name="citizenship"
class="form-control input-sm col-sm-10"
style="width:500px"
multiple
ui-select2="params.options.citizenshipOptions"
ng-model="cvLang.content.citizenship"
ng-repeat="a in [1]"
/>
ng-repeat="a in [1]" is a magical thing!!! It is not clear for me a logic of a context, but this is working. May be this can help?

Resources