Dynamic variable name directive attribute - angularjs

I have a my-edit directive that has a value attribute expecting a scope variable to bind to.
<my-edit value="myVar"></my-edit>
is there any way to do something like this:
<my-edit value="{{varName}}"></my-edit>
where varName = "myVar"
I want to nest this directive in "my-listbox" directive that has a "text-field" attribute
<my-listbox ng-model="myList" text-field="itemProp"></my-listbox>
So I was trying use a template like:
<div>
<ul>
<li ng-repeat="item in items">
<my-edit value="item.{{textField}}"></my-edit>
</li>
</ul>
</div>
But obviously it doesn't work
I guess using a text binding is also not the solution.
Is a dynamic generated template for "my-listbox" the way to go here?
I tried that in the compile function but the didn't work that well because of the nested neRepeat directive. Should it by done using $compile in link function?
Thanks

This is something pretty cool about angular, it evaluated the string that you pass to the directive. This means that you can actually just do value="item[textField]" and that will work.
For instance if you had a controller with data like this:
$scope.data = {
test: 'test_val',
other: 'other_val'
};
$scope.val = 'test';
You could just pass it like this to your directive:
<directive value="data[val]"></directive>
That is set up like this:
scope: {
value: '='
},
And the isolate scope will have scope.value = 'test_val' and will update to 'other_val' just by changing the original controller val to 'other'
I made a fiddle where this is set up for you to play with.
Hope this helps!

Related

Directive with ng-model Attribute Not Resolving Using $http

