$emit vs callback in parent child directive communication best approach - angularjs

Im trying to understand what is the best GENERIC approach to communicate between parent and child directive with isolated scopes (they might be reusable items).
meaning if child directive needs to update parent directive in some manner (both have isolated scopes),
should I
pass a callback function :
e.g:
.directive('filterReviewStepBox', function () {
return {
restrict: 'EA',
scop: {
//some data
},
template: '<div>text<reusable-dir></reusable-dir call-back="foo"></div>',
link: function (scope, elem, attrs) {
scope.foo = function () {
console.log('bar');
};
}
};
}).directive('reusableDir', function () {
return {
restrict: 'EA',
scope: {
callBack: '=callBack'
//other data
},
template: '<div>text<button type="button" ng-click="bar"></button></div>',
link: function (scope, elem, attrs) {
scope.bar = function () {
scope.callBack();
}
}
};
});
or should I use $emit():
e.g:
directive('filterReviewStepBox', function () {
return {
restrict: 'EA',
scope: {
// some data
},
template: '<div>text<reusable-dir></reusable-dir></div>',
link: function (scope, elem, attrs) {
scope.$on('foo', function () {
console.log('bar');
});
}
};
}).directive('reusableDir', function () {
return {
restrict: 'EA',
scope: { //some data
},
template: '<div>text<button type="button" ng-click="bar"></button></div>',
link: function (scope, elem, attrs) {
scope.bar = function () {
scope.$emit('foo');
};
}
};
});
I feel that emit would be easier to understand on a larger scale but worried about performance and overhead, but Im still unsure
tried looking for the best approach online but Im still stomped
EDIT
I forgot about the
require
option.
but I'm still not sure this is the most correct solution.
Since this doesn't allow me to reuse the child or grandchild and kind of makes the directive a single purpose item.

For this purpose the best is to utilize "require" attribute.
Complete guide to directives tell us this about require attribute : https://docs.angularjs.org/api/ng/service/$compile
Require another directive and inject its controller as the fourth
argument to the linking function. The require takes a string name (or
array of strings) of the directive(s) to pass in.
Require just tells the directive it should look for some parent directive and take its controller. You can tell directive to search in parent elements with ^ prefix and tell if this requirement is optional with ? prefix.
I have modified your example, so reusableDir can call filterReviewStepBox controller, but can be also used alone.
http://jsbin.com/gedaximeko/edit?html,js,console,output
angular.module('stackApp', [])
.directive('filterReviewStepBox', function () {
return {
restrict: 'EA',
scope: {
// some data
},
template: '<div>text<reusable-dir></reusable-dir></div>',
link: function (scope, elem, attrs) {
},
controller : function() {
this.notify = function(value) {
console.log('Notified : ' + value);
};
},
controllerAs: 'vm',
bindToController: true
};
}).directive('reusableDir', function () {
return {
restrict: 'EA',
scope: { //some data
},
require: '?^filterReviewStepBox',
template: '<div>text<button type="button" ng-click="bar()"></button></div>',
link: function (scope, elem, attrs, controller) {
scope.bar = function () {
if (controller) {
controller.notify('foo');
}
};
}
};
});

Personally, I try to avoid generating my own custom events. Mainly because it's incredibly easy to cause unwanted event propagation, or bubbling (often causing unwanted scope digests), but also because of the systematic 'noise' that in bigger applications can become very hard to manage.
My own personal views aside though, and if it's a more 'future-proof' approach you're looking for, the callback approach is by far the safest. Here's why...
Angular2 encourages developers to expose onChange attributes from directives and components that depend on child-to-parent communication. Note that these are nothing more than bound functions, and should be defined accordingly in the bindings or scopes of the given component or directive.
Similar to the above, Angular2 documents an ngOnChanges method that can be defined by any controller wishing to subscribe to changes to it's bindings. If supplied, this method is called before any DOM manipulation occurs (with the new bound values), so it's a perfect candidate for a more 'general' parent to child communication over the current AngularJS $scope.$watch implementation.
Something that wasn't mentioned however, is that communication via services is still recommended, in both AngularJS and Angular2.
ngOnChanges: https://angular.io/docs/js/latest/api/core/OnChanges-interface.html

