AngularJS collapse directive. Collapse all but self - angularjs

I'm struggling to find the best solution for a collapse/expand directive that behaves like an accordion, ie. only one collapse/expand directive on the page must be open at any one time.
What is the best way to go about this, and get an expanding directive to tell the other directives to collapse? Can I use an isolated scope, a parent controller, broadcast events? Basically I'm having difficulties wrapping my head around inter-directive communication.
I know that there are accordion directives available, but I want to learn building directives myself. Thanks.

I ended up using $broadcast from a parent controller. The expanding directive asks the parent controller to broadcast a collapseChange event, which all directives listen for.
Parent controller
$scope.broadcastCollapseChange = function (id) {
$scope.$broadcast('collapseChange', { 'id': id});
};
Directive
scope.collapsed = true;
var onCollapseChange = function (v) {
if (scope.collapsed == false)
scope.$parent.broadcastCollapseChange(scope.$id);
}
scope.$watch('collapsed', onCollapseChange);
scope.$on('collapseChange', function (event, args) {
if (scope.collapsed == false && args.id != scope.$id)
scope.collapsed = true;
});
Currently I have to use $parent in the directive to get the parent controller, which is not very elegant. Is there any way I can get around this?

Related

How to trigger an event in one view from a different view?

I am trying to open an Angular accordian in the header.html by clicking a button which is in the body.html. Essentially triggering an event in one view from a completely different view. Does anyone have any idea how to do this in Angular?
What you can do is using events to let your accordion directive know that something happend or use a shared service. Considering the performance, it does not make a huge difference, but only if you use $emit instead of $broadcast since the event fired via $emit bubbles up your scope hierarchy and $broadcast sends the event down. Also make sure to fire the event on the $rootScope, so it won't event bubble up anymore.
So you in case you want to use events for you could have a method on your component that fires the event via $emit on the $rootScope as follows:
function openAccordion() {
$rootScope.$emit('on-accordion-open', null);
}
You could then use this in your view, e.g. in body.html. Remember that function above is part of another directive / component or controller.
<button ng-click="vm.openAccordion()">Open Accordion</button>
Also note that I assume you are using controllerAs syntax (set to vm).
In your accordion directive you can then hook up listeners to several events for example the on-accordion-open:
$rootScope.$on('on-accordion-open', function() {
// Open the accordion
});
The other soltuion is to use a shared service. In this case I would create a AccordionServce that is aware of all instances of accordions. The service could look like this:
angular.module('myApp').service('AccordionService', function() {
var accordions = {};
this.addAccordion = function(name, accordion) {
accordions[name] = accordion;
};
this.removeAccordion = function(name) {
delete accordions[name];
};
this.getAccordion = function(name) {
return accordions[name];
};
});
In your accordion's controller you then add the accordion to the AccordionService via
accordionService.addAccordion('myAccordion', this);
The this in the snippet above is refering to the accordion controller. Thats important because if you then get an accordion in your component in the body.html, you'll get the controller instance and can call methods like open.
So in your body component you can then inject the AccordionService and get the accordion to call a method:
accordionService.getAccordion('myAccordion').open();
Make sure to define open on the accordion's controller.

Angularjs - Directive Two-way binding Isolated Scope Issue

