Communication between multi-layered directives in Angular - javascript

I'm creating a few directives that will make up a single "screen" in my app. To create this new screen you would write it like this:
<screen title="Test Title">
<side-menu align="left">
<menu-item>Test One</menu-item>
<menu-item selected="true">Test Two</menu-item>
<menu-item disabled="true">Test Three</menu-item>
</side-menu>
<content animation="fade">
<view>Content for menu option 1</view>
<view>Content for menu option 2</view>
<view>Content for menu option 3</view>
</content>
</screen>
Each <menu-item> will display one of the "views" inside of the <content> tag. It works like tabs. I've got this set up by keeping track of each <view> inside of the <content> directive in an array when they are linked. Same for <menu-item>.
My question is, now that I've got this set up, what is the best way to communicate between the <side-menu> directive and the <content> directive to hide and show the correct view when clicked? Should I use events, a common service to hold state, or is there a way I can maybe access the controller inside of the <screen> directive from the <view> and <menu-item> directives, and hold the data/state there? From my understanding I can only access the parent controller from a child directive, but not the "grand parent" controller if you will, unless I use some sort of pass-through.
I plan to have a few more components on this "screen" that will need to communicate as well so I'm hoping to determine the "correct" way to do this before I continue, or at least get some feedback and other ideas.
If any of that is confusing, I'd be happy to provide more information.

So after a bit of digging, I've learned that you can pass an array to the require property of a directive.
You can use this to find parent controllers, and grandparent controllers... etc. Previously each of my directives would have one require value such as: require: '^sideMenu' for the menuItem directive.
Now I can require the sideMenu and screen controllers into the menuItem directive by passing an array:
require: ['^screen', '^sideMenu']
Now in the link function of my menuItem directive, I can access these controllers this way:
link: function(scope, element, attrs, controllers) {
var screenCtrl = controllers[0];
var sideMenuCtrl = controllers[1];
}
Notice the controllers property is now an array of the controllers that I required, and are accessed by index. Although I feel as though my directives are little more tightly coupled now, I do like it better than using events/services.
I'm explaining all of this, because no where in the Angular docs does it mention this.

Related

Angular 1.5 to use transclude or not to use transclude