I think it is not just about the performance, you also need to take few more factors into consideration from design and maintenance perspective.
Callback method: This is perhaps the most efficient option. The child directive would just call one method of parent directive, no
overheads. There are quite a few options explained here:
https://stackoverflow.com/a/27997722/2889528
Emit (or for that matter broadcast): This method publishes the event to all $scope(s) up (emit) or down (broadcast) to hierarchy,
relatively costly but it gives you a flexibility to watch and handle
the event wherever $scope is available.
Injectable Common Service: This option can be used when you want to call some method where $scope is not available. However, it created tight dependency on service. This is more of a
pub-sub design.

Related

Can I set a scope property from a directive's link function?

I'm making a directive containing a canvas and I'm having trouble accessing it in all the places I need to. I'm currently setting up the canvas in the directive's link and drawing some initial elements on it, but I also need to access the same canvas in my directive's controller to update it. At the moment my directive declaration looks like this:
angular.module('myModule').directive('myCanvasDirective', CanvasDirective);
function CanvasDirective() {
var linker = function (scope, element, attrs) {
scope.vm.ctx = element[0].childNodes[0].getContext('2d');
//do some initial drawing using scope.vm.ctx
//this works
}
return {
priority: 1001,
restrict: 'E',
link: linker,
scope: {
displayValue: '=displayValue'
},
template: '<canvas id="myCanvas" width="80" height="80" />',
controller: MyCanvasController,
controllerAs: 'vm',
bindToController: true
};
};
function MyCanvasController() {
var vm = this;
vm.draw = function () {
vm.ctx.strokeStyle = '#808080';
//vm.ctx is unavailable here despite being attached to scope in linker
};
vm.draw();
};
How can I get access to my canvas context in MyCanvasController? As this directive is going to be used many times on a page thanks to several ngRepeats I'd prefer not to just use document.getElementById().
Link function got a controller instance, even if it wasn't exposed on scope with controllerAs.
function (scope, element, attrs, ctrl) {
ctrl.ctx = element[0].childNodes[0].getContext('2d');
ctrl.draw();
}
And
vm.ctx is unavailable here despite being attached to scope in linker
is because the controller runs before link. Although controllers have $element local dependency, all 'when DOM element is ready' logic should be delegated to link function.
Angular 1.5 encourages the usage of component and discourages the usage of link for Angular 2 migration reasons. Consider using $onInit controller method instead for this kind of things in Angular 1.5+.
I think you are breaking some of the best practices of encapsulation in your question. You should be setting the strokeStyle inside of the directive containing the canvas. You can do that by passing an additional attribute and binding in the link.
In answer to your question , you can pass the controller as a parameter to the directive. To pass as parameter:
<my-canvas-directive vmparent="vm"></my-canvas-directive>
Access in your link as
linker = function (scope, element, attrs) {
attrs.vmparent.ctx = element[0].childNodes[0].getContext('2d');
}

Extending base directive functionality to other directives

I just wanted to check is this functionality is possible in angularjs.
Eg: I have base directive with some common functions but in my whole application all the directives need this functions to be implemented so more code duplication occurs, I am expecting same functionality as extends(inheritance) in Java.
Is it possible to achieve this functionality in angularjs.?
As per my requirement i can't archive this function by moving this code to services or factory because it is like all of by directives in the application need to perform this operation on linking, so now all of my directive link function container same functionality with duplicate code.
I am expecting something like extending base directive functionality for all my directives.
[Note]
Most of the directives in my application has isolated scope.
The answer primarily lies in JS domain, and finding patterns for reusable code is a good challenge. Angular got extend, copy and merge, but that's all, modules and DI are just ways to get around the limitations of ES5.
Here's a hint on how you can create mixins from directive definition objects.
app.directive('blockBase', function () {
return {
link: function (scope, element, attrs) {
scope.title = 'block';
}
};
});
app.directive('brickBase', function () {
return {
restrict: 'E',
scope: true,
link: function (scope, element, attrs) {
// ...
}
};
});
app.directive('block', function (blockBaseDirective, brickBaseDirective) {
var blockDirective = angular.extend(brickBaseDirective[0], {
name: 'brick',
restrict: 'EA',
scope: {
title: '='
}
});
blockDirective.compile = function (element, attrs) {
// ...
return {
post: function (scope, element, attrs) {
blockBaseDirective[0].link(scope, element, attrs);
scope.title += ' offspring';
}
};
};
return blockDirective;
});
You should decide for yourself if this approach looks better than decorators.
Using named functions outside of controller/directive definitions is popular way to get rid of duplicate code, not an elegant one. And of course you can share functions or classes among directives with custom DDO properties. However, factories are still more suitable for that.

Should I use isolate scope in this case?

