I have a custom directive that creates a field as part of a larger form. Vastly simplified, it looks like this:
.directive('entityField', function () {
return {
restrict: 'E',
scope: {
field: '=',
attributes: '=',
editMode: '='
},
templateUrl: '<input ng-blur="blur()" type="text"/>',
link: function (scope, element, attrs) {
scope.blur = function() {
// call out to event listeners that blur event happened
}
}
};
});
My goal is to have the directive fire an event on blur of its input element so that the surrounding controller can handle the blur event and perform some complex validation.
So, how do I raise an event in a directive such that the surrounding controller can listen for it?
If you weren't using isolated scope, you could use $emit to send the event up to the controller's scope. However, since you are using isolated scope, your directive does not inherit from the parent controller's scope, which makes this method impossible.
Instead, you can use the $broadcast method from $rootScope to send the event down the scope prototype chain to the controller:
Directive:
.directive('entityField', function ($rootScope) {
return {
restrict: 'E',
scope: {
field: '=',
attributes: '=',
editMode: '='
},
templateUrl: '<input ng-blur="blur()" type="text"/>',
link: function (scope, element, attrs) {
scope.blur = function() {
$rootScope.$broadcast('blur');
}
}
};
});
Then use $on to catch it in your controller:
Controller:
.controller('MyController', function($scope){
$scope.$on('blur', function(){
//react to event
})
});
Hopefully this is a sufficient answer to the narrowest of interpretation of your question. I want to also mention that it is often better to use the prototypical nature of the scope and/or services for cross directive/controller communication. My answer to another question today helps to cover this topic: How to call controller function from directive?
Related
I have a custom directive as defined below (if you need controller let me know)
module.exports = angular.module('aah-yes-no-directive-module', [
require('./yes-no.controller').name,
]).directive('aahYesNo', [function YesNoDirective() {
return {
restrict: 'A',
templateUrl: 'partials/yes-no.template.html',
require: ['aahYesNo', 'ngModel'],
controller: 'aahYesNoController',
controllerAs: 'ctrl',
link: (scope, el, attrs, ctrls) => {
let ctrl = ctrls[0],
ngModelCtrl = ctrls[1];
ctrl.init(ngModelCtrl, attrs.onValue, attrs.offValue);
angular.element(el).children().addClass(attrs.size || '');
},
scope: {}
};
}]);
Why question is, why does it not update the $modelValue when it changes in the parent controller, or when $viewValue changes?
it is always undefined?
if i take out scope: {} it works, but seems to override the entire parent scope (?!)
Because scope: {} creates an isolated scope,
You can tell directive to inherit parent scope by setting scope: true or by not defining scope property.
Check this article for more details about scope in directives
I have it working, I'm not sure exactly what I changed to fix it, but unfortunately wasn't either of the suggested reasons.
It was not working properly with scope: false, I suspect I had a typo somewhere and have ended up starting again and voila....thanks for the help though!
I have this code:
.directive('hostelGridBed', function($rootScope){
return {
restrict: 'E',
scope: {
config: '=',
range: '=',
days: '='
},
link: function(scope, element){
},
controller: ['$scope', function($scope){
$scope.innerStay = '';
function switchStay(data){
$rootScope.$broadcast('SwitchStay', {data: data.config});
};
$scope.onDropComplete = function(data,evt){
//$rootScope.$broadcast
switchStay(data);
console.log('Drop completo bed!');
}
}],
templateUrl: './js/templates/hostelgridbed.html'
}
})
.directive('hostelGridDay', function(StaysFactory){
return {
restrict: 'E',
scope: {
config: '=',
date: '='
},
link: function(scope,element){
},
controller: ['$scope','$rootScope', function($scope,$rootScope){
$scope.switchStay = function(evt, data){
console.log(data);
// Here. How can I access this controller's $scope
}
$scope.$on('SwitchStay', $scope.switchStay);
}],
templateUrl: './js/templates/hostelgridday.html'
}
})
I get data from $broadcast ok, it's all good. Except that I need to access the directive's $scope from within $scope.switchStay function.
Is there a way to accomplish that?
Actually when you $broadcast or $emit the event from the directive, you could send also the directive's scope as parameter. It should work, but this is actually awful and a really bad practice.
What you have to do in these cases is to add a callback parameter to the directive (&) and provide the switchStay function as callback, and simply trigger it from the directive, passing the parameters that you need to access inside the function.
I hope it makes sense.
I am creating drag and drop functionality by creating a <dragItem> directive and a <droptTraget> directive, but I don't understand yet how to work with the inner and out scope in this way.
Here are my directives. The events triggers the functions properly, I just want the on dragstart event to store a value of the drag element and the drop event to trigger the function testAddSet() which adds the drag value to my model.
drag
angular.module('app.directives.dragItem', [])
.directive('dragItem', function(){
return { // this object is the directive
restrict: 'E',
scope: {
excercise: '='
},
templateUrl: "templates/dragTile.html",
link: function(scope, element, attrs){
element.on('dragstart', function (event) {
var dataVar = element.innerText;
// It's here that I want to send a dataVar to the $scope
});
}
};
});
drop
angular.module('app.directives.dropTarget', [])
.directive('dropTarget', function(){
return { // this object is the directive
restrict: 'E',
scope: {
day: '='
},
templateUrl: "templates/calDay.html",
link: function(scope, element, attrs){
element.on('drop', function (event) {
event.preventDefault();
// It's here that I'd like to take the value from the drag item and update my model
testAddSet() // doesn't work
$parent.testAddSet() // doesn't work
});
element.on('dragover', function (event) {
event.preventDefault();
});
}
};
});
Since you are using isolate scope, you need to define an attribute for the function binding.
angular.module('app.directives.dropTarget', [])
.directive('dropTarget', function(){
return { // this object is the directive
restrict: 'E',
scope: {
day: '=',
//Add binding here
testAddSet: '&'
},
templateUrl: "templates/calDay.html",
link: function(scope, element, attrs){
element.on('drop', function (event) {
event.preventDefault();
//Invoke the function here
scope.testAddSet({arg: value, $event: event});
});
element.on('dragover', function (event) {
event.preventDefault();
});
}
};
});
In your template, connect the function using the directive attribute.
<drop-target test-add-set="fn(arg, $event)" day="x"></drop-target>
For more information on isolate scope binding, see AngularJS $compile Service API Reference - scope.
I recommend that the event object be exposed as $event since that is customary with AngularJS event directives.
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
I think the easiest way to get your cross-directive communication is to make a scope variable on the host page and then pass it double-bound ('=') to both directives. That way, they both have access to it as it changes.
I am building a huge form that calls various directives to build a complete form. The Main Page calling the Form Builder passes the ng-model data like this:
<div form-builder form-data=“formData”></div>
Then the Form Builder Page calls various child directive to build various sections of the Form:
FormBuilder.html:
<div form-fields></div>
<div photo-fields></div>
<div video-fields></div>
.. etc.. etc...
When using $scope in controller, I had no problem accessing the $scope in the child directives like this:
function formBuilder() {
return {
restrict: 'A',
replace: true,
scope: {
formData: '='
},
templateUrl: 'FormBuilder.html',
controller: function($scope) {
$scope.formSubmit = function() {
// Submits the formData.formFields and formData.photoFields
// to the server
// The data for these objects are created through
// the child directives below
}
}
}
}
function formFields() {
return {
restrict: 'A',
replace: true,
templateUrl: 'FormFields.html',
controller: function($scope) {
console.log($scope.formData.formFields);
}
}
}
function photoFields() {
return {
restrict: 'A',
replace: true,
templateUrl: 'PhotoFields.html',
controller: function($scope) {
console.log($scope.formData.photoFields);
}
}
}
... etc..
But ever since I got rid of the $scope and started using ControllerAs, I am having all sorts of trouble accessing 2 way binding with the Parent - Child Controllers.
function formBuilder() {
return {
restrict: 'A',
replace: true,
scope: {
formData: '='
},
templateUrl: 'FormBuilder.html',
controller: function() {
var vm = this;
console.log(vm.formData); // Its fine here
vm.formSubmit = function() {
// I cannot change formData.formFields and formData.photoFields
// from Child Directive "Controllers"
}
},
controllerAs: ‘fb’,
bindToController: true
}
}
function formFields() {
return {
restrict: 'A',
replace: true,
templateUrl: 'FormFields.html',
controller: function() {
var vm = this;
console.log(vm.formData.formFields);
// No way to access 2 way binding with this Object!!!
}
}
}
function photoFields() {
return {
restrict: 'A',
replace: true,
templateUrl: 'PhotoFields.html',
controller: function() {
var vm = this;
console.log(vm.formData.photoFields);
// No way to access 2 way binding with this Object!!!
}
}
}
Whatever I try, I am reaching a road block. Things I have tried are:
Isolated Scopes: I tried passing formData.formFields and
formData.photoFields as isolated scopes to the child directive,
but I then end up getting the $compile: MultiDir error due to
nested isolated scopes so it is not possible.
If I don’t have
individual directives for each form section and have all of them in
1 directive under formBuilder directive, then it becomes a
humungous directive. The above is just a sketch but each child
directive builds 1 big form put together in the end. So merging them
together is really the last resort since it does become hard to
maintain and unreadable.
I don’t think there is a way to access
Parent directive’s ControllerAs from Child Directive's Controller any other way
from what I have seen so far.
If I use the parent’s ControllerAs in
the child directive template’s ng-model like <input type=“text” ng-model=“fb.formData.formFields.text" />, that works fine, but I
need to access the same from the Child directive’s controller for
some processing which I am unable to do.
If I get rid of the
controllerAs and use the $scope again, it works like before but I am
trying to get rid of the $scope altogether to prepare myself for
future Angular changes.
Since it is an advanced form, I need to have separate directive to handle various form sections and since nested isolated scopes are not allowed since Angular 1.2, it is making it ever harder especially when trying to get rid of $scope using ControllerAs.
Can someone guide me what are my options here please? I thank you for reading my long post.
Basically you need to use require option of directive (require option is used for communicate directive with directive). Which will give access to its parent controller by just mentioning require option in child directive. Also you need to use bindToController: true which will basically add isolated scope data to the directive controller.
Code
function formBuilder() {
return {
restrict: 'A',
replace: true,
bindToController: true,
scope: {
formData: '='
},
templateUrl: 'FormBuilder.html',
controller: function($scope) {
$scope.formSubmit = function() {
// Submits the formData.formFields and formData.photoFields
// to the server
// The data for these objects are created through
// the child directives below
}
}
}
}
Then you need to add require option to child directives. Basically the require option will have formBuilder directive with ^(indicates formBuilder will be there in parent element) like require: '^formBuilder',.
By writing a require options you can get the controller of that directive in link function 4th parameter.
Code
function formFields() {
return {
restrict: 'A',
replace: true,
require: '^formBuilder',
templateUrl: 'FormFields.html',
//4th parameter is formBuilder controller
link: function(scope, element, attrs, formBuilderCtrl){
scope.formBuilderCtrl = formBuilderCtrl;
},
controller: function($scope, $timeout) {
var vm = this;
//getting the `formData` from `formBuilderCtrl` object
//added timeout here to run code after link function, means after next digest
$timeout(function(){
console.log($scope.formBuilderCtrl.formData.formFields);
})
}
}
}
function photoFields() {
return {
restrict: 'A',
replace: true,
require: '^formBuilder',
templateUrl: 'PhotoFields.html',
//4th parameter is formBuilder controller
link: function(scope, element, attrs, formBuilderCtrl){
scope.formBuilderCtrl = formBuilderCtrl;
},
controller: function($scope, $timeout) {
var vm = this;
console.log(vm.formData.photoFields);
//to run the code in next digest cycle, after link function gets called.
$timeout(function(){
console.log($scope.formBuilderCtrl.formData.formFields);
})
}
}
}
Edit
One problem with above solution is, in order to get access to the controller of parent directive in directive controller it self, I've did some tricky. 1st include the the formBuilderCtrl to the scope variable from link function 4th parameter. Then only you can get access to that controller using $scope(which you don't want there). Regarding same issue logged in Github with open status, you could check that out here.
I have this directive which tries to watch for changes in a json object on scope. The JSON object is retrieved using a restangular based service, but somehow the $watch seems to be executed only once, logging 'undefined'.
The directive is used in the index.html of the app, so I suspect this has to do with the controller only working for the specific view or form...is there a way to get the directive to see those changes?
update: figured I could just call the TextsService from the directive itself, seems like a good solution to the problem. If anyone has better suggestions I'd welcome them still though.
service:
angular.module('main').service('TextsService', function(Restangular) {
this.getTexts = function(jsonRequestBase, jsonRequest, callback) {
Restangular.one(jsonRequestBase, jsonRequest).get().then(
function(texts) {
callback(texts);
}
);
};
});
call in controller:
TexstService.getTexts("content", "file.json", function (texts) {
$scope.mytest = texts;
});
directive:
app.directive('myDirective',
function() {
return {
restrict: 'A',
templateUrl:'test.html',
transclude: true,
link: function(scope, elm, attrs) {
scope.$watch('mytest', function(){
console.log(scope.mytest);
}, true);
I figured I could just call the TextsService from the directive itself, seems like a good solution to the problem. If anyone has better suggestions I'd welcome them still though.
Create the variable with null before receving it. Maybe that works.
The problem is that your directive's scope does not know about the mytest variable. You need to define the binding when you define the directive:
app.directive('myDirective',
function() {
return {
scope: {
myDirective: '='
},
restrict: 'A',
templateUrl:'test.html',
transclude: true,
link: function(scope, elm, attrs) {
scope.$watch('myDirective', function(newVal){
console.log(newVal);
}, true);
};
}
);
This will get whatever variable is assigned to the directive attribute and watch its' value.
And your directive in the view should look like this to bind it to the 'mytest':
<div my-directive="mytest"></div>