Trying to make a rating directive but I'm stuck at getting rating2 to work. The first rating worked because the rating1 is hardcoded within the controller. But normally I have to get the saved rating from the db, which I'm trying to do with rating2, as u can see the value is fetched but the directive is not appearing.
https://codepen.io/eldyvoon/pen/MbBNLP
<div star-rating ng-model="rating.rating1" max="10" on-rating-select="rating.rateFunction(rating)"></div>
<br>but rating2 is actually there:
{{rating.rating2}}
<star-rating ng-model="rating.rating2" readonly="rating.isReadonly"></star-rating>
Need expert of directive to help.
Initiate rating2 :
function RatingController($http) {
this.rating1 = 5;
this.rating2 = 0; //ADD THIS LINE
var self = this;
it works for me
check here
First of all, I'm not a directive expert but i'm trying to help. I think that when html is first load, the values from db not finish execute and bind into html. The best way is not using directive instead using controller to fetch data from db.
You pass a model without rating2 into your directive and the changes from the parent controller won't affect it, because variable is created afterwards. Adding a watcher in your linker on parent scope will solve the problem;
scope.$parent.$watch('', function(rating){
updateStars();
});
Other solution would be to define a starting value in your controller.
this.rating2 = 1;
Notice that it is bad design to have a scope variable for each rating. It is cleaner to have an array of ratings and you actually do not need the watcher by doing so.
https://codepen.io/hoschnok/pen/LbJPqL
angular controller
function RatingController($http) {
this.ratings = [4];
var self = this;
$http.get('https://api.myjson.com/bins/o0r69').then(function(res){
self.ratings.push(res.data.rating2);
});
}
HTML
<div ng-app="app" ng-controller="RatingController as rating" class="container">
<div ng-repeat="r in rating.ratings">
<div star-rating ng-model="r" max="10" on-rating-select="rating.rateFunction(rating)"></div>
</div>
</div>
The watcher change handler function has parameters reversed:
//INCORRECT parameters
//scope.$watch('ratingValue', function(oldValue, newValue) {
//CORRECT parameters
scope.$watch('ratingValue', function(newValue, oldValue) {
if (newValue) {
updateStars();
}
});
The first argument of the listening function should be newValue.
The DEMO on CodePen
ALSO
The ng- prefix is reserved for core directives. See AngularJS Wiki -- Best Practices
JS
scope: {
//Avoid using ng- prefix
//ratingValue: '=ngModel',
ratingValue: '=myModel',
max: '=?', // optional (default is 5)
onRatingSelect: '&?',
readonly: '=?'
},
HTML
<!-- AVOID using the ng- prefix
<star-rating ng-if='rating' ng-model="rating.rating2"
max="10" on-rating-select="rating.rateFunction(rating)">
</star-rating>
-->
<!-- INSTEAD -->
<star-rating ng-if='rating' my-model="rating.rating2"
max="10" on-rating-select="rating.rateFunction(rating)">
</star-rating>
When a custom directve uses the name ng-model for an attribute, the AngularJS framework instantiates an ngModelController. If the directive doesn't use the services of that controller, it is best not to instantiate it.

Angularjs assign a ngmodel of element when template is loaded

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>

Angular.js: Wrapping elements in a nested transclude

This seems like such a simple thing, but I am just not able to wrap my head around how to do it.
Here is what I want:
<my-buttons>
<my-button ng-click="doOneThing()">abc</my-button>
<my-button ng-click="doAnotherThing()">def</my-button>
</my-buttons>
That turns into something like this:
<ul class="u">
<li class="l"><button ng-click="doOneThing()">abc</button></li>
<li class="l"><button ng-click="doAnotherThing()">def</button></li>
</ul>
Notice how the ng-click is on the button, inside a wrapping li. However, the normal transclusion will place the ng-click on the li.
My best try is on this fiddle: http://jsfiddle.net/WTv7k/1/ There I have replaced the ng-click with a class, so it is easy to see when it works and not.
Any ideas of how to get this done? If it is really easy, maybe the tabs/pane example on the frontpage could be expanded to include a wrapper around the panes, while still keeping the attributes.
With replace:true the replacement process migrates all of the attributes / classes from the old element (<my-button ...>) to the new one (the root element in the template, <li ...> ). Transclude moves the content of the old element to the specified (ng-transclude) element. I'm not sure if there's a simple way to change which element in the template that will receive the migrated attributes.
To achieve what you want you could probably do some dom manipulation in a custom compile function in the my-button directive. However, I think it'd be a better idea to create a new isolate scope in the my-button directive:
<my-buttons>
<my-button click-fn="doOneThing()">abc</my-button>
<my-button click-fn="doAnotherThing()">def</my-button>
</my-buttons>
(notice I've changed ng-click to click-fn)
module.directive('myButtons', function () {
return {
restrict:'E',
transclude:true,
replace:true,
template:'<ul class="u" ng-transclude></ul>'
}
});
module.directive('myButton', function () {
return {
scope:{clickFn:'&'},
restrict:'E',
transclude:true,
replace:true,
template:'<li class="l"><button ng-click="clickFn()" ng-transclude></button></li>'
}
});
I've also made a working version of your fiddle.
To understand how the isolate scope works (scope:{clickFn:'&'}) I recommend you read the angular guide on directives.

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';
}
}

Getting to bound data in Angular.js

Is there a way in angular to get binding back from a template?
In other words, if I have something like this:
<div ng-repeat="item in list">
<div>{{item.name}}</div>
<div>{{item.state}}</div>
</div>
would it be possible to change the item's state by clicking on it, because the repeated div would "remember" what item it was built from?
Yes, you can use the ng-click directive to trigger a method on the current scope:
// In your view's controller:
function MyCtrl($scope, MyList) {
// You probably have something like that already to
// populate your list, using a $resource or $http GET call.
// Here I use a $resource which would be defined on your module.
$scope.list = MyList.query()
$scope.setState = function(state) {
// "this" refers to the current scope
this.item.state = state
}
}
// And in your view:
<div ng-repeat="item in list">
<div>{{item.name}}</div>
<div ng-click="setState('whatever')">{{item.state}}</div>
</div>
Or you can simply set an expression such as ng-click="item.state='whatever'" directly on the div, although this is less testable - only in end-to-end tests - and less flexible, say you want to introduce validation or something).
HTH

Resources