I'm implementing an custom input widget. The real code is more complex, but generally it looks like this:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
compile: function (tElement, tAttributes){
//flow the bindings from the parent.
//I can do it dynamically, this is just a demo for the idea
tElement.find("input").attr("placeholder", tAttributes.placeholder);
tElement.find("input").attr("ng-model", tElement.attr("ng-model"));
}
};
});
inputWidget.html:
<div>
<input />
<span>
</span>
</div>
To use it:
<input-widget placeholder="{{name}}" ng-model="someProperty"></input-widget>
The placeholder is displayed correctly with above code because it uses the same scope of the parent: http://plnkr.co/edit/uhUEGBUCB8BcwxqvKRI9?p=preview
I'm wondering if I should use an isolate scope, like this:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
scope : {
placeholder: "#"
//more properties for ng-model,...
}
};
});
With this, the directive does not share the same scope with the parent which could be a good design. But the problem is this isolate scope definition will quickly become messy as we're putting DOM-related properties on it (placeholder, type, required,...) and every time we need to apply a new directive (custom validation on the input-widget), we need to define a property on the isolate scope to act as middle man.
I'm wondering whether it's a good idea to always define isolate scope on directive components.
In this case, I have 3 options:
Use the same scope as the parent.
Use isolate scope as I said above.
Use isolate scope but don't bind DOM-related properties to it, somehow flow the DOM-related properties from the parent directly. I'm not sure if it's a good idea and I don't know how to do it.
Please advice, thanks.
If the input-widget configuration is complex, I would use an options attribute, and also an isolated scope to make the attribute explicit and mandatory:
<input-widget options="{ placeholder: name, max-length: 5, etc }"
ng-model="name"></input-widget>
There is no need to flow any DOM attributes if you have the options model, and the ngModel:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
scope: { options:'=', ngModel: '='}
};
});
And in your template, you can bind attributes to your $scope view model, as you normally would:
<div>
<input placeholder="{{options.placeholder}}" ng-model="ngModel"/>
<span>
{{options}}
</span>
</div>
Demo
Personally, when developing for re-use, I prefer to use attributes as a means of configuring the directive and an isolated scope to make it more modular and readable. It behaves more like a component and usually without any need for outside context.
However, there are times when I find directives with child / inherited scopes useful. In those cases, I usually 'require' a parent directive to provide the context. The pair of directives work together so that less attributes has to flow to the child directive.
This is not a very trivial problem. This is because one could have arbitrary directives on the templated element that are presumably intended for <input>, and a proper solution should ensure that: 1) these directives compile and link only once and 2) compile against the actual <input> - not <input-widget>.
For this reason, I suggest using the actual <input> element, and add inputWidget directive as an attribute - this directive will apply the template, while the actual <input> element would host the other directives (like ng-model, ng-required, custom validators, etc...) that could operate on it.
<input input-widget
ng-model="someProp" placeholder="{{placeholder}}"
ng-required="isRequired"
p1="{{name}}" p2="name">
and inputWidget will use two compilation passes (modeled after ngInclude):
app.directive("inputWidget", function($templateRequest) {
return {
priority: 400,
terminal: true,
transclude: "element",
controller: angular.noop,
link: function(scope, element, attrs, ctrl, transclude) {
$templateRequest("inputWidget.template.html").then(function(templateHtml) {
ctrl.template = templateHtml;
transclude(scope, function(clone) {
element.after(clone);
});
});
}
};
});
app.directive("inputWidget", function($compile) {
return {
priority: -400,
require: "inputWidget",
scope: {
p1: "#", // variables used by the directive itself
p2: "=?" // for example, to augment the template
},
link: function(scope, element, attrs, ctrl, transclude) {
var templateEl = angular.element(ctrl.template);
element.after(templateEl);
$compile(templateEl)(scope);
templateEl.find("placeholder").replaceWith(element);
}
};
});
The template (inputWidget.template.html) has a <placeholder> element to mark where to place the original <input> element:
<div>
<pre>p1: {{p1}}</pre>
<div>
<placeholder></placeholder>
</div>
<pre>p2: {{p2}}</pre>
</div>
Demo
(EDIT) Why 2 compilation passes:
The solution above is a "workaround" that avoids a bug in Angular that was throwing with interpolate values being set on a comment element, which is what is left when transclude: element is used. This was fixed in v1.4.0-beta.6, and with the fix, the solution could be simplified to:
app.directive("inputWidget", function($compile, $templateRequest) {
return {
priority: 50, // has to be lower than 100 to get interpolated values
transclude: "element",
scope: {
p1: "#", // variables used by the directive itself
p2: "=" // for example, to augment the template
},
link: function(scope, element, attrs, ctrl, transclude) {
var dirScope = scope,
outerScope = scope.$parent;
$templateRequest("inputWidget.template.html").then(function(templateHtml) {
transclude(outerScope, function(clone) {
var templateClone = $compile(templateHtml)(dirScope);
templateClone.find("placeholder").replaceWith(clone);
element.after(templateClone);
});
});
}
};
});
Demo 2