I'm building a SPA based on AngularJS. In one component of the SPA I have a document upload system, which is built via a custom directive below called docmgmt. Within the component docmgmt I have an another custom directive component called modalCompanyQuery. It is a modal window that searches the company database and returns matching company results. Upon the finding the right company the user clicks on the company name which is then passed back to the parent directive docmgmt called modalOutput.
The issue I have is that despite using two way binding '=' a new scope for modalOutput (output) is created in modalCompanyQuery. How can I pass the modalCompanyQuery search result (modalOutput) back to the parent directive docmgmt? Any help on the simplest way to return the results would be great. Thank you in advance!
Here is my code simplified
modalCompanyQuery Template
<div modal-company-query dialog-show="modalCompanyQuery.isShow" dialog-name ="Select Company" dialog-class="modalSelectCompany" dialog-icon ="fa fa-building" dialog-header="modalSelectCompany-header" company-type = "srchCompanyTypeList" output-select="modalOutput">
</div>
Directive docmgmt
angular.module("docmgmt", [])
.directive("docmgmt",['$http','sessionService','Upload','docService', function($http,sessionService,Upload,docService){
return{
link: function(scope,element,attrs){
scope.docRecord = {};
scope.rightPane = {showAction:true, showInsert:false,showUpdate:false, showRead:false};
scope.progressBar = 0;
scope.submit =[{}];
//modal company search and linking search output results to docmgmt scope
scope.modalCompanyQuery = {isShow:false};
scope.modalOutput={};
scope.test=function(){
console.log(scope.modalOutput);
}
},//return
restrict:"A",
replace:true,
templateUrl:"partials/docmgmt/docmgmt.html",//template
transclude:true,
scope:{
}
}//return
}]);
Directive modalCompanyQuery
angular.module("company", [])
.directive("modalCompanyQuery",['$http','companyService', function($http,companyService){
return{
link: function(scope,element,attrs){ // normal variables rather than actual $scope, that is the scope data is passed into scope
//Read Company
scope.getRecord = function(result){
scope.output={id:result.cs_id, type:result.type,name:result.name, active: result.active};
console.log(scope.output);
scope.isShow = false;
}//getRecord
/*AJAX search functions go here*/
},//return
restrict:"A", //assign as attribute only ie <div my-modal> Content </div>
replace:true,//replaces div with element, note if this is the case must all template must be wrapped within one root element. eg button is within ul otherwise get an error.
templateUrl:"partials/company/tpl/desktop/modal-company-query-desktop.html",//template
transclude:true, //incorporate additional data within
scope:{
isShow:"=dialogShow",//two way binding
name:"#dialogName",//name to be in header
dialogClass:"#dialogClass",// style of the dialog
dialogHeader:"#dialogHeader",//color of the dialogHeader
dialogIcon:"#dialogIcon",//font awesome icon
output:"=outputSelect"
//selectCompany:"=selectCompany",//company to be selected from search and passed back to main window
} //If on this should mean the html input is not binded to custom directive
}//return
}]);
alright, in your docmgmt directive, I see you have made the scope of the directive empty doing:
scope: {}.
I think you should do it like:
scope: {
modalOutput: "="
}
btw doing above expects an attribute in your directive template with name modal-output which must be an object type.
Try it...
After some research I found the solution. The following two links really helped me understand the problem and solution.
Understanding $emit, $broadcast and $on in AngularJS
Communication between nested directives
So I end up using $emit and $on. Outcome as follows:
Directive modalCompanyQuery
scope.getRecord = function(result){
scope.output={id:result.cs_id, type:result.type,name:result.name, active: result.active};
scope.$emit('companyRecord', {record:scope.output});
scope.isShow = false;
}//getRecord
Directive docmgmt
scope.$on('companyRecord', function (event, args) {
scope.modalOutput = args.record;
console.log('Success');
console.log(scope.modalOutput);
});
Hope this helps other people that have come across the same brickwall!

Problems with AngularJS directive child communication

