Angularjs assign a ngmodel of element when template is loaded - angularjs

I have the following directive:
app.directive("mydirect", function () {
return {
restrict: "E",
templateUrl: "mytemplate.html",
}
});
The template from mytemplate.html is:
<input ng-model="name{{comment.ID}}" ng-init="name{{comment.ID}}={{comment.Name}}" />
I load the template several times and for each time I want to change the variable assigned as the ng-model, for example ng-model="name88" (for comment.ID == 88).
But all the loaded templates have the same value.
But when I change comment.ID, all inserted templates become the last ID changed.

First of all, you cannot put expressions, like name{{comment.ID}} in ng-model - it needs to be assigned to a variable.
So, let's change the template to:
<input ng-model="comment.ID" ng-init="comment.ID = comment.Name">
It's not entirely clear what you mean by "load the template". If you mean that you create a mydirect directive for each comment object, then you are probably doing this (or at least, you should be) with something like ng-repeat:
<div ng-repeat = "comment in comments">
<mydirect></mydirect>
</div>
This is convenient - comment is both the variable used in the ng-repeat, and the variable used for the directive's template. But this is not too reusable. What if you wanted to change the structure of the comment object? And what if you wanted to place multiple directive's side-by-side, without the child scope created for each iteration of ng-repeat and assign a different comment object to each?
For this, you should use an isolate scope for the directive. You should read more about it here, but in the nutshell, the way it works is that it allows you specify an internal variable that would be used in the template and bind it to whatever variable assigned to some attribute of the element the directive is declared on.
This is done like so:
app.directive("mydirect", function () {
return {
restrict: "E",
scope: {
// this maps the attribute `src` to `$scope.model` within the directive
model: "=src"
},
templateUrl: '<input ng-model="model.ID">',
}
});
And, let's say that you have:
$scope.comment1 = {ID: "123"};
$scope.comment2 = {ID: "545"};
Then you could use it like so:
<mydirect src="comment1"></mydirect>
<mydirect src="comment2"></mydirect>
Alternatively, if you have an array of comments, whether you create them statically or load from a service call, you could just do this:
<div ng-repeat = "comment in comments">
<mydirect src="comment"></mydirect>
</div>

Related

angular directive isolated scope property updated in link function but not reflecting on directive view

I have a case where, I have a directive which get a value form Controller $scope.colorName. Directive bind it one-way and keep this value in isolated scope "colorVar". Template of directive render "colorVar" as {{ colorVar }} .
I need to change the value of "colorVar" in link function of directive. but its not reflecting on UI.
HTML:
<div ng-app="myApp" ng-controller="appCtrl">
{{colorName}}
<my-directive color='{{colorName}}'>
</my-directive>
</div>
JavaScript:
angular.module('myApp', []).controller('appCtrl',function($scope){
$scope.colorName='red';
})
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
colorVar: '#color'
},
template: '<span> {{ colorVar }} </span><span>{{extra}}</span>',
link:function(scope,element,attrs){
scope.colorVar='orange';
scope.extra='kk';
}
};
});
In Link function I have updated scope.colorVar with 'orange' but is don't reflect on UI, but scope.exta do reflect on UI.
http://jsfiddle.net/ut7628d7/1/
Any idea what am I doing wrong. and why this is happening and how to achieve it ?
Thanks.
It depends on whether you want the directive's updated value to also change the original value in the controller or not:
Modify the original variable
If you want the directive to modify the original colorName in the controller, pass colorName by reference and use a two-way binding in the template:
<my-directive color='colorName'> <!-- instead of color='{{colorName}}' -->
and in the directive:
scope: {
colorVar: '=color' // instead of '#color'
},
http://jsfiddle.net/9szjpx9d/
The output from this will be "orange orange kk" because it's the same object throughout, so when the directive changes it to 'orange' it will affect both places.
Copy the variable
If you want the directive to only modify its own value, and output "red orange kk", then keep an attribute binding as you have now, but delay the directive link function by one tick using $timeout, so that the value it sets on scope will overwrite the value received via the directive attribute:
$timeout(function() {
scope.colorVar = 'orange';
scope.extra = 'kk';
});
Meanwhile the separate original color value will remain untouched in the controller, because it was passed to the directive as a string rather than as an object reference.
http://jsfiddle.net/mpb2cuaj/
If you want to be able to change the color form inside your directive then use a two way binding. e.g.
scope: {
colorVar: '=color'
}
HTML
<my-directive color='colorName'></my-directive>
http://jsfiddle.net/0he428hw/

