Managing communication between independent AngularJS directives independently - angularjs

This is more of an organizational approach to solving this issue rather than a direct solution. My question itself is that if I have two directives which are not dependent on each other and can both work independently to serve their purposes. But if one of the directives is present then the other one needs to execute once the other is ready. In this case then what would be the logical way to make sure that it works out that way without the need to hardcode any function calls or events?
Lets say for example you have one directive which builds a grid of some sort:
angular.module('App').directive('appGrid',function() {
return function($scope, element) {
$scope.rows = ...
};
});
Then I have another directive that makes the element horizontally scrollable:
angular.module('App').directive('appPane',function() {
return function($scope, element) {
element.attachHorizontalScroll();
};
});
So an example of my HTML would look like this:
<div data-app-grid data-app-pane>
<div data-ng-repeat="row in rows">
<div data-ng-repeat="cell in row.cells">
{{ cell.data }}
</div>
</div>
</div>
Basically the appPane directive needs to run after the appGrid directive has been executed and the table is ready.
One solution I can think of is to watch the data to see when it is ready using the $scope.$watch method, but this poses a problem since the change can occur multiple times and this would be bad design to redundantly update the page and it also poses a problem if multiple directives are writing to the same scope variable that is being watched.
Another solution is to have the first directive emit an event (something like elementReady) and then have the 2nd directive take over. But what about if the first directive isn't there? Then how would the 2nd directive know when to do it's job? There could be another directive which is basically an empty directive which just fires the event for all other elements, but this is a bit of hack. Also what happens if multiple other directives fire the elementReady event?
One more solution is to create a 3rd directive which shares the logic between the two directives via a shared service. But this makes the 3rd directive fully reliant on both other directives as well as the shared services in between. This also require more, unnecessary testing code as well as actual code to write the directive (much more code compared to the 2nd solution, which would require only one + one lines of code).
Any ideas?

Have a look at the priority attribute of the directives.
Here is a copy of the exact description from the angular docs :
priority - When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which
the directives are applied. The priority is used to sort the
directives before their compile functions get called. Higher priority
goes first. The order of directives within the same priority is
undefined.
you should be able to find it in
http://docs.angularjs.org/guide/directive
under the Writing directives (long version) --- Directives definition Object section.
Hope this answers your question.

I had a similar problem. I couldn't use priority, since the wiring occurred after clicking on the element. I solved it using $rootScope. Here is a simplified example:
link : function (scope, element, attrs) {
element.on('click', function() {
$rootScope.$emit('myEvent', myData);
});
}
In the other directive you 'listen' for myEvent:
link : function (scope, element, attrs) {
$rootScope.$on('myEvent', function(data) {
// do sth
});
}

Great question. I would use a combination of attribute(s) and event(s).
Since only the user of the directives (i.e., the person writing the HTML) knows that he/she wants the two directives to interact (i.e., run dependently), I think he/she needs to specify this somehow, and attributes seem like a good way (the only way?). Once the directives know they need to interact, they can use events to do the signaling.
So, if directive B needs to wait for directive A, one or more attributes can be used to specify who should do the waiting, who should fire an event, and/or what the event name is. (This of course can be expanded to more than two directives, and more than one event.) Some possible implementations:
<div data-app-grid data-app-pane idc-wait="appPane" idc-event="idc-appGridDone">
<div data-app-grid data-app-pane idc-wait="appPane" idc-emit="appGrid" idc-event="idc-appGridDone">
By examining the attributes, the appGrid directive can determine that it doesn't need to wait, but it does need to emit event "idc-appGridDone". Similarly, by examining the attributes, the appPane directive can determine that it needs to wait for an "idc-appGridDone" event before it runs.
If the directives don't find any of these special "inter-directive communication"/"idc-" attributes, they run normally (no events, no waiting).
The "(in)dependent inter-directive communication pattern" is born. ☺

Related

How to chain nested directives which use isolated scopes in angularjs

