Predefined function to use in ng-repeat nested in a directive - angularjs

I have a list directive that basically get all the items from a service and show them in a table. When I click on an item in the table, it opens a form in relation with the type of item.
Everything works great, but now, I have one instance of that list where I need to override the event when I click on an item. So I added an attribute in the directive:
<list factory-name="Workers"
on-item-click="$state.go('worker.workerDetails', {workerId: item._id})">
</list>
So when I get to the function that gets called when I click an item, I can do something like that:
<tr ng-repeat="item in items" ng-click="edit(item)></tr>
$scope.edit = function(item) {
if ($attrs.onItemClick) {
setTimeout(function(){ $scope.$apply($attrs.onItemClick); });
} else {
edit(item);
}
};
The problem is that I cannot isolate the scope since some nested directive need to access it and I would prefer not to modify my list directive with a bunch of exception, only an override function (onItemClick);
Right now it's not working, the stateparams don't get assigned.
Thank you very much

In case you can't isolate scope and use "&" bingind, you should use $parser service.
First parse attribute:
var onItemClick = $parse($attrs.onItemClick);
then call it like this:
onItemClick($scope, { item: item });
You pass your context scope as the first argument, and an object containing local (per call) variables the second argument.
And not to polute outer scope, you can use "child scope", by specifying scope: true in directive definition.

Related

How to call a custom directive's action in AngularJS?

In AngularJS you can make a button to call an action like this:
<div ng-controller="myController">
<button ng-click="onButtonClicked()">Click me</button>
</div>
So, I'm inserting a custom directive like this:
and in my-canvas.js directive file's link function I replace the tag with a KineticJS canvas. Then, User manipulate the canvas by dragging around Kinetic shapes and, finally, when User does with the shapes what he's required to do, I want the directive to call an action defined on myController. I'm thinking about something like this:
<div ng-controller="myController">
<my-canvas ng-success="onScenarioSuccess" />
</div>
but I can't figure out how the correct way to do it.
How can I make a directive to call it's action/event programmatically?
When you want your directive to expose an API for binding to behaviors you should use an isolate scope and use the & local scope property. This allows you to pass in a function that the directive can invoke. Here is a simple example:
.directive('testDirective', function () {
return {
restrict: 'E',
scope: {
action: '&'
},
template: '<button ng-click="action()">Test</button>'
};
});
And use it like:
<test-directive action="myControllerFunction()"></test-directive>
As per the documentation:
The & binding allows a directive to trigger evaluation of an
expression in the context of the original scope, at a specific time.
Any legal expression is allowed, including an expression which
contains a function call. Because of this, & bindings are ideal for
binding callback functions to directive behaviors.
There'are some more detail in the documentation.
If you want to expose a custom event like ng-success and want to call a function on the event.
You can either do what #Beyers has mentioned using isolated scope.
Or else look at the source code of ng-click, it just wraps the javascript event inside $scope.apply, using the $parse service to evaluate the expression passed to it. Something like this can be added in your link function
var fn = $parse(attr['ngSuccess']);
element.on('someEvent', function (event) {
var callback = function () {
fn(scope, {
$event: event
});
};
scope.$apply(callback);
});
The advantage of this mechanism is that isolated scope is not created.

How to include data/scope from controller in a dynamically added directive?

