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
Related
I am looking to find the best way of sending scope through nested directives.
I have found that you can do $scope.$parent.value, but I understood that's not a best practice and should be avoided.
So my question is, if I have 4 nested directives like below, each with it's own controller where some data is being modified, what's the best way to access a value from directive4 (let's say $scope.valueFromDirective4) in directive1?
<directive1>
<directive2>
<directive3>
<directive4>
</directive4>
</directive3>
</directive2>
</directive1>
For the "presentational" / "dumb" components (directive3 and directive4), I think they should each take in a callback function which they can invoke with new data when they change:
scope: {
// Invoke this with new data
onChange: '&',
// Optional if you want to bind the data yourself and then call `onChange`
data: '='
}
Just pass the callback down from directive2 through directive4. This way directive3 and directive4 are decoupled from your app and reusable.
If they are form-like directives (similar to input etc), another option is to look into having them require ngModel and have them use ngModelController to update the parent and view. (Look up $render and $setViewValue for more info on this). This way you can use them like:
<directive4 ng-model="someObj.someProp" ng-change="someFunc()"></directive4>
When you do it like this, after the model is updated the ng-change function is automatically invoked.
For the "container" / "smart" directives (directive1 and directive2), you could also have directive2 take in the callback which is passed in from directive1. But since directive1 and directive2 can both know about your app, you could write a service which is injected and shared between directive1 and directive2.
Nested directives can always have an access to their parents' controllers via require. Let's say you want to change value from the directive1's scope from any of its nested directives. One of the possible ways to achieve that is to declare a setter in the directive1's controller setValue(value). Then in any of nested directives you need to require the directive1's controller and by doing that you'll get an access to the setter setValue(value) and other methods the controller provides.
angular
.module('yourModule')
.directive('directive1', function() {
return {
controller:['$scope', funciton($scope) {
return {
setValue: setValue
};
funciton setValue(value) {
$scope.value = value;
}
}]
// The rest of the directive1's configuration
};
})
.directive('directive4', function() {
return {
require: '^^directive1',
link: (scope, elem, attrs, directive1Ctrl) {
// Here you can call directive1Ctrl.setValue() directly
}
// The rest of the directive4's configuration
};
})
Another way is to $emit events from a child directive's controller whenever value is changed by the child. In this case the parent directive's controller should subscribe to that event and handle the data passed along with it.
I have a generic functionality implemented inside a controller. When i write a directive is it good idea to extend those controller functions inside the directive ?
Like in below implementation inside the link function.
var superCancel = scope.cancel;
// Overriding the cancel function from the controller
scope.cancel = function() {
if(element.hasClass('ng-dirty')){
element.removeClass("ng-dirty");
}
// Calling controller cancel
superCancel();
};
If your directive html is coming inside the controller in html then you can use $parent instead of rewriting
in directive:
$scope.$parent.cancel(); // only if controller coming as parent
If the controller is not coming as parent it's better to use a service or factory to implement that
Read here for more
It is better to have the directive use an attribute to set a parent scope value.
For example:
JS
app.directive("setModelApi", function() {
return {
require: "ngModel",
link: function(scope,elem,attrs, ngModelCtrl) {
scope.$eval(attrs.setModelApi, {$api: ngModelCtrl})
}
}
});
In the above example, the setModelApi directive evaluates the Angular Expression defined by the set-model-api attribute with $api exposing the ngModelController.
HTML
<input ng-model="x" set-model-api="xmodel=$api">
<button ng-click="xmodel.$setPristine()">Set Pristine</button>
The setModelApi directive sets the xmodel scope variable to the ng-model-controller API.
The button invokes the $setPristine method of the ng-model-controller API.
From the Docs:
$setPristine();
Sets the control to its pristine state.
This method can be called to remove the ng-dirty class and set the control to its pristine state (ng-pristine class). A model is considered to be pristine when the control has not been changed from when first compiled.
-- AngularJS ngModelController API Reference -- $setPristine
By using an HTML directive attribute to define the scope variable to which the API attaches, different inputs can use the directive and the connections are exposed in the HTML.
The DEMO on JSFiddle
I have a directive which consists of a form text element and a continue button along with the associated controller etc. This directive is going to be used in about 5 different pages, but on each page it is used the continue button will do something different.
My question is where can/should I put the code for the continue button if it does different things for each page?
Since its a directive I cant simply pass a different function into ng-click depending on what page im on (ie, if i simply replicated the code on each page it is used I could simply change the function called on ng-click and have that function in each of the page controllers.
Hopefully Im not being too vague with my question and you can make sense of what im asking. If not just say so and ill try to explain in more detail.
I would really appreciate some guidance on this matter.
Thanks.
There are two ways that you can do it. If you are creating your directive as a true component you can use isolated scope with & binding that binds to an expression.
Assume your directive looks like
<div do-work on-click="save()"></div>
and the generated html
<div>
<input ...>
<button ng-click="doAction()"><button>
</div>
The directive scope will be defined
scope:{
onClick:'&'
}
In your directive controller or link function you need to implement the button doAction, which in turns evaluates the onClick action
scope.doAction=function() {
scope.onClick({//if params are required});
}
Now you have linked the parent through the direct onClick reference. One thing to remember here is that this creates a directive with isolated scope.
In case you do not want isolated scope created you need to use
scope.$eval(attr.onClick); // this evaluates the expression on the current scope.
Hope this helps.
Ideally you should not create directives which are not re-usable.
In your case, you may do it like following -
create an isolated scope in the directive
add a function to be called and pass the page/ page id as parameter
call functions in controller based on parameter
Directive
myApp.directive('someDirecive', function () {
return {
// restrict options are EACM. we want to use it like an attribute
restrict: 'A',
// template : <inline template string>
// templateUrl = path to directive template.
// templateUrl: '',
scope: {
onButtonClick : '&'
},
controller: function ($scope, $element, $attrs, $transclude) {
$scope.onButtonClick = function(pageId) {
if (pageId == 1) {
// do something
}
else if (pageId == 2) {
// do something
}
}
},
//link: function (scope, iElement, iAttrs) {
//}
};
});
HTML
<div some-directive on-button-click="DoSomething(1)" />
I have a simple Angular Problem - I think it's probably a case of can't see the wood for the trees here.
I have an input field with a directive attached. The purpose is eventually to compare new with old data and show a popup. However, as soon as I add the directive attribute to the input field, the value disappears:
Plunk here: http://plnkr.co/edit/BQvKGe6kjuD0ThPBYJ4d?p=preview
HTML:
First Name:
<input type='text' ng-model='currentEditItem.strFirstName' name='strFirstName' id='strFirstName'
cm-popover="currentEditItem.personOldData.strFirstName"/>
<br><br>
ngModel: {{currentEditItem.strFirstName}} <br>
cmPopover: {{currentEditItem.personOldData.strFirstName}}
JS
var app = angular.module('app', []);
app.controller('Ctrl', function ($scope) {
$scope.currentEditItem = {};
$scope.currentEditItem.strFirstName = "Bob";
$scope.currentEditItem.personOldData = {};
$scope.currentEditItem.personOldData.strFirstName = "Roger";
});
app.directive("cmPopover", function () {
return {
scope: {
ngModel: "=",
cmPopover: "="
},
link: function (scope, elem, attrs) {
console.log("ngModel", scope.ngModel);
console.log("cmPopover", scope.cmPopover);
}
}
});
If you go to the Plunk and remove the cm-popover attribute, the input field is filled with the value from the model. When the attribute is added the value disappears although the model is still in the scope with the correct value.
In your directive you declare an isolate scope. This input's scope is now this isolate scope since it's the directive element. It's looking for the currentEditItem object which doesn't exist in the isolate scope
ngModel does not create a new isolated scope for itself so it can $watch without having to hardcode a $parent in it's internal code.
But then you add another directive on the same DOM node that creates an isolated scope for itself. Couple this with the fact that you can only have a single isolated scope on a DOM node and you basically force ngModel to use/work with the same scope cmPopover created.
So when writing ng-model="currentEditItem.strFirstName" you are actually addressing the $scope inside the cmPopover directive, no the one in the (parent) controller. You can check this is the case by using ng-model="$parent.currentEditItem.strFirstName" - and it will work.
There's quite a lengthy conversation here with a lot of possible workarounds and solutions that leads to an actual fix in release 1.2.0.
So long story short: update to at least AngularJS 1.2.0 and this will work.
I created a directive called dt-modal under the dt module. In my main app's module called demo, I use this dt-modal which has an isolated scope. I created this directive such that the HTML form written within the directive is transcluded since I want to reuse this modal for many different forms.
<dt-modal>
<form ng-controller="ReviewFormController"
name="reviewForm"
novalidate
ng-submit="reviewForm.$valid && submitReview(review)">
<!-- form contents here -->
</form>
</dt-modal>
This transcluded form has a custom controller called ReviewFormController that listens for the submit event. How can I call the close() method on the dt-modal's scope from within submitReview() in ReviewFormController?
Here is a JSBin. If you hit ESC, you can see close() in the directive run.
http://jsbin.com/cukanole/1/edit
If this isn't possible, is there a better design for this directive?
Thanks in advance!
Since you are using an isolated scope, you could pass a control object to the directive...
<dt-modal id="review-form-modal" api="modal.api">
and add the close method to it via two-way binding:
scope: {
api: '='
},
link: function($scope, $el, attrs) {
$scope.api = {
close: function() {
$el.css({
display: 'none'
})
}
}
...
Then ng-click can use the control object to call close:
<button type="submit" ng-click="modal.api.close()">Submit</button>
If you want to try this code, here it is on Plunker.
My recommendation is to use $emit to trigger the event from the controller and use $on on the directly.
Controller
scope.$emit("ValueChanged", value);
In the directive the event will be captured using $on like:
$scope.$on("ValueChanged", function(event, ars){
... //your event has been triggered.
});
Important:
Directives should be always independent components, if inside the directive there is a call to a method from a controller(outside the directive) this will create a dependency between my directive and the controller and of course this will force one not being able to exist without the other.
If I would have to apply a design principle to a directive it will be the S in SOLID, Single responsibility principle. Directives should be able to encapsulate and work independently.