Calling a function of parent controller with nested directives using isolated scope - javascript

I have a a treeview directive that I am trying to invoke a function from the parent controller and I can't seem to get the function to get called. I'm not sure if it's due to the structure of the treeview and nesting of child elements or what.
Within my html I have the directive declared as:
<div ng-controller="treeController as vm">
<tree src="myList" filter="doSomething()"></tree>
<a ng-click="clicked()"> link</a>
</div>
I declared in the directive an attribute/parameter filter which should call the doSomething() function within the main controller.
The main controller contains the following code (test to build the tree as well as invoke the function.
app.controller("treeController", ['$scope', function($scope) {
var vm = this;
$scope.doSomething = function () {
var item = data;
}
$scope.clicked = function () {
alert('clicked');
}
$scope.myList = {
children: [
{
name: "Event",
children: [
{
name: "Event Date",
children: [
{
name: "2008",
FilterType: '_eventStartDate',
Parent: '_event'
},
{
name: "2009",
FilterType: '_eventStartDate',
Parent: '_event'
}
]
},
{
name: "Event Attendee",
children: [
{
name: "Person 1",
FilterType: '_eventAttenddeeName',
Parent: '_Event'
},
{
name: "Person 2",
FilterType: '_eventAttenddeeName',
Parent: '_Event'
}
]
}
]
}]
};
}]);
The within my directive I declare the isolated scope, as well as the parameter filter (second app.directive) which I prefix with the model binding prefix '&'. Within the template I then call ng-click which should invoke the function doSomething() within the main controller. However... no dice.
app.directive('tree', function() {
//builds the tree
return {
restrict: 'E',
replace: true,
scope: {
t: '=src'
},
template: '<ul><branch ng-repeat="c in t.children" src="c"></branch></ul>'
};
});
app.directive('branch', function($compile) {
//directive that builds the children/branches
return {
restrict: 'E',
replace: true,
scope: {
b: '=src',
filter: '&'
},
template: '<li><input type="checkbox" ng-click="filter()" ng-hide="visible" /><a>{{ b.name }}</a></li>',
link: function (scope, element, attrs) {
var has_children = angular.isArray(scope.b.children);
scope.visible = has_children;
if (has_children) {
element.append('<tree src="b"></tree>');
$compile(element.contents())(scope);
}
element.on('click', function(event) {
event.stopPropagation();
if (has_children) {
element.toggleClass('collapsed');
}
});
//test to call function within directive
//scope.doSomething = function(b) {
// alert('test');
//}
}
};
});
I posted a public jsFiddle with the working code sample as well
Any suggestions on what I missed?
Right now I am just trying to invoke the method however ultimately I will need to pass as a parameter the selected item back to the controller as well but for now I'm just trying to figure out why the function in my controller will not get called.
Thanks in advance
Update:
It was suggested to move the declaration of the filter from the branch to the tree directive.
I updated my code locally so the tree directive looked like the following:
app.directive('tree', function() {
return {
restrict: 'E',
replace: true,
scope: {
t: '=src',
filter: '&'
},
template: '<ul><branch ng-repeat="c in t.children" src="c"></branch></ul>'
};
});
Note: the filter parameter was removed from the secondary directive. There was no change in the output. The function within the controller was still not called.

your tree directive does not have filter method.your branch directive only has that property
<div ng-controller="treeController as vm">
<tree src="myList" filter="doSomething()"></tree>
<a ng-click="clicked()"> link</a>
</div>
app.directive('tree', function() {
//builds the tree
return {
restrict: 'E',
replace: true,
scope: {
t: '=src',
filter: '&'
},
template: '<ul><branch ng-repeat="c in t.children" src="c" filter="doSomething()"></branch></ul>'
};
});
app.directive('branch', function($compile) {
//directive that builds the children/branches
return {
restrict: 'E',
replace: true,
scope: {
b: '=src',
filter: '&'
},
template: '<li><input type="checkbox" ng-click="filter()" ng-hide="visible" /><a>{{ b.name }}</a></li>',
link: function (scope, element, attrs) {
var has_children = angular.isArray(scope.b.children);
scope.visible = has_children;
if (has_children) {
element.append('<tree src="b"></tree>');
$compile(element.contents())(scope);
}
element.on('click', function(event) {
event.stopPropagation();
if (has_children) {
element.toggleClass('collapsed');
}
});
//test to call function within directive
//scope.doSomething = function(b) {
// alert('test');
//}
}
};
});

Update:
Sundar's comments got me down the right path here is the updated directive the main issue for me was that I am working with nested directives so the nested item (which was making the function call) was out of scope of the controller to correct this included Sundar's changes but to get the nested directive to work I had to explicitly set the controller at the parent directive level. I realize this is not a good option if you needed a directive to be used in multiple areas of an app. However for me the directive is only used in one spot so this solution works. If anyone has any other suggestions or better approaches I'd appreciate them.
app.directive('tree', function() {
return {
restrict: 'E',
replace: true,
scope: {
t: '=src',
filter: '&'
},
controller:'treeController', //explicitly set the controller of the parent directive
template: '<ul><branch ng-repeat="c in t.children" src="c" filter="doSomething(data)"></branch></ul>'
};
});
app.directive('branch', function($compile) {
return {
restrict: 'E',
replace: true,
scope: {
b: '=src',
filter: '&'
},
template: '<li><input type="checkbox" ng-click="innerCall()" ng-hide="visible" /><a>{{ b.name }}</a></li>',
link: function (scope, element, attrs) {
var has_children = angular.isArray(scope.b.children);
scope.visible = has_children;
if (has_children) {
element.append('<tree src="b"></tree>');
$compile(element.contents())(scope);
}
element.on('click', function(event) {
event.stopPropagation();
if (has_children) {
element.toggleClass('collapsed');
}
});
scope.innerCall = function() {
scope.filter(scope.b);
}
}
};
});

Related

AngularJS Directive two way binding in isolated scope not reflecting in parent scope

I am trying to pass a scope array element to a directive and changing the value of that element inside the directive but when I print the values of the scope element the changes that made inside the directive is not affected in the parent scope. I created Isolated scope and provided two way binding using '=' in scope but It is not giving any change in the parent scope.
Attaching the code
Index.html
<div ng-app="dr" ng-controller="testCtrl">
<test word="word" ng-repeat="word in chat.words"></test>
<button ng-click="find();">
click
</button>
</div>
Javascript Part
var app = angular.module('dr', []);
app.controller("testCtrl", function($scope) {
$scope.chat= {words: [
'first', 'second', 'third'
]};
$scope.find = function(){
alert(JSON.stringify($scope.chat, null, 4));
}
});
app.directive('test', function() {
return {
restrict: 'EA',
scope: {
word: '='
},
template: "<input type='text' ng-model='word' />",
replace: true,
link: function(scope, elm, attrs) {
}
}
});
Most of my search returned that putting '=' in directive scope will solve the issue, But no luck with that. can anyone point what is the issue, and how can I reflect the value in parent scope.
You pass a string to your directive, and this string isn't referenced because its not related to your array anymore
i guess you have to change your array properly
Something like the following should work:
var app = angular.module('dr', []);
app.controller("testCtrl", function($scope) {
$scope.word = 'test';
$scope.chat= {words: [
{'name':'first'}, {'name': 'second'}, {'name' : 'third'}
]};
$scope.find = function(){
alert(JSON.stringify($scope.chat, null, 4));
}
});
app.directive('test', function() {
return {
restrict: 'EA',
scope: {
word: '='
},
template: "<input type='text' ng-model='word.name' />",
replace: true,
link: function(scope, elm, attrs) {
}
}
});
<div ng-app="dr" ng-controller="testCtrl">
<pre>{{chat.words}}</pre>
<test word="word" ng-repeat="word in chat.words"></test>
<button ng-click="find();">
click
</button>
</div>
The directive can be made more efficient by using one-way (<) binding:
app.directive('test', function() {
return {
restrict: 'EA',
scope: {
̶w̶o̶r̶d̶:̶ ̶'̶=̶'̶
word: '<'
},
template: "<input type='text' ng-model='word.name' />",
replace: true,
link: function(scope, elm, attrs) {
}
}
});
One-way (<) binding has the additional advantage that it works with the $onChanges life-cyle hook.

How can i update a directive $scope from other directive contoller in Angular.js?

I have a diretive with a list of events loading from my service service:
.directive('appointments', [function () {
return {
restrict: 'CE',
scope: {
ngTemplate: '=',
},
controller: ['$scope','calendarService', function ($scope, calendarService) {
var vm = this;
vm.events = calendarService.getEvents();
}],
controllerAS:'vm',
link: function (scope, elem, attrs) {
scope.getTemplateUrl = function () {
if (angular.isDefined(scope.ngTemplate))
return scope.ngTemplate;
else
return "/list.directive.html";
}
},
template: '<div ng-include="getTemplateUrl()"></div>'
}
}])
Now in another directive i am updating this list, how can i update the list in the first controller?
.directive('appointmentsUpdate', [function () {
return {
restrict: 'CE',
scope: {
ngEventId: '=',
},
controller: ['$scope','calendarService', function ($scope, calendarService) {
var vm = this;
vm.update = calendarService.updateEvent(scope.ngEventId).then(function(res){
// Here is where the newly created item, should be added to the List (vm.events) from first directive
)
});
}],
controllerAS:'vm',
link: function (scope, elem, attrs) {
scope.getTemplateUrl = function () {
if (angular.isDefined(scope.ngTemplate))
return scope.ngTemplate;
else
return "/list.directive.html";
}
},
template: '<div ng-include="getTemplateUrl()"></div>'
}
}])
you can use angular broadcast service for this:
in first directive use this:
$rootScope.$broadcast('greeting', data_needs_to_be_send);
in other directive listen the event to update its scope:
$scope.$on('greeting', listenGreeting)
function listenGreeting($event, message){
alert(['Message received',message].join(' : '));
}
We use require property to make communication between directives.
Something like this
return {
restrict: 'AE',
require: '^ParentDirective or ^SameLevelDirective'
}
Here is the clear explanation of Driectives That Communicate by ToddMotto
Services are singletons, so if you update the list from one place (with your calendarService.updateEvent()), then if you retrieve the data from the service in the other directive, it should be the updated list.
You could use a watch to check when the list is updated:
$scope.$watch(() => calendarService.getEvents(), (newValue, oldValue) => {
// update your scope with the new list
}, true);