I'm trying to figure out how to include scope with a directive that I add to the dom on a click event in a controller.
Step 1. On a click event, I call a function in my controller that adds a directive like this
$scope.addMyDirective = function(e, instanceOfAnObjectPassedInClickEvent){
$(e.currentTarget).append($compile("<my-directive mydata='instanceOfAnObjectPassedInClickEvent'/>")($scope));
}
//I'm trying to take the `instanceOfAnObjectPassedInClickEvent` and make it available in the directive through `mydata`
The above, part of which I got from this SO answer, successfully adds the directive (and the directive has a template that gets added to the dom), however, inside the directive, I'm not able to access any of the scope data mydata it says it's undefined.
My directive
app.directive('myDirective', function(){
return {
restrict: 'AE',
scope: {
mydata: '='
//also doesn't work if I do mydata: '#'
},
template: '<div class="blah">yippee</div>',
link: function(scope,elem,attrs) {
console.log(scope) //inspecting scope shows that mydata is undefined
}
}
}
Update
I changed the name of datafromclickedscope in the OP to make it more clear. In the controller action addMyDirective (see above) instanceOfAnObjectPassedInClickEvent is an instance of an object passed into the controller method on a click event that I try to pass into the directive as mydata='instanceOfAnObjectPassedInClickEvent'. However, even if I change = to # in the directive and I try to access scope.mydata in the link function of the directive, it just shows a string like this "instanceOfAnObjectPassedInClickEvent", not the actual object data that is available to me in my method that handles the click event
When you use mydata='instanceOfAnObjectPassedInClickEvent' in a template you need instanceOfAnObjectPassedInClickEvent to defined in $scope. So before compiling you should assign a variable in $scope. I will rename this variable in code below, so that same names would not confuse you and it would be clear that a formal parameter of a function cannot be visible in a template.
$scope.addMyDirective = function(e, instanceOfAnObjectPassedInClickEvent){
$scope.myEvent = instanceOfAnObjectPassedInClickEvent;
$(e.currentTarget).append($compile("<my-directive mydata='myEvent'/>")($scope));
}
EDIT: slightly adapted jsfiddle not using JQuery no manipulate DOM

Two-way binding within nested directives and ui-select

Problem I have been working on:
The original problem (which only addressed one-way binding) can be found here:
Unable to pass/update ngModel from controller to directive
I have managed to get the one-way binding from inside the directive out to the view working. But if I want push in a selection when the page loads, for example, I cannot get the two-way binding to function. I can see that the ng-model inside the first directive is getting the data, but I tried various scope settings with the child directive and either it breaks it or it does the same thing as before - nothing.
I have a basic $watch function set up, so that when I push a simple object into the binding that is attached to ng-model in the view, the watcher assigns the $viewValue to the directive's scope object. It does this, and the directive responds only by having any existing selection wiped out, even though I can clearly see the objects inside ng-model binding assigned to ui-select.
Here is the watch function:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
console.log(newVal, scope.options);
scope.options.selected = newVal;
});
I use this function to update the view whenever we interact with the ui-select (which works fine):
scope.changer = function() {
ngModel.$setViewValue(scope.options.selected);
console.log(scope.options.selected);
};
A Plunker for tinkering
So the expected behavior is:
selections from the ui-select should be displayed in the select, and also get passed to the view
by clicking the 'Change' button, data should be displayed in the view, and get passed to the ui-select
The situation was that I had a directive with ui-select inside it. The way I was able to get data from the main controller scope through the directive scope and down to ui-select's scope was by putting a watch function on ngModel.$viewValue in the link function of the directive.
I then assigned that new value, whenever there was a change, to an object on that directive's scope, to hold it for me. Then I set a watch function in the link function of ui-select to watch scope.$parent.myVal so that if anything was ever pushed to that variable from the main controller, ui-select would see it. When that happened, I would assign that new value to $select.selected which is the container for selected items in the ui-select directive.
It should be noted that ui-select uses scope: true in it's directive, and thus becomes a child of whatever scope it is instantiated in, which allows you to access the scope of it's parent with $parent.
Watch function for the directive:
scope.$watch(function() {
return scope.$parent.myVar;
}, function(newVal) {
$select.selected = newVal;
})
Watch function to be added to ui-select:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
scope.myVar = newVal;
})
Plunker Demo

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>

AngularJS : ng-repeat list not updating - directives