I'm trying to chain two nested directives that both use isolated scopes.
<div ng-controller="myController">
<my-dir on-done="done()">
<my-dir2 on-done="done()">
</my-dir2>
</my-dir>
</div>
I would like the second directive (my-dir2) to call the done() function of the first directive (my-dir) which in turn would call the controller one.
Unfortunately I don't know how to make the second directive access the callback of the first directive (so far the second directive is looking inside the high level controller, bypassing the first directive).
I think one could possibly make use of "require" but I can't since the two directives are not related (I want to use my-dir2 inside other directives not only my-dir).
To make it clear : I don't want to use require because it means that there would be a dependency of myDir on myDir2. My point is : I want to be able to reuse myDir2 inside others directives. So I don't want myDir2 to be based on myDir but I do want to inform the upper directive (myDir) when something is done (like in a callback in js).
I have made a plunker : as you can see in the javascript console, my-dir2 is calling directly the done function from the high level controller.
Does anyone has a clean way to deal with that kind of situation ?
Thanks
Update:
to be able write directives that are independent of each other you need to use events:
use $emit('myEvent', 'myData') to fire an event that will be handled by scopes that are upward in the hierarchy.
use $broadcast('myEvent', 'myData') to fire an event that will be handled by scopes that are downward in the hierarchy.
to handle the event that was fired by $emit or $broadcast use $on('myEvent', function(event, data){\\your code})
P.S.: in your case the $emit won't work because both directives scopes are on the same level in the hierarchy so you will need to use $rootScope.$broadcast('myEvent' \*, myData*\); I've updated my plunker to reflect the needed changes http://plnkr.co/edit/eTkO6sk6hpuYPnCjlSKn?p=info
The following will make inner directive dependent on the outer directive:
basically to be able to call a function in the first directive you need to do some changes:
add require = '^myDir' to myDir2
remove the onDone from myDir2 and keep the isolated scope
scope:{}
add controller parameter to link function in myDir2 link:
function(scope,element,attrs,controller)
in myDir1 controller change the definition of the done function
from $scope.done to this.done
call controller.done() in myDir2
here is a plunker with the needed changes http://plnkr.co/edit/eTkO6sk6hpuYPnCjlSKn
I think you can do something like these:
angular.element('my-dir').controller('myDir').done();
give a try!

How to wait till angularjs is done with everything?