How to pass transclusion down through nested directives in Angular?

I am trying to figure out how to pass a transclusion down through nested directives and bind to data in the inner-most directive. Think of it like a list type control where you bind it to a list of data and the transclusion is the template you want to use to display the data. Here's a basic example bound to just a single value (here's a plunk for it).
html
<body ng-app="myApp" ng-controller="AppCtrl as app">
<outer model="app.data"><div>{{ source.name }}</div></outer>
</body>
javascript
angular.module('myApp', [])
.controller('AppCtrl', [function() {
var ctrl = this;
ctrl.data = { name: "Han Solo" };
ctrl.welcomeMessage = 'Welcome to Angular';
}])
.directive('outer', function(){
return {
restrict: 'E',
transclude: true,
scope: {
model: '='
},
template: '<div class="outer"><inner my-data="model"><div ng-transclude></div></div></div>'
};
})
.directive('inner', function(){
return {
restrict: 'E',
transclude: true,
scope: {
source: '=myData'
},
template :'<div class="inner" my-transclude></div>'
};
})
.directive('myTransclude', function() {
return {
restrict: 'A',
transclude: 'element',
link: function(scope, element, attrs, controller, transclude) {
transclude(scope, function(clone) {
element.after(clone);
})
}
}
});
As you can see, the transcluded bit doesn't appear. Any thoughts?
In this case you don't have to use a custom transclude directive or any trick. The problem I found with your code is that transclude is being compiled to the parent scope by default. So, you can fix that by implementing the compile phase of your directive (this happens before the link phase). The implementation would look like the code below:
app.directive('inner', function () {
return {
restrict: 'E',
transclude: true,
scope: {
source: '=myData'
},
template: '<div class="inner" ng-transclude></div>',
compile: function (tElem, tAttrs, transclude) {
return function (scope, elem, attrs) { // link
transclude(scope, function (clone) {
elem.children('.inner').append(clone);
});
};
}
};
});
By doing this, you are forcing your directive to transclude for its isolated scope.
Thanks to Zach's answer, I found a different way to solve my issue. I've now put the template in a separate file and passed it's url down through the scopes and then inserting it with ng-include. Here's a Plunk of the solution.
html:
<body ng-app="myApp" ng-controller="AppCtrl as app">
<outer model="app.data" row-template-url="template.html"></outer>
</body>
template:
<div>{{ source.name }}</div>
javascript:
angular.module('myApp', [])
.controller('AppCtrl', [function() {
var ctrl = this;
ctrl.data = { name: "Han Solo" };
}])
.directive('outer', function(){
return {
restrict: 'E',
scope: {
model: '=',
rowTemplateUrl: '#'
},
template: '<div class="outer"><inner my-data="model" row-template-url="{{ rowTemplateUrl }}"></inner></div>'
};
})
.directive('inner', function(){
return {
restrict: 'E',
scope: {
source: '=myData',
rowTemplateUrl: '#'
},
template :'<div class="inner" ng-include="rowTemplateUrl"></div>'
};
});
You can pass your transclude all the way down to the third directive, but the problem I see is with the scope override. You want the {{ source.name }} to come from the inner directive, but by the time it compiles this in the first directive:
template: '<div class="outer"><inner my-data="model"><div ng-transclude></div></div></div>'
the {{ source.name }} has already been compiled using the outer's scope. The only way I can see this working the way you want is to manually do it with $compile... but maybe someone smarter than me can think of another way.
Demo Plunker

