Exposing angular directive function to another module - javascript

Suppose I have a module with a directive as follows (this is a rough not tested)
I need to implement 3 basic things
Configuration for the element that will appear
Event listeners that the base controller can use
Public methods that the base controller can call
angular.module("componentModule",[]) .directive("myComp",function(){
return{
replace:true,
template:'<h2>This is my component</h2>',
scope:{config= "#"},
link:function(scope,element,attr){
this.deleteElement = function(id){
//writing the code to delete this component
//This is a API function that the user can call to delete
}
if (!scope.config.visible){
//this is a configuration object for the element
this.visible(false)}
}
} })
then i have my base HTML like containing the directive call like below
<div myComm="first" config="eleConfig"></myComp>
<div myComm="second" config="newEleConfig"></myComp>
I have a separate controller for my base HTML as follows,
angular.module("baseApp",['componentModule'])
.controller('baseCtrl',function(){
$scope.eleConfig = {
visible:true,
delete:function(e){
//This is called if we call the delete method
}
}
//this is how the delete method is to be called
$scope.first.deleteElement();
})
Question
How to call the deleteElement() method in the baseCtrl as shown above (want to do it the same way KENDO UI does)

The pattern that angular uses is to expose the directive API to the scope. This is how ng-model and ng-form both expose ngModelController and ngFormController APIs.
Here is how I would do it:
angular.module("componentModule",[])
.directive("myComp",function($parse){
return{
replace:true,
scope: {
config: '&'
},
template:'<h2>This is my component</h2>',
controller: function($scope) {
//Directive API functions should be added to the directive controller here or in the link function (if they need to do DOM manipulation)
},
link:function(scope,element, attr, ctrl){
//add to directive controller
if(scope.config().visible) {
//element should be visible, etc.
}
ctrl.deleteElement = function(){
//if this function is called we want to call the config.delete method:
if(scope.config && scope.config.delete) {
//calling the scope.config() method returns the config object from the parent
scope.config().delete(element);
}
}
if(attr.myComp) {
//change to scope.$parent
scope.$parent[attr.myComp] = ctrl;
}
}
}
})
Assuming markup of:
<div my-comp="first" config="configObject"></div>
<div my-comp="second" config="configObject"></div>
In your base controller
$scope.first.deleteElement();
or
$scope.second.deleteElement();
would delete the appropriate element.
UPDATE:
I've updated the directive based on your updated question. You want to pass a config object into the directive. The best way to do that is with an & binding. If you use the & binding, you need to remember that the directive will create a new scope, and you have to attach the controller to $scope.$parent.

In your first requirement, you said you want to write the delete function in the directive, but in the case of KendoUI the actual delete(change) function implementation is done in the base controller and the delete(change) event triggered when the component value changes, which in turn calls the delete function defined in the base controller by the directive.
If you want to implement something like KendoUI does then look at this
link toplunker
Switch on the browser console to see the log. KendoUI component's change event happens automatically when the input element changes but in this case i manually triggered the delete event after 3 seconds.

Related

Angular directives that call methods on child directives

I am looking for advice on how to implement a hierarchical structure in Angular, where a directive (<partition>) can call a method on a child directive's controller (<property-value>).
I have put together a detailed example here:
https://jsfiddle.net/95kjjxkh/1/
As you can see, my code contains an outer directive, <partition>, which displays one or more <property-value> directives within.
The <property-value> directive offers an editing method, editItem(), which allows the user to change the value of a single entry. (To keep my example short, I simply assign a random number here, but in my production app, a modal will appear, to query the user for a new value.)
This works fine. However, in the outer directive, <partition>, I would like to add the ability to create a new, blank <property-value> directive and then immediately call its editing method so that the user can enter an initial value. If no initial value is entered, the new item would be discarded.
I have seen examples of inner directives calling methods on enclosing directives, but not the other way around.
Is there a way to do this? Alternatively, is there a better way for me to build this kind of view?
You can always use $broadcast to talk both ways. To your parent as well as to your childrens.
In your Child controller you can do the following
app.directive('propertyValue', function() {
return {
require : '^partition'
restrict: 'E',
scope: {
item: '='
},
with this you will get the parent controller in child directive's link function like this
link:function(scope,element,attrs,partitionCtrl){
partitionCtrl.getChildCtrl(element)
}
in partition controller create getChildCtrl function and with that call "propertyvalue" controller function
controller: function ($scope, ItemFactory) {
// your code
var propValueCtrl =undefined;
this.getChildCtrl =function(elem)
{
propValueCtrl = elem.controller();
}
this.callChildFunction = function()
{
propValueCtrl.Edit();// whatever is the name of function
}
call this function when needed in property link function.
Hope this helps.

AngularJs Directive: How to dynamically set attributes on ng-repeat element

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
};
}]
});