A question regarding transclude within an angular 1.5.8 component, and it's uses.
Here is an example of some code;
var app = angular.module('app', [])
function AccordionController () {
var self = this;
// add panel
self.addPanel = function(panel) {
// code to add panel
}
self.selectPanel = function() {
//code to select panel
}
}
// register the accordion component
app.component('accordion', {
template: '<!---accordion-template-->',
controller: AccordionController
}
function AccordionPanelController () {
// use parents methods here
var self = this;
// add panel
self.parent.addPanel(self);
// select panel
self.parent.selectPanel(self);
}
// register the accordion-panel component
app.component('accordionPanel', {
// require the parent component
// In this case parent is an instance of accordion component
require: {
'parent': '^accordion',
template: '<!---accrodion-panel-template-->',
controller: AccordionController
}
My question is would it be better to nest all the according panels within the parent using transclude or alternatively pass in a data array to the parent which this loops out the required number of panels based on the array passed inside using a binding.
Thanks
// added
Many thanks for your reply, an example I have of transclude possibly being necessary is in the following bit of code
<modal modal-id="editCompany" title="Edit Company"> <company-form company="$ctrl.company"></company-form> </modal>
Here we have a modal component which may have a variety of other components used within it, on the example above I am adding the company form, but this could we be an contact form. is there an alternative way?
I've worked with angular pretty extensively. Two enterprise tools managing and displaying large amounts of data, dozens of interactive widget modules, all that.
Never, once, have I had anything to do with transclude. At work we are explicitly told not to use it (link functions too). I thought this was a good thing, and the way Angular 2 turned out it seemed that thinking wasn't totally without reason.
I would go with the iteration to lay out the required number of items. At work this wouldn't be a choice because transclude wouldn't be an option.
The thing with using transclude in a component architecture is that it visually breaks the idea of single responsibility and messes with the architecture.
<html>
<navigation></navigation>
<accordion>
<accordion-panel></accordion-panel>
</accordion>
<footer></footer>
</html>
In this example you know your page has a navigation menu, an accordion and a footer. But at the index level (or root component) you don't want to know / see what the accordion contains.
So the accordion-panel component should only appear in its direct parent component.
As for your other question, through the use of require or attributes you pass an array of panels that you iterate using ng-repeat inside the accordion component.
app.component('accordion', {
template: `<accordion-panel ng-repeat="..."></accordion-panel>`,
controller: AccordionController
}

Issues with AngularJS Controller

I have the following setup in my html file:
<body>
<nav ng-controller="loginCtrl">
<div>
<li>Apple</li>
<li>Mango</li>
<li>{{user}}</li>
</div>
</nav>
<div ui-view></div>
</body>
I have a navigation menu and the ui-view that displays different pages.
I also have a controller, loginCtrl, with a scope variable called $scope.user. This controller is also called in the UI-state router for the login.html file as well so that the login form can use its methods.
When a user logs in, I want to show his name in the navigation menu using the {{user}} above. The navigation menu as you can see is visible (static) regardless of other partial pages that will be loaded in the ui-view.
At the moment, it is not working and I don't know why.
My understanding is that the login form in the login.html and the navigation menu are in different files so that may be they are using the same controller (under the same module) yet may be operating in different scopes/environments (am not really sure about that).
That is why I update the value of $scope.user but it doesn't appear in the navigation menu.
Why is it not working and how can I achieve my functionality?
Using a singleton service to share same UserData object:
app.service('UserData', function(){return {name: 'default'};});
app.controller('LoginController', function($scope, UserData){
$scope.user = UserData;
});
Now, all controller instances have access to same UserData object.
When user.name has changed, all controllers can see it.
I'm wondering if it's necessary to cal your LoginController for your ui-view and a sibling element, when you could load it on a parent element
Anyway, you have several solutions to make the two-way binding work:
#vp_arth solution is really great, usually used this to share data between controllers
Move your ng-controller attribute to a common parent element, and if needed, declare another controller for your login.html, that will be a child of your LoginController. Then use an object instead of a variable in your parent scope:
$scope.user = {};
and then fill it in your child scope like this:
$scope.user.name = ...
Even if you're using the same controller as parent AND child scope, you should make this work with something like this:
$scope.user = $scope.user ? $scope.user : {}; instead of $scope.user = {};
(if it's not clear I can make a comparative fiddle to show you)
This wiki really helped me when I had issues like yours: https://github.com/angular/angular.js/wiki/Understanding-Scopes

Using the same directive in a directive [angularjs]

I have a need to use the same directive within a directive, depending on a conditional param. However, when ever i try to do it, it seems to go to an endless loop. As i understand, it tries to pre-load the templates and that causes an endless recursion and at the end, it just throws me the following error:"RangeError: Maximum call stack size exceeded".
I have created an example in fiddle.. as you can see in the example, when the param's value is "1", it creates the error (even when the second level param is valued as "2" so it shouldn't have real recursion issues in the controller/app).
https://jsfiddle.net/qh9nh1gx/
"custom-directive"'s template:
<div>
<div ng-if='info==1'><div custom-directive info='2'></div></div>
<div ng-if='info==2'>DONE,single.</div>
</div>
Thanks
I have found 2 options to deal with the issue, the first one, is exactly what Jju described - creating a new "compiler" method (it can be grabbed from the url he sent).
The second option - always using an additional template for the "recursive" parts of the directive. For example, in my directive, i had a "ng-repeat" part that depending on the items value, it could request to display the directive again. while i used "ng-include" to have the other directive, it worked.
<div ng-repeat="item in items" ng-include="'inline-possibly-recursive-template"'></div>
in that template, you can call the directive again without any issues..
I hope that it will anyone else that will stumble into that issue.
You can look into https://stackoverflow.com/a/19065910/1680674 that describe a common approach to create directive that use himself inside

AngularJS directive only called once?

I'm trying to shove mixitup inside my angular page and in order to do so I made a directive module for it
angular.module('MainCtrl', [])
.controller('MainController', function($scope) {
$scope.tagline = 'To the moon and back!';
})
.directive('mixitContainer', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
$(element).mixItUp(scope.$eval(attrs.mixitContainer));
}
};
});
Don't worry about the simplicity of the main controller, it is simply a test.
Now, the issue is that the directive only get's called once! If I go to another page and ask angular to load another controller and then go back to the home page and ask angular to load MainCtrl again, the directive isn't loaded!
Heres the with the directive:
<div id="Container" class="mixit-container" mixit-container="{load: {sort: 'order:asc'}, controls: {toggleFilterButtons: true, toggleLogic: 'and'}}">
Anyone got any ideas?
AngularJS doesn't include routing facilities. Those are provided either by ngRoute (a core but optional module), ui-router (ngRoute made super-awesome-amazing), or another replacement. You don't say which you use, and each has different behaviors.
Whichever it is, this is going to come down to the router, not the directive. The directive will get called whenever necessary. It doesn't control that itself - 'necessary' means Angular is compiling a portion of the DOM, usually from a template file, and has run into the directive. It will call the directive and ask it "what now?"
The above-named routers have different behaviors, both from each other and also based on how you configure them internally. In all of them you can arrange things so that templates do, or do not, get re-rendered. For example, in ui-router you can have 'child' states. If you have a child state active, the parent is also active... so going from the child to the parent will not re-render a template because it was already done earlier. And to make matters more complex, you can even override this by hooking the $stateChangeStart event and force the router to redraw that view even if it didn't think it needed to.
All this means... set your attention to your directive aside. It will do what you want as soon as the higher level does what you want. Get your router behaving the way you expect and you will be happy!

Using Angular, how can I show a DOM element only if its ID matches a scope variable?

I am relatively new to AngularJS.
I have a series of DIVs in a partial view. Each of the DIVs has a unique ID. I want to show / hide these DIVs based on a scope value (that matches one of the unique ID).
I can successfully write out the scope value in the view using something like {{showdivwithid}}
What would be the cleanest way to hide all the sibling divs that dont have an ID of {{showdivwithid}}
I think you are approaching the problem with a jQuery mindset.
Easiest solution is to not use the id of each div and use ngIf.
<div ng-if="showdivwithid==='firstDiv'">content here</div>
<div ng-if="showdivwithid==='secondDiv'">content here</div>
<div ng-if="showdivwithid==='thirdDiv'">content here</div>
If you don't mind the other elements to appear in the DOM, you can replace ng-if with ng-show.
Alternatively use a little directive like this:
app.directive("keepIfId", function(){
return {
restrict: 'A',
transclude: true,
scope: {},
template: '<div ng-transclude></div>',
link: function (scope, element, atts) {
if(atts.id != atts.keepIfId){
element.remove();
}
}
};
});
HTML
<div id="el1" keep-if-id="{{showdivwithid}}">content here</div>
<div id="el2" keep-if-id="{{showdivwithid}}">content here</div>
<div id="el3" keep-if-id="{{showdivwithid}}">content here</div>
First, I want to echo #david004's answer, this is almost certainly not the correct way to solve an AngularJS problem. You can think of it this way: you are trying to make decisions on what to show based on something in the view (the id of an element), rather than the model, as Angular encourages as an MVC framework.
However, if you disagree and believe you have a legitimate use case for this functionality, then there is a way to do this that will work even if you change the id that you wish to view. The limitation with #david004's approach is that unless showdivwithid is set by the time the directive's link function runs, it won't work. And if the property on the scope changes later, the DOM will not update at all correctly.
So here is a similar but different directive approach that will give you conditional hiding of an element based on its id, and will update if the keep-if-id attribute value changes:
app.directive("keepIfId", function(){
return {
restrict: 'A',
transclude: true,
scope: {
keepIfId: '#'
},
template: '<div ng-transclude ng-if="idMatches"></div>',
link: function (scope, element, atts) {
scope.idMatches = false;
scope.$watch('keepIfId', function (id) {
scope.idMatches = atts.id === id;
});
}
};
});
Here is the Plunkr to see it in action.
Update: Why your directives aren't working
As mentioned in the comments on #david004's answer, you are definitely doing things in the wrong way (for AngularJS) by trying to create your article markup in blog.js using jQuery. You should instead be querying for the XML data in BlogController and populating a property on the scope with the results (in JSON/JS format) as an array. Then you use ng-repeat in your markup to repeat the markup for each item in the array.
However, if you must just "get it working", and with full knowledge that you are doing a hacky thing, and that the people who have to maintain your code may hate you for it, then know the following: AngularJS directives do not work until the markup is compiled (using the $compile service).
Compilation happens automatically for you if you use AngularJS the expected, correct way. For example, when using ng-view, after it loads the HTML for the view, it compiles it.
But since you are going "behind Angular's back" and adding DOM without telling it, it has no idea it needs to compile your new markup.
However, you can tell it to do so in your jQuery code (again, if you must).
First, get a reference to the $compile service from the AngularJS dependency injector, $injector:
var $compile = angular.element(document.body).injector().get('$compile');
Next, get the correct scope for the place in the DOM where you are adding these nodes:
var scope = angular.element('.blog-main').scope();
Finally, call $compile for each item, passing in the item markup and the scope:
var compiledNode = $compile(itm)(scope);
This gives you back a compiled node that you should be able to insert into the DOM correctly:
$('.blog-main').append(compiledNode);
Note: I am not 100% sure you can compile before inserting into the DOM like this.
So your final $.each() in blog.js should be something like:
var $compile = angular.element(document.body).injector().get('$compile'),
scope = angular.element('.blog-main').scope();
$.each(items, function(idx, itm) {
var compiledNode = $compile(itm)(scope);
$('.blog-main').append(compiledNode);
compiledNode.readmore();
});

Categories

Resources