I am using directives to try to replace some of the often-reoccurring template code that i must write with something simpler.
lets say I have the following original markup:
<!-- section with repeating stuff in it -->
<div some-attributes etc="etc" very-long-tag="true">
<p class="lead">Some description text</p>
<div class="row section short" ng-repeat="row in things">
<div class="col-sm-6 col-md-4" ng-repeat="app in row.col">
<div class="thumbnail">
<img ng-src="im/things/{{app.image}}" alt="..." class="img-circle" width="250">
<div class="caption">
<h3>{{app.name}}</h3>
<p>{{app.desc}}</p>
</div>
</div>
</div>
</div>
</div>
and I want to simplify it by doing something like this:
<!-- section with repeating stuff in it -->
<xx title="Some description text">
<!-- this innerHTML gets passed to the directive -->
<div class="row section short" ng-repeat="row in things">
<div class="col-sm-6 col-md-4" ng-repeat="app in row.col">
<div class="thumbnail">
<img ng-src="im/things/{{app.image}}" alt="..." class="img-circle" width="250">
<div class="caption">
<h3>{{app.name}}</h3>
<p>{{app.desc}}</p>
</div>
</div>
</div>
</div>
<!-- end of innerHTML -->
</xx>
...where there are a several attributes that can be used to shorten the overall block, the directive is currently written this way:
_d.directive('xx', function() {
return {
scope: {
'color': '=',
'option': '=',
'title': '=',
'image': '=',
'image-pos': '=',
'image-size': '='
},
restrict: 'E',
transclude: false,
template: function(element, scope) {
var inside = 'x';
var content = element[0].innerHTML;
var title = scope.title;
var color = scope.color ? 'style="background-color: '+scope.color+'"' : "";
var title = scope.title ? '<h2 class="centertext marginBottom20">'+scope.title+'</h2>' : '';
return ['<div class="section row short" '+color+' ng-transclude>',
title,
content, //this may contain {{template code}}, but it always gets omitted
'</div>'
].join("\n");
},
};
});
The problem is that the existing HTML always gets omitted if it contains any {{angular template code}}.
How do I write the directive so that it still honors the template code?
Ive successfully fixed the issue with the directive, but it took several steps.
Use the correct scope properties. instead of using '=', I used '#'
That was based on the following link: What is the difference between '#' and '=' in directive scope in AngularJS?
The thing to note about scope isolation using #, =, and & affects the way you must refer to the variable in the template. for example, using = means that I would refer the variable without brackets while using # would refer to the variable with {{brackets}}.
Like I mentioned in the first point, after adjusting the scope properties, i needed to go back and refer to the variables in the correct way depending on how the scope was defined.
ng-transclude when used with {...transclude: true,...} requires that I actually put a container somewhere in the template for that transcluded content. Here's an example of that:
return ['<div class="section row short" '+color+' ng-transclude>',
title,
'<div ng-transclude>', //this is the container for the original innerHTML, transcluded
content, //this may contain {{template code}}, and gets transcluded
'</div>
'</div>'
].join("\n");
Only then did the directive work as expected. Also, props to #rob for providing me with this introductory link, https://egghead.io/lessons/angularjs-components-and-containers.
Related
I have this little component:
.component('obsbox', {
restrict: 'E',
templateUrl: 'app/components/interactions/html/interactions.obsBox.partial.html',
controller: 'InteractionsController',
controllerAs: 'vm',
bindings: {}
});
All it does it produce a box with some text in it. Nothing spectacular. What I'd like to do is pass a few attributes into the tag and then display those attributes when it resolves.
<obsbox title1="Positive" title2="Experience" score="1"></obsbox>
That should give me a box with "1" in at the top and "Positive Experience" in the bottom. I haven't been able to find anything on SO or anywhere else that explains how I would do that in a way I can understand.
There's nothing in my controller or anywhere else that would be helpful to show you. My controllers are literally doing nothing at the moment.
Any advice?
Edit:
here's the html for the component:
<div layout="row" layout-align="start center" flex="11">
<div layout="column" layout-align="center center" flex="100">
<div layout="row" layout-align="center center" flex="100">
1
</div>
<div layout="row" layout-align="center center" flex="100">
<div layout="column" layout-align="center center" flex="100" style="font-size: x-small">
<div layout="row" layout-align="center center" flex="100">
POSITIVE
</div>
<div layout="row" layout-align="center center" flex="100">
ATTITUDE
</div>
</div>
</div>
</div>
</div>
In my ideal world "1", "Positive", and "Attitude" would come from from attributes on the obsbox tag.
Have you seen the example at https://docs.angularjs.org/guide/component ?
You should fill your bindings like this:
bindings: {
title1:"#",
title2="#",
score="#"
}
# denotes text binding. See the link above for more binding options.
You can then use {{ $ctrl.title1 }} and the other bindings in the template.
For an example, see https://plnkr.co/edit/Gv3TofiO0QyjAOkODjPv?p=info
You can load it into scope using a link function in your directive:
.component('obsbox', {
// the other directive parameters
link: (scope, element, attributes, controller) => {
controller.title1 = attributes.title1;
controller.title2 = attributes.title2;
}
});
The link function is executed once, upon directive compilation.
If you need to update the value, you need to add a watch to that attribute. Angular provides a way to observe attributes with the $observe function:
// in the link function body
attributes.$observe('title1', newValue => {
controller.title1 = newValue;
});
Note that this way, it's using plain strings, not Angular expressions. This means that if you want to have an expression, you would need the $parse service in the directive, or use interpolation at usage site.
I want to create a directive which allow me to generate validation message near input - based on ngMessages(as in example).I have this working HTML example:
<div class="field">
<div class="ui right icon input">
<input type="email" name="email" ng-model="vm.user.email" placeholder="E-mail" required>
<i class="at icon"></i>
</div>
<div ng-messages="vm.signUpForm.email.$error" ng-show="vm.signUpForm.$submitted">
<div ng-messages-include="shared/validation/formErrorMessages.html"> </div>
</div>
</div>
My current directive:
var app = angular.module('app.directives', []);
app.directive('formError', function() {
return {
restrict: 'AE',
replace: 'false',
scope: {
statement: '#',
error: '#'
},
template: '<div ng-messages="error" ng-show="true"><div ng-messages-include="shared/validation/formErrorMessages.html"></div></div>'
};
});
And how I tried to run it:
<div form-error error="{{ vm.signUpForm.email.$error }}" statement="{{ vm.signUpForm.$submitted }}"></div>
It's not working - message won't appear - without any error. On message show I will also want to add class 'error' to 'div.field', but it should be easy.
Any idea how to make this directive work or maybe how to handle this in another, more comfortable way?
You made a a mistake, you should pass attributes to directive with
scope: {
statement: '=',
error: '='
},
# biding is for passing string values, not objects and error is an object, so passing it that way will not work as expected. Of course you could use attr.$observe and JSON.parse, but that is not what you wanna do here.
https://plnkr.co/edit/iRRPqLpmqdQltNjw35Nb?p=preview
Edit: Thanks to Simon Schüpbach, I was able to resolve the issue by changing the template. See the end for the solution.
Let's preface this by saying that we are beginner to soft-intermediate in Angular.
On one of our project, we are using angularjs 1.4.x and also ng-cart (https://github.com/snapjay/ngCart). It worked great but then we were confronted with a demand from our client that created new weird issues.
We added fsCounter, as a directive, to the cart page so user can add or remove items. This all work great but the users also have the option to delete an item from the cart view. Deletion works as expected BUT it seems to affect the scope to the item that takes it place.
Let me make it clearer :
Let's say we have 2 products in our cart page, it displays something like that
Product_1 {price} {counter} {total} delete_btn
Product_2 {price} {counter} {total} delete_btn
Each fsCounter is its own scope
return {
restrict: "A",
scope: {
value: "=value",
index: "=index"
},
link: //other logic
However when we delete the first item, visually and in the directives, the data seems to shift. So our second row will now inherit the first row's counter.
Directive's data looks like this:
Product_1:
itemId:3,
quantity:2,
{other data}
Product_2:
itemId:8,
quantity:5,
{other data}
But once we delete the first directive (We get the scope, remove the DOM element, destroy the scope) the second directive will now have this data:
Product_2:
itemId:3,
quantity:2,
{other data}
Here is the template code :
<div class="unItem" ng-repeat="item in ngCart.getCart().items track by $index">
<div class="photo"><img src="{[{ item.getImage() }]}" alt=""></div>
<div class="details">
<h3>{[{ item.getName() }]} <span>{[{ item.getPrice() | currency:$}]}</span></h3>
<md-select ng-model="attributes" placeholder="Attribut" class="select-attribut" ng-show="item.hasAttributes()" ng-change="item.updateSelected(attributes)">
<md-option ng-repeat="attr in item.getAttributes()" ng-selected="attr == item.getSelected()" ng-value="attr">{[{ attr }]}</md-option>
</md-select>
</div>
<div class="quantity">
<div fs-counter-dynamic value="itemQuantity"
data-min="1"
data-max="999"
data-step="1"
data-addclass="add-quantity"
data-width="130px"
data-itemid="{[{ item.getId() }]}"
data-editable
ng-model="itemQuantity"
name="quantity" id="quantity-{[{ item.getId() }]}",
index="{[{ item.getId() }]}"
></div>
</div>
<div class="total">Total : {[{ item.getTotal() | currency }]}</div>
<div class="delete"><a ng-click="ngCart.removeItemById(item.getId());"></a></div>
</div>
Is this normal behavior? Is there any way to force the directive to keeps its own data? From what I've understood, each directive has its own scope, so what I think happens is that, when we remove the first one, it keeps the data stored in some kind of array that says "directive 1 data is : " and when we delete the first directive, the second one becomes the first.
So basically, are we doing anything wrong or is there anyway to remap the data?
Hope it was clear enough,
Thanks!
Edit: added html code
Edit2: Answer :
New FsCounter template looks like this:
<div fs-counter-dynamic value="item._quantity"
data-min="1"
data-max="999"
data-step="1"
data-addclass="add-quantity"
data-width="130px"
data-itemid="{[{ item.getId() }]}"
data-editable
ng-model="item._quantity"
name="quantity" id="quantity{[{ item.getId() }]}"
></div>
Do you know ng-repeat, then you don't have such problems
<div ng-repeat="product in products">
<fs-counter index="product.index" value="product.value"></fs-counter>
</div>
and in your controller
$scope.products = [
{index:1, value:"Cola"},
{index:2,,value:"Fanta"}
]
to remove an element you just have to do
$scope.products.splice(0,1);
Edit:
I suggest to save all necessary data inside the item you use inside ng-repeat. Your problem is, that you mix data from array with other data from your $scope. It is possible to $watch changes in your directive, but if you set them with ng-repeat everything is done automatically.
$scope.products = [
{index:1, name:"Cola", price:1.50, image:"your/path.png", attributes:{...}},
{index:2, name:"Fanta", price:1.40, image:"your/path.png"}
]
And then in your html
<div class="unItem" ng-repeat="item in ngCart.products track by $index">
<div class="photo"><img ng-src="item.image" alt=""></div>
<div class="details">
<h3>{{item.name}} <span>{{item.price | currency}}</span></h3>
</div>
<div class="quantity">
<div fs-counter-dynamic value="item.quantity"
data-min="1"
data-max="999"
data-step="1"
data-addclass="add-quantity"
data-width="130px"
data-itemid="item.index"
data-editable
ng-model="item.quantity"
name="quantity" id="{{'quantity-' + $index}}",
index="item.index"
></div>
</div>
<div class="total">Total : {{ item.price * item.quantity | currency }}</div>
<div class="delete"><a ng-click="ngCart.removeItemById(item.index);"></a></div>
</div>
I use the structure provided by the yeoman with angular-generator.
The ng-click does not work in my directive, of a slider show, when I put the html directly in main.html (It only works when I put in the directive an templateurl, linked to the main.html , but this causes delay to load).
Html, that is inserted directly into main.html
<div images="images" class="slider" id="mauseOnOut">
<div class="slide" ng-repeat="image in images" ng-show="image.visible">
<a ng-href="{{image.url}}"><img ng-src="{{image.src}}" width="444" height="250"/>
<p class="texto">{{image.texto}}</p>
</a>
</div>
<ul class="minimagem" ng-show="images.length">
<li ng-repeat="image in images"><a ng-click="returner($index)"><img ng-src="{{image.src}}" width="70" height="56"/></a></li>
</ul>
<div class="arrows">
<img src="http://s5.postimg.org/qkfwdwi7n/right_arrow.png"/>
</div>
</div>
Main part of the directive (in jsFiddle have it complete)
myApp.directive('images', function ($timeout) {
return {
restrict: 'AE',
scope:{
images: '='
},
link: function (scope) {
scope.currentIndex=0;
scope.returner = function(index){
scope.currentIndex = index;
};
scope.next=function(){
scope.currentIndex<scope.images.length-1?scope.currentIndex++:scope.currentIndex=0;
};
scope.prev=function(){
scope.currentIndex>0?scope.currentIndex--:scope.currentIndex=scope.images.length-1;
};
scope.$watch('currentIndex',function(){
scope.images. forEach(function(image){
image.visible=false;
});
scope.images[scope.currentIndex].visible=true;
});
},
};
});
Put an example in jsFiddle ; when use angular 1.1, on jsFiddle, operate normally, with 1.2 or higher does not work. In my application I use the angular 1.3.10 .
How could make it work in my application? It could be to ' compile ' or in some other way , the important thing is the click staying active in the image thumbnails and arrows .
Edited: I came back with the best known directive , best to understand.
I've written a directive that dynamically creates a popover for an element:
app.directive('popover', function($compile, $timeout){
return {
link: function(scope, element, attrs) {
$timeout(function() {
// grab template
var tpl = $(element).find('.popover-template')
// grab popover parts of template
var template = {
//$compile( $(element).siblings(".pop-content").contents() )(scope)
title: tpl.find('.template-title').contents(),
content: tpl.find('.template-content').contents()
};
// render template with angular
var content = $compile(template.content)(scope);
var title = $compile(template.title)(scope);
$(element).popover({
html: true,
placement: "right",
content: content,
title: title
});
scope.$digest()
});
}
};
});
In application it looks like this:
<span popover>Click me</span>
<div ng-hide="true" class="popover-template">
<div class="template-title">
<strong>{{ x.name }} and {{ y.name }}</strong>
</div>
<div class="template-content">
<div>
<pre>f in [1,2,3]</pre>
<div ng-repeat="f in [1,2,3]">
item {{ f }}, index {{ $index }}
</div>
</div>
</div>
</div>
The popover is created and displayed. The title works correctly as well. However, ng-repeat is applied multiple times in any iteration:
As you can see, the iteration that should only include 3 elements in fact includes 3*3 elements. The directive creates popovers for exactly 3 elements, so I guess that's where my mistake lies. How can I make sure that within each popover, ng-repeat is only called once?
The problem
Since the popover-template element is already in the document when you bootstrapped the angular application (at page load), it has already been compiled once. The ng-repeat element is replaced with 3 new elements:
<!-- original -->
<div ng-repeat="f in [1,2,3]">item {{ f }}, index {{ $index }}</div>
<!-- replaced -->
<div ng-repeat="f in [1,2,3]">item 1, index 0</div>
<div ng-repeat="f in [1,2,3]">item 2, index 1</div>
<div ng-repeat="f in [1,2,3]">item 3, index 2</div>
When you compile it again in the link function, each of the 3 ng-repeats is triggered, making 3 identical copies, 9 total.
The solution
Keep your popover-template in a separate file so it is not compiled on page load. You can then load it with the $templateCache service.
In general, just make sure you don't compile your HTML multiple times.
Instead using the compiled html for the popover template, load the template using $http or templateCache.
The HTML:
<span popover>Click me</span>
<script type="text/ng-template" id="popover.html">
<div class="popover-template">
<div class="template-title">
<strong>{{ x.name }} and {{ y.name }}</strong>
</div>
<div class="template-content">
<div>
<pre>f in [1,2,3] track by $index</pre>
<div ng-repeat="f in [1,2,3]">
item {{ f }}, index {{ $index }}
</div>
</div>
</div>
</div>
</script>
The Javascript:
angular.module('app',[]).directive('popover', function($compile, $timeout, $templateCache){
return {
link: function(scope, element, attrs) {
$timeout(function() {
// grab the template (this is the catch)
// you can pass the template name as a binding if you want to be loaded dynamically
var tpl = angular.element($templateCache.get('popover.html'));
// grab popover parts of template
var template = {
title: tpl.find('.template-title').contents(),
content: tpl.find('.template-content').contents()
};
// render template with angular
var content = $compile(template.content)(scope);
var title = $compile(template.title)(scope);
$(element).popover({
html: true,
placement: "right",
content: content,
title: title
});
scope.$digest()
});
}
};
});
Also, I have made this plunker with an working example: http://embed.plnkr.co/IoIG1Y1DT8RO4tQydXnX/