Calling child directive function from parent directive in AngularJs - angularjs

How to call a method defined in child directive, within a button click listener of parent directive.
angular.module('editableDivDirective', [])
.directive("editableDiv", function() {
var directive = {};
directive.restrict = 'E';
directive.replace = 'true';
directive.scope = {};
directive.transclude = 'true';
directive.template =
'<div id="wrapper">' +
'<div required class="text-area" name="search" contenteditable="true" ng-model="formData.text"></div>' +
'<button type="submit" class="btn btn-warning add-button" id="submit" ng-click="createTodo()">Add</button>' +
'</div>';
directive.link = function(scope, element, attrs, controller) {
scope.createTodo = function(){
// do something
// Call child directive setPlaceholderText()
}
};
return directive;
})
.directive("contenteditable", function() {
var directive = {};
directive.require = ['^editableDiv','?ngModel'];
directive.restrict = 'A';
directive.scope = {};
directive.link = function(scope, element, attrs, ctrls) {
var ngModel = ctrls[1];
var editableDivController = ctrls[0];
function setPlaceholderText(){
return element.html("Hello World");
}
return directive;
})
I want to call child directive setPlaceholderText() when 'scope.createTodo()' of parent directive is called.
How can that be done.

In your parent directive
link: function($scope,$el,$attr) {
$el.find(".thing").on('click',function(event){
$scope.$broadcast('thing', $scope.someData);
});
}
In your child directive
link: function($scope. $el, $attr) {
$scope.$on('thing',function(event, someData) {
alert('My parent called me with this data: ' + someData);
});
}

You can $broadcast an event to child scopes (as mentioned), or you can also add a function to your parent directive's controller from your child directive.
Here is a jsfiddle with examples of both $broadcast to child directive and using the controller technique.
angular.module("myApp", [])
.directive("parentDirective", function () {
var directive = {};
directive.restrict = "E";
directive.scope = {};
directive.template = '<div>' +
'<button type="button" ng-click="broadcastDemo()">' +
'Broadcast to child' +
'</button>' +
'<button type="button" ng-click="controllerDemo()">' +
'Use Controller' +
'</button><br /><br />' +
'<child-directive></child-directive>';
//Setup directive controller
directive.controller = function ($scope) {
var ctrl = this;
//Store events for convenience
var events = ctrl.events = {
setPlaceHolderText: "setPlaceHolderTextEvent"
};
$scope.broadcastDemo = function () {
//$broadcast event and optional additional args
$scope.$broadcast(events.setPlaceHolderText, "Additional arg1", "Additional arg2");
};
$scope.controllerDemo = function () {
//do some work
//call the ctrl.setPlaceHolderText added by child
if (ctrl.setPlaceHolderText) {
ctrl.setPlaceHolderText();
}
};
};
return directive;
})
.directive("childDirective", function () {
var directive = {};
directive.restrict = "E";
directive.scope = {};
directive.require = ["^parentDirective", "?ngModel"];
directive.template = '<div></div>';
directive.link = function (scope, elem, attrs, ctrl) {
var parentDirCtrl = ctrl[0];
//allow parent scope(s) to $broadcast event
scope.$on(parentDirCtrl.events.setPlaceHolderText,
function (event, arg1, arg2) {
elem.html("$Broadcast: " + arg1 + " " + arg2);
});
//Add function to parent controller
parentDirCtrl.setPlaceHolderText = function () {
elem.html("Added to parent controller!");
}
};
return directive;
});
If you have multiple childDirectives and you go with adding function to parent directive controller, you will have to take precaution because each additional child directive would clobber the ctrl.setPlaceHolderText (i.e. only one elem.html would be called).
The bindonce library uses something similar to the second technique to allow child directives to add "binders" to the parent directive. The eventing model is probably cleaner for your purposes, but wanted to provide another option for directive communication.

Broadcast is not preferred, since If there is multiple instance of child or even parent child combination then broadcast will trigger all.

Related

Avoid using $timeout in Angular UI Modal

In this plunk I have an Angular UI Modal wrapped in a directive. From the controller, I call a method to open the modal, but to do so I need to use $timeout, otherwise, the DOM hasn't finished rendering the directive.
This seems to work, however, what would happen if whatever needs to be completed hasn't finished after the $timeout expires? The $timeout may work in a development environment but may fail in production. Is it a bad practice to use $timeout? How to avoid using it in this example?
HTML
<div modal control="modalCtl"></div>
Javascript
var app = angular.module('app', ['ui.bootstrap']);
app.controller('myCtl', function($scope,$timeout) {
$scope.modalCtl = {};
$timeout(function(){
$scope.modalCtl.openModal();
},100);
})
.directive('modal', function ($uibModal) {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
control: '='
};
directive.link = function (scope, element, attrs) {
scope.control = scope.control || {};
scope.control.openModal = function() {
scope.modalInstance = $uibModal.open({
template: '<button ng-click="close()">Close</button>',
scope: scope
})
};
scope.close = function () {
scope.modalInstance.close();
};
};
return directive;
});
To avoid using $timeout the directive can notify controller when everything is ready. Take a look:
.directive('modal', function ($uibModal) {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
control: '=',
onReady: '&' // <-- bind `onReady` with `onModalReady`
};
directive.link = function (scope, element, attrs) {
scope.control = scope.control || {};
scope.control.openModal = function() {
scope.modalInstance = $uibModal.open({
template: '<button ng-click="close()">Close</button>',
scope: scope
})
};
scope.close = function () {
scope.modalInstance.close();
};
scope.onReady(); // <-- notify controller
};
return directive;
});
Out HTML:
<div modal on-ready="onModalReady()" control="modalCtl"></div>
Our controller:
$scope.onModalReady = function(){
$scope.modalCtl.openModal();
}
Changed Plunker
About comment #Eduardo La Hoz Miranda
you should be ok with the timeout. I would decrease the time to 0, tho, since timeout will send your call to the bottom of the event loop.
Generally when we initialize $timeout with 0 milliseconds or with no argument as:
$timeout(function(){
$scope.modalCtl.openModal();
});
We delay $scope.modalCtl.openModal() to run before next digest cycle a.e. last in queue. So in this case directive link will run 1st from beginning to to the end and only after you will enter to $timeout.
The $timeout may work in a development environment but may fail in production.
On Production you have the same code. It should work. I believe the problem is in something else. If you are not confident with $timeout use above mentioned way I posted.
Your Logged Plunker
When link function of directive is finished, it can emit a message that it's ready.
And controller listens to this message and displays modal when received.
Code:
var app = angular.module('app', ['ui.bootstrap']);
app.controller('myCtl', function($scope,$timeout) {
$scope.modalCtl = {};
$scope.$on("hey", function() {
$scope.modalCtl.openModal();
});
})
.directive('modal', function ($uibModal) {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
control: '='
};
directive.link = function (scope, element, attrs) {
scope.control = scope.control || {};
scope.control.openModal = function() {
scope.modalInstance = $uibModal.open({
template: '<button ng-click="close()">Close</button>',
scope: scope
})
};
scope.close = function () {
scope.modalInstance.close();
};
scope.$emit("hey");
};
return directive;
});
Using timeout for any arbitrary waits on code executing is generally bad. As you state in your question, depending on the overall context of the page you are loading, you have no guarantee that the directive will be ready at the time your controller runs.
It seems like you have too many levels of abstraction here. Something is rendering a div that when it is fully rendered, shows a modal.
Wouldn't it make more sense to just have the thing that is rendering the div create and show the modal instead ?

