Custom directive is overriding scope - angularjs

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!

Related

How to access the parent scope within a directive link

So on the angular documentation website, you can define Tobias and Jeff
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: 'my-dialog.html',
link: function (scope, element) {
scope.name = 'Jeff';
}
};
});
If you do The name is {{name}} it'll say
The name is Tobias
I'm trying to access Tobias in the link function. From inside the link function, how would I get the $scope.name value equal to Tobias?
Since the scope is isolated scope: {}, directive creates a new child scope. In this case the only way to directly access parent scope property is to use scope $parent, reference to parent scope object. For example:
link: function(scope, element) {
scope.name = 'Jeff';
scope.parentName = scope.$parent.name; // Tobias
}
However this is not ideal. This is why you may want to consider more flexible approach:
<my-dialog name="name"></my-dialog>
and define a scope configuration as:
scope: {
parentName: '=name'
}
You will have to use $parent property of the the scope:
scope.$parent.name = 'Jeff';
you can get it through $parent like this:
link: function (scope, element) {
scope.name = 'Jeff';
console.log(scope.name);
console.log(scope.$parent.name);
}
As you have used transclude:true, you can omit scope:{} if you do not have any local variables. Putting scope:{} does not make sense.
so the declaration would be like following
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
templateUrl: 'my-dialog.html',
link: function (scope, element) {
// scope.name = 'Jeff';
// if name is in your parent scope, you should be able to get it here
console.log(scope.name);
}
};
});
If you look at the template you will see ng-transclude directive has been used, this means where in template the parent scope's variables will be used there. Hope it makes sense.
I'm just wondering why would you want something like this.
This way you're creating a deppendency between the controller and the directive that shouldn't exist.
If you need input data on your directive, declare it explicitly.

Angular transcluded directive as sibling scope incompatible with child directive's require: "^parent" setting

I'm having an issue with the way AngularJS handles transcluded scopes for directives. It is known that a transclude's scope is a sibling of its directive's scope, not a child. Shown here
However, I have a situation with a child directive:
<div price-chart>
<div volume-indicator></div>
</div>
The priceChart directive has a transclude: true, template: "<div ng-transclude></div>" but the (transcluded) volumeIndicator requires the parent to be a priceChart, not the sibling.
function VolumeIndicatorDirective() {
return {
restrict: "EA",
controller: "VolumeIndicatorController",
require: ["^priceChart", "volumeIndicator"],
compile: function compile(element, attrs, transclude) {
return {
pre: function preLink($scope, element, attrs, controllers) {
var priceChart = controllers[0];
var volumeIndicator = controllers[1];
priceChart.addIndicator(volumeIndicator);
},
post: angular.noop
};
}
};
}
If Angular had a sibling selector for controllers that would solve the issue:
require: ["+priceChart", "volumeIndicator"],
However, this doesn't exist so what can I do?
As per comment from zeroflagL I tried element.parent().controller() to access the directive controller but it seems to get the nearest ng-controller specifically and skips over directive controllers.
According to the docs, require's '^' syntax will try to "locate the required controller by searching the element's parents". Scope prototypal inheritance doesn't play a role here, so your code should work as expected.
And indeed it does !

Adding a custom event to a directive

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?

$watch in a link function of my directive is not updated

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>

Access Parent Scope in Transcluded Directive

I would like to access a parent directive's scope, but I can't seem to get the right combination of settings. Is this possible and is it the right approach?
I really want to avoid putting something like SOME_CONST (which would help me make DOM updates through control flow) in MyCtrl
<div ng-controller="MyCtrl">
<parent>
<child></child>
</parent>
</div>
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.obj = {prop:'foo'};
}
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
link: function(scope, elem, attrs) {
scope.SOME_CONST = 'someConst';
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{SOME_CONST}}. I really don\'t want to put everything inside the MyCtrl',
}
});
Please see this fiddle
Thanks
With transclude: true and scope: true, the parent directive creates two new scopes:
Scope 004 is a result of scope: true, and scope 005 is a result of transclude: true. Since the child directive does not create a new scope, it uses transcluded scope 005. As you can see from the diagram there is no path from scope 005 to scope 004 (except via private property $$prevSibling, which goes in the opposite direction of $$nextSibling -- but don't use those.)
#joakimbl's solution is probably best here, although I think it is more common to define an API on the parent directive's controller, rather than defining properties on this:
controller: function($scope) {
$scope.SOME_CONST = 'someConst';
this.getConst = function() {
return $scope.SOME_CONST;
}
}
Then in the child directive:
link:function(scope,element,attrs,parentCtrl){
scope.SOME_CONST = parentCtrl.getConst();
},
This is how the tabs and pane directives work on Angular's home page ("Create Components" example).
Normally the way you access a parent scope variable in a directive is through bi-directional binding (scope:{model:'=model'} - see the angular guide on directives) in the directive configuration), but since you're using transclusion this is not so straight forward. If the child directive will always be a child of the parent directive you can however configure it to require the parent, and then get access to the parent controller in the child link function:
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
controller: function($scope) {
$scope.SOME_CONST = 'someConst';
this.SOME_CONST = $scope.SOME_CONST;
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require:'^parent',
scope:true,
link:function(scope,element,attrs,parentCtrl){
scope.SOME_CONST = parentCtrl.SOME_CONST;
},
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{SOME_CONST}}. I really don\'t want to put everything inside the MyCtrl',
}
});
See this update: http://jsfiddle.net/uN2uv/
I just had the same problem and finally solved it with the angular manual ;)
In short: you need to use a controller in your parent directive and require that controller in your child directive. This way you are able to get your parent properties.
See https://docs.angularjs.org/guide/directive
Chapter: Creating Directives that Communicate
I changed your fiddle to use a controller, now you can access your constant:
https://jsfiddle.net/bbrqdmt3/1/
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.obj = {prop:'foo'};
}
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
controller: function($scope) {
this.getConst= function() {
return 'someConst';
}
},
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require : '^parent',
link: function(scope, element, attrs, ctrl) {
scope.value= ctrl.getConst();
},
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{value}}. I really don\'t want to put everything inside the MyCtrl',
}
});
There's a transclude fn in the arguments of the link fn after the controller.
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div><h1>I'm a parent header.</h1></div>',
link: function (scope, el, attrs, ctrl, transclude) {
transclude(scope, function (clone, scope) {
element.append(clone); // <-- will transclude it's own scope
});
},
controller: function($scope) {
$scope.parent = {
binding: 'I\'m a parent binding'
};
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require:'^parent',
scope:true,
link:function(scope,element,attrs,parentCtrl){
},
template: '<div>{{parent.binding}}</div>' // <-- has access to parent's scope
}
});

Resources