From Rails to Angular: Are directives used like Rails partials? - angularjs

I am probably vastly simplifying the use of directives, but to me it seems like templateUrl in directives are much like partials in that you load reusable templates with <directivename></directivename> much like <%= render 'partial' %>.
Here's an example of something I just wrote before coming up with this question:
app.directive('filters', function() {
return {
restrict: 'E',
templateUrl: "../templates/product-filters-template.html"
}
});
app.directive('results', function() {
return {
restrict: 'E',
templateUrl: "../templates/product-results-template.html"
}
});
That feels identical to cutting a piece of ERB into a _partial.html.erb file.
Are directives used in any other way?

Using directives to render the same markup (same concept as a partial) is one usage, but they can also (and more frequently in my limited experience) be used to control behavior of an element. From the documentation on directives:
What are Directives?
At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS's HTML compiler ($compile) to attach a specified behavior to that DOM element or even transform the DOM element and its children.
For example, you can use a directive to do validation of a user's input into an input element. I would strongly suggest that you read the documentation on directives.

Related

Use controllers to: Manipulate DOM

I have a ng-repeat-end-watch directive
.....
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
scope.$evalAsync(attr.ngRepeatEndWatch);
}
}
}
on my markup i have
<div ng-repeat="(i, tile) in tiles" class="w3-col tile tile-{{tile.number}}" ng-repeat-end-watch="ctlr.resize()" ng-model="tile">
and on my controller I have
rowsColumns = Math.round(Math.sqrt($scope.tiles.length)), widthHeight = $window.screen.availHeight/rowsColumns;
ctlr.resize = function(){
angular.element('.w3-col').width(widthHeight).height(widthHeight);
};
If in the developer's guide for AngularJS it recommends that you shouldn't use controllers to manipulate DOM elements
What would the recommended way be to apply the above "resize" if its not a good idea to use controllers to manipulate the DOM?
Angular is mostly for functionality, not direct DOM manipulation. You probably noticed that you access elements based on their model not id. You don't need any id in an an Angular app which makes it easier if certain html bits change. As long as the model stays the same you can do what you want with the html.
This being said if you do need to access the DOM you can still do it. You could have the code which does that outside of the controller in its own js file for example. Then you can inject it as an Angular service and reuse it in whichever controller you want.
I would try to keep things nicely separated and the controllers very thin. If you code it this way then it's easier to test things as you don't have to worry about controllers anymore. Hope this makes sense.
The best way to deal with it is writing your own directive. In fact, directives are one of the most powerful concepts in Angular.js.
Think of it as reusable components: not only for jQuery plugins, but also for any of your (sub) controllers that get used in multiple places. It’s kinda like shadow DOM. And the best part is, your controller does not need to know the existence of directives: communications are achieved through scope sharing and $scope events.

Transclude partial content

I am trying to create a "sticky table header" component for which I need to copy parts of the transcluded content of my directive.
Depending on how I transclude the content, it works only partially at best: with $compile, expressions are updated when the underlying data changes, but ng-repeat does not seem to work at all. It does not even render the first time, let alone update later. Simply appending the partial content I found does not seem to work at all: element.append($(transcludedEl).find('.wrapper'));
To illustrate my point, I have created a plunkr using three versions of the same code: http://plnkr.co/edit/xkAkzl8ID3m5Ras3Ww31
The first is super-simple direct ng-repeat, which only serves to show what should happen.
The second uses a directive that transcludes its full content, which works but is not what I need.
The third (reproduced below) uses a directive to try and include only part of its content, which is what I need, but which does not work.
The interesting bit is this:
app.directive('stickyPartial', ['$compile', '$timeout', function($compile, $timeout) {
return {
restrict: 'E',
transclude: true,
template: '<div></div>',
link: function(scope, element, attrs, controller, transclude) {
transclude(scope, function(transcludedEl) {
// this is what i want to achieve - not working
// element.append($(transcludedEl).find('.wrapper'));
// neither is this, though it does support expressions
$compile($(transcludedEl).find('.wrapper'))(scope, function(clone) {
element.append(clone);
});
});
}
};
}]);
So far, I have tried several combinations of $compile, .clone() and .html(), but to no avail. I can neither get a working partial DOM tree from the compiled template, nor a useful partial HTML source with ng-repeat intact that I can then compile manually.
As a last resort, I might try copying the DOM after angular is done (which seemed to work, previously) and then manually repeat this process every time the relevant model data changes. If there is another way, thought, I'd very much like to avoid this.
Using https://stackoverflow.com/a/24729270/2029017 I found a solution with compile that does what I want: http://plnkr.co/edit/V4yUbiAD9EAaaJXihziv

How can I reuse partials in angular

