Find closest controller scope - angularjs

Is there a way to find the closest parent scope inside a directive that is a controller scope?
Use case.
The important part is at line 212 is the js file.
The template I render from the directive has scope expressions passed in the directive attribute and I need to link them to the controller scope.
Because the directive has an isolated scope, I link it to scope.$parent because in this case the parent is the controller.
But this might not always be the case, so how can I find the closest parent controller scope.
Can I loop trough the scope checking if it's a controller scope?
var template = angular.element(html);
var linkFn = $compile(template);
var child = linkFn(scope.$parent); //HERE IS THE PROBLEM
$(element).append(child);

You may try to use jqLite/jQuery methods from within your directive to traverse DOM tree up and locate the required parent (element.parent(), etc). Then you can get the scope object related to this parent by calling scope() method on it, which you should be able to pass as an argument to your compilation function.

It's not the cleanest approach, but you can always pass in an object to the directive's scope.
angular.app('myApp', [])
.controller('myCtrl', [function() {
var self = this;
self.directiveConfig = {
method1: function() { /* do something cool */ },
method2: function() { /* do something cool */ }
};
}])
.directive('myElem', [function() {
var myElem = {
restrict: 'AE',
controller: function() { /* some custom controller */ },
link: function(scope, element, attributes, ctrl) {
/**
* scope.config will now be tied to self.directiveConfig
* as defined in the controller above
*/
},
scope: {
config: '='
}
};
});
Then, assuming your controllerAs is set to ctrl, you could do.
<my-elem config="ctrl.directiveConfig"></my-elem>
A better solution would be to put any functions you need to use like that inside a service that can be reused.

See there are many ways . One of the way could be , you could check for an variable that is defined on that controller scope like this
if(angular.isDefined(scope.$parent.variableName))
{
// controller is defined
}
else
{
// controller is not defined
}
If this is defined then it is your controller , otherwise something else. Second way would be to check parent scope of the parent like this
if(angular.isDefined(scope.$parent.$parent)) {
//controller is defined
}
else
{
//controller is not defined
}
As every controller will have a parent as $rootScope or $scope of some other controller . That means if it is not a controller then this would result in undefined and will go else condition.

requiring ngController seems to be the best solution.
.directive("mobilitTree", function() {
return {
...
require: "ngController",
...
link: function(scope, element, attrs, ngCtrl) {
...
}
};
})
.controller("AppController", function($scope, ...) {
//Only variable applied on this are public
this.controllerOnSort = function(...) { ... };
});

Related

Can I have attributes in an Angular directive which uses parent scope?

I have a directive that uses the data from the parent scope. Now, I want to have an attribute in it, but still want to use the parent scope and not transfer all the other data in more attributes and duplicate objects.
var myDirective = function () {
return {
restrict: 'A',
templateUrl: '/Areas/GetAdvice/Templates/Directives/loan-comparison-table-directive.html?1',
//scope: - Don't add anything here to use the scope of the parent
// But how to use an attribute in this way?
controller: [
'$scope', function ($scope) {
// Some Code
}
}
So, how can I add an attribute (independent variable) in the directive, when having the parent scope inherited?
EDIT:
I want just a simple boolean property - something like isDescriptionVisible (true/false). The problem is that I use the same directive twice in my view, so I cannot have a variable in the parent scope, because it will affect both directives and I won't have control on them separately.
You need to create an isolated scope for the directive. This is the exact use case for it. Otherwise you need to create separate variables to track the booleans in the parent scope.
If you don't need to pass boolean value from the parent scope you can do the following:
var myDirective = function () {
return {
restrict: 'A',
templateUrl: '/Areas/GetAdvice/Templates/Directives/loan-comparison-table-directive.html?1',
scope: {}
controller: [
'$scope', function ($scope) {
$scope.isVisible = false;
}
}
If you need to pass it from the parent scope just simply replace
scope: {}
with
scope: {
isVisible: '='
}

What is the correct way to access controller that was required inside directive controller?

I have a directive with require property:
require: '^testBox'
now I want to get testBox controller inside controller of my directive. How should I do it?
I was trying to do so:
controller: function(){
this.testBox.user
}
but looks like it does not work.
It's clear for me how to get required controller inside link function. But is there a way to get it inside controller without using link?
Code on plunker.
This is still an open issue. So at the moment you can not just inject the required controller into your directive controller. I have updated your Plunker. It's definitely a bit hacky but the problem is; You cannot expose the TextBoxCtrl to the UserCtrl in either the pre or post link function because the controller gets executed first. So my idea is to use a watcher to observe a scope varibale called textBox. Once the value is defined I declare a variable on the UserCtrl and remove the watcher. Now you can simply use it in your template like so:
{{ user.textBox.name }}
Here is the code for the link function and the controller of the user directive:
link: function($scope, $element, $attrs, ctrl) {
$scope.textBox = ctrl
},
controller: function($scope) {
var vm = this;
var watcher = $scope.$watch('textBox', function(newVal) {
if(newVal) {
vm.textBox = newVal;
watcher();
}
});
}
However, you can also go with a link function instead. The required controller will be injected as the fourth parameter.
When you use controllerAs it's just added as a property of the underlying scope object (using the name you've defined). Knowing this, you can attach the parent controller instance as a property of your child controller instance as follows:
function exampleDirective() {
return {
require: '^testBox',
link: function (scope, element, attrs, testBox) {
scope.example.testBox = testBox;
},
controllerAs: 'example',
controller: function() {
// silly example, but you get the idea!
this.user = this.testBox.user;
}
}
};

AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding

I have a directive with isolated scope with a value with two way binding to the parent scope. I am calling a method that changes the value in the parent scope, but the change is not applied in my directive.(two way binding is not triggered). This question is very similar:
AngularJS: Parent scope not updated in directive (with isolated scope) two way binding
but I am not changing the value from the directive, but changing it only in the parent scope. I read the solution and in point five it is said:
The watch() created by the isolated scope checks whether it's value for the bi-directional binding is in sync with the parent's value. If it isn't the parent's value is copied to the isolated scope.
Which means that when my parent value is changed to 2, a watch is triggered. It checks whether parent value and directive value are the same - and if not it copies to directive value. Ok but my directive value is still 1 ... What am I missing ?
html :
<div data-ng-app="testApp">
<div data-ng-controller="testCtrl">
<strong>{{myValue}}</strong>
<span data-test-directive data-parent-item="myValue"
data-parent-update="update()"></span>
</div>
</div>
js:
var testApp = angular.module('testApp', []);
testApp.directive('testDirective', function ($timeout) {
return {
scope: {
key: '=parentItem',
parentUpdate: '&'
},
replace: true,
template:
'<button data-ng-click="lock()">Lock</button>' +
'</div>',
controller: function ($scope, $element, $attrs) {
$scope.lock = function () {
console.log('directive :', $scope.key);
$scope.parentUpdate();
//$timeout($scope.parentUpdate); // would work.
// expecting the value to be 2, but it is 1
console.log('directive :', $scope.key);
};
}
};
});
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
console.log('CTRL:', $scope.myValue);
};
});
Fiddle
Use $scope.$apply() after changing the $scope.myValue in your controller like:
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
$scope.$apply();
console.log('CTRL:', $scope.myValue);
};
});
The answer Use $scope.$apply() is completely incorrect.
The only way that I have seen to update the scope in your directive is like this:
angular.module('app')
.directive('navbar', function () {
return {
templateUrl: '../../views/navbar.html',
replace: 'true',
restrict: 'E',
scope: {
email: '='
},
link: function (scope, elem, attrs) {
scope.$on('userLoggedIn', function (event, args) {
scope.email = args.email;
});
scope.$on('userLoggedOut', function (event) {
scope.email = false;
console.log(newValue);
});
}
}
});
and emitting your events in the controller like this:
$rootScope.$broadcast('userLoggedIn', user);
This feels like such a hack I hope the angular gurus can see this post and provide a better answer, but as it is the accepted answer does not even work and just gives the error $digest already in progress
Using $apply() like the accepted answer can cause all sorts of bugs and potential performance hits as well. Settings up broadcasts and whatnot is a lot of work for this. I found the simple workaround just to use the standard timeout to trigger the event in the next cycle (which will be immediately because of the timeout). Surround the parentUpdate() call like so:
$timeout(function() {
$scope.parentUpdate();
});
Works perfectly for me. (note: 0ms is the default timeout time when not specified)
One thing most people forget is that you can't just declare an isolated scope with the object notation and expect parent scope properties to be bound. These bindings only work if attributes have been declared through which the binding 'magic' works. See for more information:
https://umur.io/angularjs-directives-using-isolated-scope-with-attributes/
Instead of using $scope.$apply(), try using $scope.$applyAsync();

