I created a custom directive with an isolated scope that binds to a function from the enclosing controller and with references to a templateUrl. Here's what my code looks like:
the html
<div ng-controller='MyCtrl as my'>
<custom-directive data='my.data' on-search="my.find(param1, param2)"></custom-directive>
</div>
the directive
app.directive('customDirective', function() {
return {
restrict : 'E',
scope : {
data : '=data'
search : '&onSearch',
},
templateUrl : 'customDirective.html'
};
});
the template
<div>
<input ng-model='data.id'>
<a ng-click='find(param1, param2)'></a>
</div>
The arguments received by function find is also stored in data. The controller data binds to the directive but not the function. My log inside the function won't even show.
It seems there are different ways to do it as I have seen in many examples (see below) but none seems to work in my case.
Example 1: pass a mapping of parameter and values in the template
<div>
<input ng-model='data.id'>
<a ng-click='find.({param1: data.value1, param2: data.value2})'></a>
</div>
Example 2: put a link in the directive
app.directive('customDirective', function() {
return {
restrict : 'E',
scope : {
data : '=data'
search : '&onSearch',
},
templateUrl : 'customDirective.html',
link : function(scope, elem, attr) {
scope.retrieve({param1: scope.data.value1,
param2: scope.data.value2});
}
};
});
Example 3 : use scope.$apply(), $parse in link but haven't tried this
Could someone show me how to do it and also explain to me the link part (I don't understand that part) and if you're feeling generous, show the working alternatives as shown by the examples. Thanks
You don't have to passe params for your function just the reference so in your html
<custom-directive data='my.data' on-search="my.find"></custom-directive>
and your template directive directly call
<div>
<input ng-model='data.id'>
<a ng-click='find(data.value1, data.value2)'></a>
</div>
I also suggest you to use $scope and not the controller. So in your controller define
$scope.data = {
id: 1,
value1: "value1",
value2: "value2"
}
$scope.find = function (param1, param2) {
//Your logic
}
And in your template put directly
<custom-directive data='data' on-search="find"></custom-directive>
I hope this answer to your question
About link this text from angular js doc is pretty clear I think
Directives that want to modify the DOM typically use the link option.
link takes a function with the following signature, function
link(scope, element, attrs) { ... } where:
scope is an Angular scope object. element is the jqLite-wrapped
element that this directive matches.
attrs is a hash object with key-value pairs of normalized attribute names and their
corresponding attribute values.
In our link function, we want to update the
displayed time once a second, or whenever a user changes the time
formatting string that our directive binds to. We will use the
$interval service to call a handler on a regular basis. This is easier
than using $timeout but also works better with end-to-end testing,
where we want to ensure that all $timeouts have completed before
completing the test. We also want to remove the $interval if the
directive is deleted so we don't introduce a memory leak.
Related
I may be totally overlooking the big picture here, but what I'm trying to do, is conditionally include directives based on the object that I'm drawing my form with. Example:
$scope.formItems = [
{type : 'text', directive: 'google-country'},
{type : 'text', directive: 'google-city'},
]
This is a very very small breakdown of an object of about 40 fields, however I just wanted to be able to parse a string representation of the directive name to the value of directive in the object and have it output and run said directive on the form:
<div class="fields" ng-repeat="field in formItems">
<input type="{{field.type}}" {{field.directive}} />
</div>
Is this possible? or do I have to do something different?
I believe the problem is that the directive it self doesn't evaluate. this is how the above ng-repeat will eval:
<input type="text" {{field.directive}}>
EDIT:
I've now restricted the directive to a class, and simply included the field.directive tag inside the class and that should bind right? nup. It evaluated the right string, however the directive wasn't bound. I then did another test to make sure the directive was working by hard coding the name and that worked fine! So I'm thinking that the directives are bound before this scope is evaluated?
{{field.directive}} isn't interpolated to element attribute. It should be used either as attribute value or as text node.
app.directive('directive', function ($compile) {
return {
restrict: 'A',
priority: 10000,
link: function (scope, element, attrs) {
var oldDirective;
attrs.$observe('directive', function (directive) {
if (directive && element.attr(directive) === undefined) {
oldDirective && element.attr(oldDirective, undefined);
oldDirective = directive;
element.attr(directive, '');
$compile(element)(scope);
}
});
}
};
});
For example,
<div directive="ng-show">...</div>
It does the trick but looks like a hack, there may be more appropriate ways to design the form. 'google-country' and 'google-city' could be parameters for common directive rather than input directives.
So I'm thinking that the directives are bound before this scope is
evaluated?
That's right, the scope isn't yet ready when compile takes place. And you get interpolated attribute (including class) values only in link, so $compile should be run at this stage for the directives to take effect.
You need to create an attribute directive that as a parameter will receive the dynamic directive you want. In this directive you need to implement the compile function - this is where you will remove the current directive with the element parameter: element.removeAttr() method and add the dynamic directive to the element with element.attr(). The compile function can return the postlink function, which you should also implement in order to now recompile the element: $compile(element)(scope).
I have a directive with isolated scope as following:
application.directive("myDirective",function(){
return {
restrict: "A",
scope: {myDirective:"="},
link : function(scope) {
console.log("My directive: ",scope.myDirective) // works fine
scope.person={name:"John",surname:"Doe"}
scope.hello=function(){
console.log("Hello world!")
}
}
}
})
Corresponding view:
<div my-directive='{testValue:3}'>
Testvalue: {{myDirective}}<br/>
Hello {{person}}
<button ng-click="hello()">Say hello</button>
</div>
And it seems that i cannot use any of the fields declared in the scope. In view the "myDirecive" and "person" fields are blank and the scope's "hello" function is not executed when i press the button.
It works fine when i pass scope="true" to the directive but does not work in isolated scope.
Am i missing something here or maybe there is no way to introduce variables to the isolated scope of a directive?
UPDATE
Updated question presenting why i would rather not to use a static template. The effect i am trying to achieve is to make directive that allows to upload any html form getting the form initial data via rest/json. The whole process is rather complex and application specific therefore i cannot use any available form libraries. I present below the simplified version of the use case:
The updated directive
application.directive("myForm",function(){
return {
restrict: "A",
scope: {myForm:"="},
link : function(scope) {
console.log("Form parameters: ",scope.myForm) // works fine
scope.formData=... // Get form initial data as JSON from server
scope.submitForm=function(){
// Send scope.formData via REST to the server
}
}
}
})
The case when i would like to use this form. Of course i would like to use this directive many times with different forms.
<form my-form='{postUrl:'/myPostUrl',getFormDataUrl:'/url/to/some/json}'>
<div>Form user: {{formData.userName}} {{formData.userSurname}}
<input type="text" ng-model="formData.userAge" />
<input type="text" ng-model="formData.userEmail" />
<button ng-click="submitForm()">Submit</button>
</form>
I hope this explains why i cannot use one static html template for this scenario.
Maybe someone can explain why this is working with scope="true" and with an isolated scope i cannot access any scoped variables?
With Angular, directives either work with a template (or templateUrl) or with transcluded content.
If you're using a template, then the template has access to the isolate scope. So, if you put {{person}} in the template it would work as expected.
If you're using transcluded content - that is, the content that is a child of the node which has the directive applied to it - then, not only would you need to set transclude: true and specify where in the template the transcluded content goes - e.g. <div ng-transclude></div> to even see the content, you would also not get the results you expect, since the transcluded content has access to the same scope variables as the parent of the directive, and not to those available in the isolate scope of the directive.
Also, you should be aware that if you pass a non-assignable object to the directive's isolate scope with "=" - like you did with my-directive="{testValue: 3", then you can't make any changes to it (and, unfortunately, even to its properties even if they are scope variables).
So, to make your specific case work, do this:
application.directive("myDirective",function(){
return {
...
template: "Testvalue: {{myDirective}}<br/> " +
"Hello {{person}} " +
"<button ng-click="hello()">Say hello</button>";
};
});
and the corresponding view:
where prop is set in the View controller to: $scope.prop = {testValue: 3};
You can always alter the default behavior of transcluded scope ( though I don't recommend it):
application.directive("myDirective",function(){
return {
tranclude: true,
scope: {myDirective:"="},
link : function(scope, element, attrs, ctrl, $transclude) {
$transclude(scope, function(clone) {
element.empty();
element.append(clone);
});
scope.person={name:"John",surname:"Doe"};
scope.hello=function(){
console.log("Hello world!");
};
}
};
});
See the docs: https://docs.angularjs.org/api/ng/service/$compile#transclusion-functions
Also take a look at ngTranslude source code: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngTransclude.js
I have a directive which consists of a form text element and a continue button along with the associated controller etc. This directive is going to be used in about 5 different pages, but on each page it is used the continue button will do something different.
My question is where can/should I put the code for the continue button if it does different things for each page?
Since its a directive I cant simply pass a different function into ng-click depending on what page im on (ie, if i simply replicated the code on each page it is used I could simply change the function called on ng-click and have that function in each of the page controllers.
Hopefully Im not being too vague with my question and you can make sense of what im asking. If not just say so and ill try to explain in more detail.
I would really appreciate some guidance on this matter.
Thanks.
There are two ways that you can do it. If you are creating your directive as a true component you can use isolated scope with & binding that binds to an expression.
Assume your directive looks like
<div do-work on-click="save()"></div>
and the generated html
<div>
<input ...>
<button ng-click="doAction()"><button>
</div>
The directive scope will be defined
scope:{
onClick:'&'
}
In your directive controller or link function you need to implement the button doAction, which in turns evaluates the onClick action
scope.doAction=function() {
scope.onClick({//if params are required});
}
Now you have linked the parent through the direct onClick reference. One thing to remember here is that this creates a directive with isolated scope.
In case you do not want isolated scope created you need to use
scope.$eval(attr.onClick); // this evaluates the expression on the current scope.
Hope this helps.
Ideally you should not create directives which are not re-usable.
In your case, you may do it like following -
create an isolated scope in the directive
add a function to be called and pass the page/ page id as parameter
call functions in controller based on parameter
Directive
myApp.directive('someDirecive', function () {
return {
// restrict options are EACM. we want to use it like an attribute
restrict: 'A',
// template : <inline template string>
// templateUrl = path to directive template.
// templateUrl: '',
scope: {
onButtonClick : '&'
},
controller: function ($scope, $element, $attrs, $transclude) {
$scope.onButtonClick = function(pageId) {
if (pageId == 1) {
// do something
}
else if (pageId == 2) {
// do something
}
}
},
//link: function (scope, iElement, iAttrs) {
//}
};
});
HTML
<div some-directive on-button-click="DoSomething(1)" />
I have a custom attribute directive (i.e., restrict: "A") and I want to pass two expressions (using {{...}}) into the directive as attributes. I want to pass these attributes into the directive's template, which I use to render two nested div tags -- the outer one containing ng-controller and the inner containing ng-include. The ng-controller will define the controller exclusively used for the template, and the ng-include will render the template's HTML.
An example showing the relevant snippets is below.
HTML:
<div ng-controller="appController">
<custom-directive ctrl="templateController" tmpl="template.html"></custom-directive>
</div>
JS:
function appController($scope) {
// Main application controller
}
function templateController($scope) {
// Controller (separate from main controller) for exclusive use with template
}
app.directive('customDirective', function() {
return {
restrict: 'A',
scope: {
ctrl: '#',
tmpl: '#'
},
// This will work, but not what I want
// Assigning controller explicitly
template: '<div ng-controller="templateController">\
<div ng-include="tmpl"></div>\
</div>'
// This is what I want, but won't work
// Assigning controller via isolate scope variable from attribute
/*template: '<div ng-controller="ctrl">\
<div ng-include="tmpl"></div>\
</div>'*/
};
});
It appears that explicitly assigning the controller works. However, I want to assign the controller via an isolate scope variable that I obtain from an attribute located inside my custom directive in the HTML.
I've fleshed out the above example a little more in the Plunker below, which names the relevant directive contentDisplay (instead of customDirective from above). Let me know in the comments if this example needs more commented clarification:
Plunker
Using an explicit controller assignment (uncommented template code), I achieve the desired functionality. However, when trying to assign the controller via an isolate scope variable (commented template code), it no longer works, throwing an error saying 'ctrl' is not a function, got string.
The reason why I want to vary the controller (instead of just throwing all the controllers into one "master controller" as I've done in the Plunker) is because I want to make my code more organized to maintain readability.
The following ideas may be relevant:
Placing the ng-controller tags inside the template instead of wrapping it around ng-include.
Using one-way binding ('&') to execute functions instead of text binding ('#').
Using a link function instead of / in addition to an isolate scope.
Using an element/class directive instead of attribute directive.
The priority level of ng-controller is lower than that of ng-include.
The order in which the directives are compiled / instantiated may not be correct.
While I'm looking for direct solutions to this issue, I'm also willing to accept workarounds that accomplish the same functionality and are relatively simple.
I don't think you can dynamically write a template key using scope, but you certainly do so within the link function. You can imitate that quite succinctly with a series of built-in Angular functions: $http, $controller, $compile, $templateCache.
Plunker
Relevant code:
link: function( scope, element, attrs )
{
$http.get( scope.tmpl, { cache: $templateCache } )
.then( function( response ) {
templateScope = scope.$new();
templateCtrl = $controller( scope.ctrl, { $scope: templateScope } );
element.html( response.data );
element.children().data('$ngControllerController', templateCtrl);
$compile( element.contents() )( templateScope );
});
}
Inspired strongly by this similar answer.
I am using AngularJS v1.2.1.
The improved ng-bind-html directive allows me to trust unsafe Html into my view.
Example
HTML:
<div ng-repeat="example in examples" ng-bind-html="example.content()"></div>
JS:
function controller($scope, $sce)
{
function ex()
{
this.click = function ()
{
alert("clicked");
}
this.content() = function ()
{
//if
return $sce.trustAsHtml('<button ng-click="click()">some text</button>');
// no problem, but click is not called
//when
return $sce.parseAsHtml('<button ng-click="click()">some text</button>');
//throw an error
}
}
$scope.examples = [new ex(), new ex()];
}
My question is, how to bind HTML content that may contain Angular expressions or directives ??
If you need dynamic templates per element, as your question suggests, one solution would be to use $compile within a directive to parse the HTML within the context of the local scope. A simple version of this is shown in this Plunk.
An example directive:
app.directive('customContent', function($compile) {
return function(scope, el, attrs) {
el.replaceWith($compile(scope.example.content)(scope));
}
});
The corresponding HTML:
<div ng-repeat="example in examples">
<div custom-content></div>
</div>
Notice that, in the Plunk controller, I've pulled out the click function into the scope for simplicity, since in the template HTML you are calling click() in the context of the scope, not on the example object. There are a couple ways you could use a different click function for each example, if that's what you'd like to do. This egghead.io screencast has a good example of passing an expression into a directive explicitly; in your case, it could be a click function or the whole example object, depending on what you need.