In a Directive, passing function arguments through to the html template - angularjs

I am on day 2 of Angular and am trying to create a directive. The idea is that I have several images of quilts to display and I don't want to repeat the same HTML. Here's an index.html snippet showing a use of the new directive and two 'arguments' I'll need in the partial:
<ng-samplequilt imgPath="img/t_3x3_no_sashing/jpg"
text="This is a 3x3 quilt without sashing.">
</ng-samplequilt>
Here's the partial:
<div>
<img src="{{imgPath}}"/>
<div>
{{text}}
</div>
</div>
Finally, there's the Directive (which may or may not work):
.directive('SampleQuilt', ['imgPath','text',function(imgPath, text) {
return {
restrict: 'E',
templateUrl: 'partials/sample_quilt.html'
};
}])
So I'm clearly a little over my head. I've read a good deal of the docs and some examples, but none seem to be quite what I'm doing. Or perhaps I have not internalized enough for it to stick.
I'm not looking for a full solution here; I don't mind working through it. But I am stuck - I don't know how to get imgPath and text to make their way to the partial where they can be used.
Also, Directives have embedded controllers. How does the partial know to refer to this controller? Why does it even have access to it, given it's buried in the Directive?
Thanks for a boot in the right direction.
EDIT -
Thanks to #Dalorzo I seem to have a solution.
First, his idea about defining the scope in the Directive worked.
Second, I named the directive "SampleQuilt". This did not work - the directive did nothing/could not be found. When I renamed it to sampleQuilt, however, the internal name translation worked. For similar reasons, the HTML had to refer to img-path, not imgPath.
Here are the three files now.
The index.html snippet:
<sample-quilt img-path="img/t_3x3_no_sashing.jpg"
text="This is a 3x3 quilt without sashing.">
</sample-quilt>
The partial:
<div>
<img src="{{img-path}}"/>
<div>
{{text}}
</div>
</div>
The directive:
.directive('sampleQuilt', function() {
return {
restrict: 'E',
scope:{ imgPath: "#", text: "#" },
templateUrl: 'partials/sample_quilt.html'
};
})
;
EDIT 2 -
The above doesn't work - I was getting burned by browser caching.
It seems as if this snippet in index.html is curious...
<sample-quilt img-path="img/t_3x3_no_sashing.jpg"
text="This is a 3x3 quilt without sashing.">
</sample-quilt>
The img-path attribute can apparently be spelled three different ways: img-path, 'imgPath', and img_path. All are converted to imgPath internally. When displaying the value in the partial, imgPath is correct.
Here's the corrected partial:
<div>
<img src="{{imgPath}}"/>
<div>
{{text}}
</div>
</div>

Based on your example above I think this should be what you intent:
var app = angular.module('demo',[]);
app.directive('SampleQuilt', function() {
return {
restrict: 'E',
scope:{ imgPath: "#", text: "#" },
templateUrl: 'partials/sample_quilt.html'
};
});
By adding scope to the directive we create an "isolated scope". With this approach scope can capture attributes in 3 ways:
# Captures the attribute value from the DOM as string value.
= Evaluates the attribute as property of the parent scope.
& Evaluates the attribute as method of the parent scope.
You can read more about it here:
http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
Regarding your html:
Remove ng and don't use it as part of your directives they are reserved by the angular team and it is good to avoid them to avoid conflicts. You can read more about Angular Naming Conventions here
Cases (camel case or pascal case) means dash in angular directives so SampleQuilt needs to be used as sample-quilt in the html.
Sample:
<sample-quilt imgPath="img/t_3x3_no_sashing/jpg"
text="This is a 3x3 quilt without sashing.">
</sample-quilt>
Regarding your last question about the controller on directives. Directives returned object has a controller property that you can use like:
app.directive('SampleQuilt', function() {
return {
restrict: 'E',
controller: 'myDirController', /* <--- Controller Declaration */
scope:{ imgPath: "#", text: "#" },
templateUrl: 'partials/sample_quilt.html'
};
});
app.controller('myDirController', ['$scope', function ($scope) {
// My Directive Controller implementation
}]);

Related

Angular directive from array element in ngRepeat