Controller Required By Directive Can't Be Found

I have a directive that I'd like another directive to be able to call in to. I have been trying to use directive controllers to try to achieve this.
Directive one would be sitting on the same page as directive two, and directive one would call methods exposed by directive two's controller:
Directive 1:
'use strict';
angular.module('angularTestApp')
.directive('fileLibrary', function () {
return {
templateUrl: 'views/manage/file_library/file-library.html',
require: 'videoClipDetails',
restrict: 'AE',
link: function postLink(scope, element, attrs, videClipDetailsCtrl) {
scope.doSomethingInVideoClipDirective = function() {
videClipDetailsCtrl.doSomething();
}
}
};
});
Directive Two:
'use strict';
angular.module('angularTestApp')
.directive('videoClipDetails', function () {
return {
templateUrl: 'views/video_clip/video-clip-details.html',
restrict: 'AE',
controller: function($scope, $element) {
this.doSomething = function() {
console.log('I did something');
}
},
link: function postLink(scope, element, attrs) {
console.log('videoClipDetails directive');
//start the element out as hidden
}
};
});
File where the two are used and set up as siblings:
<div>
<div video-clip-details></div>
<!-- main component for the file library -->
<div file-library></div>
</div>
I know reading documentation I picked up that the controllers can be shared when the directives are on the same element, which makes me think I might be looking at this problem the wrong way. Can anyone put me on the right track?
From the angular.js documentation on directives
When a directive uses require, $compile will throw an error unless the specified controller is found. The ^ prefix means that this directive searches for the controller on its parents (without the ^ prefix, the directive would look for the controller on just its own element).
So basically what you are trying to do with having siblings directly communicate is not possible. I had run into this same issue but I did not want to use a service for communication. What I came up with was a method of using a parent directive to manage communication between its children, which are siblings. I posted the example on github.
What happens is that both children require the parent (require: '^parentDirective') and their own controller, both of which are passed into the link function. From there each child can get a reference to the parent controller and all of its public methods, as an API of sorts.
Below is one of the children itemEditor
function itemEditor() {
var directive = {
link: link,
scope: {},
controller: controller,
controllerAs: 'vm',
require: ['^itemManager', 'itemEditor'],
templateUrl: 'app/scripts/itemManager/itemManager.directives.itemEditor.html',
restrict: 'A'
};
return directive;
function link(scope, element, attrs, controllers) {
var itemManagerController = controllers[0];
var itemEditorController = controllers[1];
itemEditorController.itemManager = itemManagerController;
itemEditorController.initialize();
}
function controller() {
var vm = this;
// Properties
vm.itemManager = {};
vm.item = { id: -1, name: "", size: "" };
// Methods
vm.initialize = initialize;
vm.updateItem = updateItem;
vm.editItem = editItem;
// Functions
function initialize() {
vm.itemManager.respondToEditsWith(vm.editItem);
}
function updateItem() {
vm.itemManager.updateItem(vm.item);
vm.item = {};
}
function editItem(item) {
vm.item.id = item.id;
vm.item.name = item.name;
vm.item.size = item.size;
}
}
}
Note how the values passed into the require array are the parent directive's name and the current directive's name. These are then both accessible in the link function via the controllers parameter. Assign the parent directive's controller as a property of the current child's and then it can be accessed within the child's controller functions via that property.
Also notice how in the child directive's link function I call an initialize function from the child's controller. This is where part of the communication lines are established.
I'm basically saying, anytime you (parent directive) receive a request to edit an item, use this method of mine named editItem which takes an item as a parameter.
Here is the parent directive
function itemManager() {
var directive = {
link: link,
controller: controller,
controllerAs: 'vm',
templateUrl: 'app/scripts/itemManager/itemManager.directives.itemManager.html',
restrict: 'A'
};
return directive;
function link(scope, element, attrs, controller) {
}
function controller() {
var vm = this;
vm.updateMethod = null;
vm.editMethod = null;
vm.updateItem = updateItem;
vm.editItem = editItem;
vm.respondToUpdatesWith = respondToUpdatesWith;
vm.respondToEditsWith = respondToEditsWith;
function updateItem(item) {
vm.updateMethod(item);
}
function editItem(item) {
vm.editMethod(item);
}
function respondToUpdatesWith(method) {
vm.updateMethod = method;
}
function respondToEditsWith(method) {
vm.editMethod = method;
}
}
}
Here in the parent you can see that the respondToEditsWith takes a method as a parameter and assigns that value to its editMethod property. This property is called whenever the controller's editItem method is called and the item object is passed on to it, thus calling the child directive's editItem method. Likewise, saving data works the same way in reverse.
Update: By the way, here is a blog post on coderwall.com where I got the original idea with good examples of require and controller options in directives. That said, his recommended syntax for the last example in that post did not work for me, which is why I created the example I reference above.
There is no real way with require to communicate between sibling elements in the way you are trying to do here. The require works the way you have set up if the two directives are on the same element.
You can't do this however because both of your directives have an associated templateUrl that you want to use, and you can only have one per element.
You could structure your html slightly differently to allow this to work though. You basically need to put one directive inside the other (transcluded) and use require: '^videoClipDetails'. Meaning that it will look to the parent to find it.
I've set up a fiddle to demonstrate this: http://jsfiddle.net/WwCvQ/1/
This is the code that makes the parent thing work:
// In videoClipDetails
template: '<div>clip details<div ng-transclude></div></div>',
transclude: 'true',
...
// in markup
<div video-clip-details>
<div file-library></div>
</div>
// in fileLibrary
require: '^videoClipDetails',
let me know if you have any questions!