How to call controller function from directive in angular js ?

I am opening a dialog-box on click of button.I want to add endless scroll in that.
Problem:
When user scrolls at the end of dialog-box i want to call addMoreData() written in controller.
HTML of Dialog-box:
<modal-dialog show='modalShown' width='60%' height='325px' >
<div id='diaogContainer'>
<p>Modal Content Goes here<p>
</div>
</modal-dialog>
Controller:
sampleApp.controller('view3Controller', function($scope) {
$scope.modalShown = false;
$scope.toggleModal = function() {
$scope.modalShown = !$scope.modalShown;
}
**$scope.showMore = function(){
console.log('showMore');
}**
});
Directive of Dialog-box:
sampleApp.directive('modalDialog', function() {
return {
restrict: 'E',
scope: {
show: '='
},
replace: true, // Replace with the template below
transclude: true,
link: function(scope, element, attrs) {
scope.dialogStyle = {};
if (attrs.width)
scope.dialogStyle.width = attrs.width;
if (attrs.height)
scope.dialogStyle.height = attrs.height;
scope.hideModal = function() {
scope.show = false;
};
},
template: "<div class='ng-modal' ng-show='show'><div class='ng-modal-overlay'ng-click='hideModal()'></div><div class='ng-modal-dialog' hello **scrolly='showMore()'** ng-style='dialogStyle'><div class='ng-modal-close' ng-click='hideModal()'>X</div><div class='ng-modal-dialog-content' ng-transclude></div></div></div>"
};
});
Directive to load more data:
sampleApp.directive('hello', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var raw = element[0];
element.bind('scroll', function () {
console.log(raw.scrollTop +'-------'+raw.offsetHeight+'----'+raw.scrollHeight);
if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
// here is problem
// I am not able to call function through this attr
//
**scope.$apply(attrs.scrolly);**
}
});
}
};
});
You can't pass in a function to a directive through an attribute, you can however pass it through an isolate scope. Pass a reference to the function you wish to call to the directive:
sampleApp.directive('hello', function () {
return {
restrict: 'A',
scope:{
onScrollEnd:'&'
},
link: function (scope, element, attrs) {
var raw = element[0];
element.bind('scroll', function () {
console.log(raw.scrollTop +'-------'+raw.offsetHeight+'----'+raw.scrollHeight);
if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
scope.onScrollEnd();
}
});
}
};
});
Now assuming you have the addMoreData() function defined on your controller, you can pass it to the directive this this:
<div hello on-scroll-end='addMoreData()'></div>
EDIT
I think the problem is that the hello directive can't access functions on the parent controller since the modalDialog directive is using an isolated scope, therefore making everything o the parent controller invisible. Pass the function to through the isolate scope of the modalDialog Directive as well:
scope: {
show: '=',
onScrollEnd:'&'
},
you can try like this.
Directive part
var module = angular.module('direc');
module.directive("direcApp", ['$timeout', function ($timeout) {
return {
restrict: 'E',
templateUrl: "template/template.html",
compile: function (iel, iattr) {
return function (scope, el, attr) {
}
},
scope: {
type: "#",
items: '=',
onClick: '&',
val: "="
},
controller: function ($scope) {
$scope.selectItem = function (selectedItem) {
$scope.val = selectedItem;
if (angular.isFunction($scope.onClick)) {
$timeout($scope.onClick, 0);
}
};
}
};
}]);
Controler part
var app = angular.module('app', ['direc']);
app.controller("appCtrl", ['$scope', '$http', function ($scope, $http) {
var t = {
count: function () {
return $scope.$$watchersCount; // in angular version 4 get total page listener
},
val1: "",
onClick: function () {
console.log($scope.data.val1);
},
items: [{ text: 'Seçenek 1', value: '1' },
{ text: 'Seçenek 2', value: '2' },
{ text: 'Seçenek 3', value: '3' },
{ text: 'Seçenek 4', value: '4' },
{ text: 'Seçenek 5', value: '5' }]
};
angular.extend(this, t);
}]);
Html part
<div ng-controller="appCtrl as data">
<div><b>Watcher Count : {{data.count()}}</b></div>
<direc-app items="data.items"
val="data.val1"
on-click="data.onClick1()"
>
</selection-group>
</div>
Add data as parameter to directive: scope: { data: '='}, and in directive just data.push({name:'i am new object'})
Add function parameter to directive as suggested in previous answer.