I am working on a project that will have any number of HTML cards, and the format and positioning of the cards is the same, but the content of them will differ.
My plan was to implement the content of each card as an angular directive, and then using ngRepeat, loop through my array of directives which can be in any order, displaying each directive in a card. It would be something like this:
inside my controller:
$scope.cards = [
'directice-one',
'directive-two',
//etc..
]
my directive:
.directive('directiveOne', function () {
return {
restrict: "C",
template: '<h1>One!!</h1>'
};
})
.directive('directiveTwo', function () {
return {
restrict: "C",
template: '<h1>Two!!</h1>'
};
})
//etc..
my html:
<div class="card" ng-repeat="item in cards">
<div class="{{item}}"></div>
</div>
But.. the directives don't render. So I just get a div with class="directive-one".
This might be a design flaw on my part, some sort of syntax error, or just a limitation of angular. I'm not sure.
I've also considered making a directive <card>, and then passing the templateUrl: into it, but that would cause me to lose my access to $scope and the javsacript capabilities that I would have if each card was it's own directive.
So, advise, code help, anything would be very helpful!
I choose directives only when I need to use them in HTML mark up. For example, assuming cards layout is same and it takes different information based on user preference.
HTML File
<my-card Name="First" Option="Myoptions"></Card>
<my-card Name="Second" Option="GenOptions"></Card>
Directive
angular.module("testapp").directive("MyCard", function() {
scope: {
name: '#',
Option: '#'
Controller: "myCardController",
templateURL: "~/myCard/myCardTemplate.html"
}
});
In Template you can implement the information passed from HTML page via the directive.
Hope this helps.
Do take note that the above approach is preferred when you are developing a framework sort of things. For example you develop a web framework and the header takes 5 parameters and these 5 parameters needs to be passed via mark up. Most important thing is that the framework/header is independent
In your controller, you need to require the directive modules. Then assign them to a scope variable which would be that array you have. Will update with code when I get to desktop, tried doing with phone kinda tuff.

AngularJS : directives which take a template through a configuration object, and show that template multiple times