Question in one sentence:
How do I, in a directive, access the controller of a child directive?
Longer description:
I'm writing a couple of directives to handle input from a remote controller (think TV controller). I am doing this because HTML does not have inherently good focus/cursor handling. My problem is that I am new to AngularJS and it feels like it is working against me.
In a particular view for example I want to be able to do something like this:
<div>
<my-linear-focus-container direction="horizontal">
<my-grid default-focused="true" style="...">{{gridItems}}</my-grid>
<my-button style="..."></my-button>
</my-linear-focus-container>
</div>
All views and "widgets" that wants to handle keys needs to have a FocusNode directive. The nodes will together create a focus tree and keys will propagate from the focused node in the tree down the branch to the root. When a new node is focused proper signaling will occur among the relevant tree nodes (lost focus, received focus, etc).
The linearFocusContainer's responsibility will be to switch focus between child widgets/directives. So if child A has focus (and does not listen to the right key) and the user presses right the linearFocusContainer will give focus to child B which lies right next to child A.
LinearFocusContainer directive
{
"restrict": "E",
"scope": {},
"template": "<div rs-focus-node keys='keys'></div>",
"link": function (scope) {
$scope.keyListeners = {
left: function () { /* focus child left of current focused */ },
right: function () { /* focus child right of current focused */ },
...
}
$scope.focusEventListeners = {
onFocusReceived: function () { /* focus to default child */ },
...
}
}
}
Heres my problem. For this to work I need access to the FocusNode directive "owned by" the LinearFocusContainer directive inorder to focus/access other children.
$scope.keyListeners = {
left: function () { focusNode.getChildren()[0].takeFocus(); }
}
That would also give me possibility to do:
focusNode.setKeyListeners({
...
});
And such instead of writing to a variable in the scope.
You cannot access the child controller directly, Angular does not support this.
What you can do is have the child require the parent and pass the child controller to the parent as:
app.directive('parent', function() {
return {
...
controller: function() {
var childController;
this.setChildController = function(c) {
childController = c;
};
}
};
});
app.directive('child', function() {
return {
...
require: ['parent', 'child'],
controller: function() {
...
},
link: function(scope, elem, attrs, ctrls) {
ctrls[0].setChildController(ctrls[1]);
}
};
});
This demonstrates the principle, you can adjust it accordingly if there are more than one children.
Addressing the comment [...] the child does not know what directive the parent is. [...] Can require take "generic types"?
So no, require cannot take generic types (and this would be a useful functionality, I've been running on it a lot lately). I can suggest 2 solutions:
Introduce an extra "coordinator directive". E.g for the case described in the comment, assume the following HTML:
<linear-focus-container focus-coordinator>
<my-grid default-focused="true" style="...">{{gridItems}}</my-grid>
</linear-focus-container>
Both the LinearFocusContainer directive and the myGrid will require the focusCoordinator and cooperate through it. It could even implement some useful common functionality among the different possible types of parent directives.
(highly untested and probably DANGEROUS) All the parent directives put an object with a standard API to the DOM via angular.element.data() under a well defined name. The child directives walk up the hierarchy of their DOM parents looking for this well defined name. At the very least do not forget to remove this object on $destroy.
Why don't you use event broadcast/emit mechanism provided by Angular JS.
$scope.$broadcast will broadcast a event down all the child scopes. You can catch in a child scope using scope.$on and similarly to notify parent scope for a change, you can use `$scope.$emit'.
From parent controller,
$scope.$broadcast('eventName', a_value_or_an_object);
And in the child controller,
scope.$on('eventName', function($event, value_or_object){});
By default, $emit will cause event to be propagated towards $rootScope, which means it will first hit parent, then parent's parent scope.
And if you want to cancel further propagation, you can use $event.preventDefault() in the eventListener.

AngularJs: to Propagate event between directives by $emit

I have under one controller two directives :
<div ng-controller="ctrl">
<div data-dy-items-list items="items"></div> // FIRST DIRECTIVE
<div data-dy-subitems-list subitems="subitems"></div> //SECON DIRECTIVE
</div>
In the Second directive template, I have one button and in the directive.js file in the controller section I did this :
$scope.clickButton= function () {
......
$scope.$emit("UPDATE_PARENT","updated");
}
In the first directive, I would like to do this in the controller section:
$scope.update = false;
$scope.$on("UPDATE_PARENT", function (event,message){
$scope.update = true;
console.log('update: ' + $scope.update);
});
But it doesn't work!!!
$emit dispatches an event upwards through the scope hierarchy. Your directives are siblings and thus $emit won't work.
The solution might be to $broadcast an event from a parent scope. Do it from ctrl if that's an option for you, or inject $rootScope to the directive and do $rootScope.$broadcast from there:
$rootScope.$brodcast("UPDATE_PARENT","updated");
Mind that $broadcasting events from $rootScope might seem to be an anti-pattern for AngularJS. It strongly depends on the usecase. There are other solutions to your problem:
One of them is to create a parent directive for both of your directives.
Another one is to use an intermediatory service which will hold values. Then you can do $watch on the service data and react accordingly.
You can $emit the event to the ctrl and then ctrl will $broadcast it down to the other directive.
Choose whatever fits your needs best.
I just got same problem.
Event between two directives was now passed, not with scope.$emit, not with scope.$broadcast.
After looking around, I did this trick,
Use scope.$parent.$broadcast with your $parent scope:
Directive 1:
scope.$parent.$broadcast('NO_LIVE_CONNECTION', {});
Directive 2:
scope.$on('NO_LIVE_CONNECTION', function (event, params) {
console.log("No live DB connections ...");
scope.state.showErrorMessage = true;
});

Angular communication between controllers and directives

I have this piece of code which allows a user to leave comments on a list of items.
I created a directive and listen to keydown in order to let the user submit a comment if keyCode == 13.
Not sure if I should include the code to post a comment within the directive. What is the best way to communicate between controllers and directives?
I also check whether or not the input is empty before submitting the comment. It works but not sure this is Angular best practice?
Here is my plunker.
you don't need to write a directive, if you want to use ng-keydown..
example:
template:
<input type="text" ng-model="myText" ng-keydown="checkKeyCode($event)">
controller: -- written in coffeescript
$scope.checkKeyCode = ($event)->
if $event.keyCode == 13 and $scope.myText?
$scope.doSomething()
You generally don't want your directives knowing anything about your controller, so the best(Angular) way of communicating between controllers and directives is through bi-directional bindings.
In your situation, I think best practice, again IMO, would be to create a directive for the button -- not the input. You'd tell the button which "input" (by id) to monitor. Something like:
<input id="input-{{item.id}}" type="text" ng-model="currMessage" />
<button class="btnMessage" ng-click="addMessage(currMessage, item)" default-input="input-{{item.id}}">Add</button>
ETA: Here's what the directive would end up looking like
http://plnkr.co/edit/HhEAUUq0IZvzblbRksBH?p=preview
myApp.directive('defaultInput', function () {
return {
restrict:'A',
link: function(scope, element, attrs) {
attrs.$observe('defaultInput', function(value) {
var inputElement = angular.element(document).find('#' + value);
inputElement.bind('keydown', function(e) {
if (e.keyCode == 13) {
element.click();
}
});
});
}
};
});
It could get tricky because the $observe callback will fire every time your controller's scope.items changes, so you'd need to somehow unbind and rebind (I know you're using jQuery, but I'm not seeing angular.unbind in the docs).
Another option, if you wanted to stick closer to your original approach:
http://plnkr.co/edit/3X3usJJpaCccRTtJeYPF?p=preview
HTML
<input id="input-{{item.id}}" type="text" ng-model="currMessage" enter-fires-next-button />
JavaScript
myApp.directive('enterFiresNextButton', function() {
return function(scope, element, attrs){
element.on('keydown', function(e){
if(e.keyCode == 13) {
element.next('button').click();
}
});
}
});
What is the best way to communicate between controllers and directives?
It depends... I like to first determine which type of scope is appropriate for a directive: no new scope, new scope, or new isolate scope. See When writing a directive in AngularJS, how do I decide if I need no new scope, a new child scope, or a new isolated scope?
Once that has been decided, the next decision is to determine if the communication should really be going to a service. If so, the controller and directive would both inject the service and interact with it, rather than each other.
If a service is not required, attributes are used to facilitate the communication between the controller and the directive. How that is done is determined by the type of scope the directive creates. Tip: if an isolate scope is not used, use $parse to get and set properties inside the directive, or to call methods on the controller from inside the directive -- see
How to set angular controller object property value from directive in child scope
https://stackoverflow.com/a/12932075/215945 - an example of calling a controller function with arguments

Resources