Check if controller has passed optional function down to *child* directive - angularjs

I have a scenario as follows:
Controller A >> Directive "parent" >> Directive "child"
Controller B >> Directive "parent" >> Directive "child"
Both directives have isolated scopes, and I cannot use transclusion.
Controller A defines a function that is passed down to the child directive (using the "&?" notation in both directives).
<div parent fx="doFx()"></div>
Controller B does NOT specify that function.
<div parent></div>
The parent directive simply passes down the function to the child directive.
<div child fx="doFx()"></div>
How can I check in the child directive (without accessing the parent's scope) whether or not the controller did in fact pass a function?
Thanks in advance for any pointers!

Well it is possible with third argument passed to linking function.
angular.module('app', []);
angular.module('app').controller('Example', function () {
this.fn1 = function () {
return true;
};
});
angular.module('app').directive('someDir', function () {
return {
restrict: 'E',
template: '{{isPassed}}',
scope: {
fx: '&'
},
link: function (scope, element, attrs) {
scope.isPassed = attrs['fx'] !== undefined && attrs['fx'].length > 0;
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.js"></script>
<div ng-app="app" ng-controller="Example as Ex">
A: <some-dir></some-dir> <br>
B: <some-dir fx></some-dir><br>
C: <some-dir fx="Ex.fn1"></some-dir>
</div>
edit:
You can also use specific return value.
const PASSED = 'PASSED';
angular.module('app', []);
angular.module('app').controller('Example', function() {
this.fn1 = function() {
return PASSED;
};
});
angular.module('app').directive('parent', function () {
return {
restrict: 'E',
template: '<child fx="fx()"></child>',
scope: {
fx: '&'
}
};
});
angular.module('app').directive('child', function () {
return {
restrict: 'E',
template: '{{isPassed}}',
scope: {
fx: '&'
},
link: function (scope) {
scope.isPassed = scope.fx() === PASSED;
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.js"></script>
<div ng-app="app" ng-controller="Example as Ex">
A: <parent></parent><br>
B: <parent fx="Ex.fn1()"></parent><br>
</div>
But in my opinion best solution would be to use first attrs to check if fx is passed and explicitly pass isPassed to child directive.
angular.module('app', []);
angular.module('app').controller('Example', function() {
this.fn1 = function() {};
});
angular.module('app').directive('parent', function () {
return {
restrict: 'E',
template: '<child fx="fx()" is-passed="isPassed"></child>',
scope: {
fx: '&'
},
link: function (scope, element, attrs) {
scope.isPassed = attrs['fx'] !== undefined && attrs['fx'].length > 0;
}
};
});
angular.module('app').directive('child', function ($timeout) {
return {
restrict: 'E',
template: '{{isPassed}}',
scope: {
fx: '&',
isPassed: '<'
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.js"></script>
<div ng-app="app" ng-controller="Example as Ex">
A: <parent></parent><br>
B: <parent fx="Ex.fn1()"></parent><br>
</div>

Related

how to pass function defined inside directive to another directive?

I need to pass function defined in directive to another one , my code is something like below
angular
.module('myApp')
.controller('myAppCtrl', function($scope) {})
.directive('first', function() {
return {
restrict: 'E',
scope: {
originFn: '&',
},
template: '<button ng-click="originFn()" >Click</button>',
controller: function() {
var self = this;
self.originFn = function() {
console.log('called from First');
};
},
};
})
.directive('second', function() {
return {
restrict: 'E',
scope: {
passedFn: '&',
},
template: '<button ng-click="passedFn()" >Click</button>',
};
});
html
<div ng-controller="myAppCtrl">
<first origin-fn="originFn" />
<second passed-fn="originFn" />
</div>;
when click on second button origin function not called ,
How can pass function to be called inside second directive ?
Both of directives are siblings, there is nothing in common, one directive can't call other directive controller without being it child.
So, if you can nest second directive into the first, you can use require functionality to get the parent directive controller.
.directive('second', function () {
return {
restrict: 'E',
require: '^^first',
scope: {},
template: '<button ng-click="innerFunc()" >Click</button>',
link: function ($scope, $elem, $attr, firstCtrl) {
// -------------------------------------^
$scope.innerFunc = firstCtrl.originFn;
}
};
})
<div ng-controller="myAppCtrl">
<first>
<second></second>
</first>
</div>
For more complete example.

Trigger a function inside a directive by watching a variable

Assume I have the following directive:
angular.module("myModule", [])
.directive('myDirective', function ($timeout) {
return {
restrict: 'EA',
scope: {
value: '=',
valueIsValid: '='
},
link: function (scope, element, attrs) {
scope.returnValueIsValid = function() {
// return valueIsValid = true or false
};
}
}
});
invoked the following way:
<div my-directive value="value" value-is-valid="valueIsValid"></div>
The value variable is input to the directive, where the directive should return if value is valid or not in the valueIsValid variable. Is there a way to trigger the validation function by just watching valueIsValid ?
I assume valueIsValid will be true when value>0
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl',function($scope, $timeout) {
});
myApp.directive('myDirective', function ($timeout) {
return {
restrict: 'EA',
scope: {
value: '=',
valueIsValid: '='
},
link: function (scope, element, attrs) {
scope.$watch("value",function(a,b){
if(a && a != 0){
scope.valueIsValid =true;
}else{
scope.valueIsValid =false;
}
})
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MyCtrl">
<input type="number" value="0" ng-model="value"/>
<div my-directive value="value" value-is-valid="valueIsValid"></div>
value is valid:- {{valueIsValid}}
</div>

two-way data binding inside attribute directive that requires element directive

I have an element directive (e-dir) and an attribute directive (a-dir) on the same element:
<e-dir a-dir attr="msg"></e-dir>
I pass msg into e-dir's isolate scope via the attr attribute:
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
}
};
});
In this way, msg is bound (two-ways) with $scope.attr (in EDirCtrl) or scope.attr (in e-dir's link function).
Is there a simple way I can achieve the same two-way data-binding inside a-dir's directive? Or would you recommend another, simpler approach?
The closest thing I've been able to come up with is to set eDirCtrl.attr = $scope.attr; inside e-dir's controller (EDirCtrl):
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
},
controllerAs: 'eDirCtrl'
};
});
Then, have a-dir require e-dir, and access attr via e-dir's controller (eDirCtrl.attr):
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
eDirCtrl.attr = 'eDirCtrl.attr';
}
});
But, it's not bound two-ways. As you can see this code snippet:
var app = angular.module('app', []);
app.controller('Ctrl', function Ctrl($scope) {
$scope.msg = 'initial message';
})
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
template: '<div>$scope.attr: {{attr}}</div>'+
'<div>eDirCtrl.attr: {{eDirCtrl.attr}}</div>',
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
$timeout(function() {
$scope.attr = 'changing $scope.attr also changes msg';
}, 2000);
},
controllerAs: 'eDirCtrl'
};
});
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
$timeout(function() {
eDirCtrl.attr = 'changing eDirCtrl.attr does not effect $scope.attr or msg';
}, 4000);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="Ctrl">
msg: <input type="text" ng-model="msg"><br>
<e-dir a-dir attr="msg"></e-dir>
</div>
The reason the two-way binding isn't working is that attr is being bound to a string rather than an object. In JavaScript, primitives (booleans, numbers, strings) are immutable, so when you change one, the previous instance is discarded and a new one is used. This breaks Angular's two-way binding and any changes to scope.msg are not propagated through attr into the directive.
You can get this to work as expected by setting msg on an object e.g. scope.test.msg and binding attr to test (the object) rather than msg (a string).
I've updated your code snippet to do this:
var app = angular.module('app', []);
app.controller('Ctrl', function Ctrl($scope) {
$scope.test = {msg : 'initial message'};
})
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
template: '<div>$scope.attr: {{attr.msg}}</div>'+
'<div>eDirCtrl.attr: {{eDirCtrl.attr.msg}}</div>',
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
$timeout(function() {
$scope.attr.msg = 'changing $scope.attr also changes msg';
}, 2000);
},
controllerAs: 'eDirCtrl'
};
});
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
$timeout(function() {
eDirCtrl.attr.msg = 'changing eDirCtrl.attr does not effect $scope.attr or msg';
}, 4000);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="Ctrl">
msg: <input type="text" ng-model="test.msg"><br>
<e-dir a-dir attr="test"></e-dir>
</div>

