I'm somewhat new to AngularJs, so forgive me if this is a newb question, but I've looked around a bit and haven't been able to figure this out.
I'm trying to send an attribute of an object into a directive and I'm not quite sure why this isn't working.
I've got a scope variable that's an object, something like:
$scope.player = {name:"", hitpoints:10};
In my HTML, I'm attempting to bind that to a directive:
<span accelerate target="player.hitpoints" increment="-1">Take Damage</span>
In my directive, I'm attempting to modify player.hitpoints like this:
scope[attrs.target] += attrs.increment;
When I trace it out, scope[attrs.target] is undefined, even though attrs.target is "player.hitpoints." When I use target="player", that traces out just fine but I don't want to have to manipulate the .hitpoints property explicitly in the directive.
Edit: I've made a jsfiddle to illustrate what I'm trying to do: http://jsfiddle.net/csafo41x/
There is a way to share scope between your controller and directive. Here is very good post by Dan Wahlin on scope sharing in Directive - http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-2-isolate-scope
There are 3 ways to do so
# Used to pass a string value into the directive
= Used to create a two-way binding to an object that is passed into the directive
& Allows an external function to be passed into the directive and invoked
Just a very basic example on how the above mentioned scope are to be used
angular.module('directivesModule').directive('myIsolatedScopeWithModel', function () {
return {
scope: {
customer: '=' //Two-way data binding
},
template: '<ul><li ng-repeat="prop in customer">{{ prop }}</li></ul>'
};
});
There are a number of things going on here:
#1 - scope
Once you define your isolated scope (along the lines of #Yasser's answer), then you don't need to deal with attrs - just use scope.target.
#2 - template
Something actually needs to handle the click event. In your fiddle there is just <span class="btn"...>. You need ng-click somewhere. In your case, you probably want the directive to handle the click. So modify the directive's template and define the click handler in the directive's scope:
...
template: "<button class='btn' ng-click='click()'>Button</button>",
link: function(scope, element, attrs)
{
scope.click = function(){
scope.target += parseInt(attrs.increment);
}
}
...
#3 - transclude
Now, you need to get the contents of the directive to be the contents of the button within your directive's template. You can use transclude parameter with ng-transclude - for location, for that. So, the template above is modified to something like the following:
...
template: "<button class='btn' ng-click='click()'><div ng-transclude</div></button>",
transclude: true,
...
Here's your modified fiddle
Related
I would like to create a directive that has transcluded content that the directive can bind to and modify. The directive has an isolate scope. I imagine it working something like this:
<my-directive bound-item-name="childObj">
<input ng-model="childObj.someField">
</my-directive>
At runtime, I want to use childObj as an alias for an object on my-directive's isolate scope called activeObject. Essentially, you might think of this as similar to the way ng-repeat lets you use a statment like obj as alias in objList and in the transcluded content alias refers to the individual instance.
I can't seem to figure out how I can actually do this... if I change the transluded content to refer to $parent.activeItem it does work the way I intended, but I feel like that's expecting the transcluded content to know too much about how the directive works. It seems like modifying in the compile function might work, except I can't see, in the docs, how I can actually do that with the transcluded content. Forcing the transcluded content to share its scope with the directive would be OK, although I see no evidence that there's some way to do that.
This must be possible, but how?
Fiddling around with this some more, I am able to get it to work by modifying scope.$$childHead[scope.boundItemName] instead of using scope.activeObject in the directive. While this works I'd like to not rely on undocumented internal objects, if possible.
The link function of the directive is given the transclude function as the 5th parameter.
link: function(scope, element, attrs, ctrls, transclude){
// ...
}
This transclude function takes a scope variable that you can create and another function - called "clone linking function" - that places the pre-linked transcluded content in the DOM. The transclude function links against that scope variable that you provided.
Here's how it works.
transclude: true,
scope: {}, // you are free to use whatever scope you need
link: function(scope, element, attrs, ctrls, transclude){
var boundObj = {}; // your object
var alias = attrs.boundItemName;
// let's create an isolate scope for the transcluded content
var newScope = scope.$new(true);
newScope[alias] = boundObj;
transclude(newScope, function(preLinkContent){
element.append(preLinkContent);
});
}
Then, if you used your example:
<my-directive bound-item-name="foo">
<input ng-model="foo.text">
</my-directive>
Then, the transcluded ng-model would write into your internal boundObj's .text property.
Demo
i am trying to find out when and how to use the link function in angular directives.
Say i have the following directive:
app.directive("lbArticle", function() {
return {
restrict : "E",
templateUrl: 'tpl/directives/information/article.html',
scope: {
article: '='
},
link: function(scope,element, attr){
scope.info = attr.article;
}
};
});
Now passing an object to the article attachment of the HTML
<lb-article article='{{myObject}}'> </lb-article>
When this happens and the directive is rendered it should set a variable called info equal to myObject
So if myObject looked like this:
myObject{name: 'Hello', created: '2015-04-04'; }
Then the following should display these variables:
my directive html
<span class="block text-ellipsis">{{info.name}}</span>
<small class="text-muted">{{info.created | fromNow}}</small>
However this does not work?
As far as i can read for the documentation the link function should be used for DOM manipulation and also for setting variables that might be rendered before the actual directive?
This line scope.info = attr.article; is redundant, since you have access to article via the two-way binding =article. So you can replace all occurrences of info with article in the template, since that is available in scope. You also need to remove the curly brackets from <lb-article article='{{myObject}}'> </lb-article> for two-way binding to work, since you want a reference to the object.
There's a nice, easy to follow article on directives, which covers most of these concepts (link/complile functions, two/one-way binding, scopes, etc.).
Use
<lb-article article='myObject'> </lb-article>
and try now.
I have a directive where a list(array) of items is passed in through the scope of the controller into the scope of the directive whereby the template of the directive then has access to the items.
I would like to have it so that the list of items is passed to the directive (where it is then used within the link function) and then not directly accessible through the directive's template.
i.e. if we had the following directive:
directive('itemList', function(){
return {
scope: {
items: '='
}
link: function(scope, elem, attrs){
var item-list = scope.items;
// ... want to manipulate first ...
}
}
})
the variable scope.items is now available to any template that the directive uses. Whereas I don't want that to be the case and would like to pass in something to the directive without making it known to the scope. Is this possible?
Since the directive scope pretty much by definition is the scope used by the directive's template, I don't see any obvious way of doing this in a strict information hiding way. But, why not just use a different name for the passed scope variable and what the template binds to? For example, if you said scope: { myPrivatePassedItems: '=items' }, you can now work with scope.myPrivatePassedItems as much as needed before setting it as scope.items. With this approach, the HTML in both the usage of the directive and the directive's template just sees "items", but internally your directive has its own "private" variable.
I should add that the above is the simple change needed for the one-way data flow from the consumer to the directive template. If you also need to update the original items array, you will also want to add a scope.$watch on the scope.items array after you have done your initial setup. You would then need to carry those changes (possibly with modifications) back to scope.myPrivatePassedItems in order to update the consumer's array.
You can use the $parse service to retrieve the value without using scope: {}, but you will lose the 2 way data binding that you inherently get from using scope: { items: '=' }. As mentioned by Dana Cartwright, if you need 2 way binding, you have to set this up manually with watches.
directive('itemList', function($parse){
return {
link: function(scope, elem, attrs){
var itemList = $parse(attrs['items'])(scope);
// do stuff with itemList
// ...
// then expose it
scope.itemList = itemList;
}
};
});
I am trying to understand $parse, based on the documentation. But I am having trouble to get my test code working. Am I using $parse service the right way?
The main part of the code is:
app.directive('try', function($parse) {
return {
restrict: 'E',
scope: {
sayHello: "&hello"
},
transclude: true,
template: "<div style='background:gray;color:white'>Hello I am try: <span ng-transclude></span><div>",
link: function($scope, $elem, $attr) {
var getter = $parse($attr.sayHello);
// var setter = getter.assign;
$elem.on('click', function() {
getter($scope);
$scope.$apply();
});
}
};
});
See my code at: http://plnkr.co/edit/lwV5sHGoCf2HtQa3DaVI
I haven't used the $parse method, but this code achives what you are looking for:
http://plnkr.co/edit/AVvxLR4RcmWhLo8eqYyd?p=preview
As far as I can tell, the $parse service is intended to be used outside of an isolate scope.
When you have an isolate scope, like in your directive, you can obtain a reference to the parent scope's function using the 'sayHello': '&' as proposed in Shai's answer. The $parse service might still work as expected even with an isolate scope, if you are able to pass in the parent scope instead of the directive's scope when calling getter($scope), but I haven't tested that.
Edit: This is indeed the case - using getter($scope.$parent) works fine. When an isolate scope is used in your directive, the $scope variable no longer refers to the correct context for the getter function returned by the $parse service. Access the correct one by using $scope.$parent.
However, if you are avoiding an isolate scope, your approach works well. Try removing the scope: { ... } section out of your directive definition entirely and you'll see it works fine. This is handy if you are creating a directive for event binding that might be applied to an element in conjunction with another directive that has an isolate scope, say a dragenter directive (which isn't provided by Angular). You couldn't use Shai's method in that case, since the isolate scopes would collide and you'd get an error, but you could use the $parse service.
Here's an updated plunker with the scope removed from the directive definition: http://plnkr.co/edit/6jIjc8lAK9yjYnwDuHYZ
I have an Angular directive which permit to render an user, and create a link to view the user profile, declared as:
.directive('foafPerson', function() {
return {
restrict: 'E',
templateUrl: 'templates/person.html',
scope: {
personModel: '=',
onClickBindTo: '=',
onPersonClick: '&'
}
};
As you can see, I'm trying 2 solutions to be able to visit and load the full user profile: onClickBindTo and onPersonClick
I use it like that to render a list of persons + their friends:
// display the current user
<foaf-person person-model="person" on-person-click="changeCurrentProfileUri(person.uri)" on-click-bind-to="currentProfileUri"></foaf-person>
// display his friends
<div class="profileRelationship" ng-repeat="relationship in relationships">
<foaf-person person-model="relationship" on-person-click="changeCurrentProfileUri(relationship.uri)" on-click-bind-to="currentProfileUri"></foaf-person>
</div>
On the template, I have a link that is supposed to change an attribute of the controller (called currentProfileUri)
<a href="" ng-click="onClickBindTo = personModel.uri">
{{personModel.name}}
<a/>
I can see that the controller scope variable currentProfileUri is available in the personTemplate.html because I added a debug input: <input type="text" ng-model="onClickBindTo"/>
Unfortunately, when I modify the input value, or when I click on the link, the currentProfileUri of the controller is not updated. Is this normal or am I missing something?
With the other method it seems to work fine:
<a href="" ng-click="onPersonClick()">
{{personModel.name}}
<a/>
So to modify a model of the parent scope, do we need to use parent scope functions?
By the way, passing an expression with &, I tried another solution: not using a function declared in the controller scope:
<foaf-person person-model="relationship" on-person-click="currentProfileUri = relationship.uri"></foaf-person>
How comes it does not work?
My controller has nothing really fancy:
$scope.currentProfileUri = 'https://my-profile.eu/people/deiu/card#me';
$scope.$watch('currentProfileUri', function() {
console.debug("currentProfileUri changed to "+$scope.currentProfileUri);
loadFullProfile($scope.currentProfileUri);
})
$scope.changeCurrentProfileUri = function changeCurrentProfileUri(uri) {
console.debug("Change current profile uri called with " + uri);
$scope.currentProfileUri = uri;
}
I am new to Angular and read everywhere that using an isolate scope permits a two-way data binding with the parent scope, so I don't understand why my changes are not propagated to the parent scope and my debug statement doesn't fire unless I use the scope function changeCurrentProfileUri
Can someone explain me how it works?
In your example the scopes hierarchy is the following:
controller scope
ng-repeat scope
foaf-person scope
so when you declare two-way binding for 'currentProfileUri' it is actually bound to the scope created by ng-repeat, not by the controller, and when your code changes value of onClickBindTo then angularjs executes 'currentProfileUri = newValue' in the ng-repeat scope.
Solution is to use objects instead of primitive values for two-way bindings - in this case scopes inheritance always work in proper way. I mean something like that:
// display the current user
<foaf-person person-model="person" on-click-bind-to="currentProfile.uri"></foaf-person>
// display his friends
<div class="profileRelationship" ng-repeat="relationship in relationships">
<foaf-person person-model="relationship" on-click-bind-to="currentProfile.uri"></foaf-person>
</div>
I've prepared a js-fiddle which illustrates this behavior