I'm creating a directive to show differences in text. For this directive, I need a couple of buttons which I've split up in directives. A simpliefied example would be:
.directive('differenceViewer', function($templateCache, $compile) {
return {
restrict: 'E',
scope: {
oldtext: '#',
newtext: '#',
template: '#',
heading: '#',
options: '=',
itemdata: '&',
method: '&'
},
replace: false,
link: (scope, element, attr) => {
element.append(angular.element($compile($templateCache.get(scope.template))(scope)));
}
};
}).directive('diffbutton', function() {
return {
restrict: 'E',
scope: {
method: '&'
},
template: '<button class="btn btn-sm btn-success" ng-click="method()">Rollback</button>',
replace: true,
terminal: true,
link: (scope, element, attr) => {
scope.directiveClick = function(){
console.log("directive method"); // is never called
}
}
}
})
I compile the html via a template script:
<script type="text/ng-template" id="differenceViewer.html">
<div class="ibox-footer">
<div class="row">
<div class="col-md-12">
<diffbutton method="clickedBtn()">Foo</diffbutton>
</div>
</div>
</div>
</script>
Where the diffbutton is created inside the html compiled by differenceViewer.
I need to call a method in the controller that is responsible for creating all the difference-views.
app.controller('MainCtrl', function($scope) {
$scope.clickedBtn = function() {
console.log('foo'); // is never called
}
})
Here's a plunker demonstrating the issue.
What do I need to change in order to be able to forward the button click on my directive in a directive to the controller method?
I've been working with the answers of this question but still can't make it work.
Note that, if I add
scope.clickedBtn = function() {console.log("bar");}
to the differenceViewer directive, it gets called - however I need to call the method in the controller instead.
Pass on a method from the parent to the child element and then trigger it on click. For example (pseudo code coming in )
<parent-directive>
<child-directive totrigger="parentClickCallback()"></child-directive>
</parent-directive>
then in your parent directive you set
$scope.parentClickCallback = function(){//do whatever you neeed}
and on your child in its scope binding you set
scope:{
totrigger:'&'
}
and on that child button you simply add
<button ng-click="totrigger()">ClickMe</button>
every time you click that button it will trigger parentClickCallback by reference attached to totrigger.
Why would you need to complicate your code so much I'm not really sure. If you not happy with scope binding just require controller in your directive and pass the controller bound function.
Related
This is follow up to these 2 questions:
Pass argument between parent and child directives
Parent directive controller undefined when passing to child directive
I have this part working; however, when the value for ng-disabled for parent directive changes, the child directive values don't get updated.
Please see thin plunkr example.
HTML:
<div ng-app="myApp">
<div ng-controller="MyController">
{{menuStatus}}
<tmp-menu ng-disabled="menuStatus">
<tmp-menu-link></tmp-menu-link>
<tmp-menu-link></tmp-menu-link>
</tmp-menu>
<button ng-click="updateStatus()">Update</button>
</div>
</div>
JavaScript(AngularJS):
angular.module('myApp', [])
.controller('MyDirectiveController', MyDirectiveController)
.controller('MyController', function($scope){
$scope.menuStatus = false;
$scope.updateStatus = function(){
$scope.menuStatus = $scope.menuStatus?false:true;
}
})
.directive('tmpMenu', function() {
return {
restrict: 'AE',
replace:true,
transclude:true,
scope:{
disabled: '=?ngDisabled'
},
controller: 'MyDirectiveController',
template: '<div>myDirective Disabled: {{ disabled }}<ng-transclude></ng-transclude></div>',
link: function(scope, element, attrs) {
}
};
})
.directive('tmpMenuLink', function() {
return {
restrict: 'AE',
replace:true,
transclude:true,
scope:{
},
require:'^^tmpMenu',
template: '<div>childDirective disabled: {{ disabled }}</div>',
link: function(scope, element, attrs, MyDirectiveCtrl) {
console.log(MyDirectiveCtrl);
scope.disabled = MyDirectiveCtrl.isDisabled();
}
};
})
function MyDirectiveController($scope) {
this.isDisabled = function() {
return $scope.disabled;
};
}
How can I detect change in parent directive and pass it to child directive without adding angular watcher.
Solution 1
i've set up a working plnkr here: https://plnkr.co/edit/fsxMJPAc05imhBqefaRk?p=preview
the reason of this behaviour is that tmpMenuLink kept a copy of the value returned from MyDirectiveCtrl.isDisabled(). no watcher is set up , so the only way to resolve this is to manually watch for any changes and then update the field.
scope.$watch(function(){
return MyDirectiveCtrl.isDisabled();
}, function(){
scope.disabled = MyDirectiveCtrl.isDisabled();
})
Solution 2
An alternative without watchers is to pass the reference of an object instead of a primitive type, something like:
$scope.menuStatus = {status: false};
new plnkr here: https://plnkr.co/edit/RGEK6TUuE7gkPDS6ygZe?p=preview
I want to recreate nsClick behavior with my directive ( changing priority).
So this is my code:
angular.module('MyApp').directive('nsClickHack', function () {
return {
restrict: 'E',
priority: 100,
replace: true,
scope: {
key: '=',
value: '=',
accept: "&"
},
link: function ($scope, $element, $attrs, $location) {
$scope.method();
}
}
});
and the line I'm trying to bind to:
<li ng-repeat="item in items" ns-click-hack="toggle(); item.action()">
toggle and item.action are from other directives.
Can you point me where I was making mistake?
If you are trying to re-create ng-click, then it's probably better to look at the source of the ngClick directive.
For example, it does not create an isolate scope since only one isolate scope can be created on an element and it tries to be accommodating towards other directives. The alternative is to $parse the attribute value, which is what the built-in implementation is doing.
If you are just creating a "poor's man" version of ngClick, then, sure, you could use a callback function "&" defined on the scope, and invoke it when the element is clicked:
.directive("nsClickHack", function(){
return {
restrict: "A",
scope: {
clickCb: "&nsClickHack"
},
link: function(scope, element){
element.on("click", function(e){
scope.clickCb({$event: e}); // ngClick also passes the $event var
});
}
}
});
The usage is as you seem to want it:
<li ng-repeat="item in items" ns-click-hack="toggle(); item.action()">
plunker
Here's the explanation:
I have the current controller that creates an array of $scope.plan.steps which will be used to store every step:
.controller('PlanCtrl', function ($scope, $http) {
$scope.plan = {
steps: [{}]
};
$scope.addStep = function () {
$scope.tutorial.steps.push({});
}
}
Then I have the following directive which has an isolated scope and that is associated to the index of the $scope.plan.steps array:
.directive('planStep', function () {
return {
template: '<input type="text" ng-model="step.name" />{{step}}',
restrict: 'E',
scope: {
index: '=index'
},
transclude: true,
controller: function($scope, $element, $transclude) {
$scope.removeStep = function() {
$scope.$emit('removeStep', $scope.index);
$element.remove();
$scope.$destroy();
}
}
};
});
These two communicate, create, and delete objects inside of the controller's scope, however, how can I allow the directive to update the controller's scope array in real time?
I've tried doing a $watch on the directive's isolated scope changes, $emit the changes to the controller, and specify the $index... But no luck.
I've created a plunker to reproduce what I currently have: Link
So far I can create and delete objects inside of the array, but I cannot get a single object to update the controller's object based on the $index.
If the explanation was not clear, by all means, let me know and I will elaborate.
Thank you
WHen you do things like this inside ng-repeat you can take advantage of the child scope that ng-repeat creates and work without isolated scope.
Here's the same directive without needing any angular events
.directive('planStep', function() {
return {
template: '<button ng-click="removeStep(step)">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
transclude: true,
controller: function($scope, $element, $transclude) {
var steps = $scope.plan.steps// in scope from main controller
/* can do the splicing here if we want*/
$scope.removeStep = function(step) {
var idx =steps.indexOf(step)
steps.splice(idx, 1);
}
}
};
});
Also note that removing the element with element.remove() is redundant since it will automatically be removed by angular when array gets spliced
As for the update, it will update the item in real time
DEMO
The way you set up 2-way binding for index you could set one up for step as well? And you really do not need index to remove the item, eventhough your directive is isolated it relies on the index from ng-repeat which probably is not a good idea.
<plan-step ng-repeat="step in plan.steps" index="$index" step="step"></plan-step>
and in your directive:
scope: {
index: '=index',
step:'='
},
Demo
Removing $index dependency and redundant element remove() and scope destroy (when the item is removed from the array angular will manage it by itself):
return {
template: '<button ng-click="removeStep()">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
scope: {
step:'='
},
transclude: true,
controller: function($scope, $element, $transclude) {
$scope.removeStep = function() {
$scope.$emit('removeStep', $scope.step);
}
}
and in your controller:
$scope.$on('removeStep', function(event, data) {
var steps = $scope.plan.steps;
steps.splice(steps.indexOf(data), 1);
});
Demo
If you want to get rid of $emit you could even expose an api with the isolated scoped directive with function binding (&).
return {
template: '<button ng-click="onDelete({step:step})">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
scope: {
step:'=',
onDelete:'&' //Set up function binding
},
transclude: true
};
and register it on the view:
<plan-step ng-repeat="step in plan.steps" step="step" on-delete="removeStep(step)"></plan-step>
Demo
preamble: It seems like this question has been asked and answered before, but I cannot seem to get it working, so if my question boils down to "can you debug my code?", I apologize.
I would like to write the following code:
<radio-set ng-model="obj.prop" name="obj_prop">
<radio-set-button ng-value="'public'">Public</radio-set-button>
<radio-set-button ng-value="'protected'">Protected</radio-set-button>
<radio-set-button ng-value="'private'">Private</radio-set-button>
</radio-set>
This renders a bunch of radio buttons and labels which need to populate whatever is passed to the <radio-set>'s ngModel. I'm missing something scope-related.
.directive("radioSet", function () {
return {
restrict: 'E',
replace: true,
scope: {
ngModel: '=?',
ngChange: '&',
name: '#'
},
transclude: true,
template: '<div class="radio-set" ng-transclude></div>',
controller: function () {}
};
})
.directive("radioSetButton", function () {
return {
restrict: 'E',
replace: true,
require: ['^radioSet', '?ngModel'],
scope: {
ngModel: '=?', // provided by ^radioSet?
ngValue: '=?',
ngChange: '&', // provided by ^radioSet?
name: '#' // provided by ^radioSet?
},
transclude: true,
link: function (scope, element, attr) {
element.children().eq(0).attr("name", scope.name); // scope.name is null
},
template: '<label class="radio-set-button">' +
'<input type="radio" name="name" ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
'<div class="radio-content" ng-transclude></div>' +
'</label>'
};
})
Both the parent and child directives need their own scope definition, but it is unclear to me how to access to the radioSet's scope from within radioSetButton.
thanks for the help.
fiddle: http://jsfiddle.net/pmn4/XH5K2/2/
Transclusion
I guess i have to tell you that the transclusion you used in your directive does not work as you expect because in short: The transcluded directive doesn't inherit the scope you'd expect, actually it inherits the scope of the outer controller, but there are plenty of answers on this topic:
Access Parent Scope in Transcluded Directive
How to solve
To access a parents directive there are basically two ways:
1.) Require the parents directive's controller and create some API to do stuff on the parent
2.) Use the fifth parameter of the link function to access the transclude function, here you could change the injected scope and you could set it to the parents directive scope
Since the first solution is more intuitive i will go with this one:
On the radioSetdirective i set up a bidirectional databinding to the object in my Controller and i create a getter and setter method to interact with the value.
In the "child"'s directive i require the parent directive's controller which i get passed as the fourth parameter in my link function. I setup a click handler on the element to get the click and here i call the parents setter method with my value. To visualize the current selected object i add an ng-class directive which conditionally adds the active class.
Note: This way you can use the ngModel directive as well. It has an API to interact with the model.
The second solution uses the transclude function which you can use to pass in a scope. As i dont have time right now and as it adds more complexity i'd recommend using the first solution.
Recommendation
For your example transclusion might not be the right choice, use one directive and add the choices to the template or pass them into the directive. As i dont know what your intentions are i provided this solution. (I didn't know what the purpose of this name property is?)
The Code
Fiddle: http://jsfiddle.net/q3nUk/
Boilerplate:
var app = angular.module('myApp', []);
app.controller('MainController', function($scope) {
$scope.object = {
'property' : 'public'
};
});
The Directives:
app.directive('radioSet', function() {
return {
scope : {
radioValue : '='
},
restrict : 'E',
transclude : true,
replace : true,
template : '<div class="radioSet" ng-transclude></div>',
controller : function($scope) {
this.getRadioValue = function() {
return $scope.radioValue;
}
this.setRadioValue = function(val) {
$scope.$apply(function() {
$scope.radioValue = val
});
}
}
};
});
app.directive('radioSetButton', function() {
return {
restrict : 'E',
transclude : true,
replace : true,
scope : true,
template : '<div class="radioSetButton" ng-class="{active:isActive()}" ng-transclude></div>',
require : '^radioSet',
link : function(scope, elem, attrs, radioSetController, transclude) {
scope.isActive = function() {
return attrs.buttonValue === radioSetController.getRadioValue();
};
elem.on('click', function() {
radioSetController.setRadioValue(attrs.buttonValue);
});
}
};
});
The HTML:
<html>
<body ng-app="myApp">
<div ng-controller="MainController">
<p>{{ object.property }}</p>
<radio-set radio-value="object.property">
<radio-set-button button-value="public">Public</radio-set-button>
<radio-set-button button-value="private">Private</radio-set-button>
<radio-set-button button-value="protected">Protected</radio-set-button>
</radio-set>
</div>
</body>
</html>
CSS:
.radioSetButton {
display : block;
padding : 10px;
border : 1px solid black;
float : left;
}
.radioSetButton:hover {
cursor : pointer;
}
.radioSetButton.active {
background-color : grey;
}
I'm creating a reusable component/widget as a directive using a template and isolated scope. I'd like to be able to also send a callback into the directive and call it in the widget. Is this possible?
Something like...
mainView template:
<my-widget callback="someFunction"></my-widget>
directive:
return {
restrict: 'E',
scope: {
callback: '='
},
templateUrl: '/partials/widget.html',
}
And the template:
<input type="text" ng-change="callback()" />
So when the widget value is changed, it triggers the callback function that was passed in the main view
What you're looking for is &. Quoting the old angular docs: "& or &attr - provides a way to execute an expression in the context of the parent scope".
Try this as your directive code:
return {
restrict: 'E',
scope: {
callback: '&'
},
templateUrl: '/partials/widget.html',
}
You can also $watch for it in the link or controller.
.directive('myDirective', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) { {
scope.$watch(attrs.myDirective, function(value){ ... });
}
...
Turns out I needed to do as suggested and use an & rather than an =, but also this still wouldn't work until I added ng-model as well to my input:
<input type="text" ng-model="myValue" ng-change="callback()" />