I have several angular directives.
I want to include them in an accordion directive with an ng-repeat depending on the type.
I want it to be reusable, so I don't want the accordion to have knowledge of the type of directives it is rendering.
That's why I don't want to use an ng-switch directive inside the accordion.
Here is a very simplified demo. In reality those directives will have their own attributes for angular to compile.
var testApp = angular.module('testApp', []);
(function() {
function Element1() {
return {
template: '<span>hello</span>',
restrict: 'E',
replace: true
}
}
testApp.directive('elementOne', Element1);
}());
(function() {
function Element2() {
return {
template: '<span>world</span>',
restrict: 'E',
replace: true
}
}
testApp.directive('elementTwo', Element2);
}());
(function() {
function Accordion() {
return {
template: '<ul><li ng-repeat="element in elements"><button>Toggle</button> Here should be the directive</li></ul>',
restrict: 'E',
replace: true,
controller: function($scope) {
$scope.elements = [{
type: 1
}, {
type: 2
}];
}
}
}
testApp.directive('elementAccordion', Accordion);
}());
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="testApp">
<element-accordion></element-accordion>
</div>
Thanks!
I've built something very similar a few weeks ago. I wanted to render a list of directives and select the type of directive based on the type of an element in an array (just like you). I tried to avoid ngSwitch because I've really many directives and want to add new directives without changing the ngSwitch statement every time. Let me show a simplified version of what I did. I hope this will help you to build your own version.
First, we need a global directory of element diretives and a helper function to register new directives. In my case, the helper function even creates the directive, but I'll skip this to keep it simple:
angular.module("app").provider('elementDirectory', function() {
return {
elements: [],
$get: function () {
return {
elements: this.elements
}
}
};
});
function registerElementDirective(type, directiveName) {
angular.module("app").config(['elementDirectoryProvider', function (elementDirectoryProvider) {
elementDirectoryProvider.elements.push({
type : type,
directive : directiveName
});
}]);
}
Now, we can create some directives like this:
angular.module("app").directive('elementA', function() {
return {
template: '<span>ElementA</span>',
}
});
registerElementDirective('A', 'element-a');
The most interesting part is a directive I called elementSwitch. It takes an array of elements and dynamically adds an angular.element for each element. Therefore, it creates prototypes for each element in the elementDirectoy and uses the clone() method on changes. (I think we could skip this part, it was an optimization).
angular.module('app').directive('elementSwitch', elementSwitch);
elementSwitch.$inject = ['$compile', 'elementDirectory'];
function elementSwitch($compile, elementDirectory) {
var prototypes = {};
elementDirectory.elements.forEach(function (e) {
var directiveElem = angular.element('<' + e.directive + '>');
prototypes[e.type] = directiveElem;
});
return {
scope: {
elements: '=elementSwitch'
},
link: function (scope, elem, attr) {
var childScopes = [];
scope.$watchCollection('elements', function (newValue, oldValue) {
childScopes.forEach(function (s) {
s.$destroy();
});
childScopes = [];
elem.empty();
newValue.forEach(function (element, index) {
var childScope = scope.$new();
childScopes.push(childScope);
var directiveElem = prototypes[element].clone();
directiveElem.attr('element', '_elements[' + index + ']');
$compile(directiveElem)(childScope);
elem.append(directiveElem);
});
scope._elements = newValue;
});
}
}
}
Here is a complete example: https://codepen.io/hansmaad/pen/oLvRmj
It doesn't do the same thing you want, but you should get an idea how you could achieve your goal.
Related
I want dynamically set attributes for child directive.
But I can not do it.
In the example I want to set attribute name with value John.
UPD: In this case, I can not determine the exact set of attributes. And I need to define them dynamically. For example, this is part of a third-party library.
angular.module('app', []);
angular.module('app')
.directive('wrapper', wrapper);
angular.module('app')
.directive('inWrapper', inWrapper);
angular.bootstrap(
document.getElementById('root'), ['app']
);
function wrapper() {
return {
transclude: true,
template: '<div ng-transclude></div>',
link: function linkFn(scope, element, attrs, ctrl, transcludeFn) {
transcludeFn(
scope,
function(clone) {
clone.find('in-wrapper').attr('name', 'John');
}
);
}
}
}
function inWrapper() {
return {
scope: {
name: '#'
},
template: 'Hello, {{name}}',
link: function linkFunc(scope) {
if (!scope.name) {
scope.name = 'Empty';
}
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div id="root">
<wrapper>
<in-wrapper></in-wrapper>
</wrapper>
</div>
I tried to set attributes dynamically but failed.
I suggest instead providing data for attributes in the item area directly.
In the code examples, I modified elements inside the transclude function. And provided data for attribute elements in the controller. Code not ideal. Only for demonstration idea.
P.S. Thanks #CodeMonkey for criticizing the original idea
angular.module("app", []);
angular.module("app").directive("wrapper", wrapper);
angular.module("app").directive("inWrapper", inWrapper);
angular.bootstrap(document.getElementById("root"), ["app"]);
function wrapper() {
return {
restrict: "E",
transclude: true,
controller: function linkFn($scope, $element, $attrs, $transclude) {
var transcludedContent, transclusionScope,
statuses = [
'danger',
'success',
'info'
];
status = 0;
$transclude(function(clone, scope) {
$element.append(clone);
transcludedContent = clone;
transclusionScope = scope;
for (var i = 0, ii = clone.length; i < ii; i++) {
var node = clone[i];
if (node.nodeType !== 3 ||
node.nodeValue.trim()) {
node.classList.add(statuses[status]);
status++;
}
}
});
// find scope for angulars element and provide data
var elements = $element.find("in-wrapper");
for (var i = 0, ii = elements.length; i < ii; i++) {
var isolateScope = angular.element(elements[i]).isolateScope();
isolateScope.name = 'World' + isolateScope.$id;
}
}
};
}
function inWrapper() {
return {
scope: {
name: "#"
},
template: '<div>Hello, <span ng-bind="name"></span></div>'
};
}
.danger {
color: #ff0000;
}
.success {
color: #00ff00;
}
.info {
color: #0000ff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div id="root">
<wrapper>
<in-wrapper></in-wrapper>
<in-wrapper></in-wrapper>
<in-wrapper></in-wrapper>
</wrapper>
</div>
You should be able to link the attribute to a scope variable. If you are talking about designing the directive yourself, you can define the controller's scope variables as "=" and they will link bidirectionally to the value passed to the attribute. Check the AngularJS: Developer Guide for more info.
You might also be able to assign the value of the attribute using {{...}} annotation to inject a scope variable directly into the markup.
I have a service that's being used in different controllers, and I would like to consolidate them into a directive that can be applied to the different pages. To start, I'm just trying to get a simple example to work. Say I have two controllers, both of which have a common function:
angular.module('myApp')
.controller('CtrlOne', function() {
$scope.watchVar = 0;
$scope.changeVar = function() {
$scope.watchVar = 1;
}
});
angular.module('myApp')
.controller('CtrlTwo', function() {
$scope.watchVar = a;
$scope.changeVar = function() {
$scope.watchVar = b;
}
});
Then, in a directive I'm trying to determine the controller at runtime, so that this variable can be watched regardless of the controller the page has (as long as it uses the 'watchVar' variable):
angular.module('myApp').directive('myDirective', function () {
return {
scope: {
ctrl: '#myDirective',
watchVar: '=',
},
controller: ctrl,
link: function(scope, element, attrs) {
scope.$watch('watchVar', function(newValue, oldValue) {
if (newValue) {
console.log("watchVar changed to: "+newValue);
} else {
console.log("watchVar remained: "+oldValue);
}
}, true);
}
};
});
With the HTML being something like this:
<div p2a-filter-cache='CtrlOne'>
Is this even possible?
I'm trying to call directive method on button click from the calling controller.
Here is the directive code:
myApp.directive("helloDirective", function() {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
controller: 'helloDirectiveController',
controllerAs: 'model'
}
})
.controller("helloDirectiveController", function() {
var self = this;
self.actions = {
get: function() {
return self.msg;
},
set: function(msgData) {
self.msg = msgData;
}
});
I have call the get and set method from controller..
myApp.controller("indexController", [function() {
var self = this;
self.helloParam ={};
self.get = function() {
//how to call the Directive get method from here
}
}]);
i tried to create a fiddle here
plnkr
The idea
For me, the cleanest solution (so far) for your problem is a solution used by Angular Material developers (I don't know if they were the authors, but I found it there). I've used it once in my project and it worked like a charm.
The idea is to create a global registry for directives' actions. Directives would be stored there by unique ids. We can also create a service dedicated for each directive, just in case we need some external logic.
The solution
1. Components registry
Firstly, we need a components registry. It can be a really simple service:
angular.module('app');
.service('$componentsRegistry', function() {
var self = this;
var components = {};
self.put = function(id, actions) {
components[id] = actions;
}
self.get = function(id) {
return components[id];
}
})
The components registry has methods for storing and getting components by ids. Of course, there might be much more methods and they might be more complicated, but this is just a simple example.
2. Service for our directive
Let's say we have a simple show-message directive, so we can create a $showMessageService:
angular.module('app')
.service('$showMessageService', ['$componentsRegistry', function($componentsRegistry) {
return function(id) {
return $componentsRegistry.get(id);
}
}])
For now, the only task of the service is to return our directive's actions. But it can be extended in the future, of course.
3. Directive's code
The last thing we need is our show-message directive:
angular.module('app')
.directive('showMessage', function($componentsRegistry) {
return {
restrict: 'E',
scope: {
directiveId: '#' // Unique id is passed from the view
},
template: '<div>{{ text }}</div>',
link: function(scope) {
scope.text = 'TEST TEXT';
// Create actions
scope.actions = {
set: function(value) {
scope.text = value;
}
}
// Store actions in the components registry
$componentsRegistry.put(scope.directiveId, scope.actions);
}
}
})
In a link function, we need to simply register our actions in components registry. We pass the unique id from the view so that developer has control over it inside views/controllers.
Example of usage
And now we can finally use the directive in our application. Here is a simple code which shows how to do that:
View
<div ng-controller="testController as test">
<show-message directive-id="testDirective"></show-message>
<button ng-click="test.changeText()">CHANGE</button>
</div>
Controller
angular.module('app')
.controller('testController', function(['$showMessageService', $showMessageService) {
var self = this;
self.changeText = function() {
$showMessageService('testDirective').set('NEW');
}
}])
I've Change The Directive Code, moved Controller code to link and it's working fine.
testApp.directive("helloDirective", function () {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
link: function (scope, element, attrs) {
var self = scope.model;
var assignMethod = function () {
if (self.param == undefined) {
self.param = {};
}
self.param.actions = {
get: function () {
return self.msg;
},
set: function (msgData) {
self.msg = msgData;
}
};
};
assignMethod();
},
controller: function () { },
controllerAs: 'model'
}
});
now i can call the directive get Method from calling controller like,
self.helloParam = {};
self.click = function () {
alert(self.helloParam.actions.get());
}
I am working on an application that has same directives but the content change and template depends on the id of a controller in a module.
app.directive('glTranslate', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var key = element.text();
attrs.$observe('options', function(value) {
var options;
try {
options= JSON.parse(value);
element.text( I18n.t( key, options ) );
} catch (err) {
// SyntaxError maybe thrown as the options string is dynamically updated through Angular bindings
// and the options string may be momentarily be syntactically incorrect
console.log("Error parsing options: " + value + "\nError:"+ err.message + "\n");
}
});
}
};
});
If I understand you correctly, to set a directive's template dynamically based on some condition, you can use ng-include inside the directive. The pseudocode below works. It includes an example directive with dynamic template chosen based attribute passed to it and simple HTML usage. Needless to say, although the template contents vary, here they use the same directive controller and its accompanying functionality. Hope this helps.
.directive("dynaDirective", function() {
return {
template: '<ng-include src="getTemplateUrl()"/>',
scope: {
someData: '='
},
restrict: 'E',
controller: function($scope) {
//function used on the ng-include to resolve the template
$scope.getTemplateUrl = function() {
//basic handling
return "templateName" + $scope.someData.type;
}
$scope.doSomething1 = {
//......
}
$scope.doSomething2 = {
//......
}
}
};
});
<dyna-directive some-data="someObject"></dyna-directive>
I have a directive.This directive handles drag and drop of child element. The list of the child elements will be generated by the directive from an given array of data. To render each Element I'd like to give the directive a render. At this time I use a fiter. But I think that is not a job for a filter. Is there a way to use an other directive for it?
directive:
module.directive('dndSortable', ['$filter', '$log',
function ($filter, $log) {
return {
restrict: 'A',
scope: {
dndValues: '=',
dndRenderFilter: '#'
},
link: function (scope, element) {
angular.forEach(dndValues, function(value){
//my first idea was to use a fiter this will create a element
//but I think thats not the function of fiters
//maybe there is a way to use a directive here
var dndElement = $filter(scope.dndRenderFilter)(value);
// ... add event handling for dnd to the dndElement
});
}
}
}]);
filter:
module.filter('mySortListRenderFilter', function () {
return function (imageValue) {
var img,
prefix = 'http://prefix/url';
if (!angular.isUndefined(imageValue) || !imageValue.hasOwnProperty('scr')) {
img = angular.element('<img>');
img.attr('src', prefix + imageValue.src);
img.addClass('teaser-gallery-image');
}
return img;
};
});
html:
<div
dndSortable
dnd-values="mySortableList"
dnd-render-filter="mySortListRenderFilter">
</div>