How to expose a public API from a directive that is a reusable component?
Solution 1
Does this work for you?
angular.directive('extLabel', function() {
return {
restrict: 'E',
scope: {
api: '='
},
link: function(scope, iElement, iAttrs) {
scope.api = {
doSomething: function() { },
doMore: function() { }
};
}
};
});
From containing parent
<ext:label api="myCoolApi"></ext:label>
And in controller
$scope.myCoolApi.doSomething();
$scope.myCoolApi.doMore();
Solution 2
I like Andrej's and use this pattern regularly, but I would like to suggest some changes to it
angular.directive('extLabel', function {
return {
scope: {
api: '=?',
configObj: '='
},
// A controller, and not a link function. From my understanding,
// try to use the link function for things that require post link actions
// (for example DOM manipulation on the directive)
controller: ['$scope', function($scope) {
// Assign the api just once
$scope.api = {
changeLabel: changeLabel
};
function changeLabel = function(newLabel) {
$scope.configObj.label = newLabel;
}
}]
}
});
<ext-label name="extlabel1" config-obj="label1"></ext-label>
<ext-label api="label2api" name="extlabel2" config-obj="label2"></ext-label>
<ext-label name="extlabel3" config-obj="label3"></ext-label>
In controller of course label2api.changeLabel('label')
Solution 3
I faced this problem when writing a directive to instantiate a dygraph chart in my Angular applications. Although most of the work can be done by data-binding, some parts of the API require access to the dygraph object itself. I solved it by $emit()
ing an event:
'use strict';
angular.module('dygraphs', []);
angular.module('dygraphs').directive('mrhDygraph', function ($parse, $q) {
return {
restrict: 'A',
replace: true,
scope: {data: '=', initialOptions: '@', options: '='},
link: function (scope, element, attrs) {
var dataArrived = $q.defer();
dataArrived.promise.then(function (graphData) {
scope.graph = new Dygraph(element[0], graphData, $parse(scope.initialOptions)(scope.$parent));
return graphData.length - 1;
}).then(function(lastPoint) {
scope.graph.setSelection(lastPoint);
scope.$emit('dygraphCreated', element[0].id, scope.graph);
});
var removeInitialDataWatch = scope.$watch('data', function (newValue, oldValue, scope) {
if ((newValue !== oldValue) && (newValue.length > 0)) {
dataArrived.resolve(newValue);
removeInitialDataWatch();
scope.$watch('data', function (newValue, oldValue, scope) {
if ((newValue !== oldValue) && (newValue.length > 0)) {
var selection = scope.graph.getSelection();
if (selection > 0) {
scope.graph.clearSelection(selection);
}
scope.graph.updateOptions({'file': newValue});
if ((selection >= 0) && (selection < newValue.length)) {
scope.graph.setSelection(selection);
}
}
}, true);
scope.$watch('options', function (newValue, oldValue, scope) {
if (newValue !== undefined) {
scope.graph.updateOptions(newValue);
}
}, true);
}
}, true);
}
};
});
The parameters of the dygraphCreated
event include the element id as well as the dygraph object, allowing multiple dygraphs to be used within the same scope.
Solution 4
In my opinion, a parent shouldn't access a children scope. How would you know which one to use and which one to not use. A controller should access his own scope or his parent scopes only. It breaks the encapsulation otherwise.
If you want to change your label, all you really need to do is change the label1/label2/label3 variable value. With the data-binding enabled, it should work. Within your directive, you can $watch
it if you need some logic everytime it changes.
angular.directive('extLabel', function {
return {
scope: {
name: '@',
configObj: '='
},
link: function(scope, iElement, iAttrs) {
scope.$watch("configObj", function() {
// Do whatever you need to do when it changes
});
}
}
});
Solution 5
Use these directives on the element that you want to go prev and next:
<carousel>
<slide>
<button class="action" carousel-next> Next </button>
<button class="action" carousel-prev> Back </button>
</slide>
</carousel>
.directive('carouselNext', function () {
return {
restrict: 'A',
scope: {},
require: ['^carousel'],
link: function (scope, element, attrs, controllers) {
var carousel = controllers[0];
function howIsNext() {
if ((carousel.indexOfSlide(carousel.currentSlide) + 1) === carousel.slides.length) {
return 0;
} else {
return carousel.indexOfSlide(carousel.currentSlide) + 1;
}
}
element.bind('click', function () {
carousel.select(carousel.slides[howIsNext()]);
});
}
};
})
.directive('carouselPrev', function () {
return {
restrict: 'A',
scope: {},
require: ['^carousel'],
link: function (scope, element, attrs, controllers) {
var carousel = controllers[0];
function howIsPrev() {
if (carousel.indexOfSlide(carousel.currentSlide) === 0) {
return carousel.slides.length;
} else {
return carousel.indexOfSlide(carousel.currentSlide) - 1;
}
}
element.bind('click', function () {
carousel.select(carousel.slides[howIsPrev()]);
});
}
};
})
![Admin](/assets/logo_square_200-5d0d61d6853298bd2a4fe063103715b4daf2819fc21225efa21dfb93e61952ea.png)
Admin
Updated on June 24, 2022Comments
-
Admin about 2 years
Having a directive in angular that is a reusable component, what is the best practice to expose a public API that can be accessed from the controller? So when there are multiple instances of the component you can have access from the controller
angular.directive('extLabel', function { return { scope: { name: '@', configObj: '=' }, link: function(scope, iElement, iAttrs) { // this could be and exposed method scope.changeLabel = function(newLabel) { scope.configObj.label = newLabel; } } } });
Then when having:
<ext-label name="extlabel1" config-obj="label1"></ext-label> <ext-label name="extlabel2" config-obj="label2"></ext-label> <ext-label name="extlabel3" config-obj="label3"></ext-label>
How can I get the access the scope.changeLabel of extLabel2 in a controller?
Does it make sense?
-
Admin almost 11 yearsThe "label" example was just a simple abstraction. That's the point of my question: -"Is there a way to expose an API from a reusable component that can be accessed from the controller?"
-
Benoit Tremblay almost 11 yearsYou really shouldn't create your directive the same way as a controller. Use $watch and events to interact with your directive, not the scope.
-
Admin almost 11 yearsYes, I understand that this approach is not the most aligned with the MVC principles because it couples the controller with the directive but the need to send parameters (options) to the component from the controller makes it a but more natural.
-
Chandermani almost 11 yearsI believe the other way around is more acceptable. Parent can access child, keeping child generic. If child accesses parent then it cannot be reused across different parent elements.
-
Trevor about 10 yearsYes, parent should access a child scope directly, but in my mind, the OP's request is like creating a component class and having another class call a method on that component class [e.g. Bank class calls Account.deposit()].
-
bennyl almost 10 yearsThis is a great way, some notes though - 1. you must unsubscribe the watch for the api in the directive or you will find yourself in an infinite loop. 2. since one not always want to access the api you should define the api as "=?"
-
Andrej Kaurin almost 10 yearsI am not sure if I understand number 1...and will agree with number 2.
-
bennyl almost 10 yearsif you make a change to the result of the watched expression inside the watch function then the watch function will get called again (which will make the change again) and this can cause an infinite loop (in my case angular notice that and stopped after the 11th try with an exception)
-
Vall3y about 9 yearsWhy use $watch, when you could just assign it regularly? Also you probably want to do it in a directive controller instead of a link function, although it won't matter in many examples
-
kdbanman almost 9 yearsDoes the
.directive()
method go on the carousel element, or somewhere else? -
doug65536 almost 9 years@kdbanman The module. The module is created with
angular.module(name, [ dependencies... ])
. An existing module can be appended to by fetching the existing module withangular.module(name);
-
Andrej Kaurin over 8 yearsYup, I removed watch part as it is really useless.
-
Robert Hickman over 8 yearsMy concern with this is that the api is not necessarily defined when you try to use it. How would you ensure this?
-
Andrej Kaurin over 8 yearsSimple scope.$watch('myCoolApi', function(api) { if(api) { // api is ready here // you can also check for api method if you want } });
-
CodeGems over 7 yearsI prefer using directive 'controller' instead of 'link'