How-to change an isolated scope attribute of AngularJS directive from an external directive callback method?

I need your help ! :)
I'm quite new to AngularJS (v1.2) development, and I'm playing with directives.
The problem
I pass a parent ctrl model attr to my directive, that has its inner model in its isolated scope. The 2-ways data binding works fine between the parent ctrl attr and the directive's scope attr.
My problem is then that I want to update the directive's model attr from an external callback function of my parent ctrl, WITHOUT knowing which is the parent ctrl attribute to update...
This is because an event from my directive (like add an element in a list) must perform an async call to a REST WS to save the new element in the back-end, and update the angularJS associated model with the 'fresh' data received in the response (for example my data with an ID after it has been saved in the database...)
Reproduce it
I don't know if this is clear, it should be hard to understand "as is", so I made a JSFiddle to demonstrate this with a very basic example : TEST IT HERE.
If you type something in the "INTERNAL Callback" input text, you will call a method in the directive's scope. The model received from the async call (for example "{id:100, name:'JSFiddle'}") will be set to the directive's inner model, and reflected to the parent's scope model as well. FINE !
BUT if you type something in the "EXTERNAL Callback" input text, you will call a method in the parent's controller scope, with a 'model' method attribute that is the directive's scope attribute. The model received from the async call (for example "{id:100, name:'JSFiddle'}") will be set to the callback method model, and NOT reflected to the parent's scope model NOR the directive's model...
The only way I've found is to pass the directive's scope as the external callback attribute, and then update the desired attribute in this scope. That will work, but this is strange and awful in my opinion to pass the whole directive's scope to the callback method... You can see it in action HERE.
Of course, you can tell me that I just have to update my parent controller model in my external callback method, but as I can use MANY directives in the same view/controller, used in "ng-repeat" items, using the same callback method, I can't know which parent ctrl scope attribute I should update... See this case HERE.
Any ideas ?
The example code
HTML :
<div ng-controller="MyCtrl">
<h1>Parent Ctrl</h1>
<p>ctrlModel.id = {{ctrlModel.id}}</p>
<p>ctrlModel.name = {{ctrlModel.name}}</p>
<hr/>
<h1>Directive</h1>
<my-directive my-model="ctrlModel" on-change="ctrlOnChange(model)"></my-directive>
</div>
JAVASCRIPT:
var myApp = angular.module('myApp',[]);
function MyCtrl($scope, $log) {
$scope.ctrlModel = {id:0, name:'StackOverflow'};
$scope.ctrlOnChange = function(paramModel){
$log.info('MyCtrl.ctrlOnChange / paramModel BEFORE = ' + JSON.stringify(paramModel));
// Async call to save the model remotely and get it with an ID for example
/*
$http.post('/api/save-this-model-please', model)
.then(function(response){
model = response.data;
// Here my LOCAL METHOD model 'paramModel' will be updated with saved data, including an ID !== 0
// 'ctrlModel' in PARENT scope of 'MyCtrl' AND DIRECTIVE model 'scope.directiveModel' are not synced with these "fresh" data...
});
*/
paramModel = {id:100, name:'JSFiddle'};
$log.info('MyCtrl.ctrlOnChange / paramModel AFTER = ' + JSON.stringify(paramModel));
};
}
myApp.directive('myDirective', function ($log) {
return {
restrict: 'E',
template: '<div><p>directiveModel.id = {{directiveModel.id}}</p><p>directiveModel.name = {{directiveModel.name}}</p><p>INTERNAL Callback : <input ng-model="directiveModel.name" ng-change="onChangeInternalCallback()"></p><p>EXTERNAL Callback : <input ng-model="directiveModel.name" ng-change="onChangeExternalCallback({model:directiveModel})"></p></div>',
replace: true,
transclude: false,
scope: {
directiveModel: '=myModel',
onChangeExternalCallback: '&onChange'
},
link: function (scope) { // , element, attrs
scope.onChangeInternalCallback = function(){
$log.info('myDirective.onChangeInternalCallback / directiveModel BEFORE = ' + JSON.stringify(scope.directiveModel));
// Async call to save the model remotely and get it with an ID for example
/*
$http.post('/api/save-this-model-please', scope.directiveModel)
.then(function(response){
scope.directiveModel = response.data;
// Here my DIRECTIVE model 'scope.directiveModel' will be updated with saved data, including an ID !== 0
// With 2-ways data binding, 'ctrlModel' in PARENT scope of 'MyCtrl' is ALSO synced with these "fresh" data...
});
*/
scope.directiveModel = {id:100, name:'JSFiddle'};
$log.info('myDirective.onChangeInternalCallback / directiveModel AFTER = ' + JSON.stringify(scope.directiveModel));
};
}
};
});