Adding ng-click to div created with angular.element

I have the following directive that has two children divs. The second div (created with angular.element) should be clickeable. Since I created it with angular.element, I'm trying to add ng-click with the attr function, but this doesn't work. What's wrong with this code?
app.directive('mydir', function () {
var directive = {};
directive.restrict = 'EA';
directive.replace = true;
directive.scope = {
control: '=',
};
directive.template = '<div id="root"></div>';
directive.link = function (scope, element, attrs) {
var wrap = angular.element('<div id="wrap"></div>');
element.append(wrap);
var node = angular.element('<div id="node"></div>');
node.attr('ng-click', 'toggle()'); // <--- this doesn't work
wrap.append(node);
scope.toggle = function () {
alert('clicked');
};
});
return directive;
});
The element has to be compiled using angular's $compile service:
app.directive('mydir', function ($compile) { // added dependency here
var directive = {};
directive.restrict = 'EA';
directive.replace = true;
directive.scope = {
control: '=',
};
directive.template = '<div id="root"></div>';
directive.link = function (scope, element, attrs) {
var wrap = angular.element('<div id="wrap"></div>');
element.append(wrap);
var node = angular.element('<div id="node"></div>');
node.attr('ng-click', 'toggle()'); // <--- this doesn't work
var content = $compile(node)(scope);
wrap.append(content);
scope.toggle = function () {
alert('clicked');
};
});
return directive;
});
Here's a short tutorial on using $compile. Hope that helps

How to test function within directive link

