How can I send an object back from a directive into the parent controller?
I've defined the following directive:
app.directive('inspectorSharedObjects', function () {
return {
restrict: 'E',
scope: {
filterText: '=filter',
type: '=',
selectObject: '&onSelect'
},
controller: function ($scope) {
$scope.dot = function (tags) {
return "label-dot-" + tags[0];
}
},
link: function (scope, element, attrs) {
},
templateUrl: 'partials/InspectorSharedObjectListPartial.html'
};
});
... which I call in the following way:
<inspector-shared-objects ng-repeat="group in modelSharedObjects" type="group" filter="filterText" on-select="selectObject(obj)"></inspector-shared-objects>
... with the following template:
<div class="object-group-header" ng-click="isActive = !isActive" ng-class="{active : isActive}">
<span>{{ type.name }}</span>
<span ng-if="filterText">({{ filteredList.length }})</span>
<i class="fa fa-plus-circle"></i>
</div>
<div class="object-group-list" ng-show="isActive">
<ul>
<li ng-repeat="obj in filteredList = (type.contents | filter:filterText | orderBy:'name')" ng-class="dot(obj.tags)" ng-click="selectObject(obj)">{{ obj.name }}</li>
</ul>
</div>
An ng-click on the li within a list should send the selected obj back the parent controller. The above code calls that parent controller's function, but the object I'm trying to pass comes in as undefined.
I read through the following question: calling method of parent controller from a directive in AngularJS - which I think is trying to do the same thing, but I can't see what I'm doing different than the answer (or my interpretation of it).
How can I the obj coming from the directive's template passed back up to the parent controller?
UPDATE: Here is a JSFiddle: http://jsfiddle.net/EvilClosetMonkey/7GMEG/
When you click on the bulleted values the console should spit out the object.
Change the type of binding from one-way to two-way (& to = in the isolate scope attributes object).
FIDDLE
When you use =, the object (function here) is passed by reference, so you just pass it by name (rather than as a function call like you had before). Then you can invoke it and all is well.
But when you use &, what angular does is wrap what you send in an eval and returns a function wrapping that. So your function that you called in each repetition of the li element would have been something like this:
function(obj){
return $eval('selectObject("whatever"))
}
And that's why you would get "whatever" logged, no matter what you pass as obj.
NOTE: Since you use a nested ngRepeat, each li element is 2 child scopes under the controller scope. Calling $parent.$parent.selectObject(obj) would also work as a result. You shouldn't do this and it doesn't really pertain to your question, just a friendly reminder as that kind of thing is brought up a lot on angular SO questions.
You can pass the value of 'group' that you're getting from your ng-repeat.
<inspector-shared-objects ng-repeat="group in modelSharedObjects" type="group" filter="filterText" on-select="selectObject(group)"></inspector-shared-objects>
Related
I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?
First, I thought of this solution:
template
...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...
*directive
link: function(scope, elm, attrs) {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
...
Result
It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.
temporary fix
in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;
And then in the template,
a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')
While it works fine, it is obviously not DRY at all. HELP!
Instead of removing attributes, change your click handler.
Add $event to the list of arguments and conditionally use preventDefault().
<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>
In your controller:
$scope.addToPlaylist = function(event,playlist) {
if (!$scope.adding) return;
//otherwise
event.preventDefault();
//do add operation
};
When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.
From the Docs:
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:
link: function(scope, elm, attrs) {
elm.ready(function() {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
});
}
However, this is unlikely to work in your situation, as #charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.
I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.
Update:
I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.
The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:
app.component('playlistComponent', {
// We can define out template as a function that returns a string:
template: function($element, $attrs) {
var action = 'add' in $attrs
? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
: 'ng-href="playlist/{{playlist._id}}"';
return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
action + '></a>';
},
// Components always use controllers rather than scopes
controller: ['playlistService', function(playlists) {
this.playlists = playlists;
this.addToPlaylist = function(playlist, track) {
// Some logic
};
}]
});
This is the controller of the main template:
app.controller('OverviewCtrl', ['$scope', '$location', '$routeParams', 'websiteService', 'helperService', function($scope, $location, $routeParams, websiteService, helperService) {
...
$scope.editWebsite = function(id) {
$location.path('/websites/edit/' + id);
};
}]);
This is the directive:
app.directive('wdaWebsitesOverview', function() {
return {
restrict: 'E',
scope: {
heading: '=',
websites: '=',
editWebsite: '&'
},
templateUrl: 'views/websites-overview.html'
}
});
This is how the directive is applied in main template:
<wda-websites-overview heading="'All websites'" websites="websites" edit-website="editWebsite(id)"></wda-websites-overview>
and this is method is called from directive template (website-overview.html):
<td data-ng-click="editWebsite(website.id)">EDIT</td>
QUESTION: When EDIT is clicked, this error appears in the console:
TypeError: Cannot use 'in' operator to search for 'editWebsite' in 1
Does anyone know what goes on here?
Since you defined an expression binding (&), you need to explicitly call it with an object literal parameter containing id if you want to bind it in the HTML as edit-website="editWebsite(id)".
Indeed, Angular needs to understand what this id is in your HTML, and since it is not part of your scope, you need to add what are called "locals" to your call by doing:
data-ng-click="editWebsite({id: website.id})"
Or as an alternative:
data-ng-click="onClick(website.id)"
With the controller/link code:
$scope.onClick = function(id) {
// Ad "id" to the locals of "editWebsite"
$scope.editWebsite({id: id});
}
AngularJS includes an explanation of this in its documentation; look for the example involving "close({message: 'closing for now'})" at the following URL:
https://docs.angularjs.org/guide/directive
TL;DR; - You are assuming that the bound function is being passed to the child component, as it would be in React. This is incorrect. In fact, AngularJS is parsing the string template and creating a new function, which then calls the parent function.
This generated function expects to receive an object with keys and values, rather than a plain variable.
Longer Explanation
This happens when you have bound a function using '&', and have tried to call that function from your controller, passing a plain variable rather than an object containing the name of the plain variable.
The object keys are needed by the templating engine to work out how to pass values into the bound function.
eg. you have called boundFunction('cats') rather than boundFunction({value: 'cats'})
Worked Example
Say I create a component like this:
const MyComponent = {
bindings: {
onSearch: '&'
},
controller: controller
};
This function (in the parent) looks like this:
onSearch(value) {
// do search
}
In my parent template, I can now do this:
<my-component on-search="onSearch(value)"></my-component>
The binding here will be parsed from the string. You're not actually passing the function. AngularJS is making a function for you which calls the function. The binding created in the template can contain lots of things other than the function call.
AngularJS somehow needs to work out where to get value from, and it does this by receiving an object from the parent.
In myComponent controller, I need to do something like:
handleOnSearch(value) {
if (this.onSearch) {
this.onSearch({value: value})
}
}
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
I have an observation directive which render observation.html
observation.js
angular.module('bahmni.clinical')
.directive('observation', function () {
var controller = function ($scope) {
console.log($scope.observation);
};
return {
restrict: 'E',
controller: controller,
scope: {
observation: "="
},
templateUrl: "views/observation.html"
};
});
I call observation directive from the observation.html. This will be done recursively.
observation.html
<fieldset>
<div class="form-field"
ng-class="{'is-abnormal': observation.abnormal, 'is-text': isText(observation)}">
<span class="field-attribute"><label>{{observation.concept.shortName || observation.concept.name}}</label></span>
<span class="value-text-only" ng-if="!observation.groupMembers">{{observation.getDisplayValue()}}</span>
<span class="label-add-on" ng-hide="!observation.unit"> {{observation.concept.units}}</span>
<div class="footer-note fr">
<span class="value-text-only time">{{observation.observationDateTime | date :'hh:mm a'}}</span>
</div>
</div>
</fieldset>
<div ng-repeat="observationMember in observation.groupMembers">
<observation observation="observationMember"></observation>
</div>
I call this for first time from someother directive.
someother.js
<observation observation="observation"></observation>
If i refresh the browser, The tab will be irresponsive. Don't know what is happening. Not able to debug because of the irresponsive tab.
I would really appretiate your answer.
ng-include fixed it.
used the following line
template: '<ng-include src="\'views/observation.html\'" />'
instead of -
templateUrl: "views/observation.html"
It sounds like an endless loop! I think your problem is how the defined local scope variable is defined. observationhas an two way databinding. So you will overwrite it with every recursive call. Try this to avoid the endless loop
scope: {
observation: "&"
},
This will create an local directive scope which are not affected from the parent scope.
Keep in mind there will be three ways to define local scope properties you can pass:
# 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
UPDATE 2
Your main problem seems that your variable observation will be overwritten on each recursive call. So you have two options:
Option 1: You use # and serialize your object with JSON.stringify(observation). This string can you provide via string interpolation to your directive. <observation observation="{{observation}}"></observation>
Option 2: You use & and pass the object to an helper function, which will be create and return an clone from your object.
I have a form directive that uses a specified callback attribute with an isolate scope:
scope: { callback: '&' }
It sits inside an ng-repeat so the expression I pass in includes the id of the object as an argument to the callback function:
<directive ng-repeat = "item in stuff" callback = "callback(item.id)"/>
When I've finished with the directive, it calls $scope.callback() from its controller function. For most cases this is fine, and it's all I want to do, but sometimes I'd like to add another argument from inside the directive itself.
Is there an angular expression that would allow this: $scope.callback(arg2), resulting in callback being called with arguments = [item.id, arg2]?
If not, what is the neatest way to do this?
I've found that this works:
<directive
ng-repeat = "item in stuff"
callback = "callback"
callback-arg="item.id"/>
With
scope { callback: '=', callbackArg: '=' }
and the directive calling
$scope.callback.apply(null, [$scope.callbackArg].concat([arg2, arg3]) );
But I don't think it's particularly neat and it involves puting extra stuff in the isolate scope.
Is there a better way?
Plunker playground here (have the console open).
If you declare your callback as mentioned by #lex82 like
callback = "callback(item.id, arg2)"
You can call the callback method in the directive scope with object map and it would do the binding correctly. Like
scope.callback({arg2:"some value"});
without requiring for $parse. See my fiddle(console log) http://jsfiddle.net/k7czc/2/
Update: There is a small example of this in the documentation:
& or &attr - provides a way to execute an expression in the context of
the parent scope. If no attr name is specified then the attribute name
is assumed to be the same as the local name. Given and widget definition of scope: {
localFn:'&myAttr' }, then isolate scope property localFn will point to
a function wrapper for the count = count + value expression. Often
it's desirable to pass data from the isolated scope via an expression
and to the parent scope, this can be done by passing a map of local
variable names and values into the expression wrapper fn. For example,
if the expression is increment(amount) then we can specify the amount
value by calling the localFn as localFn({amount: 22}).
Nothing wrong with the other answers, but I use the following technique when passing functions in a directive attribute.
Leave off the parenthesis when including the directive in your html:
<my-directive callback="someFunction" />
Then "unwrap" the function in your directive's link or controller. here is an example:
app.directive("myDirective", function() {
return {
restrict: "E",
scope: {
callback: "&"
},
template: "<div ng-click='callback(data)'></div>", // call function this way...
link: function(scope, element, attrs) {
// unwrap the function
scope.callback = scope.callback();
scope.data = "data from somewhere";
element.bind("click",function() {
scope.$apply(function() {
callback(data); // ...or this way
});
});
}
}
}]);
The "unwrapping" step allows the function to be called using a more natural syntax. It also ensures that the directive works properly even when nested within other directives that may pass the function. If you did not do the unwrapping, then if you have a scenario like this:
<outer-directive callback="someFunction" >
<middle-directive callback="callback" >
<inner-directive callback="callback" />
</middle-directive>
</outer-directive>
Then you would end up with something like this in your inner-directive:
callback()()()(data);
Which would fail in other nesting scenarios.
I adapted this technique from an excellent article by Dan Wahlin at http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters
I added the unwrapping step to make calling the function more natural and to solve for the nesting issue which I had encountered in a project.
In directive (myDirective):
...
directive.scope = {
boundFunction: '&',
model: '=',
};
...
return directive;
In directive template:
<div
data-ng-repeat="item in model"
data-ng-click='boundFunction({param: item})'>
{{item.myValue}}
</div>
In source:
<my-directive
model='myData'
bound-function='myFunction(param)'>
</my-directive>
...where myFunction is defined in the controller.
Note that param in the directive template binds neatly to param in the source, and is set to item.
To call from within the link property of a directive ("inside" of it), use a very similar approach:
...
directive.link = function(isolatedScope) {
isolatedScope.boundFunction({param: "foo"});
};
...
return directive;
Yes, there is a better way: You can use the $parse service in your directive to evaluate an expression in the context of the parent scope while binding certain identifiers in the expression to values visible only inside your directive:
$parse(attributes.callback)(scope.$parent, { arg2: yourSecondArgument });
Add this line to the link function of the directive where you can access the directive's attributes.
Your callback attribute may then be set like callback = "callback(item.id, arg2)" because arg2 is bound to yourSecondArgument by the $parse service inside the directive. Directives like ng-click let you access the click event via the $event identifier inside the expression passed to the directive by using exactly this mechanism.
Note that you do not have to make callback a member of your isolated scope with this solution.
For me following worked:
in directive declare it like this:
.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
scope: {
myFunction: '=',
},
templateUrl: 'myDirective.html'
};
})
In directive template use it in following way:
<select ng-change="myFunction(selectedAmount)">
And then when you use the directive, pass the function like this:
<data-my-directive
data-my-function="setSelectedAmount">
</data-my-directive>
You pass the function by its declaration and it is called from directive and parameters are populated.