Bind string value of directive to directive in ng-repeat?

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).

AngularJS - adding new values to directive's isolated 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

How do I assign an attribute to ng-controller in a directive's template in AngularJS?

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.

How do I inject a variable into a directive's scope?

Let's say I've got a directive that looks like this:
directive('attachment', function() {
return {
restrict: 'E',
controller: 'AttachmentCtrl'
}
})
That means I can write a list of 'attachment' elements like this:
<attachment ng-repeat="a in note.getAttachments()">
<p>Attachment ID: {{a.id}}</p>
</attachment>
In the above snippet, let's assume that note.getAttachments() returns a set of simple javascript object hashes.
Since I set a controller for the directive, I can include calls to that controller's scope functions inline.
Here's the controller:
function AttachmentCtrl($scope) {
$scope.getFilename = function() {
return 'image.jpg';
}
}
And here is the modified HTML for when we include a call to that $scope.getFilename function inline (the new 2nd paragraph):
<attachment ng-repeat="a in note.getAttachments()">
<p>Attachment ID: {{a.id}}</p>
<p>Attachment file name: {{getFilename()}}
</attachment>
However, this isn't useful. This will just print the same string, "image.jpg", as the file name for each attachment.
In actuality, the file name for the attachments is based on attachment ID. So an attachment with ID of "2" would have the file name of "image-2.jpg".
So our getFilename function needs to be modified. Let's fix it:
function AttachmentCtrl($scope) {
$scope.getFilename = function() {
return 'image-' + a.id + '.jpg';
}
}
But wait — this won't work. There is no variable a in the scope. We can use the variable a inline thanks to the ng-repeat, but that a variable isn't available to the scope bound to the directive.
So the question is, how do I make that a available to the scope?
Please note: I realize that in this particular example, I could just print image-{{a.id}}.jpg inline. But that does not answer the question. This is just an extremely simplified example. In reality, the getFilename function would be something too complex to print inline.
Edit: Yes, getFilename can accept an argument, and that would work. However, that also does not answer the question. I still want to know, without workarounds, whether you can get a into the scope without using it inline.
For example, maybe there is a way to inject it directly into the controller so it would be written as:
function AttachmentCtrl($scope, a) { ... }
But where would I pass it in from? Is there something I can add to the directive declaration? Maybe an ng-* attribute I can add next to the ng-repeat? I just want to know if it's possible.
But wait — this won't work. There is no variable "a" in the scope. We can use the variable a inline thanks to the
ng-repeat, but that a variable isn't available to the scope bound to
the directive.
Actually variable a is in the scope associated with the directive controller. Each controller created by the directive gets the child scope created by the ng-repeat iteration. So this works (note $scope.a.id):
function AttachmentCtrl($scope) {
$scope.getFilename = function() {
return 'image-' + $scope.a.id + '.jpg';
}
Here's a fiddle that shows the controller scope, directive scopes, and ngRepeat scopes.
"If multiple directives on the same element request new scope, only one new scope is created. " -- Directive docs, section "Directive Definition Object"
In your example, ng-repeat is creating a new scope, so all directives on that same element get that same new (child) scope.
Also, if you do ever come across a case where you need to get a variable into a controller, using attributes would be better than using ng-init.
Another way would be to use ng-init and set a model property for child scope. See this fiddle
Relevant code would be
<div ng-app='myApp' ng-controller='MyCtrl'>
<attachment ng-repeat="a in attachments" ng-init='model=a'>
<p>Attachment ID: {{model.id}}</p>
<p>Attachment file name: {{getFilename()}}</p>
</attachment>
</div>
and
function AttachmentCtrl($scope) {
$scope.getFilename = function () {
return 'image-' + $scope.model.id + '.jpg';
}
}
Just pass it into your function.
View:
<attachment ng-repeat="a in note.getAttachments()">
<p>Attachment ID: {{ a.id }}</p>
<p>Attachment file name: {{ getFilename(a) }}
</attachment>
Controller:
function AttachmentCtrl ($scope) {
$scope.getFilename = function (a) {
return 'image-' + a.id + '.jpg';
}
}

Resources