I have a directive for which i am trying to write some unit test for:
return {
restrict: 'E',
replace: true,
controllerAs: 'vm',
transclude: true,
template: '<ul>' +
'<li ng-show="vm.hideItem">Home</li>' +
'<li ng-show="vm.hideItem">Products</li>' +
'<li ng-show="!vm.hideItem">Cart</li>' +
'<li ng-show="vm.hideItem">Contact Us</li>' +
'</ul>',
link: function(scope, element, attrs, vm) {
function getData(index) {
if (index){
vm.hideItem = true
}
else {
var li = element.find("li");
li.attr("context-driven", "");
}
}
function getIndex(){
index = myService.getIndex();
getData(index);
}
getIndex();
},
controller: function(){}
};
I have the following, which pass:
describe('<-- myDirective Spec ------>', function () {
var scope, $compile, element, myService;
beforeEach(angular.mock.module('MyApp'));
beforeEach(inject(function (_$rootScope_, _$compile_, _myService_) {
scope = _$rootScope_.$new();
$compile = _$compile_;
myService = _myService_;
var html = '<my-directive></my-directive>';
element = $compile(angular.element(html))(scope);
scope.$digest();
}));
it('should return an unordered list', function () {
var ul = element.find('ul');
expect(ul).toBeDefined();
});
How can i test the call of getIndex, getData and ensure myService has been called?
The key to successful directive testing is moving all view-related logic to controller, i.e.
this.getIndex = function () {
index = myService.getIndex();
getData(index);
}
After the element was compiled in spec, the controller instance can be retrieved and spied with
var ctrl = element.controller('myDirective');
spyOn(ctrl, 'getIndex').and.callThrough();
The good thing about writing specs is that they show design flaws. In current case it is DOM manual operation in getData. It is not clear from the code what context-driven attribute is for, but the same thing has to be achieved in Angular (data binding) and not jQuery (DOM manipulation) fashion in order to be test-friendly.

Angularjs directive template = $scope.(some variable from another controller)

This is part of my AngularJS application
.controller('Littlebear',function($scope) {
$scope.spread='<h1>this is the</h1> Littlebear spread.'+
'<img ng-src="src/images/retro.png" alt="picture" ng-click="click()">';
})
.directive('spread', function($compile) {
var templateTemp='<p> try again </p>';
var directive = {};
directive.compile = function(element, attributes) {
var linkFunction = function($scope, element, atttributes) {
// bind element to data in $scope
templateTemp=$scope.spread;
return templateTemp;
};
return linkFunction;
};
directive.restrict = 'E'; /* restrict this directive to elements */
directive.template = templateTemp;
return directive;
})
I would like to set template = $scope.spread inside the directory.
If I console.log the templateTemp inside the linkFunction the value of templateTemp is exacly what I am looking for but ouside of that function is templateTemp=' try again ';
can anyone suggest any solution?
(PS: as you might imagine I am quite new to AngularJS)
Thanks Vincent
In link function you can do something as below.
In link function, I have compiled the desired html and replaced the directive element with the same.
.controller('Littlebear',function($scope) {
$scope.spread='<h1>this is the</h1> Littlebear spread.'+
'<img ng-src="src/images/retro.png" alt="picture" ng-click="click()">';
})
.directive('spread', function($compile) {
var templateTemp='<p> try again </p>';
var directive = {};
directive.compile = function(element, attributes) {
var linkFunction = function($scope, element, atttributes) {
// you can change and compile the html like this
//element.replaceWith($compile($scope.spread)); //Please Check this line<-----
//Please try this.
var html =$scope.spread;
var e =$compile(html)($scope);
element.replaceWith(e);
};
return linkFunction;
};
directive.restrict = 'E'; /* restrict this directive to elements */
directive.template = templateTemp;
return directive;
})

How to declare Kendo UI object in AngularJS directive link

I have the following kendoDatePicker included in an AngularJS directive; the directive is supposed to show the picker opening the calendar but it doesn't. What's wrong with this code? This is the plunk.
HTML:
<dir2></dir2>
Javascript:
var app = angular.module("app", [ "kendo.directives" ]);
function MyCtrl($scope) {
}
app.directive('dir2', function() {
var directive = {};
directive.restrict = 'A';
directive.template = '<input kendo-date-picker="picker" />';
directive.link = function (scope, element, attrs) {
scope.picker.open();
};
return directive;
});
You should define directive.restrict = 'E' instead of 'A'.
'A' refers for attribute, 'E' for element.
app.directive('dir2', function() {
var directive = {};
directive.restrict = 'E';
directive.template = '<input kendo-date-picker="picker" />';
directive.link = function (scope, element, attrs) {
scope.picker.open();
};
return directive;
});
This is the solution:
where it says
scope.picker.open();
it should say
$timeout(function() {
scope.picker.open();
}, 1);

Resources