I'm looking to create a custom directive that will take a template as a property of a configuration object, and show that template a given number of times surrounded by a header and footer. What's the best approach to create such a directive?
The directive would receive the configuration object as a scope option:
var app = angular.module('app');
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
config: '=?'
}
...
}
}
This object (called config) is passed optionally to the directive using two way binding, as show in the code above. The configuration object can include a template and a number indicating the number of times the directive should show the template. Consider, for example, the following config object:
var config = {
times: 3,
template: '<div>my template</div>'
};
It would, when passed to the directive, cause the directive to show the template five times (using an ng-repeat.) The directive also shows a header and a footer above and below the template(s):
<div>the header</div>
<div>my template</div>
<div>my template</div>
<div>my template</div>
<div>the footer</div>
What's the best way to implement this directive? Note: When you reply, please provide a working example in a code playground such as Plunker, as I've run into problems with each possible implementation I've explored.
Update, the solutions I've explored include:
The use of the directive's link function to append the head, template with ng-repeat, and footer. This suffers from the problem of the template not being repeated, for some unknown reason, and the whole solutions seems like a hack.
The insertion of the template from the configuration object into middle of the template of the directive itself. This proves difficult because jqLite seems to have removed all notion of a CSS selector from its jQuery-based API, leading me to wonder if this solution is "the Angular way."
The use of the compile function to build out the template. This seems right to me, but I don't know if it will work.
You could indeed use ng-repeat but within your directive template rather than manually in the link (as that wouldn't be compiled, hence not repeated).
One question you didn't answer is, should this repeated template be compiled and linked by Angular, or is it going to be static HTML only?
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
config: '=?'
},
templateUrl: 'myTemplate',
link: function(scope) {
scope.array = new Array(config.times);
}
}
}
With myTemplate being:
<header>...</header>
<div ng-repeat="item in array" ng-bind-html="config.template"></div>
<footer>...</footer>
I'd think to use ng-transclude in this case, because the header & footer wrapper will be provided by the directive the inner content should change on basis of condition.
Markup
<my-directive>
<div ng-repeat="item in ['1','2','3']" ng-bind-html="config.template| trustedhtml"><div>
</my-directive>
Directive
var app = angular.module('app');
app.directive('myDirective', function($sce) {
return {
restrict: 'E',
transclude: true,
template: '<div>the header</div>'+
'<ng-transclude></ng-transclude>'+
'<div>the footer</div>',
scope: {
config: '=?'
}
.....
}
}
Filter
app.filter('trustedhtml', function($sce){
return function(val){
return $sce.trustedHtml(val);
}
})

AngularJS - Generate HTML on the fly based on a directive

Say I want to generate some Html on the fly but based on the same definition in my directive, how would I achieve this?
My directive is currently expecting JSON:
app.directive('myDirective', [function () {
return {
template: '<div>A far more impressive template lives here...</div>',
restrict: 'AE',
scope: {
eventData: '='
},
link: function ($scope, $element, $attrs) {
//some cool functions...
}
};
}]);
Within my ng-repeat element, I have this kinda thing, where the event-data contains the named scope item from the ng-repeat, clause.
<div ng-repeat="event in myCtrl.events">
<my-directive event-data="event"></my-directive>
</div>
So how do I go about manually passing in some JSON to use as the data attribute so I can then generate fragments elsewhere, e.g. outside a scoped ng-repeat?
What I am hoping to do is add a function to a controller of mine which just returns a new instance of my directive and then I can plant that somewhere else (I am actually hoping to use as the content of a Google Maps info window).
I hope I have made some sort of sense here!
Thanks

How to use the 'replace' feature for custom AngularJS directives?

Why does replace=true or replace=false not have any impact in the code below?
Why isn't the "some existing content" being displayed when replace=false?
Or putting it more humbly, can you kindly explain what is the replace=true/false feature in directives and how to use it?
Example
JS/Angular:
<script>
angular.module('scopes', [])
.controller('Ctrl', function($scope) {
$scope.title = "hello";
})
.directive('myDir', function() {
return {
restrict: 'E',
replace: true,
template: '<div>{{title}}</div>'
};
});
</script>
HTML:
<div ng-controller="Ctrl">
<my-dir><h3>some existing content</h3></my-dir>
</div>
See it in Plunker here:
http://plnkr.co/edit/4ywZGwfsKHLAoGL38vvW?p=preview
When you have replace: true you get the following piece of DOM:
<div ng-controller="Ctrl" class="ng-scope">
<div class="ng-binding">hello</div>
</div>
whereas, with replace: false you get this:
<div ng-controller="Ctrl" class="ng-scope">
<my-dir>
<div class="ng-binding">hello</div>
</my-dir>
</div>
So the replace property in directives refer to whether the element to which the directive is being applied (<my-dir> in that case) should remain (replace: false) and the directive's template should be appended as its child,
OR
the element to which the directive is being applied should be replaced (replace: true) by the directive's template.
In both cases the element's (to which the directive is being applied) children will be lost. If you wanted to perserve the element's original content/children you would have to translude it. The following directive would do it:
.directive('myDir', function() {
return {
restrict: 'E',
replace: false,
transclude: true,
template: '<div>{{title}}<div ng-transclude></div></div>'
};
});
In that case if in the directive's template you have an element (or elements) with attribute ng-transclude, its content will be replaced by the element's (to which the directive is being applied) original content.
See example of translusion http://plnkr.co/edit/2DJQydBjgwj9vExLn3Ik?p=preview
See this to read more about translusion.
replace:true is Deprecated
From the Docs:
replace ([DEPRECATED!], will be removed in next major release - i.e. v2.0)
specify what the template should replace. Defaults to false.
true - the template will replace the directive's element.
false - the template will replace the contents of the directive's element.
-- AngularJS Comprehensive Directive API
From GitHub:
Caitp-- It's deprecated because there are known, very silly problems with replace: true, a number of which can't really be fixed in a reasonable fashion. If you're careful and avoid these problems, then more power to you, but for the benefit of new users, it's easier to just tell them "this will give you a headache, don't do it".
-- AngularJS Issue #7636
Update
Note: replace: true is deprecated and not recommended to use, mainly due to the issues listed here. It has been completely removed in the new Angular.
Issues with replace: true
Attribute values are not merged
Directives are not deduplicated before compilation
transclude: element in the replace template root can have unexpected effects
For more information, see
AngularJS $compile Service API Reference - Issues with replace:true

Avoid using extra DOM nodes when using nginclude

I'm struggling to wrap my mind around how to have an ng-include not use an extra DOM element as I'm building an angular app from a plain-HTML demo. I'm working with pretty slim HTML with fully developed, tightly DOM-coupled CSS (built from SASS) and refactoring is something I want to avoid at all costs.
Here's the actual code:
<div id="wrapper">
<header
ng-controller="HeaderController"
data-ng-class="headerType"
data-ng-include="'/templates/base/header.html'">
</header>
<section
ng-controller="SubheaderController"
data-ng-class="subheaderClass"
ng-repeat="subheader in subheaders"
data-ng-include="'/templates/base/subheader.html'">
</section>
<div
class="main"
data-ng-class="mainClass"
data-ng-view>
</div>
</div>
I need <section> to be a repeating element but have its own logic and different content. Both, content and number of repetitions are dependent on business logic. As you can see, putting the ng-controller and the ng-repeat on the <section> element will not work. What would, however, is to insert a new DOM node, which is what I'm trying to avoid.
What am I missing out? Is this best practice or is there a better way?
EDIT: just to clarify as asked in comments, the final HTML I'm trying to generate would be:
<div id="wrapper">
<header>...</header>
<section class="submenuX">
some content from controller A and template B (e.g. <ul>...</ul>)
</section>
<section class="submenuY">
different content from same controller A and template B (e.g. <div>...</div>)
</section>
<section class="submenuZ">
... (number of repetitions is defined in controller A e.g. through some service)
</section>
<div>...</div>
</div>
The reason I want to use the same template B (subheader.html), is for code cleanliness. I conceive subheader.html to have some kind of ng-switch in order to return dynamic content.
But basically, the underlaying quiestion is: is there a way to include the contents of a template transparently, without using a DOM node?
EDIT2: The solution needs to be reusable. =)
Some of the other answers suggest replace:true, but keep in mind that replace:true in templates is marked for deprecation.
Instead, in an answer to a similar question, we find an alternative: It allows you to write:
<div ng-include src="dynamicTemplatePath" include-replace></div>
Custom Directive:
app.directive('includeReplace', function () {
return {
require: 'ngInclude',
restrict: 'A', /* optional */
link: function (scope, el, attrs) {
el.replaceWith(el.children());
}
};
});
(cut'n'paste from the other answer)
Edit: After some research and for the sake of completeness, I've added some info. Since 1.1.4, the following works:
app.directive('include',
function () {
return {
replace: true,
restrict: 'A',
templateUrl: function (element, attr) {
return attr.pfInclude;
}
};
}
);
Usage:
<div include="'path/to/my/template.html'"></div>
There is, however, one gotcha: the template cannot be dynamic (as in, passing a variable through scope because $scope, or any DI for that matter, is not accessible in templateUrl - see this issue), only a string can be passed (just like the html snippet above). To bypass that particular issue, this piece of code should do the trick (kudos to this plunker):
app.directive("include", function ($http, $templateCache, $compile) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
var templateUrl = scope.$eval(attributes.include);
$http.get(templateUrl, {cache: $templateCache}).success(
function (tplContent) {
element.replaceWith($compile(tplContent.data)(scope));
}
);
}
};
});
Usage:
<div include="myTplVariable"></div>
You can create a custom directive, linking to the template with the templateUrl property, and setting replace to true:
app.directive('myDirective', function() {
return {
templateUrl: 'url/to/template',
replace: true,
link: function(scope, elem, attrs) {
}
}
});
That would include the template as-is, without any wrapper element, without any wrapper scope.
For anyone who happens to visit this question:
As of angular 1.1.4+ you can use a function in the templateURL to make it dynamic.
Check out this other answer here
With the right setup, you can define your own ngInclude directive that can run instead of the one provided by Angular.js and prevent the built-in directive to execute ever.
To prevent the Angular-built-in directive from executing is crucial to set the priority of your directive higher than that of the built-in directive (400 for ngInclude and set the terminal property to true.
After that, you need to provide a post-link function that fetches the template and replaces the element's DOM node with the compiled template HTML.
A word of warning: this is rather draconian, you redefine the behavior of ngInclude for your whole application. I therefore set the directive below not on myApp but inside one of my own directives to limit its scope. If you want to use it application-wide, you might want to make its behavior configurable, e.g. only replace the element if a replace attribute is set in the HTML and per default fall back to setting innerHtml.
Also: this might not play well with animations. The code for the original ngInclude-directive is way longer, so if you use animations in your application, c&p the original code and shoehorn the `$element.replaceWith() into that.
var includeDirective = ['$http', '$templateCache', '$sce', '$compile',
function($http, $templateCache, $sce, $compile) {
return {
restrict: 'ECA',
priority: 600,
terminal: true,
link: function(scope, $element, $attr) {
scope.$watch($sce.parseAsResourceUrl($attr.src), function ngIncludeWatchAction(src) {
if (src) {
$http.get(src, {cache: $templateCache}).success(function(response) {
var e =$compile(response)(scope);
$element.replaceWith(e);
});
}
});
}
};
}];
myApp.directive('ngInclude', includeDirective);

Resources