Avoiding too many Angularjs Directives - angularjs

I have the following directive:
.directive('myDirective', function() {
restrict: 'A',
templateUrl: 'app/templates/someTemplate/html',
});
in my template (someTemplate.html) I have the following:
<div>
<div>Some div</div>
<input type="button" value="button" />
</div>
I want to add behavior to the button and div. I can go the route of adding directives like so:
<div>
<div div-click>Some div</div>
<input type="button" value="button" button-click />
</div>
and adding more directives and binding click events via element.bind(... but is there a best practice? Should I be adding behavior in the 'myDirective' containing those elements? via jQuery or jQlite . The clickable elements inside the template are not meant to be resuable..so should I just use jQuery to find those elements and bind event listeners to them? I can see how their can be a directives explosion by constantly using the directive route, what is the best practice?

The question for me would be on what exactly the directives should be for.
It sounds to me, as if you are trying to wrap functionality that you know from other frameworks like jQuery into directives. this leads to stuff like:
var app = angular.module("module.directives", []);
app.directive('myDirective', function() {
restrict: 'A',
templateUrl: 'app/templates/someTemplate/html',
link: function(scope, el) {
el.on("click", function() { console.log(42); });
}
});
While certainly possible, this is (at least for me) considered "bad" style.
The difference with Angular is, that it does not use the DOM as the "Model" part of the framework, like jQuery or Prototype do. Coming from these libraries this is something to wrap your head around, but actually, for starters, it boils down to this:
Work with the scope and let the changes to the scope be reflected in the DOM.
The reflection part is actually the short and easy one: Angular does this out of the box (i.e. "most of the time").
Reconsidering your example with the click - Angular provides excellent event handlers in the form of directives. ng-click is a very good example for this:
<div>
<div ng-click="method()">Some div</div>
<input type="button" value="button" ng-click="method2()" />
</div>
This directive takes an expression - it looks a bit like the old days, where you would bind javascript directly to elements, like this:
here
It's way different though - Angular will look for the names method and method2 on the current scope you are in. Which scope you are currently in depends on the circumstances (I heavily suggest the docs at this point)
For all of our intents and purposes, lets say, you configure a controller inside your directive from earlier:
var app = angular.module("module.directives", []);
app.directive('myDirective', function() {
restrict: 'A',
templateUrl: 'app/templates/someTemplate/html',
controller: ['$scope', function(scope) {
scope.active = false;
scope.method = function() { console.log(42); };
scope.method2 = function() { scope.active = !scope.active };
}]
});
You can define this in many places, even as late as during the link phase of a directive. You can also create an extra controller in a separate module. But let's just stick with this for a moment:
In the template - when you click on your div the scope's method will be called. Nothing fancy, just console output. method2 is a little bit more interesting - it changes the active variable on the scope. And you can use this to your advantage:
<div>
<div ng-click="method()">Some div</div>
<input type="button" value="button" ng-click="method2()" />
<span ng-show="active">Active</span>
</div>
When you click on your button, the span will be turned on and of - the ng-show directive handles this for you.
This has gotten a bit longer than expected - I hope though, that this sheds some light on the "best practises" (which are quite dependent on what you actually want to accomplish).

Related

Can an angular directive contain a controller?

Or perhaps a better question is, should a directive contain a controller?
For reasons of separation, my index.html is a simple file. Everything is rendered into it via templates. So my index.html is real simple:
<body ng-app="myapp"><mainmenu></mainmenu><div ng-view></div></body>
I don't really need a directive for mainmenu, but it allows me to put the menu in a separate template file. The main menu itself contains user info, login/logout, and a search box.
<div class="leftmenu" ng-show="isLogin()">
<ul class="menu">
<li>Part1</li>
<li>Part2</li>
<li>Part3</li>
</ul>
<div ng-controller="Search" class="Search><input type="text" ui-select2="s2opts" style="width:250px;" ng-model="search" data-placeholder="search"></input></div>
</div>
<div class="rightmenu">
<ul ng-show="isLogin()" class="menu">
<li>My Account</li>
<li>Logout</li>
</ul>
<ul ng-show="!isLogin()" class="menu">
<li>Login</li>
<li>Register</li>
</ul>
</div>
So there is the menu part, with its own controller, and the search, with its own, embedded between the two parts.
Of course, my mainmenu directive unit tests fail because SearchController isn't defined. But this leaves me wondering if I am going about this wrong. Should I even have it like this, a section of html with an explicit ng-controller defined inside it? Doesn't this create all sorts of weird dependencies?
How should I better structure this? A search directive that is included so I can unit test it separately? Something feels wrong here structurally...
UPDATE:
As requested, a basic fiddle: http://jsfiddle.net/nj4n44zx/1/
As specified by the Angular documentation, the best practice is to define a controller inside a directive only to expose an API to another directive. Otherwise the link function is sufficient.
See at the bottom of :
Angular directives
By experience using controllers inside a directives shadow what you are doing in your scope. It does not help to have a easy readable code.
I do prefer using the main controller where the directive is included. With a non isolated scope you have access to everything from the link function.
I usually deal with it like that:
app.directive('topMenu', function() {
return {
restrict: 'E', // or whatever You need
templateUrl: '/partials/topmenu', //url to Your template file
controller: function($scope) {
$scope.foo = "bar";
}
};
});
Then, in that template You don't have to add ng-controller.
sure your directive can contain a controller because you declare a directive like this
myApp.directive('mainMenu', function() {
return {
restrict: 'E',
replace: true,
scope: true,
templateUrl: 'menu.html',
controller:['$scope', function($scope) {
//define your controller here
}]
};
});

ng-repeat inside compiled html for directive

I have two directives:
window.app.directive('placeholder', function ($compile, $route, $rootScope) {
return {
restrict: 'AC',
link: function (scope, element, attr) {
// Store the placeholder element for later use
$rootScope["placeholder_" + attr.placeholder] = element[0];
// Clear the placeholder when navigating
$rootScope.$on('$routeChangeSuccess', function (e, a, b) {
element.html('');
});
}
};
});
window.app.directive('section', function ($compile, $route, $rootScope) {
return {
restrict: 'AC',
link: function (scope, element, attr) {
// Locate the placeholder element
var targetElement = $rootScope["placeholder_" + attr.section];
// Compile the template and bind it to the current scope, and inject it into the placeholder
$(targetElement).html($compile(element.html())(scope));
element.html('');
}
};
});
I use them to basically swap out one section with html in another.
If I have the following html:
<div placeholder="footer"></div>
<div section="footer">
<ul ng-model="items">
<li ng-repeat="item in items"> {{item.Description}}</li>
</ul>
</div>
The ng-repeat doesn't seem to be working. If I simply output {{items}} below the , it displays fine. Also, I know binding is working because I can change items and it will update.
Lastly, if I move the ul outside the section it works fine.
So, my question is why does this not work (compile ng-repeat inside directive).
Am I missing something?
EDIT:
What is confusing me, is I can do this:
<div section="footer">
<!-- This Works -->
{{items}}
<!-- This also works -->
<input type="text" ng-model="items[0].Description" />
<!-- This doesn't work -->
<ul ng-model="items">
<li ng-repeat="item in items"> {{item.Description}}</li>
</ul>
</div>
This isn't going to work. It can't evaluate something from another scope without having an exact copy of it in its scope. If you want two directives to communicate use require and setup a way for them to do that if they aren't in a parent child relationship.
A couple of things you should think about. Essentially what you are doing is called transclusion. Section directive would use ng-transclude to capture the client's defined code. Use transclusion and maybe you can evaluate the template into html in the scope of section then using directive communication allow it to pass the HTML block (already evaluated) to the other directive. The only problem is making sure this happens when things change through binding. You're probably going to need some $watches on variables in section in order for placeholder to be notified when things change.
You will probably need a 3rd directive so allow section and placeholder to communicate through. In this example say I have a 3rd directive called broadcaster. Then section and placeholder will require broadcaster (ie require: '^broadcaster'), and it will define some interface for each of the directives to send HTML from section -> placeholder. Something like this:
<div broadcaster>
<div placeholder="footer"></div>
<div section="footer">
<ul>...transcluded content</ul>
</div>
</div>

Angular Directive not replacing Element

I'm trying to create a directive that will work against xml that i am injecting into the dom via a service.
Here is my a relatively reduced example, having removed the async service call for data and also the template: jsBin - transforming elements using directive
Everything works well with regard getting my post elements' header attribute into an h2 tag but it is leaving a element within my page which will fail for some browsers.
to clarify, this is what i get:
<post class="ng-isolate-scope ng-scope" heading="test heading">
<div class="ng-scope">
<h2 class="ng-binding">test heading</h2>
<div>test</div>
</div>
</post>
and this is what i would expect:
<div class="ng-scope">
<h2 class="ng-binding">test heading</h2>
<div>test</div>
</div>
I think Adam's answer is the better route, but alternatively and easier if you include jquery you can do this in your link function:
var e =$compile(template)(scope);
element.replaceWith(e);
You aren't using template correctly in your directive. Your link function should not applying your template as you are in the example code.
var linker = function(scope, element, attrs) {
var template = getTemplate();
element.html(template);
$compile(element.contents())(scope);
};
Instead of that, just do this:
return {
restrict: 'E',
replace: true,
scope: {
heading: '#'
},
template: '<div><h2>{{heading}}</h2><div>test</div></div>'
};
In your post directive. 'replace: true' will not impact anything if you are compiling and manipulating the DOM yourself.
At the same time, though, I have no idea what your compile directive is for and why you have a factory that returns an HTML string. All of that code looks like it could be reduced to a single directive. I can't really comment on what you're trying to do, but once you start using $compile all over the place, odds are you aren't doing things the 'Angular way'.

AngularJS DOM selection inside tabset

I'm a bit of a Angular newbie, but have run into what seems to be a pretty dumb problem.
I've created a directive that requires me to find specific DOM element. This works nice and good, except that once I place my directive inside the bootstrap UI tabset, bad things happen. Basically I think since the tabset directive uses custom HTML elements (i.e. , DOM selection no longer works. I've also tried using a jQuery selector, which also fails. Here's a simplified version of my code:
--html
<body ng-controller="mainController">
<tabset>
<tab heading="Title 1">
<my-directive id="myWidget"></my-directive>
</tab>
</tabset
</body>
--js
app.directive('registrationGrid', function() {
return {
restrict: 'E',
link: function($scope, element, attrs) {
var element = document.getElementById("myWidget");
//do some stuff here
}
})
There has to be an easy way around this, but I'm at a loss. I'm tempted to just update the tabs directive to work off of attributes instead of it's own element, but was hoping for an easier fix.

How can I set a form contained inside a ng-include to be prestine?

I have the following code:
<div modal="modal.shouldBeOpen" close="close()" options="opts">
<div class="modal-body">
<form novalidate name="itemForm" style="margin-bottom: 0px;">
Which is contained inside the included file modal.html
<div data-ng-controller="AdminController">
<ng-include src="'/Content/app/admin/partials/grid-subject.html'"></ng-include >
<ng-include src="'/Content/app/admin/partials/modal.html'"></ng-include>
</div>
In my AdminController controller I am trying to use the following code to reset the form to pristine:
$scope.itemForm.$setPristine();
When I do this it tells me that "itemForm" is undefined.
Is there a way I can set the contents of the form to pristine. I assume this is a scope problem but I am not sure how to fix it. I
tried the one solution of removing the second include and pasting the code in directly. This solution works.
However we want to be able to reuse code
so I would like to be able to do this with an include for modal.html
Note that the reason we would like to do this is because we have something like the following on our modal.html:
<button
class="btn float-right"
data-ng-disabled="itemForm.$pristine"
data-ng-click="modalReset()"
data-ng-show="modal.resetButton">
Reset</button>
</form>
So we are actually inside of the itemForm and would like to set it to $pristine from the button inside.
This answer will break all the rules (i.e., DOM traversal inside a controller), but here it is anyway...
.controller('AdminController', ['$scope','$element',
function($scope, $element) {
$scope.$on('$includeContentLoaded', function() {
var childFormController = $element.find('form').eq(0).controller('form');
console.log(childFormController);
childFormController.$setPristine();
});
}]);
We wait for the ng-included content to load, then from the $element where AdminController is defined, we look for form elements, pick the first one, then get its FormController.
Plunker
If you are only calling $setPristine() as a result of some user interaction, you won't need to look for the $includedContentLoaded event – I only had to do that because I didn't want to create any UI component to trigger the operation, and when the controller first runs, the form doesn't exist yet.
See also AngularJS: Access formController of a form placed inside transcluded directive from parent controller which deals with the similar problem of trying to access a child from a parent.
A cleaner solution: define a directive (use it on the ng-include element) and pass it an AdminController function as an attribute. In the directive's link function, call that method and pass the FormController as a parameter. Then the AdminController will have a reference to the desired FormController. (I did not bother coding this up, as I'm not sure you want a solution where you have to use a directive along with ng-include.)
Well, one way to do it is to broadcast an event, like so:
angular.module('myApp',[])
.controller('AdminCtrl',function($scope){
$scope.modalReset = function(){
$scope.$broadcast('modal-reset');
};
})
.controller('ModalCtrl', function($scope){
$scope.$on('modal-reset', function(){
$scope.itemForm.$setPristine();
});
});
This way you don't have to traverse the dom.
Do not break the rules :) Just define the variable (empty object) in the controller and use it while defining your form. Since angular JS uses scope prototypes under the hood, when form will try to access the inner scope (to bootstrap the variable), it will first go via scope chain and try to find the same variable in the parent's scope.
<!—- The vars should live in the controller. I placed them here for the example. -—>
<div ng-controller=“controllerName” ng-init="form={}; model={}" >
<div ng-include=“ ‘path-to-the-template’ ”></div>
</div>
<!—- Inside path-to-the-template -—>
<form name="form.createUser">
<input name="name" ng-model="model.name" />
<input name="email" ng-model="model.email" />
</form>
Link for reference http://blog.152.org/2014/07/angular-form-element-not-attaching-to.html
If you want to achieve this as the result of some user interaction, in my opinion a much more cleaner and 'angular' way of doing it would be to use a custom directive which will set the form to pristine (i.e. when the user wants to clear the form by pressing esc or clicking a button or whatever).
app.directive("formCleaner",
function () {
return {
restrict: 'E',
require: '^form',
scope: {
callback: '&',
defaultText:'#'
},
template: '<button type="button" ng-click="setFormToPristine()" class="btn btn-warning" >{{defaultText}}</button>',
link: function (scope, element, attrs, formCtrl) {
scope.setFormToPristine = function () {
formCtrl.$setPristine();
scope.callback();
};
}
};
});
and simply hook it up to some button in your form:
<form name="testForm">
<input type="text" ng-model="someModel" />
<hr/>
<input type="button" value="submit form" class="btn btn-primary" ng-disabled="testForm.$pristine"
ng-click=submitForm(testForm) />
<form-cleaner callback="resetFormCallback(testForm)" default-text="Clear Form"></form-cleaner>
</form>
And if you're looking to set the form to pristine directly from the controller, (not as a result of some user interaction) such as success response from a POST, then one way would be to assign a callback to the directive which will be responsible for clearing the form and then invoking that callback from the controller. In your view:
<form-cleaner callback="resetFormCallback(testForm)" default-text="Clear Form"></form-cleaner>
and the controller:
$scope.resetFormOnSubmitCallback=function(cb){
$log.warn("simulating $http POST call.....");
$timeout(function() {
cb();
$scope.someModel=null;
}, 3000)
}
and the directive:
return {
restrict: 'E',
require: '^form',
scope: {
callback: '&',
defaultText:'#',
ngDisabled:'='
},
template: '<button type="button" ng-disabled="ngDisabled" ng-click="submitForm()" class="btn btn-primary" >{{defaultText}}</button>',
link: function (scope, element, attrs, formCtrl) {
var setFormToPristine=function(){
$log.log("setting form to prsitine....");
formCtrl.$setPristine();
};
scope.submitForm = function () {
scope.callback({
onFormSubmittedCallback:setFormToPristine
});
};
}
};
See plunk

Resources