ngClass binding not updating via directive communication

I want to nest two directives and the inner directive has a ng-class bound to a function that takes a scope attribute from inner and outer scopes and return a Boolean
This is the HTML:
<ul my-toolbar disabled-when="myCtrl.isProcessing" >
<li my-action-button action="myCtrl.action()" disable-when="myCtrl.isSad()" />
</ul>
This is my outer directive:
myApp.directive("myToolbar", function() {
return {
restrict: 'A',
scope: {
disabled: '=disabledWhen'
},
transclude: true,
controller: function($scope) {
this.isDisabled = function() {
return $scope.disabled;
}
}
};
});
And this is my inner directive:
myApp.directive("myActionButton", function() {
return {
restrict: 'A',
scope: {
action: '&',
disabled: '=disabledWhen'
},
replace: true,
template: "<li ng-class='{disabled: isDisabled()}'><a ng-click='isDisabled() || action()' /></li>",
link: function(scope, elem, attrs, toolbarCtrl) {
scope.isDisabled = function() {
return toolbarCtrl.isDisabled() || scope.disabled;
};
}
};
});
Now the problem is that the ng-class='{disabled: isDisabled()}' binding is initialized once in the beginning but not updated when myCtrl.isProcessing changes!
Can someone please explain why? and how can I fix this without changing my design?
#Jonathan as requested I put my angular code in a fiddle and (this is part that's irritating me now) it works!
http://jsfiddle.net/shantanusinghal/ST3kH/1/
Now, I'll go back to seeing why it doesn't work for me in my production code!! *puzzled

Categories

Resources