What is the scope of templates bindings in a directive without a controller

Here is a simple angular application. It shows/hides a text based on user clicking a button.
<div ng-controller="exampleController as ctrl">
<example></example>
</div>
app.controller('exampleController', function () {});
app.directive('example', function () {
return {
restrict: 'E',
template: '<p ng-show=\"showMe\">Text to show</p><button ng-click=\"clickMe()\">Click me</button>',
scope: {},
link: function (scope) {
scope.clickMe = function () {
scope.showMe = !scope.showMe;
};
}
};
});
When I remove the controller it doesn't work. Note that the directive creates an isolate scope, so my understanding is that it doesn't depend on the controllers scope.
<div>
<example></example>
</div>
app.directive('example', function () {
return {
restrict: 'E',
template: '<p ng-show=\"showMe\">Text to show</p><button ng-click=\"clickMe()\">Click me</button>',
scope: {},
link: function (scope) {
scope.clickMe = function () {
scope.showMe = !scope.showMe;
};
}
};
});
What is wrong here?
But anyways you should have ng-app
<body ng-app="demo">
<div>
<example></example>
</div>
</body>
var app = angular.module("demo", [])
app.directive('example', function () {
return {
restrict: 'E',
template: '<p ng-show=\"showMe\">Text to show</p><button ng-click=\"clickMe()\">Click me</button>',
scope: {},
link: function (scope) {
scope.clickMe = function () {
scope.showMe = !scope.showMe;
};
}
};
});
Fiddle