Similar to this question, I want to set focus on the last <select> whenever it gets added. As there's a single method doing it, I need no directive and no watch and no events. My function
$scope.addNew = function() {
$scope.items.push({});
$timeout(function() {
$("select").focus();
});
};
works nicely, except when called directly from the controller function definition like
angular.module('myModule').controller('MyCtrl', function($scope, $timeout) {
$scope.items = {};
...
$scope.addNew();
}
It looks like the timeout happens before the DOM gets constructed and $("select") is empty. With a delay of some 100 ms it works again, but this is a bad hack.
Contrary to what's said in the answer to the linked question, timeout doesn't suffice.
So what's a reliable way to wait for angularjs being really done with the DOM and everything?
Update:
It probably doesn't work because of the select to be focused being embedded in directives (including ng-repeat and some own ones) That's why there initially was no DOM element to focus on.
According to the comments, I need a directive. What's unclear is how exactly to do it. I tried and failed and found out a simpler solution.
What I need
I wasn't very explicit with this, so let me clarify.
I'm working with a table where each row contains some editable fields.
In addNew, I want to set focus on the first editable field of the new row.
In my case this happens to be the very last select.
It worked except at the very beginning, when I was adding the very first row from the controller body.
Why I'm opposed to using a directive
To my limited understanding, it's completely backwards:
A directive modifies the look, behavior, or structure of a given element. But there's no element which should be modified. I tried to put a directive on everything from the select itself to the whole body.
It needs to watch something or listen to an event, but I only want to invoke a function manually.
It didn't work (for me and others as the comments to the linked question shows).
I am going to try and influence you to use a directive here, just to perform the behavior.
Here is a fiddle.
Basic premise is adding the behavioral directive to the element inside repeater:
<table>
<tr ng-repeat="item in items">
<td>{{item}}: <input type="text" auto-focus/></td>
</tr>
</table>
Then your directive would put focus on the last added element:
app.directive('autoFocus', function(){
return function link(scope, elem){
elem[0].focus();
}
});
No watchers or events needed unless I am missing something that you require.
Code that manipulates the DOM should go in a directive, but if you switch to a directive and still have reason to wait until Angular is finished updating the scope and the dom, use $scope.$evalAsync:
$scope.$evalAsync( function() {
// This will wait until Angular is done updating the scope
// Do some stuff here
//
});
The solution was very trivial: Instead of calling $scope.addNew(); directly, I put it in $scope.init invoked from <form ng-init="init()">.
According to the documentation
The only appropriate use of ngInit is for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.
this seems to be wrong (or maybe not, as ngRepeat si involved). I'm only using it to postpone the call to $scope.addNew();, where neither timeout nor posting events worked.

AngularJS: Using services in directives

This is an angularjs app. I have a service that handles the loading of content (ajax). While the service is getting the content, a number of things throughout the app hide, later showing again (depending on the content returned). They might have the same scope, different scope, whatever. They just need to hide while content is loading, and then show when it's done. Pretty normal stuff.
Right now, I have separate controllers watching a "loading" property of the service and using regular angular directives (ng-show, ng-hide, etc.) to show/hide. But this feels like overkill. I'd prefer to write a custom "loading" directive that injects the loading service and does the watching and showing/hiding.
My question is: Is what I want to do "bad"? The controller way, I end up boilerplating a bunch of code, maybe up to like 5 or 6 times, or even more as the app grows. The custom directive way, I write it once and use an attribute where I need it. Yeah - there's a dependency on that service, but that just doesn't feel like the end of the world that some people have made me start to think I should think it is.
For what it's worth, I feel like I've heard "separation of concerns" so many times I've become paralyzed by it. It leads me to overthink everything because I want to do things the right way, but it sure doesn't feel like I'm being very productive.
If I understood correctly, you have a bunch elements that should hidden when a particular service is loading data, and then be displayed again when the data is loaded, right?
In that case, events might be a good solution:
they can be global to the appliciation (which i think is what you are aksing for).
they allow for avoiding direct coupling between elements (also one of your concerns).
So, in your service, just broadcast events when stuff happens:
$rootScope.$broadcast('loading-data');
axajStuffOrWhatever(function() {
$rootScope.$broadcast('data-loaded');
});
Then, wrap the show/hide behaviour in a directive that will listen to those events.
.directive('hideWhileLoadingData', function() {
return {
link: function(scope, el, attrs) {
scope.$on('loading-data', function() {
el.css('display', 'none');
});
scope.$on('data-ready', function() {
el.css('display', 'block');
});
}
};
});
Use the directive:
<div hide-while-loading-data>something</div>
The advantage of using events here, is that later on, they could be originated by a different service, or by multiple services, and the directive will not be affected by that as long as the events are the same.
For more complex behaviour, you could also parametrize the events and the directive, so different elements will react to different kind of stuff.
I've made an example of this.
In my opinion all scopes which depend on this service should be children of one parent scope. If you have the parent scope responsible for talking with the service then any directive of any scope can access it via $parent on the $scope.

Making data templated in available to ng-model

When using server-side templating and client-side angularjs, I'm not able to get angularjs to recognize values I've templated in on the server.
For example (or on jsfiddle):
<div ng-app>
<div ng-controller="Ctrl">
<textarea ng-model="data" placeholder="Enter a name here">Templated in</textarea>
{{data}}
</div>
</div>
Angularjs will always replace the value in the text area with the value of $scope.data (which is null). What I want is for the value of $scope.data to take on "Templated in", on app bootstrap, then to continue normally from there.
How can I template in from the server a value, then have angularjs model bind that value once on the client?
Use ng-init
<textarea ng-model="data" placeholder="Enter a name here"
ng-init="data='Templated in'"></textarea>
See also AngularJS - Value attribute on an input text box is ignored when there is a ng-model used? and
rails + angularjs loading values into textfields on edit
I came across this answer while trying to figure this out for myself, but never liked that I had to move information from the part of the field where the HTML spec wants it. That just didn't feel like the right approach to me.
For a while, I ended up taking advantage of the fact that Angular sits on top of jQuery (or the embedded jqLite subset if the full jQuery isn't available) to solve this without moving the content, or even touching ng-init. Specifically, you can access the contents of the textarea using (the Angular/jqLite version of) standard jQuery methods during the controller initialization phase.
So I just did
var doc = angular.element(document.documentElement);
$scope.data = doc.find('textarea').eq(0).val();
in my controller, exactly where I would initialize any other scope variable. (Alternately, here's the modified jsFiddle...)
With the full jQuery library available, the code is even simpler, since you have access to full jQuery selectors at that point and can jump straight to $('textarea').eq(0).val() (or even add an id to the field and select on that: $('#data-textarea').val()).
The nice thing about this approach is that it will work for any form elements, though since most of those are <input> tags, and jqLite doesn't support selectors, finding the exact one you want can be a tad tricky. I would simply include the full jQuery library at that point and take advantage of the selectors, but that's me.
This approach also has the major drawback of placing DOM-aware code in the controller, which completely breaks both Angular conventions (The Angular Way (TM)) and the MVC/MVVM programming paradigm. Not the best solution.
UPDATE: So I eventually realized I would need a more long-term solution, one which didn't violate so many best practices. The answer actually comes from the most essential element of Angular, without which none of the rest of it would work (you could argue that for other components, but it's simply more true for this one): Directives. More specifically:
app.directive('input', ['$parse', function ($parse) {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attrs) {
if(attrs.value) {
$parse(attrs.ngModel).assign(attrs.value, scope);
}
}
};
}]);
This has a number of advantages. First, it doesn't break Angular conventions that help reinforce MVC/MVVM. Second, it doesn't even touch jqLite/jQuery, nor the underlying DOM functions. Third, it has the desired effect of preserving HTML conventions for defining default values, allowing (or at least simplifying) the use of Angular with other existing technologies, such as server-side templating engines.
Why doesn't Angular do this by default? Well, I don't know the actual answer without a bit more research, but a likely answer is that HTML conventions favor static page content, and Angular is designed for dynamic page content. That means breaking out of HTML conventions in many places, not letting it limit what is possible with Angular apps. Since controllers are expected to carry the responsibility of initializing models (and in most cases such an expectation is the correct one), the Angular team would have incentive to ignore the contents of a value attribute (and its analogs among the other form tags).
Of course, with this approach, for any applicable elements that already exist before the controller is initialized, the controller init can override the value attribute. This behavior is not consistent, however, across the entire application, because new elements will trigger the directive evaluation, but not the controller init phase. (Partials with their own controllers hold this behavior as well - elements in place prior to the partial's controller init can be overridden by said init, but other elements added after that won't re-trigger the init.)
There are a number of other ways to write such a directive, and it can be extended to do any number of other things as well, but hopefully this approach helps someone else.

