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.
Related
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.
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 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.
I created a custom directive in angular so that I can fade out a form on submit and replace it with a template with a custom message.
The desired workflow is as follows:
The user completes the form and clicks submit.
The controller updates the model with a new object and emits a "formSubmitted" event with some args.
The directive listens for the event and fades out the form.
The directive loads a partial html filled with the args from the event.
I resolved the first 3 steps, but I wasn't able to get around the final step (I don't want to hardcode the html as Strings, I want to pull it from another file if possible).
How can it be done?
Edit: some sample code (simplified):
This is the form:
<section class="hero-unit {{containerClass}}" tk-jq-replace-children>
<h2>{{formTitle}}</h2>
<form class="form-search" name="solform" novalidate>
<fieldset>
...
This is the controller:
if( formWasSavedOk ){
$scope.formSubmittedMsg = msg;
//Here emits the custom event
$scope.$emit( 'solicitud:formSubmitted' );
}
This is the directive:
.directive( 'tkJqReplaceChildren', function( $compile ){
return {
link: function( $scope, $iElement/*, $iAttrs*/ ){
//The directive listens for the event
$scope.$on( 'solicitud:formSubmitted', function( /*event*/ ){
$iElement
.children()
.fadeOut(1000)
.promise()
.done(function(){
var $detachedElments = $(this).detach();
//The html is compiled and added to the DOM
$iElement.html( $compile( "<h2>{{formSubmittedMsg}}</h2>" )($scope) );
$scope.$apply();
});
});
}
};
});
<h2>{{formSubmittedMsg}}</h2> is the code I want to pull from app/partials/create/createdOk.html (it is way more than just a header, that's why I want to load it from a file)
I'm not sure if you are looking for the $http service. I have created a plunker http://plnkr.co/edit/13kFLh9RTsIlO4TaFIFQ?p=preview, which doesn't cover the first three steps, but covers the 4th step you need.
In the plunker click on the text "Click here to submit the form", and notice the new text is is inserted. This new text is from the external file called tmpl.html. In the firebug console, you can notice a get call after you clicked the text, to fetch the tmpl.html
I believe the "Angular way" to fetch an external html snippet would be to use the ng-include directive:
<ng-include
src="{string}"
[onload="{string}"]
[autoscroll="{string}"]>
</ng-include>
As for why your directive didn't work, my guess is that you're fetching the html at the directive's link phase, rather than compile phase. There's a nice explanation on the difference on this answer, but it's pretty much this:
If you are going to make DOM transformation, it should be compile if
you want to add some features are behavior changes, it should be in
link.
I would recommend moving most of that logic away from the directive, to the controller: fetching the data using a $resource or $http service, and then passing the results to the newly created scope of the ng-directive.
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;
});