Angularjs controller required by directive can not be found in transclude content

Recently I use angular to develop a directive, there is an directive which like ng-repeat to generate some records, I used transclude to implement it. but it raise an error that "Controller 'aArea', required by directive 'bSpan', can't be found!".
1. ModuleA code
var moduleA = angular.module("moduleA", []);
moduleA.directive("aArea", function () {
return {
restrict: 'E',
transclude:'element',
scope: {
amount:"="
},
template: '<div id=\"cc\" ng-transclude></div>',
controller: function ($scope,$element,$attrs) {
this.getData = function (data) {
return data + " is ok";
}
},
compile: function (tElement, attrs, linker) {
var parentElement = tElement.parent();
return {
pre: function () {
},
post: function (scope) {
linker(scope.$parent,function (clone,scope) {
parentElement.append(clone);
});
linker(scope.$parent, function (clone, scope) {
parentElement.append(clone);
});
linker(scope.$parent, function (clone, scope) {
parentElement.append(clone);
});
}
}
}
}
});
moduleA.directive("bSpan", function () {
return {
restrict: 'E',
scope: {
data: "=",
},
template: '<span style=\"background-color:gray;color:orange\">{{data}}</span>',
require: "^aArea",
link: function ($scope, $element, $attrs, controller) {
var data = "abc";
}
}
});
2. ModuleB COde
var moduleB = angular.module("moduleB", []);
moduleB.directive("myItem", function () {
return {
restrict: 'E',
scope: {
item: "=",
itemTemplate: '='
},
priority: 1000,
terminal:false,
template: '<ng-include src=\"itemTemplate\"/>',
controller: function ($scope, $element, $attrs) {
var data = "";
}
}
})
3. ModuleC Code
var moduleC = angular.module("moduleC", ["moduleA", "moduleB"]);
moduleC.controller("Ctr", function ($scope) {
$scope.item = {};
$scope.item.dataAmount = 1000;
$scope.item.templateUrl = "item-template.html";
})
4. Html Code
<body>
<div ng-app="moduleC">
<div ng-controller="Ctr">
<a-area>
<my-item item="item" item-template="item.templateUrl"></my-item>
</a-area>
</div>
</div>
</body>
5. template code
<div>
<span style="display:block">hello every one</span>
<b-span data="item.dataAmount"></b-span>
</div>
You should not use the transclude function (that you called linker) of the compile function - it is deprecated.
From $compile documentation:
Note: The transclude function that is passed to the compile function is deprecated, as it e.g. does not know about the right outer scope. Please use the transclude function that is passed to the link function instead.
Following this guidance (and a few other minor changes for the better), change the aArea directive as follows:
compile: function(tElement, tAttrs) {
// don't use the template element
//var parentElement = tElement.parent();
return function(scope, element, attrs, ctrls, transclude) {
transclude(function(clone, scope) {
element.after(clone);
});
transclude(function(clone, scope) {
element.after(clone);
});
transclude(function(clone, scope) {
element.after(clone);
});
};
}
In fact, you don't even need the transclude function at all and you don't need to do transclude: "element". You could just change to transclude: true and use <div ng-transclude> 3 times in the template.

Resources