Communicating with sibling directives

Goal: Create behaviors using directives with communication between 2 sibling elements (each their own directive).
A behavior to use in example: The article content is hidden by default. When the title is clicked, I want the related article content to display.
The catch: The related article elements need to associate to each other without being nested in a single parent element or directive.
<div article="article1">this is my header</div>
<div id="article1" article-content>this is content for the header above</div>
<div article="article2">this is my header</div>
<div id="article2" article-content>this is content for the header above</div>
I know it would be easier to place the content inside the article directive, however this question is to find out how to solve a situation like this.
Can the content directive pass itself to the related article directive somehow?
This code isn't very useful as it is now, but it's a starting point. How would I accomplish this?
.directive('article', function(){
return {
restrict: "A",
controller: function($scope) {
$scope.contentElement = null;
this.setContentElement = function(element) {
$scope.contentElement = element;
}
},
link: function(scope, element) {
element.bind('click', function(){
// Show article-content directives that belong
// to this instance (article1) of the directive
}
}
}
}
.directive('articleContent', function(){
return {
require: "article",
link: function(scope, element, attrs, articleCtrl) {
// Maybe reference the article i belong to and assign element to it?
// I can't though because these are siblings.
}
}
}
None of the directive require options will allow you to require sibling directives (as far as I know). You can only:
require on the element, using require: "directiveName"
tell angular to search up the DOM tree using require: "^directiveName"
or require: "^?directiveName" if you don't necessarily need the parent controller
or require: "^\?directiveName" if you don't necessarily need the parent DOM wrapper
If you want sibling to sibling communication, you'll have to house them in some parent DOM element with a directive controller acting as an API for their communication. How this is implemented is largely dependent on the context at hand.
Here is a good example from Angular JS (O Reilly)
app.directive('accordion', function() {
return {
restrict: 'EA',
replace: true,
transclude: true,
template: '<div class="accordion" ng-transclude></div>',
controller: function() {
var expanders = [];
this.gotOpened = function(selectedExpander) {
angular.forEach(expanders, function(expander) {
if(selectedExpander != expander) {
expander.showMe = false;
}
});
};
this.addExpander = function(expander) {
expanders.push(expander);
}
}
}
});
app.directive('expander', function() {
return {
restrict: 'EA',
replace: true,
transclude: true,
require: '^?accordion',
scope: { title:'#' },
template: '<div class="expander">\n <div class="title" ng-click="toggle()">{{ title }}</div>\n <div class="body" ng-show="showMe" \n ng-animate="{ show: \'animated flipInX\' }"\n ng-transclude></div>\n</div>',
link: function(scope, element, attrs, accordionController) {
scope.showMe = false;
accordionController.addExpander(scope);
scope.toggle = function toggle() {
scope.showMe = !scope.showMe;
accordionController.gotOpened(scope);
}
}
}
})
Usage (jade templating):
accordion
expander(title="An expander") Woohoo! You can see mme
expander(title="Hidden") I was hidden!
expander(title="Stop Work") Seriously, I am going to stop working now.
Or you can create a service just for directive communication, one advantage of special service vs require is that your directives won't depend on their location in html structure.
The above solutions are great, and you should definitely consider using a parent scope to allow communication between your directives. However, if your implementation is fairly simple there's an easy method built into Angular that can communicate between two sibling scopes without using any parent: $emit, $broadcast, and $on.
Say for example you have a pretty simple app hierarchy with a navbar search box that taps into a complex service, and you need that service to broadcast the results out to various other directives on the page. One way to do that would be like this:
in the search service
$rootScope.$emit('mySearchResultsDone', {
someData: 'myData'
});
in some other directives/controllers
$rootScope.$on('mySearchResultsDone', function(event, data) {
vm.results = data;
});
There's a certain beauty to how simple that code is. However, it's important to keep in mind that emit/on/broadcast logic can get nasty very quickly if you have have a bunch of different places broadcasting and listening. A quick google search can turn up a lot of opinions about when it is and isn't an anti-pattern.
Some good insight on emit/broadcast/on in these posts:
http://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
http://nathanleclaire.com/blog/2014/04/19/5-angularjs-antipatterns-and-pitfalls/
If there is a list of articles and its content we can do it without any directive, using ng-repeat
<div ng-repeat="article in articles">
<div article="article1" ng-click='showContent=true'>{{article.header}}</div>
<div id="article1" article-content ng-show='showContent'>{{article.content}}</div>
</div>
So you need to define the article model in controller. We are making use of local scope created by ng-repeat.
Update: Based on your feedback, you need to link them together.You can try
<div article="article1" content='article1'>this is my header</div>
<div id="article1" article-content>this is content for the header above</div>
and in your directive
use
link: function(scope, element,attrs) {
element.bind('click', function(){
$('#'+attrs.content).show();
}
}
And the final method could be to use $rootScope.$broadcast and scope.$on methods to communicate between to controllers. But in this approach you need to track from where the message came and who is the intended recipient who needs to process it.
I had the exact same problem and I was able to solve it.
In order to get one directive to hide other sibling directives, I used a parent directive to act as the API. One child directive tells the parent it wasn't to be shown/hidden by passing a reference to its element, and the other child calls the parent toggle function.
http://plnkr.co/edit/ZCNEoh
app.directive("parentapi", function() {
return {
restrict: "E",
scope: {},
controller: function($scope) {
$scope.elements = [];
var on = true;
this.toggleElements = function() {
if(on) {
on = false;
_.each($scope.elements, function(el) {
$(el).hide();
});
} else {
on = true;
_.each($scope.elements, function(el) {
$(el).show();
});
}
}
this.addElement = function(el) {
$scope.elements.push(el);
}
}
}
});
app.directive("kidtoggle", function() {
return {
restrict: "A",
require: "^parentapi",
link: function(scope, element, attrs, ctrl) {
element.bind('click', function() {
ctrl.toggleElements();
});
}
}
});
app.directive("kidhide", function() {
return {
restrict: "A",
require: "^parentapi",
link: function(scope, element, attrs, ctrl) {
ctrl.addElement(element);
}
}
});
I had the same issue with a select all/ select item directive I was writing. My issue was the select all check box was in a table header row and the select item was in the table body. I got around it by implementing a pub/sub notification service so the directives could talk to each other. This way my directive did not care about how my htlm was structured. I really wanted to use the require property, but using a service worked just as well.

Directive-to-directive communication in AngularJS?

I already know that you can set up a controller within a directive, and that other directives can call the functions on that controller. Here's what my current directive looks like:
app.directive("foobar", function() {
return {
restrict: "A",
controller: function($scope) {
$scope.trigger = function() {
// do stuff
};
},
link: function(scope, element) {
// do more stuff
}
};
});
I know that I could call it like this:
app.directive("bazqux", function() {
return {
restrict: "A",
require: "foobar",
link: function(scope, element, attrs, fooBarCtrl) {
fooBarCtrl.trigger();
}
};
});
However, I want to be able to call trigger from any directive, not just my own custom ones, like this:
<button ng-click="foobar.trigger()">Click me!</button>
If that doesn't work, is there a way to bring in a third directive to make it happen? Like this?
<button ng-click="trigger()" target-directive="foobar">Click me!</button>
Thanks!
Sounds like you need an angular service. http://docs.angularjs.org/guide/dev_guide.services
This will allow you to share functionality across directives.
Here's a similar question: Sharing data between directives
One simple way of accomplishing application-wide communication between any components would be to use global events (emitted from the $rootScope). For example:
JS:
app.directive('directiveA', function($rootScope)
{
return function(scope, element, attrs)
{
// You can attach event listeners in any place (controllers, too)
$rootScope.$on('someEvent', function()
{
alert('Directive responds to a global event');
});
};
});
HTML:
<button ng-click="$emit('someEvent')">Click me!</button>
Here you're emitting an event from the child scope but it will eventually reach the $rootScope and run the previous listener.
Here's a live example: http://plnkr.co/edit/CpKtR5R357tEP32loJuG?p=preview
When talking on irc it turned out that the communication is unnecessary:
I've got an attribute-restricted directive which performs some DOM manipulation on its parent element when it's "triggered"
A solution is to keep the logic inside the same directive and just to apply the dom changes to the parent.
http://jsfiddle.net/wt2dD/5/
scope.triggerSmthOnParent = function () {
element.parent().toggleClass('whatewer');
}

Resources