Tricky scope binding in angularjs directives

I want to write 'edit in place' directive in angularjs.
I want that directive is reusable, therefore I have following requirements on the directive:
it must be an attirbute that can deocorate any element, that makes sense (div,span,li)
it must support edit button, clicking on that will change set ot displayd elements into input fileds. Typically properties of one object, e.g. contact (number, name)
I disocvere trickery behaviour of scope visibility in the directive that can be seen in this fiddle http://jsfiddle.net/honzajde/ZgNbU/1/.
Comenting out in the directive: template and scope -> contact.number and contact.name are displayed
Comenting out in the directive: scope -> contact.number only is displayed
Not commenting out anything -> nothing is displayed
=> when both are commented out just adding template to the directive makes it render contact.number even though template is not used.
I am asking what are the rules of the game?
<div>
<div ng-controller="ContactsCtrl">
<h2>Contacts</h2>
<br />
<ul>
<li ng-repeat="contact in contacts">
<span edit-in-place="" ng-bind="contact.number"></span> |
<span edit-in-place="" >{{contact.name}}</span>
</li>
</ul>
<br />
<p>Here we repeat the contacts to ensure bindings work:</p>
<br />
<ul>
<li ng-repeat="contact in contacts">
{{contact.number}} | {{contact.name}}
</li>
</ul>
</div>
</div>
var app = angular.module( 'myApp', [] );
app.directive( 'editInPlace', function() {
return {
restrict: 'A',
//scope: { contact:"=" },
template: '<span ng-click="edit()" ng-bind="value"></span><input ng-model="value"></input>',
link: function ( $scope, element, attrs ) {
// Let's get a reference to the input element, as we'll want to reference it.
var inputElement = angular.element( element.children()[1] );
// This directive should have a set class so we can style it.
element.addClass( 'edit-in-place' );
// Initially, we're not editing.
$scope.editing = false;
// ng-click handler to activate edit-in-place
$scope.edit = function () {
$scope.editing = true;
// We control display through a class on the directive itself. See the CSS.
element.addClass( 'active' );
// And we must focus the element.
// `angular.element()` provides a chainable array, like jQuery so to access a native DOM function,
// we have to reference the first element in the array.
inputElement[0].focus();
};
// When we leave the input, we're done editing.
inputElement.prop( 'onblur', function() {
$scope.editing = false;
element.removeClass( 'active' );
});
}
};
});
app.controller('ContactsCtrl', function ( $scope ) {
$scope.contacts = [
{ number: '+25480989333', name: 'sharon'},
{ number: '+42079872232', name: 'steve'}
];
});
You are running into problems because you are misusing angular.
First, a directive should be self-contained, but you are pulling functionality out of it, which makes it less universal and less reusable. In your code, you have functionality in the DOM and in the controller that belongs in the directive. Why?
Second, it's also unclear from your markup and javascript specifically want you want to accomplish when all these pieces are strung together.
Third, in most cases, directives should have their own isolated scope, which is done by declaring a scope object with attributes it should bind. You shouldn't be passing an expression (i.e. {{contact.name}}) inside the directive as it will break the binding and your contact will not be updated when the edit-in-place finishes. The proper way is to establish bi-directional binding through an = property on the scope. ng-bind isn't what you want here: that's scope-specific, so we use it inside the directive's scope. As Valentyn suggested, you could do some magic to get around this, but it's not a good idea and it's super-simple to set it up the right way. What's the issue with doing this by an attribute?
This is all bad Ju-ju.
As I pointed out in your other question on this same topic, you must make your directive self-contained and work with angular, rather than against it. Here's an attribute-based version of the fiddle I gave you previously, meeting the first of your requirements. Please let me know what is wrong specifically with this implementation and we can talk about the angular way of fixing it.
Lastly, if you provide further context on what you need in terms of a "button", I'll incorporate that into the fiddle too.
[update]
It is possible to make the directives work your way, but you will run into problems eventually (or right now, it would seem). All components in an angular app (or any app for that matter) should be as self-contained as is feasible. It's not a "rule" or limitation; it's a "best practice". Similarly, communication between directive components can occur through a controller, but it shouldn't. Ideally, you shouldn't reference the DOM in a controller at all - that's what directives are for.
If your specific purpose is a row that is editable, then that is your directive. It's okay to have a lower-level generic edit-in-place directive that the larger directive uses, but there is still the higher-level directive too. The higher-level directive encapsulates the logic between them. This higher-level component would then require a contact object.
Lastly, no, there isn't necessarily a big difference between ng-bind="var" and {{var}}. But that's not the issue; the issue was where that binding takes place. In your example, a value was passed to the directive instead of a bi-directionally-bound variable. My point was that the directive needs access to the variable so it can change it.
Summary: You are coding in a very jQuery-style way. That's great for coding in jQuery, but it doesn't work so well when coding in Angular. In fact, it causes a lot of problems, like the ones you're experiencing. In jQuery, you would, for example, dynamically insert DOM elements, declare and handle events, and manually bind variables all within a single code block, all manually. In Angular, there is a clean separation of concerns and most of the binding is automatic. In most cases, it leads to javascript code at least two-thirds smaller than the jQuery alternative. This is one of those cases.
That said, I have created a Plunker that contains a more sophisticated version of both the edit-in-place as well as a new higher-level directive to incorporate additional features: http://plnkr.co/edit/LVUIQD?p=preview.
I hope this helps.
[update 2]
These are the answers to your new round of questions. They may be good for your edification, but I already gave you the "angular way" to fix your problem. You will also find that I already addressed these questions (in broader strokes) earlier in my original answer as well as in my update. Hopefully, this makes it more apparent.
Question: "Comenting out in the directive: template and scope -> contact.number and contact.name are displayed"
My Reply: When you do not specify a scope, the directive inherits its parent scope. You bound and interpolated the name and number within the context of the parent, so it "works". Because the directive will alter the value, however, this is not a good way way to solve it. It really should have its own scope.
Question: "Comenting out in the directive: scope -> contact.number only is displayed"
My Reply: You bound a scope property of the parent to the "contact.number" directive, so it will get placed inside during the $digest loop - after the directive has been processed. On the "contact.name", you put it inside the directive, which can only work if the directive codes for transclusion.
Question: "Not commenting out anything -> nothing is displayed"
My Reply: Right. If the directive has its own scope (and this one definitely should), then you must use a defined directive scope property to communicate values, as my several code samples demonstrate. Your code, however, tries to use the parent scope in the directive when we explicitly forbid that by using the scope property in its definition.
Summary: While this second update may be informative (and I hope that it is), it doesn't answer the question beneath your questions: how do I use angular components correctly so that the scope I'm using is always what I think it is? My first post and the subsequent update, answer that question.
Here is little bit updated your fiddle, but it need further improvements to meet full list of your requirements: http://jsfiddle.net/5VRFE/
Key point is:
scope: { value:"=editInPlace" },
Some notes: its better to use ng-show ng-hide directivies for visual appearing-hiding instead of changing css classes. Also its better to spread functionality into different directives to have better separation of concerns (check ngBlur directive)
About your confusion of scope check guide about scopes paragraph "Understanding Transclusion and Scopes": Each directive have separate isolated scopes, if you want to have access from directive's template to controller's scope use directive scope binging ("scope" field of directive definition object). And also transcluded elements have a scope of from where you defined transcluding template.
From the first view those isolated scope sounds little bit strange, but when you have good structured directives (note also that one directive can require another and share bindings) you can find it extremly usefull.

Resources