I am new to angular and I have the following code:
Here is my html:
<drawer title="'Add a new Item'"
visible="showAddItemDrawer"
on-close="showAddApplicationItem = false">
<!-- items list refresh does not work from inside the drawer. -->
<add-item></add-item>
</drawer>
<!--list refresh works if i place the code out here.
<add-item></add-item> -->
<div ng-repeat="item in itemList">
<item-list></item-list>
</div>
Here is my directive.js:
directive("additem", function () {
return {
restrict:"E",
transclude:true,
controller:"ItemsCtrl",
templateUrl:"partials/add-item.html" //contains a form to add item
};
})
.directive("itemlist", function () {
return {
restrict:"E",
transclude:true,
templateUrl:"partials/item-list.html" //contains code to display the items in a list
};
})
I have a form in add-item.html to add an item. The form shows up when you click on add button(like an accordion). I call the push() to add a new item to the scope.
The list update works if i place the directive call outside the drawer..
If i place it inside the drawer,the scope is not getting updated until i hit refresh.
Can anyone point me what I am doing wrong with the directives? Thanks a lot!
Edit: Added additional code :
In the form to add an item:
<button type="submit"
ng-click="addItemService()">
Add Item
</button>
addItemService() code:
$scope.addItemService = function () {
var data = {
"name": $scope.itemName,
};
ItemService.addItem(data, $scope.listgroupid)
.success(function (result) {
$scope.itemName = "";
viewList(); //The function that sets the scope of the list
})
.error(function () {
});
};
viewlist() code:
var viewList = function () {
ListService.getList($scope.listgroupid)
.success(function (result) {
$scope.itemList = result;
//In the angular inspector, I am able to see the new item in the variable result
})
.error(function () {
});
};
EDIT: function scope is different from variable scope, use $scope.$parent.itemList when referencing new variable value
i had an issue where the directive scope (while debugging) and the batarang inspected scope (when selecting an element in the dev tools and typing $scope.varname in the console) were not matching after a successfully resolved promise.
the promise updates a list for a ng-repeat which contains a directive.
the debugged scope held the values from the last data before the promise was loading new data.
so i added a timeout around the data-loaded event broadcast which pushes the broadcast into the next digest cycle and angular gets informed about the update.
$scope.$apply most often throws the "apply already in progress" error.
the timeout solves this a little more softly...
$timeout( function () {
// broadcast down to all directive children
$scope.$broadcast('dataLoaded');
}, 1);
need to see the code wherein you are calling push for item.
In the absence of that I suspect this is scope issue. Remember that ngRepeat and drawer reside in difference scopes by default and until you have explicitly made them share certain variables , changes inside drawer won't be reflected in ngRepeat. For more on scopes inside directives read http://docs.angularjs.org/api/ng.$compile.
The 'isolate' scope takes an object hash which defines a set of local
scope properties derived from the parent scope. These local properties
are useful for aliasing values for templates. Locals definition is a
hash of local scope property to its source:
# or #attr - bind a local scope property to the value of DOM
attribute. The result is always a string since DOM attributes are
strings. 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: { localName:'#myAttr' },
then widget scope property localName will reflect the interpolated
value of hello {{name}}. As the name attribute changes so will the
localName property on the widget scope. The name is read from the
parent scope (not component scope).
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value
of the attr attribute. 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: {
localModel:'=myAttr' }, then widget scope property localModel will
reflect the value of parentModel on the parent scope. Any changes to
parentModel will be reflected in localModel and any changes in
localModel will reflect in parentModel. If the parent scope property
doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION
exception. You can avoid this behavior using =? or =?attr in order to
flag the property as optional.
& 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}).
You can either use a $rootScope.$broadcast() to fire an event that your directive would capture, and then let the directive update the list with ng-repeat or you could call a $scope.$apply() with the updation wrapped in it within the controller to indicate angular that he list is updated, but when using $scope.$apply() be careful, and make sure it that it is delayed/deferred until the next digest cycle.
A library called Underscore.js lets you delay $apply until the next digest cycle.

Resources