What would be considered as best practice, attaching directive to element or binding event inside the controller?
Directive
<openread-more what-to-expand="teds-bets-readmore" />
myApp.directive('openreadMore', function () {
return {
restrict: 'AE',
replace: false,
template: '<a class="attach-event" what-to-expand="readmore1">Event</a></span>',
link: function (scope, elem, attrs) {
elem.on('click', function () {
// attached code on click
});
}
}
});
Just attaching it inside the controller
homepageCtrls.controller('homepageCtrl', function ($scope, $http) {
angular.element(document.querySelectorAll('.attach-event')).on('click', function () {
// attached code on click
});
});
The second option seems shorter and much cleaner, but i don't know if it's considered as best practice or not.
Just use the ng-click directive.
<openread-more what-to-expand="teds-bets-readmore" ng-click="doSomeAction()" />
And on the controller:
homepageCtrls.controller('homepageCtrl', function ($scope, $http) {
$scope.doSomeAction = function() {
// onClick logic here...
};
});
Edit
In case you are binding other kind of events, just make this question to yourself: "Will I have different behaviours for this event depending on the current view or application state?".
If the answer is yes then you should register the event handlers on the controllers. If the answer is no (which means you will have always the same behaviour) then register and handle the events on the directive.
Nevertheless, you should not access UI elements on the controllers (e.g. don't use selectors or anything similar). The controllers are supposed to be reusable, which means you should be able to use them on different UIs, with different UI elements. The best approach is to define a directive that allows you to bind specific events, like Angular UI Event Binder.
Related
I have an Angular app where I'm using ui-grid. I want to have a custom action on a cell of the grid that calls a method from my app. So basically, this means calling a method that's somewhere up in the parent hierarchy, from a directive.
This would be achieved by calling something like: $scope.$parent.$parent.$parent.$parent.foo(). But that doesn't seem too nice.
One option would be to create a recursive function that goes up the ancestry of the $scope. That's nicer, but still seems a bit weird.
Also... Is it good practice to try to achieve something like this?
You're correct that $parent.$parent.$parent is definitely not a good practice.
If the method you're calling is another directive, you can require that directive in your child directive and then, the parentDirective's controller function will be injected as the fourth parameter to your link function:
In your DDO:
return {
require : '^parentDirective',
restrict : 'E',
link : function (scope, elem, attrs, parentDirectiveController) {}
}
If what you're trying to call is on a factory/service, you can inject that factory/service into your directive, although this sometimes is a code smell, depending on what you're trying to inject.
Finally, another way to do it is to use event propagation. From your directive, you can use $scope.$emit to send information up to parent controllers:
From the directive:
$scope.$emit('directiveDidStuff', {
data : 'blah'
});
In the parent controller:
$scope.$on('directiveDidStuff', function (evt, params) {
this.data = params.data; // equals blah
});
You can achieve the same by using "&" through one of the scope variable in directive.Like this, you can bind your event to the controller method and from the method, you could do your desired things or if the original business logic which you wants to achieve on onClick of the grid is used across many modules than you can bisect it in service and make it reusable and call the service from the event method. Let me know if you do have any doubts with the approach.
Key Code of example:
Html
<my-component attribute-foo="{{foo}}" binding-foo="foo" isolated-expression- foo="updateFoo(newFoo)" >
Directive
var myModule = angular.module('myModule', [])
.directive('myComponent', function () {
return {
restrict:'E',
scope:{
/* NOTE: Normally I would set my attributes and bindings
to be the same name but I wanted to delineate between
parent and isolated scope. */
isolatedAttributeFoo:'#attributeFoo',
isolatedBindingFoo:'=bindingFoo',
isolatedExpressionFoo:'&'
}
};
})
I'm refactoring some of my Angular JS application, and I'm going to learn more about directives.
I've read many times that bind a controller to a directive is a good practice, if we want to share logic and get code clean.
Bind a controller to a directive to share common tasks between many directives is pretty simple and I understand the interest of this pattern. But my question is why do we need to use a controller ?
(Example code come from this site)
Pattern 1 : Use controller to share logic between directives
Bind a controller to directive :
app.directive("superhero", function () {
return {
restrict: "E",
controller: function ($scope) {
$scope.abilities = [];
// [...] additional methods
this.addFlight = function() {
$scope.abilities.push("flight");
};
},
link: function (scope, element) {
element.addClass("button");
element.bind("mouseenter", function () {
console.log(scope.abilities);
});
}
};
});
Share logic with another directives :
app.directive("flight", function() {
return {
require: "superhero",
link: function (scope, element, attrs, superheroCtrl) {
superheroCtrl.addFlight();
}
};
});
When I want to share logic between my controller I create a Factory that I inject into my controller. So why do not use the same pattern ?
Pattern 2 : Use factory to share logic between directives
Declare the new factory :
app.factory("myAwesomeFactory", function () {
return {
addFlight: function () { /* ... */ }
};
});
Use the factory into directive :
app.directive("flight", function(myAwesomeFactory) {
return {
require: "superhero",
link: function (scope, element, attrs) {
myAwesomeFactory.addFlight();
}
};
});
I can't understand why the first method is better than the second.
Bonus question : Why do we use this keyword in controllers which are binded to directives ?
Thanks a lot. I've found lots of tutorials about how to bind a controller to directive. But no one explains why we need to do this way.
The biggest reason I've run across is that, since services are singletons, you can run into serious problems by having multiple directives relying on logic from the same service. This is why anything that has to do with the view is done through the controller. While you can sometimes get away with using the service within the directive, it's better to avoid the practice altogether if possible.
I want to create an angular directive inside of a link function, however; the directive created is not able to be compiled.
See this JSFiddle: http://jsfiddle.net/v47uvsj5/5/
Uncommenting this directive in the global space works as expected.
app.directive('test', function () {
return {
templateUrl: 'myform', // wraps script tag with id 'myform'
restrict: 'E',
require: "^mydir",
replace: true,
scope: {
},
link: function (scope, element, attrs, mydirCtrl) {
scope.remove = function () {
element.remove();
mydirCtrl.remove();
}
}
}
});
But the exact same code inside the link function fails.
The reason I want to do this is because I want the user (who is going to be myself) to be able to provide only a script tag's id via an id attribute to my main directive which will in turn create a 'wrapper' directive with a 'remove' method. This way, in the script tag, all one needs to do is implement the 'remove'.
Check out this fiddle:
http://jsfiddle.net/v47uvsj5/8/
What I did in this fiddle was daisy chain your directives, which is the correct thing to do. When your app runs, it does a binding of each of your directive and builds your html as it's being compiled, then it links events to it. Links and compilation happen after binding all directives to the DOM.
So <test></test> becomes <div></div> if you give it a template. If there is no template, nothing really builds your directive against the DOM, it just becomes empty, but you can still run a jquery script if you want.
Think of it like this, when your app loads up, it registers all the directives to be binded with the associated templates. Afterwards, the app then "compiles" those directives by binding any kind of events to the newly established DOM. At this point, if no directives are registered during app load, the compile function ignores it. In your case, you tried to bind the 'test' directive after the app load, and during the compilation.
This mechanism is analogous to how jquery's "on" works. When you do a "click" event on an already loaded DOM element, this fires up. But when you load html AFTER the DOM is finished, nothing works unless you use "on".
To be fair, the developers of angular did mention how there's a steep learning curve for handling directions, and will be revised to make it much easier in 2.0. You can read about it in this blog here: Angular-2.0
Anyways,
This is how your html should look like:
<mydir><test></test></mydir>
and this is how you daisy chain:
var app = angular.module('app', []);
app.directive('mydir', function ($compile, $templateCache) {
return {
template: '',
restrict: 'E',
controller: function () {
console.log("got it!");
}
}
}).directive('test', function () {
return {
templateUrl: 'myform',
restrict: 'E',
require: "^mydir",
replace: true,
scope: {
},
link: function (scope, element, attrs, mydirCtrl) {
scope.remove = function () {
element.remove();
mydirCtrl.remove();
}
}
}
});
I have a directive that broadcasts an event when a table row gets clicked. Here is the directive:
angular.module('app').directive('encounterItemTable', function () {
return {
restrict: 'A',
replace: true,
templateUrl: 'views/encounter.item.table.html',
scope: {
encounters : '='
},
link: function(scope) {
scope.getSelectedRow = function(index) {
scope.$broadcast('selectedRow', { rowIndex: index });
};
}
};
});
Here is the markup that calls the getSelectedRow
<tr ng-class="{selected: $index==selectedIndex}" ng-repeat="encounter in encounters | filter:search" data-id="{{encounter.id}}" ng-click="getSelectedRow($index)">
My getSelectedRow() function gets called when the row gets clicked. The index is correct. The controller on the other hand never hears anything. Here is the code in the controller.
$scope.$on('selectedRow', function(event, data) {
$scope.selectedIndex = data.rowIndex;
$scope.selectedEncounter = $scope.encounters[data.rowIndex];
});
Why would the controller not hear the event? What am I missing?
I use $rootScope.$broadcast(event, data). I use events to decouple components i.e. components emitting events and listeners for events don't need to know about each other.
In your case where the event could reasonably contained to the component (directive) then you have to care about where in the DOM the relative positions of the listener/emitter are. I haven't run into this myself so generally use $rootScope.$broadcast() another benefit being any component in the app can listen to these events so something in a sidebar could update in relation to the events from the table (which probably not be in the same DOM hierarchy)
It is $rootScope.$broadcast. If it were to broadcast only to the current scope, your controller wouldn't see it.
$broadcast sends events down to children. directive is nested within controller so need to send event up to a parent. Use scope.$emit to push events up through parents. Read the section in scope docs titled Scope Events Propagation
Here is my problem. For example, we have the following directive, which uses some jQuery widget behind the scenes :
module.directive('myWidget', [function() {
return {
require: "ngModel",
restrict: "A",
replace: true,
templateUrl: "templates/myWidget.html",
link: function(scope, element, attrs, ctrl) {
element.widget_name().on('value_updated', function(event) {
scope.$apply(function() {
var newModelValue = event.some_value;
ctrl.$setViewValue(newModelValue);
});
});
scope.$watch(attrs["ngModel"], function(value){
element.widget_name('set_value', value);
});
}
};
}]);
So, if model's value changes, then the handler which is registered using $watch to listen for changes in model will be executed, and, consequently, widget's 'set_value' method will be executed too. This means that 'value_updated' event will be triggered.
My question is: what is the best practice to implement similar behavior in directives to avoid extra calls of DOM event handlers and watchers?
Instead of scope.$watch(), I suggest implementing ctrl.$render(). $render should only be called if something inside Angular changes the model. Fiddle example.
This solves a problem you did not mention. Unfortunately, it does not solve the problem you did mention. In the fiddle, a blur event is bound, rather than some widget.on() event. Maybe that would work for you – i.e., only update the model on blur, rather than every keystroke (this assumes your widget is accepting keystrokes, however).
Maybe you could also ask the widget author to provide a "set" method that does not trigger an event. Then that could be used in the $render() method.