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

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

Related

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);
}
})

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

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
}]);

Being wrapped by a directive, how can I access its scope?

How can I access the directive's isolate scope in the directive's body? My DOM looks like this:
<div ng-app="app">
<directive>
<p>boolProperty: {{boolProperty|json}}</p>
</directive>
</div>
The boolProperty is assigned inside the directive's link function:
angular.module("app", []).directive("directive", function() {
return {
scope: {},
link: function($scope) {
$scope.boolProperty = true;
}
};
});
The problem is, the child <p> inside the directive binds to the directive's parent scope, not the directive's isolated scope. How can I overcome this?
Click here for jsFiddle.
There are couple of problems in your code.
The default restrict option is A for attribute so anyways your directive will not be compiled because you are using it as an element. Use restrict: 'E' to make it work.
As per the documentation, the scope of the transcluded element is not a child scope of the directive but a sibling one. So boolProperty will always be undefined or empty. So you have to go up the scope level and find the proper sibling.
<div ng-app="app">
<directive>
<p>boolProperty: {{$parent.$$childHead.boolProperty}}</p>
</directive>
</div>
and need to use transclusion in the directive as:
angular.module("app", []).directive("directive", function() {
return {
restrict: 'E',
scope: {},
transclude: true,
template: '<div ng-transclude></div>',
link: function(scope) {
scope.boolProperty = true;
}
};
});
However, this approach is not advisable and break later If you add a new controller before the directive because transcluded scope becomes 2nd sibling unlike 1st as before.
<div ng-app="app">
<div ng-controller="OneCtrl"></div>
<directive>
<p>boolProperty: {{$parent.$$childHead.boolProperty || $parent.$$childHead.$$nextSibling.boolProperty}}</p>
</directive>
</div>
Here is the Working Demo. The approach I mentioned is not ideal so use at your own risk. The #CodeHater' s answer is the one you should go with. I just wanted to explain why it did not work for you.
You forgot about two things:
By default AngularJS uses attrubute restriction, so in your case in directive definition you should specify restrict: "E"
You should use child scope, but not isolated. So set scope: true to inherit from parent view scope.
See updated fiddle http://jsfiddle.net/Y9g4q/1/.
Good luck.
From the docs:
As the name suggests, the isolate scope of the directive isolates everything except models that you've explicitly added to the scope: {} hash object. This is helpful when building reusable components because it prevents a component from changing your model state except for the models that you explicitly pass in.
It seems you would need to explicitly add boolProperty to scope.
<div ng-app="app" ng-controller="ctrl">
<directive bool="boolProperty">
<p>boolProperty: {{boolProperty|json}}</p>
</directive>
</div>
JS
angular.module("app", []).controller("ctrl",function($scope){
$scope.boolProperty = false;
}).directive("directive", function() {
return {
restrict:"E",
scope: {boolProperty:'=bool'},
link: function($scope) {
$scope.boolProperty = "i'm a boolean property";
}
};
});
Here's updated fiddle.

ng-repeat in combination with custom directive

I'm having an issue with using the ng-repeat directive in combination with my own custom directive.
HTML:
<div ng-app="myApp">
<x-template-field x-ng-repeat="field in ['title', 'body']" />
</div>
JS:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
compile: function(element, attrs, transcludeFn) {
element.replaceWith('<input type="text" />');
}
};
});
See jSFiddle
The problem here is that nothing is replaced. What I'm trying to accomplish is an output of 2x input fields, with the 'x-template-field' tags completely replaced in the DOM. My suspicion is that since ng-repeat is modifying the DOM at the same time, this won't work.
According to this Stack Overflow question, the accepted answer seems to indicate this has actually worked in earlier versions of AngularJS (?).
Wouldn't element.html('...') work?
While element.html('...') actually injects the generated HTML into the target element, I do not want the HTML as a child element of the template tag, but rather replace it completely in the DOM.
Why don't I wrap my template tag with another tag that has the ng-repeat directive?
Basically, for the same reason as above, I don't want my generated HTML as a child element to the repeating tag. While it would probably work decently in my application, I would still feel like I've adapted my markup to fit Angular and not the other way around.
Why am I not using the 'template' property?
I haven't found any way to alter the HTML retrieved from the 'template' / 'templateUrl' properties. The HTML I want to inject is not static, it's dynamically generated from external data.
Am I too picky with my markup?
Probably. :-)
Any help is appreciated.
Your directive needs to run before ng-repeat by using a higher priority, so when ng-repeat clones the element it is able to pick your modifications.
The section "Reasons behind the compile/link separation" from the Directives user guide have an explanation on how ng-repeat works.
The current ng-repeat priority is 1000, so anything higher than this should do it.
So your code would be:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
priority: 1001, <-- PRIORITY
compile: function(element, attrs, transcludeFn) {
element.replaceWith('<input type="text" />');
}
};
});
Put your ng-repeat in the template. You could modify attributes of element and accordingly in directive to determine if ng-repeat is needed, or what data to use inside the directive compiling
HTML(attribute):
<div ng-app="myApp" template-field></div>
JS:
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'A',
template:'<input type="text" value="{{field}" ng-repeat="field in [\'title\',\'body\']" />'
};
});
DEMO: http://jsfiddle.net/GDfxd/3/
Also works as an element :
HTML(element):
<div ng-app="myApp" >
<template-field/>
</div>
JS
angular.module('myApp', [])
.directive('templateField', function () {
return {
restrict: 'E',
replace:true,
template:'<input type="text" value="{{field}}" ng-repeat="field in [\'title\',\'body\']" />'
};
});
DEMO: http://jsfiddle.net/GDfxd/3/

How to use `replace` of directive definition?

In this document: http://docs.angularjs.org/guide/directive , it says that there is a replace configuration for directives:
template - replace the current element with the contents of the HTML. The replacement process migrates all of the attributes / classes from the old element to the new one. See the Creating Components section below for more information.
javascript code
app.directive('myd1', function(){
return {
template: '<span>directive template1</span>',
replace: true
}
});
app.directive('myd2', function(){
return {
template: '<span>directive template2</span>',
replace: false
}
});
html code
<div myd1>
original content should be replaced
</div>
<div myd2>
original content should NOT be replaced
</div>
But the final page is looking like:
directive template1
directive template2
It seems the replace doesn't work. Do I miss anything?
Live demo: http://plnkr.co/edit/rGIgmjO81X2UxJohL4HM?p=preview
You are getting confused with transclude: true, which would append the inner content.
replace: true means that the content of the directive template will replace the element that the directive is declared on, in this case the <div myd1> tag.
http://plnkr.co/edit/k9qSx15fhSZRMwgAIMP4?p=preview
For example without replace:true
<div myd1><span class="replaced" myd1="">directive template1</span></div>
and with replace:true
<span class="replaced" myd1="">directive template1</span>
As you can see in the latter example, the div tag is indeed replaced.
As the documentation states, 'replace' determines whether the current element is replaced by the directive. The other option is whether it is just added to as a child basically. If you look at the source of your plnkr, notice that for the second directive where replace is false that the div tag is still there. For the first directive it is not.
First result:
<span myd1="">directive template1</span>
Second result:
<div myd2=""><span>directive template2</span></div>
Replace [True | False (default)]
Effect
1. Replace the directive element.
Dependency:
1. When replace: true, the template or templateUrl must be required.
Also i got this error if i had the comment in tn top level of template among with the actual root element.
<!-- Just a commented out stuff -->
<div>test of {{value}}</div>

Resources