Angular: Call directive controller function from separate controller - javascript

Let's say I have an app called myApp. There is a directive for a menu component. This directive has a controller defined on it. Now, from another controller that loads the view, I want to call methods of that menu directive's controller.
What does the code look like to do that? How can the other controller call methods on the menu directive's controller?

I think the issue is that you're trying to go controller -> directive when you should be binding a function to some delegating call that goes controller <- directive.
For an example of this, you can check out angular-ui/bootstrap's tabs directive onSelect and onDeselect:
.directive('tab', ['$parse', function($parse) {
return {
require: '^tabset',
restrict: 'EA',
replace: true,
templateUrl: 'template/tabs/tab.html',
transclude: true,
scope: {
active: '=?',
heading: '#',
onSelect: '&select', //This callback is called in contentHeadingTransclude
//once it inserts the tab's content into the dom
onDeselect: '&deselect'
},
// etc.
Using the & expression allows you to bind to a function as a callback.
An example of this usage would be a scope-bound function in your target controller:
$scope.selectedTab = function(){ alert('woohoo'); };
Which is 'bound' to the directive's function in the view:
<tab select="selectedTab()">
The tabs directive also has a good example of how to communicate between two different directives controllers. See tabset controller's ctrl.select. A tab directive requires a parent tabSet controller with the require:
.directive('tab', ['$parse', function($parse) {
return {
require: '^tabset',
The tab directive can access the tabset controller's select function in the compile function:
compile: function(elm, attrs, transclude) {
return function postLink(scope, elm, attrs, tabsetCtrl) {
scope.$watch('active', function(active) {
if (active) {
tabsetCtrl.select(scope);
}
});
scope.disabled = false;
if ( attrs.disabled ) {
scope.$parent.$watch($parse(attrs.disabled), function(value) {
scope.disabled = !! value;
});
}
scope.select = function() {
if ( !scope.disabled ) {
scope.active = true;
}
};
tabsetCtrl.addTab(scope);
scope.$on('$destroy', function() {
tabsetCtrl.removeTab(scope);
});
//We need to transclude later, once the content container is ready.
//when this link happens, we're inside a tab heading.
scope.$transcludeFn = transclude;
};
}
So there you have how to execute code from some non-directive controller as a result of some action happening in your directive and how to execute some inter-directive functions as a result of directive require constraints.
edit: For anyone interested, here's the documentation for a directive's expressions: http://docs.angularjs.org/api/ng/service/$compile#comprehensive-directive-api

Related

Call controller function from nested directive in Angularjs

I have a set of directives that create a nested list with arbitrary depth. I am looking to use ng-click to send a selected <li>'s object back to the controller. The current attempt looks as so:
angular.module('crmProductgallery').directive('categorytree', function() {
return {
template: '<ul><categorytree-node ng-repeat="item in items" getCategories="getCategories"></categorytree-node></ul>',
restrict: 'E',
replace: true,
// controller: 'CrmProductgallerygallery',
scope: {
items: '=items',
getCategories: '&',
}
};
});
angular.module('crmProductgallery').directive('categorytreeNode', function($compile) {
return {
restrict: 'E',
template: '<li ng-click="getCategories()" getCategories="getCategories">{{item.title}}</li>',
// replace: true,
// controller: 'CrmProductgallerygallery',
// scope: {
// getCategories: '&',
// items: "="
// },
link: function(scope, elm, attrs) {
scope.$watch('items', function() {
$(elm).on('click', function(e) {
scope.getCategories(scope.item);
console.log(scope.item);
// https://stackoverflow.com/questions/15390393/two-nested-click-events-with-angularjs
e.stopPropagation();
});
if (scope.item.children) {
var children = $compile('<categorytree items="item.children" getCategories="getCategories"></categorytree>')(scope);
elm.append(children);
}
});
}
};
});
The code produces the following unordered list:
Currently, when I click on a <li>, scope.item is logged inside my scope.$watch() function, but the controller function getCategories(item) is not called.
If I uncomment out the scope declaration...
...in the categorytreeNode directive, I will get Cannot read properties of undefined (reading 'children). The list does not get created. scope.item no longer exists when trying that.
If I uncomment controller: 'CrmProductgallerygallery', the ng-click works and my controller function is called, but the list elements get duplicated over and over again. Here is what I mean:
Controller Function works on click now:
But now my list is duplicating:
You will also notice I placed getCategories="getCategories" everywhere I could think of trying. None of it works though. I think it is because the <ul> element is getting assigned ng-isolate-scope:
The controller function simply is this:
$scope.getCategories = function(item) {
console.log("getCategories");
console.log(item);
};
Question:
How can I get ng-click on the <li> elements to call my getCategories() controller function

Pass directive attribute into linked controller?

I have a directive i'm using to do the same search filtering across multiple pages. So the directive will be using a service and get pretty hefty with code. Because of that I want to link to a controller instead of have the controller inside the directive like this:
.directive('searchDirective', function($rootScope) {
return {
restrict: 'E',
templateUrl:'searchtemplate.html',
controller: 'searchCtrl',
controllerAs: 'search'
};
});
I also want access to parent scope data inside the template, so I don't want to use a isolated scope.
Anyway here's what i'm not sure how to do. My directive looks like this:
<search-directive filter="foo"/>
How do I pass in the value in the filter attribute so that I can access it in my controller using $scope.filter or this.filter?
If I were using an isolated scope it'd be simple. If i had the controller in the same page I could use $attrs. But since i'm using a controller from another spot and don't want an isolated scope i'm not sure how to get the attrs values into the controller.
Any suggestions?
What about using the link function and passing the value to the scope?
return {
restrict: 'E',
templateUrl:'searchtemplate.html',
controller: 'searchCtrl',
controllerAs: 'search',
link: function (scope, element, attr) {
scope.filter = attr.filter;
}
};
searchDirective.js
angular
.module('searchDirective', []).controller('SearchCtrl', SearchCtrl)
.directive('SearchDirective', directive);
function directive () {
var directive = {
templateUrl:'searchtemplate.html',
restrict: "E",
replace: true,
bindToController: true,
controller: 'searchCtrl as search',
link: link,
scope: { filter:'=' } // <-- like so here
};
return directive;
function link(scope, element, attrs) {}
}
SearchCtrl.$inject = [
'$scope',
'$filter'];
function SearchCtrl(
$scope,
$filter) {
/** Init SearchCtrl scope */
/** ----------------------------------------------------------------- */
var vs = $scope;
// ....
Also I highly recommend checking out this AngularJS style guide, how you are writing your directive above is how I use to do it too. John Papa shows some way better ways: https://github.com/johnpapa/angular-styleguide
Directives:
https://github.com/johnpapa/angular-styleguide#directives
Controllers:
https://github.com/johnpapa/angular-styleguide#controllers
Flip the values of bindToController and scope around.
{
....
scope: true,
bindToController: { filter:'=' }
...
}
I have just hit the same issue over the weekend, and made a simple complete example here: bindToController Not Working? Here’s the right way to use it! (Angular 1.4+)

How to access a require'd controller from another controller?

I have an Angular 1.3 module that looks something like this (directive that requires the presence of a parent directive, using controllerAs):
angular.module('fooModule', [])
.controller('FooController', function ($scope) {
this.doSomething = function () {
// Accessing parentDirectiveCtrl via $scope
$scope.parentDirectiveCtrl();
};
})
.directive('fooDirective', function () {
return {
// Passing in parentDirectiveCtrl into $scope here
link: function link(scope, element, attrs, parentDirectiveCtrl) {
scope.parentDirectiveCtrl = parentDirectiveCtrl;
},
controller: 'FooController',
controllerAs: 'controller',
bindToController: true,
require: '^parentDirective'
};
});
Here I'm just using $scope to pass through parentDirectiveCtrl, which seems a little clunky.
Is there another way to access the require-ed controller from the directive's controller without the linking function?
You must use the link function to acquire the require-ed controllers, but you don't need to use the scope to pass the reference of the controller to your own. Instead, pass it directly to your own controller:
.directive('fooDirective', function () {
return {
require: ["fooDirective", "^parentDirective"],
link: function link(scope, element, attrs, ctrls) {
var me = ctrls[0],
parent = ctrls[1];
me.parent = parent;
},
controller: function(){...},
};
});
Be careful, though, since the controller runs prior to link, so within the controller this.parent is undefined, until after the link function runs. If you need to know exactly when that happens, you can always use a controller function to pass the parentDirective controller to:
link: function link(scope, element, attrs, ctrls) {
//...
me.registerParent(parent);
},
controller: function(){
this.registerParent = function(parent){
//...
}
}
There is a way to avoid using $scope to access parent controller, but you have to use link function.
Angular's documentation says:
Require
Require another directive and inject its controller as the fourth
argument to the linking function...
Option 1
Since controllerAs creates namespace in scope of your controller, you can access this namespace inside your link function and put required controller directly on controller of childDirective instead of using $scope. Then the code will look like this.
angular.module('app', []).
controller('parentController', function() {
this.doSomething = function() {
alert('parent');
};
}).
controller('childController', function() {
this.click = function() {
this.parentDirectiveCtrl.doSomething();
}
}).
directive('parentDirective', function() {
return {
controller: 'parentController'
}
}).
directive('childDirective', function() {
return {
template: '<button ng-click="controller.click()">Click me</button>',
link: function link(scope, element, attrs, parentDirectiveCtrl) {
scope.controller.parentDirectiveCtrl = parentDirectiveCtrl;
},
controller: 'childController',
controllerAs: 'controller',
bindToController: true,
require: '^parentDirective'
}
});
Plunker:
http://plnkr.co/edit/YwakJATaeuvUV2RBDTGr?p=preview
Option 2
I usually don't use controllers in my directives at all and share functionality via services. If you don't need to mess with isolated scopes of parent and child directives, simply inject the same service to both of them and put all functionality to service.
angular.module('app', []).
service('srv', function() {
this.value = '';
this.doSomething = function(source) {
this.value = source;
}
}).
directive('parentDirective', ['srv', function(srv) {
return {
template: '<div>' +
'<span ng-click="srv.doSomething(\'parent\')">Parent {{srv.value}}</span>' +
'<span ng-transclude></span>' +
'</div>',
transclude: true,
link: function(scope) { scope.srv = srv; }
};
}]).
directive('childDirective', ['srv', function(srv) {
return {
template: '<button ng-click="srv.doSomething(\'child\')">Click me</button>',
link: function link(scope) { scope.srv = srv; }
}
}]);
Plunker
http://plnkr.co/edit/R4zrXz2DBzyOuhugRU5U?p=preview
Good question! Angular lets you pass "parent" controller. You already have it as a parameter on your link function. It is the fourth parameter. I named it ctrl for simplicity. You do not need the scope.parentDirectiveCtrl=parentDirectiveCtrl line that you have.
.directive('fooDirective', function () {
return {
// Passing in parentDirectiveCtrl into $scope here
link: function link(scope, element, attrs, ctrl) {
// What you had here is not required.
},
controller: 'FooController',
controllerAs: 'controller',
bindToController: true,
require: '^parentDirective'};});
Now on your parent controller you have
this.doSomething=function().
You can access this doSomething as
ctrl.doSomething().

Bind model to function call return value

I am working on an angular project and I use a directive to create an isolated scope. The directive looks like this:
var directive = module.directive('question', function () {
return {
restrict: 'E',
templateUrl: 'question.html',
transclude: true,
scope: {
quiz: '=quiz'
},
link: function (scope, attr, element) {
scope.$watch(function () {
return scope.quiz;
},
function (oldVal, newVal) {
scope.currentQuestion = scope.quiz;
});
}
};
});
For I do not want to bind to a property (or field) in my Controller, I created a function and call the directive this way:
<question quiz="quiz.getCurrentQuestion()">... (transcluding stuff)</question>
Please note that quiz is my Controller using the as-Syntax.
The way I process the directive is working, but I don't like to create a two-way-binding ( to an R-value?).
Now I tried to just pass the function using &-binding but this just turns out odd results in the link-function and breaks everything.
Can I use the function-binding using & and somehow call the function (in my template or in the link-function) to get the result I need to make it work like two-way-binding?
Thank you for your help.
EDIT
The return value of the getCurrentQuestion-function is an object which looks like
{
questionNumber: 1,
answers: [],
getQuestionText() : function(...),
...
}
So nothing to special, I hope...
EDIT 2
When I use
...
scope: {
quiz: '&quiz'
}
then in the $watch-function I get
function(locals) { return parentGet(scope, locals); } for scope.quiz
And if I call the function like scope.quiz() I get undefined as result.
Couldn't find any way to watch a function in scope binding. However, there are other solutions. If you want single way binding you can use '#', but that means that you would have to parse the JSON in the watch ( working example):
var directive = module.directive('question', function () {
return {
restrict: 'E',
templateUrl: 'question.html',
transclude: true,
scope: {
quiz: '#'
},
link: function (scope, attr, element) {
scope.$watch('quiz', function (newVal, oldVal) {
scope.currentQuestion = angular.fromJson(newVal);
});
}
};
});
It works, but if you have a high rate of updates, the overhead can be annoying. What I would do, is use a service that holds all the questions, and both controller and directive can talk to. When the current question is changed, the controller should pass to the directive only the id of the new question (using simple # bind), and the directive would query the service for the question.

Is it possible to call functions from parent directive controller, when subsidiary directive has isolated scope

Here is the jsfiddle.net exemplifying my situation.
app=angular.module("app",[]);
app.directive("submitForm",function(){
return{
scope:{},
restrict: 'A',
controller:function($scope, $element){
$scope.submitted=false;
this.submit=function(){
$scope.submitted=true;
};
this.getSubmit=function(){
return $scope.submitted;
};
this.submitOn=function(){
return $scope.$broadcast("submitOn");
};
},
link: function(scope, element, attrs,ctrl){
element.find("button").on("click",function(){
scope.submitted=true;
ctrl.submitOn();
});
}
}
})
.directive('errorRender',function(){
return{
restrict: 'A',
//scope: {},
require:['ngModel','^submitForm'],
controller: function($scope, $element){
$scope.$broadcast("requireErrorEnable");
$scope.$broadcast("requireErrorDisable");
$scope.$broadcast("maxlengthErrorEnable");
$scope.$broadcast("maxlengthErrorDisable");
},
compile: function compile(tElement, tAttrs) {
return function postLink(scope, element, attrs, ctrl) {
modelCtrl=ctrl[0];
formCtrl=ctrl[1];
scope.$on("submitOn",function(){
alert("submitOn!!!");
});
scope.$on("requireErrorEnable",function(){
element.attr("placeholder","error");
});
scope.$on("requireErrorDisable",function(){
element.attr("placeholder","");
});
scope.$watch(function(scope){
return ctrl[0].$error.required;
},
function(newValue, oldValue, scope){
if(ctrl[0].$error.required){
if((ctrl[0].$dirty && !ctrl[0].$viewValue)){
scope.$emit("requireErrorEnable");
}
}else{
scope.$emit("requireErrorDisable");
}
});
}
}
}
});
If I use the directive errorRender in an isolated scope, I can't fire the function submitForm of the directive's controller in this case. Otherwise all directives errorRender fire at the same time (as we can expect).
directives can share controllers via the require property, that you correctly specified. this is what Angular's docs on directives say about accessing the directive's controller (emphasis added):
"require - 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. If an array is used, the injected argument will be an array in corresponding order. If no such directive can be found, or if the directive does not have a controller, then an error is raised"
IMHO, when applied to your case, this means that the link function where the controller is injected is the one in your child directive, not in the parent

Categories

Resources