AngularJS call method from an ancestor scope inside directive

I have an Angular app where I'm using ui-grid. I want to have a custom action on a cell of the grid that calls a method from my app. So basically, this means calling a method that's somewhere up in the parent hierarchy, from a directive.
This would be achieved by calling something like: $scope.$parent.$parent.$parent.$parent.foo(). But that doesn't seem too nice.
One option would be to create a recursive function that goes up the ancestry of the $scope. That's nicer, but still seems a bit weird.
Also... Is it good practice to try to achieve something like this?
You're correct that $parent.$parent.$parent is definitely not a good practice.
If the method you're calling is another directive, you can require that directive in your child directive and then, the parentDirective's controller function will be injected as the fourth parameter to your link function:
In your DDO:
return {
require : '^parentDirective',
restrict : 'E',
link : function (scope, elem, attrs, parentDirectiveController) {}
}
If what you're trying to call is on a factory/service, you can inject that factory/service into your directive, although this sometimes is a code smell, depending on what you're trying to inject.
Finally, another way to do it is to use event propagation. From your directive, you can use $scope.$emit to send information up to parent controllers:
From the directive:
$scope.$emit('directiveDidStuff', {
data : 'blah'
});
In the parent controller:
$scope.$on('directiveDidStuff', function (evt, params) {
this.data = params.data; // equals blah
});
You can achieve the same by using "&" through one of the scope variable in directive.Like this, you can bind your event to the controller method and from the method, you could do your desired things or if the original business logic which you wants to achieve on onClick of the grid is used across many modules than you can bisect it in service and make it reusable and call the service from the event method. Let me know if you do have any doubts with the approach.
Key Code of example:
Html
<my-component attribute-foo="{{foo}}" binding-foo="foo" isolated-expression- foo="updateFoo(newFoo)" >
Directive
var myModule = angular.module('myModule', [])
.directive('myComponent', function () {
return {
restrict:'E',
scope:{
/* NOTE: Normally I would set my attributes and bindings
to be the same name but I wanted to delineate between
parent and isolated scope. */
isolatedAttributeFoo:'#attributeFoo',
isolatedBindingFoo:'=bindingFoo',
isolatedExpressionFoo:'&'
}
};
})

How to update directives based on service changes?

I have a directive(parent-directive) containing a slider(mySlider), that on stop event, call an angular $resource service with 2 params and the service return an object.
Directives structure:
<parent-directive>
<div ui-slider="slider.options" ng-model="mySlider" id="my-slider">
<span child-directive-one></span>
<span child-directive-two></span>
<span child-directive-three></span>
<div>
<span child-directive-four></child-directive-four>
</div>
</parent-directive
Whenever the user drag the slider, the service is called with different params and retieve new result, based on it I need to update the child directives.
I have in mind three ways:
using ng-model for all child elements instead directives, binding them on the scope of a controller in parent-directive;
the second one, that I don't know how to do it, is to create a controller in the parent-directive, that send and receive data from the service and share it to child-directives in order to update them.
the last one is to to create a state variable in the service and update it using a controller like to point 1.(see it above) and use a $watch to supervise the variable state and when it's changed then update the child-directives.
How should I proceed?
Please have a look here to see a brief code:
http://jsfiddle.net/v5xL0dg9/2/
Thanks!
ngModel is intended for two way binding, i.e. controls that allow the user to interfere with the value. From the description, it seems they are display-only components. So I would advise against using the ngModel.
Normally child directives require their parent. This allows them to call methods on the parent controller. What you need is the opposite: the parent controller needs to call methods on the children. It can be done: the children call a registerChild() method, and the parent iterates all registered children when it needs to call them. I find this implementation cumbersome.
Services are globals/singletons. I would vote against tying the service implementation to the UI needs.
My advice looks like your implementation of option 3, but with the parent controller holding the data:
1) Place the data you want to share with the child directives in a member variable of the parent controller:
myApp.directive('parentDirective', ['myService', function(myService){
...
controller: function($scope) {
...
this.sharedThing = ...;
}
}]);
The sharedThing can be updated when the service returns new data, or any other time it is necessary.
2) Have the children require the parent (just like your option 2), and watch this property:
myApp.directive('childDirectiveOne', function() {
return {
...
require: 'parentDirective',
link: function(scope, elem, attrs, parentDirective) {
scope.$watch(
function() {
return parentDirective.sharedThing;
},
function(newval) {
// do something with the new value; most probably
// you want to place it in the scope
}
});
}
};
});
Depending on the nature of the data, a deep watch may be required.

Categories

Resources