I need to require a property from a controller from another directive in my main directive, which is easy:
return {
...
require['myOtherController'],
link: function(scope,element,attrs,controller){
scope.propFromOtherCtrl = controller[0].propFromOtherCtrl;
}
But: the controller of my directive gets loaded first.
So at first, propFromOtherCtrl is undefined in the controller until the link function got executed.
Right now, i am $scope.$watching the property in my controller until it is defined and then kick off the initialization of my controller manually.
//directive controller
$scope.$watch(function(){return $scope.propFromOtherCtrl; },function(n,o){
if(n !== o){
// init
}
});
But that seems rather hacky. Is there a more elegant way of doing this?
I can not share the scope between the two directives directly because of the architecture of my app.
If the architecture is the only reason you cannot share the scope between two controllers, you can always use $controller to inherit one controller from the other, then you would have access to its scope regardless of the location in the html dom:
$controller('myOtherController', { $scope: $scope });
Another alternative is inserting the html part that triggers the directive into an ng-if that doesn't get initialized until the other controller is ready:
ng-if="propFromOtherCtrlLoaded"
Finally, in case neither of these suit you, using $watch is not that bad, except a small addition, stopping to listen to the changes, would make it more efficient:
var unreg = $scope.$watch(function(){return $scope.propFromOtherCtrl; },function(n,o){
if(n !== o){
// init
unreg();
}
});
Related
I have an angular directive, which can take a parent controller function as attribute. During DOM manipulation, this angular directive is cloned, compiled and placed into the beginning part of the DOM. As a result, the passed in parent controller function does not work anymore, since there is no parent controller anymore. How could I solve this? Could I pass the parent controller function from the original to the clone when I'm compiling it? I have tried this, but it is not working:
$timeout(function () {
$scope.test ="passed content";
var compiledClonedDirective = $compile(clonedDirective)($scope);
divInTheBeginningPartOfTheDOM.prepend(clonedDirective);});
function getBack() {
console.log($scope.test);
}
This getBack() function is used in the cloned directive. When it is activated, it logs undefined. Any hints?
One solution was to copy the parent controller function to rootScope, and then call it from the rootScope in the cloned and replaced directive (directive's controller). But I would like to avoid using rootScope if possible.
I am creating reusable UI components with AngularJS directives. I would like to have a controller that contains my business logic with the nested components (directives). I want the directives to be able to manipulate a single property on the controller scope. The directives need to have an isolate scope because I might use the same directive more than once, and each instance needs to be bound to a particular controller scope property.
So far, the only way I can apply changes back to the controller's scope is to call scope.$apply() from the directive. But this breaks when I'm inside of an ng-click callback because of rootScope:inprog (scope operation in progress) errors.
So my question: What is the best way to make my controller aware when a child directive has updated a value on the controller's scope?
I've considered having a function on the controller that the directive could call to make an update, but that seems heavy to me.
Here is my code that breaks on an ng-click callback. Keep in mind that I don't just want to solve the ng-click issue. I want the best overall solution to apply reusable directives to modify a parent scope/model.
html
<div ng-controller="myCtrl">
<my-directive value="val1"></my-directive>
</div>
controller
...
.controller('myCtrl', ['$scope', function ($scope) {
$scope.val1 = 'something';
}});
directive
...
.directive('myDirective', [function () {
return {
link: function(scope) {
scope.buttonClick = function () {
var val = 'new value';
scope.value = val;
scope.$apply();
};
},
scope: {
value: '='
},
template: '<button ng-click="buttonClick()"></button>'
};
}]);
The purpose of two-way data binding in directives is exactly what you're asking about -- to "[allow] directives to modify a parent scope/model."
First, double-check that you have set up two-way data binding correctly on the directive attribute which exposes the variable you want to share between scopes. In the controller, you can use $watch to detect updates if you need to do something when the value changes. In addition, you have the option of adding an event-handler attribute to the directive. This allows the directive to call a function when something happens. Here's an example:
<div ng-controller="myCtrl">
<my-directive value="val1" on-val-change="myFunc"> <!-- Added on-change binding -->
<button ng-click="buttonClick()"></button>
</my-directive>
</div>
I think your question about $scope.apply is a red herring. I'm not sure what problem it was solving for you as you evolved this demo and question, but that's not what it's for, and FWIW your example works for me without it.
You're not supposed to have to worry about this issue ("make controller aware ... that [something] modified a value on a scope"); Angular's data binding takes care of that automatically.
It is a little complicated here because with the directive, there are multiple scopes to worry about. The outer scope belongs to the <div ng-controller=myCtrl>, and that scope has a .val property, and there's an inner scope created by the <my-directive> which also has a .val property, and the buttonClick handler inside myDirective modifies the inner one. But you declared myDirective's scope with value: '=' which sets up bidirectional syncing of that property value between the inner and outer scope.
So it should work automatically, and in the plunker I created from your question code, it does work automatically.
So where does scope.$apply come in? It's explicitly for triggering a digest cycle when Angular doesn't know it needs to. (And if you use it when Angular did know it needed a digest cycle already, you get a nested digest cycle and the "inprog" error you noticed.) Here's the doc link, from which I quote "$apply() is used to execute an expression in angular from outside of the angular framework". You need to use it, for example, when responding to an event handler set up with non-Angular methods -- direct DOM event bindings, jQuery, socket.io, etc. If you're using these mechanisms in an Angular app it's often best to wrap them in a directive or service that handles the Angular-to-non-Angular interface so the rest of your app doesn't have to worry about it.
(scope.$apply is actually a wrapper around scope.$digest that also manages exception handling. This isn't very clear from the docs. I find it easier to understand the name/behavior of $digest, and then consider $apply to be "the friendlier version of $digest that I'm actually supposed to use".)
One final note on $apply; it takes a function callback argument and you're supposed to do the work inside this callback. If you do some work and then call $apply with no arguments afterwards, it works, but at that point it's the same as $digest. So if you did need to use $apply here, it should look more like:
scope.buttonClick = function() {
scope.$apply(function() {
scope.value = newValue;
});
});
I'm trying to come up with some code which allows me to run a function in the controller but only once the whole dom is setup and ready (including the directives link function run etc.).
I'm currently communicating between ctrl/service and the directive via $rootScope broadcasts. The first broadcast at the time of the controller loading is not being picked up by the directive. The reason is of course that the controller loads before the directive link function runs. I've read a few similar questions on SO where people recommended on using $timeout for these calls. This unfortunately doesn't always work and I don't want to clutter my ctrl/services with lots of $timeout calls. Therefore I'm looking for another solution to my problem.
Communication pattern is as follows:
1.) Controller tells Service to prepare some data (via function call in service)
2.) Service tells directive to display data (via broadcast)
3.) Directive displays data ...or not in my case :(
EDIT:
As timing is essential in my app, I'm basically looking for a way to initiate a function in the controller as soon as all angular components have finished loading. That function in the controller will display content by assigning a value to a scope variable. At the same time it will start taking the time. I can of course only start doing that once the directives are loaded, otherwise the tmining is wrong or the directive is not yet ready to display content etc.
I've read through a blog post by Ben Nadel, which basically shows how directives are loaded. I was hoping to setup an outer directive which loads last so I can trigger the finished loading from there. Unfortunately that doesn't work as soon as any of the inner directives use a templateUrl.
http://www.bennadel.com/blog/2603-directive-controller-and-link-timing-in-angularjs.htm
Using $timeout would be terrible. Don't do that. You can't define how long a server call is going to take.
I would recommend using this pattern:
Have the controller use a service to load some data, and have the
promise in the controller assign the return data to a scope variable.
Pass that scope variable into your directive.
Setup a watch in the directive link function, when it loads it will go from undefined to desired value. Done!
// in your controller
YourService.all().then(function(data) {
$scope.data = data;
});
// in your view
<some-directive your-data="data"></some-directive>
// in your directive
angular.module('blah.directives').directive('someDirective', function() {
return {
scope: {
yourData: '='
},
link: function(scope, element, attrs) {
var watcher = scope.$watch('yourData', function() {
if(scope.yourData === undefined) return;
// at this point your data will be loaded, do work
// optionally kill watcher at this point if it's not going to update again
watcher();
});
}
}
});
I have to access variable defined in directive and access it in the controller using angularjs
directive :
app.directive('htmlData', function ($compile) {
return {
link: function($scope, element, attrs) {
$(element).on('click', function() {
$scope.html = $compile(element)($scope).html();
});
return $scope.html;
}
};
});
and use $scope.html in controller.
Since you are not creating an isolate scope (or a new scope) in your directive, the directive and the controller associated with the HTML where the directive is used are both using/sharing the same scope. $scope in the linking function and the $scope injected into the controller are the same. If you add a property to the scope in your linking function, the controller will be able to see it, and vice versa.
As you set the variable in the $scope, all you got to do is to bind to it normally. In your case, osmehting like:
<div html-data>{{html}}</div>
Maybe you're not seeing the update because it lacks a $scope.$apply().
Anyway, let me say that I see two problems on you code. First of, you could use ng-click directive to attach click events to your HTML.
Secondly, why would you recompile your HTML every time? There is almost no need for that. You may have a big problem, because after first compilation, your template is going to be lost, so recompiling will render it useless.
If you need to get the element, you can inject $element.
I've been using directives in AngularJS which build a HTML element with data fetched from the $scope of the controller. I have my controller set a $scope.ready=true variable when it has fetched it's JSON data from the server. This way the directive won't have to build the page over and over each time data is fetched.
Here is the order of events that occur:
The controller page loads a route and fires the controller function.
The page scans the directives and this particular directive is fired.
The directive builds the element and evaluates its expressions and goes forward, but when the directive link function is fired, it waits for the controller to be "ready".
When ready, an inner function is fired which then continues building the partial.
This works, but the code is messy. My question is that is there an easier way to do this? Can I abstract my code so that it gets fired after my controller fires an event? Instead of having to make this onReady inner method.
Here's what it looks like (its works, but it's messy hard to test):
angular.module('App', []).directive('someDirective',function() {
return {
link : function($scope, element, attrs) {
var onReady = function() {
//now lets do the normal stuff
};
var readyKey = 'ready';
if($scope[readyKey] != true) {
$scope.$watch(readyKey, function() {
if($scope[readyKey] == true) {
onReady();
}
});
}
else {
onReady();
}
}
};
});
You could use $scope.$emit in your controller and $rootScope.on("bradcastEventName",...); in your directive. The good point is that directive is decoupled and you can pull it out from project any time. You can reuse same pattern for all directives and other "running" components of your app to respond to this event.
There are two issues that I have discovered:
Having any XHR requests fire in the background will not prevent the template from loading.
There is a difference between having the data be applied to the $scope variable and actually having that data be applied to the bindings of the page (when the $scope is digested). So if you set your data to the scope and then fire an event to inform the partial that the scope is ready then this won't ensure that the data binding for that partial is ready.
So to get around this, then the best solution is to:
Use this plugin to manage the event handling between the controller and any directives below:
https://github.com/yearofmoo/AngularJS-Scope.onReady
Do not put any data into your directive template HTML that you expect the JavaScript function to pickup and use. So if for example you have a link that looks like this:
<a data-user-id="{{ user_id }}" href="/path/to/:user_id/page">My Page</a>
Then the problem is that the directive will have to prepare the :user_id value from the data-user-id attribute, get the href value and replace the data. This means that the directive will have to continuously check the data-user-id attribute to see if it's there (by checking the attrs hash every few moments).
Instead, place a different scope variable directly into the URL
My Page
And then place this in your directive:
$scope.whenReady(function() {
$scope.directive_user_id = $scope.user_id;
});