Extending base directive functionality to other directives - angularjs

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.

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

$emit vs callback in parent child directive communication best approach

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.

How can I include HTML into a page with AngularJS from an HTML file?

I have some code that I have taken over. It looks like this:
<div id="init"
data-admin-template
ng-show="ss.display.init"
data-template-url="init.html">
</div>
There's a directive adminTemplate:
app.directive('adminTemplate', ['stateService', function (stateService) {
return {
restrict: 'A',
templateUrl: function (elem, attrs) {
return "/Content/app/admin/templates/" + attrs.templateUrl;
},
link: function (scope, element, attrs) {
scope.stateService = stateService;
}
};
}]);
I am not very familiar with templates and it seems that this directive does not really do much. Is there a way I can do this without the directive?
As #Oli said in the comments, ng-include is definitely one solution. There are others as well - angular likes to offer you just enough options to leave you doubting yourself - but i'm wondering what you'd actually gain by changing it.
With ng-include you'd need to add a controller to to your html, to supply stateService (if you don't do it here, you'd have to add it to every different admin template). So you'd end up with:
<div id="init"
ng-include="/Content/app/admin/templates/init.html"
ng-show="ss.display.init"
ng-controller="AdminController">
</div>
So you end up with the same amount of attributes, you need the whole template path and it becomes less readable. Looking at what you have now, it's clear to see the intent.
You could also go one step further and give it the flexibility of being an element or attribute
<admin-template
id="init"
ng-show="ss.display.init"
data-template-url="init.html">
</admin-template>
app.directive('adminTemplate', ['stateService', function (stateService) {
return {
restrict: 'EA',
templateUrl: function (elem, attrs) {
return "/Content/app/admin/templates/" + attrs.templateUrl;
},
link: function (scope, element, attrs) {
scope.stateService = stateService;
}
};
}]);
It may appear to be doing very little, but my feeling is that the previous developer has refactored out repetition, to get to this little directive. I'd argue that as it is it removes boiler plate, allows easy re-usability and communicates intent well in your mark up.

How to prevent duplicated attributes in angular directive when replace=true

I've found that angular directives that specify replace: true will copy attributes from the directive usage into the output rendered by the template. If the template contains the same attribute, both the template attribute value and the directive attribute value will be combined together in the final output.
Directive usage:
<foo bar="one" baz="two"></foo>
Directive:
.directive('foo', function() {
return {
restrict: 'E',
replace: true,
template: '<div bar="{{bar}}" baz="baz"></div>',
scope: {
bar: '#'
},
link: function(scope, element, attrs, parentCtrl) {
scope.bar = scope.bar || 'bar';
}
};
})
Output:
<div bar="one " baz="two baz" class="ng-isolate-scope"></div>
The space in bar="one " is causing problems, as is multiple values in baz. Is there a way to alter this behavior? I realized I could use non-conflicting attributes in my directive and have both the template attributes and the non-conflicting attributes in the output. But I'd like to be able to use the same attribute names, and control the output of the template better.
I suppose I could use a link method with element.removeAttr() and element.attr(). It just seems like there should be a better solution.
Lastly, I realize there is talk of deprecating remove: true, but there are valid reasons for keeping it. In my case, I need it for directives that generate SVG tags using transclusion. See here for details:
https://github.com/angular/angular.js/commit/eec6394a342fb92fba5270eee11c83f1d895e9fb
No, there isn't some nice declarative way to tell Angular how x attribute should be merged or manipulated when transplanted into templates.
Angular actually does a straight copy of attributes from the source to the destination element (with a few exceptions) and merges attribute values. You can see this behaviour in the mergeTemplateAttributes function of the Angular compiler.
Since you can't change that behaviour, you can get some control over attributes and their values with the compile or link properties of the directive definition. It most likely makes more sense for you to do attribute manipulation in the compile phase rather than the link phase, since you want these attributes to be "ready" by the time any link functions run.
You can do something like this:
.directive('foo', function() {
return {
// ..
compile: compile
// ..
};
function compile(tElement, tAttrs) {
// destination element you want to manipulate attrs on
var destEl = tElement.find(...);
angular.forEach(tAttrs, function (value, key) {
manipulateAttr(tElement, destEl, key);
})
var postLinkFn = function(scope, element, attrs) {
// your link function
// ...
}
return postLinkFn;
}
function manipulateAttr(src, dest, attrName) {
// do your manipulation
// ...
}
})
It would be helpful to know how you expect the values to be merged. Does the template take priority, the element, or is some kind of merge needed?
Lacking that I can only make an assumption, the below code assumes you want to remove attributes from the template that exist on the element.
.directive('foo', function() {
return {
restrict: 'E',
replace: true,
template: function(element, attrs) {
var template = '<div bar="{{bar}}" baz="baz"></div>';
template = angular.element(template);
Object.keys(attrs.$attr).forEach(function(attr) {\
// Remove all attributes on the element from the template before returning it.
template.removeAttr(attrs.$attr[attr]);
});
return template;
},
scope: {
bar: '#'
}
};
})

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.

Resources