I am creating a table where each row will be represented by a directive element.
For most rows the same general directive can be used, but a couple of rows will have very specialized information forcing me create specific directives.
A row in my table will look something like this (but with more info per cell):
| Name | Capabilities | Special Info/Functions | General Functions |
I've considered using ng-Include in the different directive-templates like this:
<tr><td ng-include="'name.html'"></td><td ng-include="'capablilites.html'"></td>
<td ng-include="'specialX.html'"></td><td ng-include="'general.html'"></td></tr>
But the ng-include directive creates a new scope, which I could do without.
What is the smartest and DRYest way of doing this? All suggestions are welcome.
html
<tr ng-repeat="info in ctrl.infocollection">
<specialized-info info="info"></specialized-info>
</tr>
Directive
(function() {
'use strict';
angular
.module('myModule')
.directive('specializedInfo', specializedInfo);
function specializedInfo() {
var directive = {
restrict: 'E',
templateUrl: '/app/views/templatename.html',
scope: {
info: '=' //use # in one way
},
controller : infoCtrl,
controllerAs: 'vm',
bindToController: true
};
return directive;
}
function infoCtrl() {
var vm = this;
vm.properties....
//some logic for template
}
})();
In the directive controller, you can set the template properties based on the info being passed in and bind appropriately.
I used ngBindHTML and $templateCache to achieve that.
The view :
<tr><td ng-bind-html="nameTemplate"></td><td ng-bind-html="capabilitesTemplate"></td>
<td ng-bind-html="specialTemplate"></td><td ng-bind-html="generalTemplate"></td></tr>
The controller :
...
$scope.nameTemplate = $sanitize($templateCache.get('name.html'));
...
You may need to sanitize your HTML before you bind it to the view, that's why I used it.
The way I eventually chose to solve this was with a base-directive which contained the a standard template and a controller with all standard functions.
I configured the base-directive to transclude content and append it at a spesific element (the td that could contain specialized information).
I created a couple of extension-directives that would contained their own specialized templates with buttons, input-fields or just text.
In addition to the partials, the extensions also (in most cases) contained their own controllers which could make changes to the base-directive scope (both overwriting methods and changing variables).
I decided to use a custom transclude function in the base-directive link instead of going with the ng-transclude directive. This allowed the directives to actually share scope instead of just having a reference through $scope.$parent (spared me of some headache).
In the end my directives ended up looking pretty simple and DRY and could be instatiated with a simple:
<tr my-base-directive><my-extension></my-extension></tr>
My frustration relieving link function:
link: function(scope, el, attrs, ctrl, transcludeFn) {
transcludeFn( scope, function( clone ) {
el.find('.append').append( clone );
});
}
PS: If anyone feel cheated of "correct answer", my requirements grew a bit during development (as they always do) and became more clear after posting the question.

AngularJS : Directive shell that allows user defined content with directive scope

Here's a plunker example you can see: http://plnkr.co/edit/NQT8oUv9iunz2hD2pf8H
I have a directive that I would like to turn into a web component. I've thought of several ways as to how I can achieve that with AngularJS but am having difficulty with a piece of it. I'm hoping someone can explain my misstep rather than tell me a different way to do it.
Imagine you have a directive component that sets up some shell with css classes maybe some sub components, etc.. but lets the user define the main content of the component. Something like the following:
<my-list items="ctrl.stuff">
<div>List Item: {{ item.name }}</div>
</my-list>
The HTML for the list directive could be something like the following (with OOCSS):
<ul class="mas pam bas border--color-2">
<li ng-repeat="items in item track by item.id" ng-transclude></li>
</ul>
Normally this can be solved in the link function by linking the directives scope to the new content. And it does work for other components. However introducing the ng-repeat seems to break that portion of the control. From what I can tell, the appropriate place might be the compile function but the documentation says the transcludeFn parameter will be deprecated so I'm not sure how to proceed.
I should also note that when using the beta AngularJS, there is either a bug or a new paradigm coming, because this is no longer a problem. It seems like the transcluded content always gets access to the directives scope as well as the outer controllers scope.
I really appreciate any enlightenment on this.
It's by design that content added via ng-transclude will bind with an outer controller scope, not a scope of the current element that ng-transclude is on.
You could solve the problem by copy the ng-transclude's code and modify it a bit to give a correct scope:
.directive('myTransclude', function () {
return {
restrict: 'EAC',
link: function(scope, element, attrs, controllers, transcludeFn) {
transcludeFn(scope, function(nodes) {
element.empty();
element.append(nodes);
});
}
};
});
And replace the ng-transclude with my-transclude in your directive template.
Example Plunker: http://plnkr.co/edit/i7ohGeRiO3m5kfxOehC1?p=preview

Is it recommended or good practice to use a directive that matches standard html tags?

I wanted to write a directive that only applied to IMG tags throughout my whole page, and initially I thought I would have to decorate each tag with a custom directive name, such as:
<img my-img />.
But, while I was putting together some sample code for this question, I decided to see if the directive would match on the element IMG itself. And it worked!
Here's what I did: http://plnkr.co/edit/z4n4a3MN89nRNYyXKCih?p=preview
app.directive('img', function () {
return {
restrict: 'E',
link: function (scope, element) {
element.bind('load', function () {
element.addClass('fadeIn');
});
element.bind('error', function () {
element.removeClass('fadeIn');
});
}
};
});
As you can see, I wanted all images on a page to fade in when they loaded. I wanted to do this in an angular fashion without using jQuery, so I thought this was a good approach, but is it good practice? In my case, I really do want this logic to apply to all the images on my page (and there may be hundreds), so I thought this would be a clean way of doing it, but for the life of me I haven't found anywhere where anyone else does this (i.e., matching a directive to an IMG tag or any standard tag for that matter).
I think I would avoid the img directive. Take note that Angular has already added their own directives which match html element names (e.g. - form, input, select, script), so it seems conceivable that there could potentially be a conflict if they (or any library you use) utilize the same directive name. And do you really want to fade in all images? What if you use an image as a decoration on the page?
It seems like it would be best to instead add the attribute. It's very intuitive with nominal effort. If you don't care about the built in attributes, you could also create your own element (e.g. ).

Resources