Using functions from directive controller within link function of same directive

Perhaps I have a fundamental misunderstanding of how directive controllers work, from what I understand they are used as a sort of API to be exposed to other directives & controllers. I am trying to get the controller and link function to communicate internally.
For example I would like to be able to set a variable via the controller function and then use it in the link function:
var app = angular.module('test-app', []);
app.directive('coolDirective', function () {
return {
controller: function () {
this.sayHi = function($scope, $element, $attrs) {
$scope.myVar = "yo"
}
},
link: function(scope, el, attrs) {
console.log(scope.myVar);
}
}
});
How can I access myVar or sayHi within the link function? Or have I just missed the point completely?
Both controller's $scope (defined in the controller, not in the sayHi function) and link scope are the same. Setting something in the controller will be usable from the link or viceversa.
The problem you have is that sayHi is a function that is never fired so myVar is never set.
Since sayHi is not in the scope, you need a reference to the controller and to do it, you can add a fourth parameter like this:
link: function(scope, element, attr, ctrl) {}
Then you could do a ctrl.sayHi() (But again, those params of sayHi belongs to the controller function.)
If you ever need to require another controller and still wanting to use its own directive, then you will need to require it too. So if this coolDirective needs to access to the controller of notCoolAtAll you could do:
require: ['coolDirective', 'notCoolAtAll']
That will do the trick. The link function will receive then an array of controllers as the fourth param and in this case the first element will be coolDirective ctrl and the second one the notCoolAtAll one.
Here is a little example: http://plnkr.co/edit/JXahWE43H3biouygmnOX?p=preview
Rewriting your code above, it would look something like this:
var app = angular.module('test-app', []);
app.directive('coolDirective', function() {
return {
controller: function($scope) {
// bind myVar property to scope
$scope.myVar = 'yo';
// bind sayHi method to scope
$scope.sayHi = sayHi;
// abstracting out the sayHi function
function sayHi() {
console.log($scope.myVar);
}
},
link: function(scope, el, attrs) {
// execute the sayHi function from link
scope.sayHi(); // "yo" in console
}
};
});
Good Luck.

Resources