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.
Related
Note: I am modifying this question to improve rating.
I developed the following directive with isolated scope:
app.directive('outerIsolated', function () {
return {
restrict: 'A',
scope: {
theData:'=',
...
},
replace: true,
templateUrl: './photo-list-template.html',
link: function ($scope, elm, attrs) {
...
...
...
}
};
});
And also, I developed the following inner directive with inherited scope.
app.directive('innerInherited', ['$compile', '$timeout', '$parse', function ($compile, $timeout, $parse) {
return {
scope: false;
link: function (scope, el, attrs, ngModel) {
...
...
... }
};
}]);
The Problem:
If I use the directive outerIsolated as a parent directive for the inner directive innerInherited, then all references to inherited scope variables won't work.
The innerInherited directive works just fine on its own and has been used extensively. Now, I can't change it to isolated scope. This directive is actually called check-if-required and will loop across all child input fields to find out if anyone is required field, and make it required.
Just few days ago, I learned about directive with isolated scope which can be used to develop reusable components. I liked the idea, and I developed one call upload-photo-list which I referred to it here as outerIsolated.
Is there anyway I can solve this problem easily?
One direct way, is reverse the nesting of the directive. So, I tried to use directive with inherited scope in the outer level instead, but, the problem now is that the link function of the outer directive didn't see the elements of the inner directive after being replaced by the template. I even used this code to try to wait until the document is ready this this:
angular.element(document).ready(function (){...}
... but still, the outer directive cannot reach the HTML Elements generated by the inner directive.
Appreciate your help to find a solution.
Thank you.
Old Question:
Note: this part is obsolete. I kept it here for tracking purposes only.
I am building a simple example using ng-signature-pad and signature-pad plugins.
Click here to see the sample HTML file as per the download
I noticed that the following script tag works only if I place them before the </body> tag (same as the provided sample source code in the link above):
<script src="js/app.js"></script>
If I place the above script tag in the <head> tag it is not affected.
Could someone explain to me why?
I would need to look at the project you are referencing in more detail, but I would imagine that the library and the app.js that you are using are referencing elements on the page.
If you have the scripts in the HEAD tag, those elements are not there. By putting them at the bottom of the BODY element, you are ensuring that the elements are in fact in the browser.
The quickest way to solve my problem is as follows...
I had to use the directive with the inherited scope check-if-required to be on the outer level, and the directive with the isolated scope upload-photo-list in the inner level.
However, I had to make two modifications for the directive check-if-required:
Add $timeout in the link function to ensure the inner directive has finished rendering its HTML before looping through all the input elements as follows:
code:
var children;
$timeout(function() {
children = $(":input", el);
el.removeAttr('check-if-required');
angular.forEach(children, function(value, key) {
...
...
});
})
Must compile the element with respect to the scope of the element:
code:
angular.forEach(children, function(value, key) {
if(typeof (value.id) != 'undefined') {
if (scope.isFieldRequired(value.id)) {
angular.element(value).attr('required', true);
$compile(value)(angular.element(value).scope());
}
})
So far, this solution works well for me.
Any feedback to improve is welcomed.
Tarek
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
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.
I am having troubles implementing a custom directive with transclude: true that can have a "transclusion" content that is using ngRepeat.
[My case]
I want to implement a set of directives, that are fetching the data that they are supposed to show from a $http service. For that I want to use preLink phase interceptor that Angular provides, so I can catch the data and set it to the scope. That way if I have some dynamic (since this term is well overloaded - I mean a data which structure is unknown until the request is done) data coming from the service, I rely on that, that I will be able to retrieve a list with that dynamic data and store it inside the scope, then loop through that data via ngRepeat inside the HTML. Here comes my problem...
[My Problem]
Angular is not using the list that I am assigning to the scope during preLink.
[A plunkr]
I maded a plunker that illustrated just the problem that I am having.
http://plnkr.co/edit/XQOm4KWgKxRhn3pOWqzy?p=preview
[My question]
I really believe that such functionality is covered by angular and I am just missing something in the puzzle.
Can anyone tell me how to implement such behaviour?
Thanks!
EDIT:
Thank you rchawdry for your answer. Here are some details on my intentions. To make it simple I will try to give you an example.
Let's assume that we have these directives:
1. "page" - This directive is a labeled container for all the page content. Visually it is represented as some div - for header, for content and for other fancy stuff if needed. The directive does not know what is its data before the page loads. As the page loads the directive must retrieve the information for itself and its children from a REST resourse! Then the directive is setting the information needed for itself (label and other stuff) and stores its children content in childrenList scope variable. It creates a scope.
2. "section" - This section can be child of "page". Since "page" is retrieving its data from a server, then the information about how many "section"s does our "page" have is dynamic and we don't know how many "section"s we need to show on the screen. This depends on sectionList that is coming from the back-end. The section itself is almost the same as "page" - it is a labeled container, with the differences that - a). "section" is container of elements; b). "section" does retrieve its data from its parrent instead of making $http request. This directive creates a scope.
3. "element" - For this example, in order not to define many different elements and complicate it, let's assume that I have one element, called "element". It can consist of some "input" with "span" and "button" if needed. It is similar to the "section" with that, that it retrieves the data to show from it's parrent (in the general case, this is "section" or "page"). On the other hand it is different than "section" by the fact that it has no transcluded content.
Now after we have some of the concept here is what I am trying to achieve:
<page>
<element id='element1' someOtherStuffHere...></element>
<section id='static_section1' someOtherStuffHere...>
<element id='element2' someOtherStuffHere...></element>
</section>
<div class="row" ng-repeat="section in sections">
<section dynamic_id='dynamic_section'>
<div class="row" ng-repeat="elem in elements">
<element dynamic_id='dynamic_element'></element>
</div>
</section>
</div>
</page>
well, I believe that what your trying to achieve will be able by adding a ng-repeat attribute to the transcluded template.
by letting angular know about the 'repeat', it is supposed to work.
since plunkr is currently unavaliable, I can't prodivde any preview and do not have your original code. Ill try to recall it:
template: "<div id='container'>" +
"<div class='content' ng-repeat='item in [1]' ng-transclude'></div>" +
"</div>"
edit: http://plnkr.co/edit/xba4pU666OGxBtKtcDwl?p=preview
You've got a scope problem. The controller is using a variable that isn't defined in the controller (arrayListItemsPre and arrayListItemsPost). While they are declared in the directives, accessing them in a transcluded scope is a little tricky.
The easy way is to do the following. This will present the scope variables up to the controller where they can be used.
app.directive('container', function($compile) {
return {
transclude: true,
scope: true,
replace: true,
restrict: 'E',
template: "<div class='container'>" +
"<div class='content' ng-transclude></div>" +
"</div>",
compile: function(cElem, cAttrs) {
return {
pre : function preLink(scope, iElement, iAttrs) {
console.log("Hello from Container preLinkPhase");
scope.$parent.arrayListItemsPre = [1, 2];
},
post : function postLink(scope, iElement, iAttrs) {
scope.$parent.arrayListItemsPost = [1, 2];
}
};
}
};
});
There are other ways to do this that are better but it requires understanding why you're trying to iterate on variables that are defined in the directive. If you're going to be using this directive in another page that has different array elements, you'd have to change the directive code.
I am trying to define a directive to show a checkmark when a question has been answered.
questModule.directive('showCheckmarkOnDone', function () {
return {
transclude: true,
template: "<div ng-transclude></div><img src='img/checkmark.svg' " +
"ng-show='scope.questions[type][number-1].done' " +
"class='checkmark' style='height: {{height}}px; width: {{height}}px;'>",
link: function (scope, element, attrs) {
scope.height = $(".app").height()/12;
scope.number = attrs.number;
scope.type = attrs.type;
}
};
});
The scope.questions[type][number-1].done exists in my controller for the page, and I can see that it is being updated correctly when I press the done button. However, the directive does not register the change. I tried putting a $watch in the link function - that didn't help either. I think I'm a bit confused about how to get my directive scope to play nicely with my controller scope - any thoughts on how I can give this directive access to an object that exists in an outside controller? (scope.questions)
This is not a valid way to define directive scope:
scope: '#'
You can either (a) not define it, (b) set it to true, or (c) set it to {} (an object). See the docs for more info: http://docs.angularjs.org/guide/directive (find the header: "Directive Definition Object")
In this case, I imagine if you remove it, you may be OK, because it will allow scope.questions to be visible from your directive. If you reproduce the issue in jsfiddle.net or plnkr.co, it would be much easier to assist you.
Edit:
Your directive generally should have 1 parent element
You should not use scope in your directive's HTML, it's implied
I think you said as much in your comment, but you should strive to make your directive more generic by passing in scope.questions[0][0].done instead of looking it up in your directive's HTML using attributes.
Here's a working example: http://jsfiddle.net/EZy2F/1/