Dynamically register directive at runtime - angularjs

Normally, directives are registered on a module by using the directive method:
.directive('MyDirective', function (MyDep) {
return {
restrict: 'E',
template: '<div></div>',
controller: function ($scope) {
}
}
});
However, what if I dynamically want to register directives during runtime? For example, say a user pulls the following data from the server:
{
directives: {
dir1: {
restrict: 'E',
template: '<div></div>',
},
dir2: {
restrict: 'E',
template: '<div></div>',
}
}
}
Is there any way I can take this data use it to dynamically register new directives for my app? If successful, I should then be able to dynamically generate and $compile HTML which depends on them.

This is a subset of the "lazy loading Angular artifacts" problem (I have explored here and there exist other resources too). An idea is to use a config function to "steal" the $compileProvider (ref) and then call $compileProvider.directive(...) on demand, based on your data.
A rough sketch of the idea is:
var cachedCompileProvider;
angular.module(...).config(['$compileProvider', function($compileProvider) {
cachedCompileProvider = $compileProvider;
});
And then (e.g. from inside someplace that has access to $http):
$http.get(...).then(function(response) {
angular.forEach(response.data.directives, function(dirDefinition, dirName) {
cachedCompileProvider.directive(dirName, dirDefinition);
});
});
(Of course you cannot receive controller functions from a JSON response like the above, you will have to use other techniques - but hopefully you get the idea.)

Related

Utility functions for directives

Say I want to make an angular directive that generates links to resources that look like this:
link/to/resource/1234
from an object that looks like:
resource = {
id: 1234,
otherProperty: 'foo'
}
How can I do this effectively w/ a directive? Ie, I'd like to not have to repeat the part that goes '/link/to/resource/{{id}}'. I can't seem to get that to work right. One of the several things I've tried:
app.directive('myResource', function() {
return {
restrict: 'E',
scope: {
resource: '='
},
baseUrl: 'link/to/{{resource.id}}',
template: '{{baseUrl}}'
};
});
which ends up rendering:
Other things I've tried (like making baseUrl a function/sticking it in scope) have resulted in similar things/errors.
Is there a way to get something like this to work?
One way to handle this is to use the directive's link function to set the variable up for you, like this:
link: function(scope) {
scope.baseUrl= 'link/to/'+scope.resource.id;
},
template: '{{baseUrl}}'
Here's a working fiddle
Alternatively you could use this approach:
link: function(scope) {
scope.baseUrl= 'link/to/';
},
template: '{{baseUrl}}{{resource.id}}
Here's the fiddle
just write a directive controller
app.directive('myResource', function() {
return {
restrict: 'E',
scope: {
resource: '='
},
controller: function($scope){
$scope.getBaseUrl = function(resource){
return 'link/to/'+resource.Id;
};
},
template: '<a ng-href="http://{{getBaseUrl(resource)}}">{{getBaseUrl(resource)}}</a>'
};
});
a function makes more sense,because you wont have manage resource state change.You may answer that the Id is unlikely to change but in my opinion,it's better practice in general.
http://plnkr.co/edit/I2QKNB1o8jvZf7kCDT2v?p=preview

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.

Angular Directive Different Template

I have a directive myDirective with variable type. If I run <my-directive type="X"> I want the directive to use templateUrl: x-template.html.
If I do <my-directive type="Y"> I want the directive to use templateUrl: y-template.html.
This is my current directive.
app.directive('myDirective', function() {
var myDirective = {
templateUrl: 'X-template.html',
restrict: 'E',
scope: {
type: '='
},
};
return myDirective;
});
I read thru stackoverflow and angular documentation but have not found anything that I need.
I am now trying to do something along the lines of:
if ($scope.type === 'X') {
templateUrl: 'X-template.html',
}
else if ($scope.type === 'Y') {
templateUrl: 'Y-template.html',
}
But do not know where to do it.
Do you guys know if this is possible and how?
Angular will accept a function as the template option, so you could do something like so:
.directive('myDirective', function () {
return {
templateUrl: function (tElement, tAttrs) {
if (tAttrs) {
if (tAttrs.type === 'X') {
return 'X-template.html';
}
if (tAttrs.type === 'Y') {
return 'Y-template.html';
}
}
}
}
});
For more info, see the documentation for the $compile service.
You can work around this issue using ng-include inside compile:
app.directive('myDirective', function() {
return {
restrict: 'E',
compile: function(element, attrs) {
element.append('<div ng-include="\'' + attrs.type + '-template.html\'"></div>');
}
}
});
fiddle
If you're willing to live on the bleeding edge with a build on the 1.1.x code path (note the warning attached to every 1.1.x build notes entry so I don't dilute this answer by repeating it again here), you're in luck--this very feature was just added in the 1.1.4 release on April 3rd. You can find the release notes for 1.1.4 here and the feature's task log includes a Jasmine test that demonstrates how to use the new functionality.
If you're more conservative and are using a 1.0.x release, then you won't be able to accomplish this as easily, but it can be done. Mark Rajcok's solution looks like it would fit your requirements as-stated, but I would just add a few additional notes:
Aside from its 1.1.4 release, compile-time directives don't support modification at runtime.
As of 1.1.4, you can safely modify the attributes of compile-time directives, but only from another compile-time directive.
You may want to consider replaceWith() instead of append() since <my-directive> is not a standard-defined HTML element type.
If your X and Y templates contain additional directives, I don't think you'll be able to pass attributes on <my-template> through to the root element of your template so easily.
A directive with replace: true will transfer attributes from the source element to its replacement root, but I do not think that ngInclude will do the same from is host to the root of the included template.
I also seem to recall that ngInclude does not require that its template have exactly one root element.
You could perhaps preserve attributes on a replacement parent by using replaceWith() instead of append() and wrapping the <div ng-include=""> tag within a <div></div>. The outer <div> could hold attributes and would still be accessible after the <div ngInclude> element replaced itself with loaded content.
Be aware that ngInclude creates a new scope. This subjects you to a flashing yellow klaxons warning about the dangers of primitive scope models. For more information, see this fine page from Angular's GitHub depot.
I can propose another alternative for those on 1.0.x, but it involves a fair amount of code. It's a more heavy-weight operation, but it has the upside of not only being able of switching between templates, but full-fledged directives as well. Furthermore, its behavior is more readily dynamic.
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'partials/directive/my-directive.html',
link: function(scope, element, attrs, ctrl) {
// You can do this with isolated scope as well of course.
scope.type = attrs.type;
}
}
);
my-directive.js
<div ng-switch on="{{type}}">
<div ng-switch-where="X" ng-include="X-template.html"></div>
<div ng-switch-where="Y" ng-include="Y-template.html"></div>
</div>
my-directive.html
This is my version for optionally overriding a default template
templateUrl: function (elem, attrs) {
if (attrs.customTemplate) {
return '/path/to/components/tmpl/' + attrs.customTemplate + '.html';
} else {
return '/path/to/components/tmpl/directive.html';
}
}
e.g on a directive
<div my-directive custom-template="custom"></div>
I solve this problem so:
app.directive("post", function ($templateCache, $compile) {
function getTemplate(mode) {
switch (mode) {
case "create":
return "createPost.html";
case "view":
return "viewPost.html";
case "delete":
return "deletePost.html";
}
}
var defaultMode = "view";
return {
scope: {},
restrict: "AE",
compile: function (elem, attrs, transclude) {
return function ($scope, $element, $attr) {
function updateTemplate() {
$element.html("");
$compile($templateCache.get(getTemplate($scope.mode)).trim())($scope, function (clonedElement, scope) {
clonedElement.appendTo($element);
});
}
$scope.mode = $attr.mode || defaultMode;
$scope.$watch("mode", updateTemplate);
}
}
}
});
It's probably not the best way to do this, but I have no extra scope.
Ok, this might help someone here :-)
To inject your custom attr into your link or controller function use the following.
I'm at work right now but will post a fiddle later if I get a chance :-)
.directive('yourDirective', function() {
return {
restrict: 'EA',
template: '<div></div>', // or use templateUrl with/without function
scope: {
myAttibute: '#myAttr' // adds myAttribute to the scope
},
link: function(scope) {
console.log(scope.myAttibute);
}
}
}
// HTML ""
// Console will output "foo"

Using directives inside directives causes binding issues

We're using Angular and we're having trouble with resolving variables in directives.
This fiddle shows our issue:
Here's the full code: http://jsfiddle.net/VX5LE/65/
//data-translate should handle the translating of the useableButton text
app.directive('window', ['translateService', function (translateService) {
return {
restrict: 'E',
transclude: true,
scope: {
useableButtons: '='},
replace: true,
template:
'<div>' +
'<button data-ng-repeat="useableButton in useableButtons" data-translate>{{useableButton}}</button>' +
'</div>'
};
}]);
I have seen some answers that solve this by:
Using a filter to translate these. - That is actually our current solution but that hinders us with different functionality.
Attaching watches in the controller. - We actually want to avoid watches in our controllers as it makes the code quite dirty if you have a lot of them.
Preferably I would like to see a solution that resides inside of the translate directive without cluttering the controllers.
You can do this by interpolating the values manually, then parsing it with the $eval function of the desired scope.
Here is the fiddle: http://jsfiddle.net/VX5LE/66/
The code of the translate-directive:
app.directive('translate', ['translateService', '$interpolate', function (translateService, $interpolate) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var pHTML = element.html();
var parsed = $interpolate(pHTML);
var translated_result = translateService.translate(scope.$eval(parsed));
element.text(translated_result);
}
}
}]);

