Access parent controller methods from directive? - angularjs

I have a question with some page about item search.
Let's say I have:
A controller with result items to display
A service that performs the search queries and interacts to the controller so it updates the result items to display
A directive which has the form with all the inputs of the search fields.
One of the buttons is a "View more items" button. It should only be shown if the response that I receive from a query tells me that there are more items to view.
I made a function inside the service which checks this property and returns a boolean to the directive which uses it with a ng-show on the button to show or hide it.
The problem is that I don't know how to interact with the controller within this scope.
Should the directive call one service method and this method should interact with the controller in some way?
Before this I was wrapping the directive with a form tag (outside the directive's scope) and then I could use the ng-submit to perform some action on the controller to call the service. Like this:
<form ng-submit="myController.submitSearch">
<search-options-directive></search-options-directive>
<button type="submit">
</form>
But now what I'm trying to do is to put the form and the buttons inside the
<search-options-directive></search-options-directive>
So now I don't have access to call controller methods directly.
How should I approach this?

My answer is a bit more specific to your title, "Access parent controller methods from directive." Within your directive you'll have to add the method that you want to share:
return {
scope: {
myFavoriteMethod:'&'
}
}
Then on:
<search-options-directive my-favorite-method="myMethodOnTheParentController"></search-options-directive>
In your directive you'll need to add to your html:
ng-click="myFavoriteMethod()"
But in your example you're using a ng-submit button which binds the expression to onsubmit event (overriding the original submit action), which is different from ng-click, but you should be able to use the same pattern.

Related

AngularJS: Calling a directive function from a controller the right way

I have two scenarios:
A function defined in a controller is called from a directive (see plunk).
The directive includes a '&' scope restriction to relate the controller and directive functions. If you click on the text, the element click event is triggered in the directive and the controller function is called. $scope.$apply() is used to notify AngularJS of the click event and refresh the value of var on the screen.
A function defined in a directive is called from a controller (see plunk).
The directive doesn't have any scope restriction, meaning that $scope in the controller and scope in the directive are shared. I defined func1() in the directive that can be called from the controller (try clicking on the text), however it seems intrusive as the controller needs to know the name of the function.
Is there a way to define func1() in such a way that the function name is declared in the directive div, similar to scenario 1?
Both options are possible and acceptable depending on how you are building the component.
Option 1 - Controllers should register listeners/callbacks and set attributes on directives (when using isolated scope - which i recommend). Controllers should not call a directives function but instead should change an attribute that has been bound on the directive. The directive if setup correctly should be watching this attribute and update accordingly.
Directives should not know who their controllers are in my opinion, it promotes decoupled code. Instead, controllers should be setting callback functions that directives can invoke at the appropriate time (i.e. letting the controller know something was clicked, selected, deleted, swiped, etc...)
Option 2 - Is acceptable to me when you are building a web component that provides the structure and behavioral logic but does not define the actual individual components UI (not including a possible skeleton UI). For example a list directive. The individual list may not be defined in the directive but "plugged in" for each context or use, giving us a more modular and reusable component. It does require us to know some things about the directive. It also requires that we modify the transclusion function to use the directives scope on the transcluded content instead of the original controllers scope. For an example you can checkout a list component I made as an example to this point.
http://github.com/Spidy88/ng-web-components
A snippet of text from this html page. The sf-list directive is an isolated scope directive that defines a lists behavior. However we can still define what each individual list item looks like with modified transclusion. It relies on us to call selectItem though in order to trigger selection behavior on the list.
<sf-list items="ctrl.emails" listener="ctrl.adapter">
<div class="list-item email" ng-click="selectItem(item)" ng-repeat="item in items track by item.id">
<div class="from">{{ item.from }}:</div>
<div class="subject">{{ item.subject }}</div>
</div>
</sf-list>

AngularJS: ng-click listen on a different / specific controller

I have a login form directive with its own controller. The controller basically does the user login when the form is submittted fine. This works absolutely fine.
I am trying to contain my user login / logout functions in one controller in this case the LoginCtrl. I have a logout button outside the directive somewhere in the header part. Is it possible for me to call the doLogout() function within the LoginCtrl when that button is clicked ?
The only solution I have so far is to broadcast event on the rootScope and listen for the same on the LoginCtrl.
Any other alternatives ?
You could keep the actual functionality wrapped in a service (might want to do that anyway). This way you can use standard dependency injection to inject the service into whatever controller contains the logout button, and let it handle everything like a route change, etc.
The directive is not necessarily bind to a controller, in which case it can has its own scope.
That is to say, you can pass the callback directly to the directive via the directive scope "&" operator.

Access to form controller hidden due to angular ui tab isolated/inherited scope

I have a simple case:
<div ng-controller="myController">
<tabset>
<tab>
<form name="myForm"></form>
</tab>
</tabset>
</div>
and now, in myController method, I would like to access myForm to call $setPristine:
$scope.myForm.$setPristine()
but I can not. tabset / tab creates isolated/inherited scope. It's just a sample, but I run into this problems when using angular js directives that create isolated scopes many times.
How can I get over this issue? In the past I did something like this (with ng-table that also creates new scope):
ng-init="$parent.saataaTable = this"
but it's far from perfect.
This was one of the most difficult concepts for me to get around and my solution is simple but kind of difficult to explain so bear with me.
Solution 1: Isolate Scopes
When you are only dealing with only isolate scopes (scope: {...}) or no scope (scope: false), you're in luck because the myForm will eventually be there. You just have to watch for it.
$scope.$watch('myForm', function(val) {
if (myForm) {
// now I can call $setPristine
}
});
Solution 2: Child Scopes
This is when you set scope: true or transclude: true. Unless you perform a custom/manual transclusion you will not get myForm on the controller's scope.
The trick is to access the form's controller directly from the form element. This can be done by the following:
// at the form element
element.data('$formController');
// or at the control (input, select, etc.)
element.inheritedData('$formController');
// where 'element' is a jqLite element (form or ng-form)
This sets you up for a new issue: how do we know when and how we can get that element and it's data.
A quick answer is that you need to set up a dummy $watch on your controller's scope to look for (in your case) myForm. When this watch is processed you will then be able to attempt to locate the form. This is necessary due to the fact that typically when your controller first executes the FormController won't yet be on the element's data object.
A quick and simple way to find the form is to simply get all of the forms. NOTE: if there are multiple forms within the element you'll have to add some logic to find the right one. In this case our form is a form element and it's the only one. So, locating it is fairly easy:
// assuming you have inject $element into your controller
$element.find('form').data('$formController');
// where $element is the root element the controller is attached to
// it is injected just like '$scope'
Once you have the controller you can access everything you would normally. It is also important to note that Solution 2 will always work once that FormController is on the element.
I have set up a Plunk to demonstrate the code here, but please note that is a demonstration so not all best practices were kept in mind.
EDIT
I found it important to note that if you don't want to worry about the scopes of the nested directives you can just watch the form name on the scope and handle things there.
$scope.$watch('myForm', function(val) {
if (angular.isDefined(val)) {
// now I have access
} else {
// see if i can `find` the form whose name is 'myForm'
// (this is easy if it is a form element and there's only one)
// then get the FormController for access
}
}
I could not make it work using the answer above, but I found a work-around.
In the form, I created a hidden input field with a ng-model and ng-init that set its value to the form. Then in my submit function in the controller I can access the formController via this ng-model
So, in the HTML, I create a hidden field inside the form:
<input id="test" ng-model="data.myForm" ng-init="data.myForm=myForm" hidden>
And in the Controller I can get hold of the formController via data.myForm
$scope.data.myForm.$setPristine();
It is probably not very good, so I will instead avoid to rely on the $pristine and $dirty properties of the formController and find another way to detect if the form has changed (using a master copy of the object, like they do in the sample in the documentation)

Error: $apply already in progress

I'm using this http://plnkr.co/edit/sqXVCJ?p=preview on my Angular UI accordion. I've put the directive attribute on the anchor (which is in the template) and I've overriden the template to remove the ng-click="isOpen = !isOpen" and replaced it with a function call "callSubmit". All the accordions have views loaded into them, all of the views have forms as well. The purpose of the callSubmit function is to submit the form which works fine but I get the error above.
I've read through various posts regrding adding the $timeout service (which didnt't work) and adding the safe apply method which gave me recursion and digest errors.
I'm not sure what else I can try, the function works fine, but I just keep getting that error.
!--- button
<button type="submit" class="btn btn-default hidden" id="btnSubmit_step1" ng-click="submitForm()">Trigger validation</button>
!-- function
$scope.callSubmit = function() {
document.getElementById('btnSubmit_step1').click();
};
edit: The reason that I have been triggering the button click from the controller is due to the fact that the form method is in another scope & a different controller.
So if I use broadCast from the rootScope like below. I get the broadcast event to the other controller without issue. The controller that receives the broadcast has the form in it's scope but when I try to execute the function I get nothing, no form submission at all.
$$scope.callSubmit = function() {
//document.getElementById('btnSubmit_step1').click();
$rootScope.$broadcast('someEvent', [1,2,3]);
};
$scope.$on('someEvent', function(event, mass) {
$scope.submitForm();
});
Don't click the button from a controller.
$scope.callSubmit = function() {
$scope.submitForm();
};
The documentation is clear...
Do not use controllers to:
Manipulate DOM — Controllers should contain only business logic.
Putting any presentation logic into Controllers significantly affects
its testability. Angular has databinding for most cases and directives
to encapsulate manual DOM manipulation.
Clicking a button on a page is manipulating the DOM.

Communication between AngularJS Directive & Parent Controller

I am new to AngularJS and currently stuck with a design question. Sorry in advance as this is going to be long-winded.
Scenario#1
I'm using a third-party directive (type-ahead) which exposes a event "selected" via $emit. I need to update the model on the type-ahead "selected" event which in-turn drives some other logic.
I feel that handling the selected event in the parent controller (testController) is not ideal since if there are multiple typeahead directives in the same scope, how do I associate the event with the element when im doing this wire-up outside the directive ?
So watching on the model property for changes(name1) seems to be the only clean option. Am I correct ?
<div ng-app="testApp">
<div ng-controller="testController">
<type-ahead ng-model="name1" source="typeAhead1Data"></type-ahead>
<!--<type-ahead ng-model="name2" source="typeAhead2Data"></type-ahead>-->
</div>
</div>
angular.module('testApp').controller('testController', ["$scope", function ($scope) {
$scope.typeAhead1Data = ['abc','def','ghi'];
//$scope.typeAhead2Data = ['jkl','mno','pqr'];
//This seems like a bad idea since what if I had another type-ahead
//control in the scope of the same controller...
$scope.$on('typeahead:selected', function (e, val) {
//logic to be performed on type-ahead select
$scope.name1 = val;
});
/*
// the other approach that came to mind is doing a watch
$scope.$watch('name1', function () {
//logic to be performed on type-ahead select
});
*/
}]);
Scenario#2
Lets say I have a directive that adds a menu to every list item in an unordered list.
The menu item click should trigger an action. If the directive raises an event via $emit, I will run into the same issue of associating the event with the element and performing the necessary post-processing.
In jquery, the way I would have done this is add a class to the list item and attach an event using the class selctor.
Any thoughts ?
thanks
You could create a child scope for the directive with scope:true option and then move the .$on('typeahead:selected') event inside the link function of the directive. That way you could have a separate event listener for each directive.

Resources