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
Related
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.
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
I want to create a component that displays itself as a collapsible box.
When it is expanded, it should show the transcluded content; when it is collapsed it should only show its label.
myApp.directive('collapsingBox', function() {
return {
restrict: 'E',
transclude: true,
require: '^ngModel',
scope: {
ngModel: '='
},
template: '<div ng-controller="CollapseController" class="collapsingBox"><div class="label">Title: {{ ngModel.title }}</div><br/><div ng-transclude ng-show="expanded">Test</div></div>',
link: function($scope, element, attr) {
element.bind('click', function() {
alert('Clicked!');
$scope.toggle();
});
}
};
});
This component should be reusable and nestable, so I wanted to manage the values (like "title" and "expanded") in a controller that gets instantiated for every use of the directive:
myApp.controller('CollapseController', ['$scope', function($scope) {
$scope.expanded = true;
$scope.toggle = function() {
$scope.expanded = !$scope.expanded;
};
}]);
This "almost" seems to work:
http://plnkr.co/edit/pyYV0MAikXThvMO8BF69
The only thing that does not work seems to be accessing the controller's scope from the event handler bound during linking.
link: function($scope, element, attr) {
element.bind('click', function() {
alert('Clicked!');
$scope.toggle(); // this is an error -- toggle is not found in scope
});
}
Is this the correct (usual?) way to create one instance of the controller per use of the directive?
How can I access the toggle-Function from the handler?
Rather than using ng-controller on your directive's template, you need to put the controller in your directive's controller property:
return {
restrict: 'E',
transclude: true,
require: '^ngModel',
scope: {
ngModel: '='
},
template: '<div class="collapsingBox"><div class="label">Title: {{ ngModel.title }}</div><br/><div ng-transclude ng-show="expanded">Test</div></div>',
controller: 'CollapseController',
link: function($scope, element, attr) {
element.bind('click', function() {
alert('Clicked!');
$scope.toggle();
});
}
};
As it is CollapseController's scope will be a child scope of your directive's scope, which is why toggle() isn't showing up there.
I'm relative new to AngularJS and trying to create a directive for add some buttons. I'm trying to modify the controller scope from inside the directive but I can't get it to work. Here is an example of my app
app.controller('invoiceManagementController', ['$scope', function ($scope) {
$scope.gridViewOptions = {
isFilterShown: false,
isCompact: false
};
}]);
app.directive('buttons', function () {
return {
restrict: 'A',
template: '<button type="button" data-button="search" title="Filter"><i class="glyphicon glyphicon-search"></i></button>',
scope: {
gridViewOptions: '='
},
transclude: true,
link: function (scope, element, attr, ctrl, transclude) {
element.find("button[data-button='search']").bind('click', function (evt) {
// Set the property to the opposite value
scope.gridViewOptions.isFilterShown = !scope.gridViewOptions.isFilterShown
transclude(scope.$parent, function (clone, scope) {
element.append(clone);
});
});
}
};
});
My HTML like following
{{ gridViewOptions.isFilterShown }}
<div data-buttons="buttons" data-grid-view-options="gridViewOptions"></div>
The scope inside the directive does change but is like isolated, I did try paying with the scope property and transclude but I'm probably missing something, would appreciate some light here
When you modify scope inside of your directive's link function, you are modifying your directive's isolated scope (because that is what you have set up). To modify the parent scope, you can put the scope assignment inside of your transclude function:
transclude(scope.$parent, function (clone, scope) {
// Set the property to the opposite value
scope.gridViewOptions.isFilterShown = !scope.gridViewOptions.isFilterShown
element.append(clone);
});
Ok finally found a solution for this after some more research today. Not sure if the best solution, but this works so good for now.
app.controller('invoiceManagementController', ['$scope', function ($scope) {
$scope.gridViewOptions = {
isFilterShown: false,
isCompact: false
};
}]);
app.directive('buttons', function () {
return {
restrict: 'A',
template: '<button type="button" data-button="search" data-ng-class="gridViewOptions.isFilterShown ? \'active\' : ''" title="Filter"><i class="glyphicon glyphicon-search"></i></button>',
scope: {
gridViewOptions: '='
},
link: function (scope, element, attr, ctrl, transclude) {
element.find("button[data-button='search']").bind('click', function (evt) {
scope.$apply(function () {
// Set the property to the opposite value
scope.gridViewOptions.isFilterShown = !scope.gridViewOptions.isFilterShown;
});
});
}
};
});
I'm trying to use ngRepeat in a directive - but I really don't know how to do it. Am I close?
Mowers is an array in my controller where this directive is used.
.directive('slider', function() {
return function() {
template: '<slider><film><frame ng-repeat="mower in mowers">{{mower.series}}</frame></film></slider>';
replace: true;
scope: true;
link: function postLink(scope, iElement, iAttrs) {
// jquery to animate
}
}
});
Yes, you're close, but the syntax was off. and you need to assign something to mowers on your scope.
.directive('slider', function() {
return {
restrict: 'E', //to an element.
template: '<film><frame ng-repeat="mower in mowers">{{mower.series}}</frame></film>',
replace: true,
scope: true, // creates a new child scope that prototypically inherits
link: function postLink(scope, iElement, iAttrs) {
scope.mowers = [
{ series: 1 },
{ series: 2 },
{ series: 3 }
];
}
}
});
Also, you need to not reference <slider> in itself.
The above code also assumes you've made directives for <film> and <frame>.
Finally, if you wanted to pass the mowers array in from an external scope, you'd change your scope setting to this:
scope: {
mowers: '='
}
And you could then set it like so:
<slider mowers="myMowers"></slider>
where myMowers is a variable on your controller or parent directive's scope.
I hope that helps.