Angularjs passing object to directive

Angular newbie here. I am trying to figure out what's going wrong while passing objects to directives.
here's my directive:
app.directive('walkmap', function() {
return {
restrict: 'A',
transclude: true,
scope: { walks: '=walkmap' },
template: '<div id="map_canvas"></div>',
link: function(scope, element, attrs)
{
console.log(scope);
console.log(scope.walks);
}
};
});
and this is the template where I call the directive:
<div walkmap="store.walks"></div>
store.walks is an array of objects.
When I run this, scope.walks logs as undefined while scope logs fine as an Scope and even has a walks child with all the data that I am looking for.
I am not sure what I am doing wrong here because this exact method has worked previously for me.
EDIT:
I've created a plunker with all the required code: http://plnkr.co/edit/uJCxrG
As you can see the {{walks}} is available in the scope but I need to access it in the link function where it is still logging as undefined.
Since you are using $resource to obtain your data, the directive's link function is running before the data is available (because the results from $resource are asynchronous), so the first time in the link function scope.walks will be empty/undefined. Since your directive template contains {{}}s, Angular sets up a $watch on walks, so when the $resource populates the data, the $watch triggers and the display updates. This also explains why you see the walks data in the console -- by the time you click the link to expand the scope, the data is populated.
To solve your issue, in your link function $watch to know when the data is available:
scope.$watch('walks', function(walks) {
console.log(scope.walks, walks);
})
In your production code, just guard against it being undefined:
scope.$watch('walks', function(walks) {
if(walks) { ... }
})
Update: If you are using a version of Angular where $resource supports promises, see also #sawe's answer.
you may also use
scope.walks.$promise.then(function(walks) {
if(walks) {
console.log(walks);
}
});
Another solution would be to add ControllerAs to the directive by which you can access the directive's variables.
app.directive('walkmap', function() {
return {
restrict: 'A',
transclude: true,
controllerAs: 'dir',
scope: { walks: '=walkmap' },
template: '<div id="map_canvas"></div>',
link: function(scope, element, attrs)
{
console.log(scope);
console.log(scope.walks);
}
};
});
And then, in your view, pass the variable using the controllerAs variable.
<div walkmap="store.walks" ng-init="dir.store.walks"></div>
Try:
<div walk-map="{{store.walks}}"></div>
angular.module('app').directive('walkMap', function($parse) {
return {
link: function(scope, el, attrs) {
console.log($parse(attrs.walkMap)(scope));
}
}
});
your declared $scope.store is not visible from the controller..you declare it inside a function..so it's only visible in the scope of that function, you need declare this outside:
app.controller('MainCtrl', function($scope, $resource, ClientData) {
$scope.store=[]; // <- declared in the "javascript" controller scope
ClientData.get({}, function(clientData) {
self.original = clientData;
$scope.clientData = new ClientData(self.original);
var storeToGet = "150-001 KT";
angular.forEach(clientData.stores, function(store){
if(store.name == storeToGet ) {
$scope.store = store; //declared here it's only visible inside the forEach
}
});
});
});

Resources