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/
Related
I am using ng-repeat to print a list of posts to the page via the WordPress REST-API. I am using Advanced Custom Fields on each post. From what I can see everything is working, but the post data is not showing in one container, yet it is displaying in another. I should also mention that this is set up like tabs. (user clicks a tab for a post and it displays that posts data)
var homeApp = angular.module('homeCharacters', ['ngSanitize']);
homeApp.controller('characters', function($scope, $http) {
$scope.myData = {
tab: 0
}; //set default tab
$http.get("http://bigbluecomics.dev/wp-json/posts?type=character").then(function(response) {
$scope.myData.data = response.data;
});
});
homeApp.filter('toTrusted', ['$sce',
function($sce) {
return function(text) {
return $sce.trustAsHtml(text);
};
}
]);
HTML:
<section class="characters" ng-app="homeCharacters" ng-controller="characters as myData">
<div class="char_copy">
<h3>Meet the Characters</h3>
<div ng-repeat="item in myData.data" ng-bind-html="item.content | toTrusted" ng-show="myData.tab === item.menu_order">
<!--this is the section that will not display-->
<h3>{{ item.acf.team }}</h3>
<h2>{{ item.acf.characters_name }} <span>[{{item.acf.real_name}}]</span></h2>
<p class="hero_type">{{ item.acf.hero_type }}</p>
{{ item.acf.description }}
Learn More
</div>
</div>
<div class="char_tabs">
<!--if I put the identical {{}} in this section it WILL display-->
<nav>
<ul ng-init="myData.tab = 0" ng-model='clicked'>
<li class="tab" ng-repeat="item in myData.data" ng-class="{'active' : item.menu_order == myData.tab}">
<a href ng-click="myData.tab = item.menu_order">
<img src="{{ item.featured_image.source }}" />
<h3>{{ item.title }}</h3>
</a>
</li>
</ul>
</nav>
</div>
</section>
I should also mention that I use Ng-inspector, and it does show the data being pulled in. I can confirm this via the console. I have checked to ensure no css is in play; the div is totally empty in the DOM.
I appreciate all the help the GREAT angular community has shown!
The problem is you had used ng-bind-html over ng-repeat element which is changing the inner content of ng-repeat div. I guess as you have inner transcluded template inside ng-repeat directive, you should not be using ng-bind-html there in a place.
Markup
<div ng-repeat="item in myData.data" ng-show="myData.tab === item.menu_order">
<!--this is the section that will not display-->
<h3>{{ item.acf.team }}</h3>
<h2>{{ item.acf.characters_name }} <span>[{{item.acf.real_name}}]</span></h2>
<p class="hero_type">{{ item.acf.hero_type }}</p>
{{ item.acf.description }}
Learn More
</div>
I'm having the following problem:
I want to use a directive at different places in an app and don't want to specify the parent object and directive object every time i use the directive.
Look at this plnkr:
http://plnkr.co/edit/yUoXXZVJmoesIQNhoDDR?p=preview
Its just a $scope.data object that stores a multilevel array.
$scope.data=
[
{"name": "LEVEL0_A", "subitems":
[
{"name":"Level1_A", "subitems":[{"name":"A"},{"name":"B"}]},
{"name":"Level1_B", "subitems":[{"name":"C"},{"name":"D"}]},
...
...
and so on
and there is a little sample custom directive, called deleteItem, that does exactly that.
.directive('deleteItem', function() {
return {
scope:{
item:'=',
parent:'='
},
template: '<ng-transclude></ng-transclude>Delete',
transclude:true,
controller: function($scope){
$scope.deleteItem=function(currentItem,currentParent){
currentParent.subitems.splice(currentParent.subitems.indexOf(currentItem),1);
};
}
};
});
here you see the html template
<body ng-app="myApp">
<div ng-controller="myController">
<div ng-repeat="level0 in data">
<h2>{{level0.name}}</h2>
<div ng-repeat="level1 in level0.subitems">
<div delete-item parent="level0" item="level1">
{{level1.name}}
</div>
--------------------
<div ng-repeat="level2 in level1.subitems">
<div delete-item parent="level1" item="level2">
Name: {{level2.name}}
</div>
</div>
<hr>
</div>
</div>
</div>
</body>
I mean it works, but actually i feel that there must be some way finding the item and parent without specifically linking them to the scope manually.
I'd be really glad if someone could point me in the right direction.
Thanks
Markus
If you do something like this.
$scope.deleteItem=function(currentItem,currentParent){
currentParent.subitems.splice(currentParent.subitems.indexOf(currentItem),1);
};
Then your directive becomes dependent upon the structure of data outside it's scope. That means that the directive can only delete items if it follows exactly that pattern. What if you want to use the delete button on data that isn't from an array?
The better approach is to use the API feature & to execute an expression on the outer scope.
app.directive('deleteItem', function () {
return {
scope: {
remove: '&deleteItem'
},
template: '<ng-transclude></ng-transclude><a ng-click="remove()">Delete</a>',
transclude: true
};
});
When the user clicks "Delete" the remove() API is called and the template handles how that item is removed.
<div ng-repeat="level0 in data">
<h2>{{level0.name}}</h2>
<div ng-repeat="level1 in level0.subitems">
<div delete-item="level0.splice($index,1)">
{{level1.name}}
</div>
--------------------
<div ng-repeat="level2 in level1.subitems">
<div delete-item="level1.splice($index,1)">
Name: {{level2.name}}
</div>
</div>
<hr>
</div>
</div>
I have some content on my site that doesn't format well because different browsers/screens render the font-size a little differently. To counteract this, I'm attempting to use Angular to get the height of some <p> tags, and if they're taller than my layout allows, lower their font size.
The <p> tags I'm trying to manipulate are contained in a directive which generates multiple content boxes based on some JSON.
I have created this directive:
spaModule.directive ("resizeParagraph", function() {
return function (scope, element, attrs) {
while (element.height() > 400) {
element.css("font-size", (parseInt(element.css("font-size")) -1 + "px"));
}
}
});
This is the directive which creates those boxes (this works):
<div ng-repeat="data in homeCtrl.homeData" class="content-box">
<img class="content-image" ng-src="images/home/{{ data.imageSrc }}"/>
<div class="sub-content">
<h1>
{{ data.heading }}
</h1>
<p resize-paragraph class="large-text">
{{ data.body }}
</p>
<a ng-href="#/{{ data.linkUrl }}" class="box-link">
{{ data.linkValue }}
</a>
</div>
</div>
I'm at home creating custom directives with a source URL, but this is my first go at creating a logical attribute-based directive.
What have I done wrong?
Try adding jQuery, before loading angular.js. In this post, it is written that Angular is using its own library jqLite to substitute for jQuery when the jQuery library is not included. jqLite does not include a height() function. To be able to use height(), you have to include the full jQuery library.
Just add <script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.js"></script> before the line where you include angular.js.
I tested it with the following code:
<style type="text/css">
.large-text {
font-size: 600px;
}
</style>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.js"></script>
<script src="angular.js"></script>
<script type="text/javascript">
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.homeCtrl = {};
$scope.homeCtrl.homeData = [
{
heading: 'Heading',
body: 'Body',
linkValue: 'LinkValue'
}
];
});
app.directive("resizeParagraph", function() {
return function (scope, element, attrs) {
while (element.height() > 100) {
element.css("font-size", (parseInt(element.css("font-size")) -1 + "px"));
}
}
});
</script>
<div ng-app="myApp" ng-controller="myCtrl">
<div ng-repeat="data in homeCtrl.homeData" class="content-box">
<img class="content-image" ng-src="images/home/{{ data.imageSrc }}"/>
<div class="sub-content">
<h1>
{{ data.heading }}
</h1>
<p resize-paragraph class="large-text">
{{ data.body }}
</p>
<a ng-href="#/{{ data.linkUrl }}" class="box-link">
{{ data.linkValue }}
</a>
</div>
</div>
</div>
EDIT
After some testing, I found the reason why it is not working as expected. The function inside the directive is called, before the expression {{data.body}} is executed in the template. It means that at the moment the directive is called, the text inside the paragraph is literally {{data.body}}. What you want is to postpone the execution of the directive after the expression has been executed. You can do it as follows:
app.directive("resizeParagraph", function() {
return function (scope, element, attrs) {
scope.$watch(name, function () {
while (element.height() > 50) {
element.css("font-size", (parseInt(element.css("font-size")) -1 + "px"));
}
})
}
});
I can also confirm that element.height() and element.context.offsetHeight return the same value. The height of the element in px. So, it doesn't matter which of the two you'll use.
I hope this helps.
I'm trying to make an planification app with AngularJS. The main feature is to create a Task.
I want to put the source of the task in a directive :
<section id="runningTasks" ng-controller='RunningTaskCtrl as ctrl'>
<task class="task" ng-repeat='task in ctrl.tasks'></task>
</section>
For each task, I add it in the div.
Here is my directive definition :
.directive('task', function(){
return {
restrict: 'EA',
replace:'true',
templateUrl: '/Planificator/directives/task/task.html',
link : function(scope, element, attrs){
var date = $(element).find(".datepicker");
date.datepicker();
date.datepicker("option", "dateFormat", "dd-mm-yy");
}
};
})
And the content of task.html :
<div class="task" ng-click="task.editting = true" task>
<h1>{{ task.title }}</h1>
<p>
{{ task.comment }}
</p>
<div class="edit-task" ng-show="task.editting">
<form ng-submit="ctrl.propose(task)">
... form stuff ...
</form>
</div>
</div>
My problem is when I run my page, I get an error :
Error: [$compile:multidir] http://errors.angularjs.org/1.2.26/$compile/multidir?p0=task&p1=task&p2=tem…3D%20true%22%20task%3D%22%22%20ng-repeat%3D%22task%20in%20ctrl.tasks%22%3E
at Error (native)
(the clean link : Angular error generator)
I already had this problem before and I just put the content of the template in the ngRepeat and doesn't think anymore, but this time I would like to be things in the good way.
Thank you for the answers !
Your problem is:
<div class="task" ng-click="task.editting = true" task>
Since this is part of the template created from the task directive you are trying to add the task directive over and over.
Change to:
<div class="task" ng-click="task.editting = true">
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.