I am trying to use a Bootstrap Popover in an AngularJS ngRepeat directive. The problem is the title attribute isn't being interpolated. How can I resolve this issue?
View
<div ng-app="app" id="ng-app">
<ul ng-controller="Main">
<li ng-repeat="i in items" data-toggle="popover" title="{{ i.title }}" <!-- Title isn't interpolated so Popover doesn't render a title -->
data-content="Events: {{ day.events.length }}"
init-popover>{{ i.data }}</li>
</ul>
Code
var app = angular.module('app', []);
app.controller('Main', ['$scope', function($scope){
$scope.items = [
{ title: 'Title 1', data: 'lorem' },
{ title: 'Title 2', data: 'ipsum' },
];
}]);
app.directive('initPopover', function() {
return function(scope, element, attrs) {
if (scope.$last){
$('[data-toggle="popover"]').popover({container: 'ul'});
}
};
});
FIDDLE
You can make it work but it's pretty ugly, here is my solution
var app = angular.module('app', []);
app.controller('Main', ['$scope', function($scope){
$scope.items = [
{ title: 'Title 1', data: 'Test in ngRepeat' },
{ title: 'Title 2', data: 'Test in ngRepeat' },
];
}]);
app.directive('initPopover', function() {
return function(scope, element, attrs) {
if (scope.$last) {
attrs.$observe('title', function () {
$('[data-toggle="popover"]').popover({container: 'ul'});
});
}
};
});
You shouldn't use title="{{i.title}}", but ng-attr-title="{{i.title}}"...
I had the same problem. I'm using bootstrap v3.3.6 and Angular v1.5.0
Adding attribute data-original-title="{{someTitle}}" fixed the problem.
<button type="button" class="btn btn-lg btn-danger" data-toggle="popover" title="{{someTitle}}" data-original-title="{{someTitle}}" data-content="Some Content">Click to toggle popover</button>
Also, as bootstrap requires, call .popover() in directive
module.directive('myDir', ['jQuery', function (jQuery) {
return {
link: function () {
jQuery('[data-toggle="popover"]').popover();
}
};
}]);
Add $timeout to directive will solve your problem.
$timeout will gets call when last element of ng-repeat rendered on UI & run $digest cycle.
Directive
app.directive('initPopover', function($timeout) {
return function(scope, element, attrs) {
if (scope.$last) {
$timeout(function() {
angular.element('[data-toggle="popover"]').popover({
container: 'ul'
});
});
}
};
});
Working Fiddle
Hope this could help you. Thanks.
Consider using ui-bootstrap for native Angular directives instead of Bootstrap's jQuery version. They're a lot easier to work with in an Angular project.
Edit: If you can't do that, here's a solution:
<li ng-repeat="i in items" data-toggle="popover" data-ng-attr-popover_title="{{ i.title }}"
data-content="Popover Content"
init-popover>{{ i.data }}</li>
With a small change to the directive:
app.directive('initPopover', function() {
return function(scope, element, attrs) {
if (scope.$last){
$('[data-toggle="popover"]').popover({ container: 'ul', title: attrs.popoverTitle });
}
};
ng-attr-popup_title will create an HTML attribute called popupTitle on the element with the Angular syntax inside {{}} parsed correctly. Then, in the directive, we can just examine that attribute to set the popover title.
Fiddle: here
Just to add to the knowledge base here. Here's the angular way, in case anyone needs. I based my solution on Mark Coleman's PopOver tutorial, and of course what has been contributed by Nate Barbettini.
My situation was I have a nested ng-repeat over items in items, so I need to have a title for my popup that is based on an array stored in a scope, while the content of the popup is from another array stored in the array of the scope.
I added ng-attr-title to my HTML element, like so:
<div pop-over items='week.items' ng-attr-title="{{'Week number:'+ week.week}}"> Total sales this week : {{week.sales}}</div>
Then, in my directive, I followed mostly what Mark Coleman did.
angular.module('vegadirectives', [])
.directive('popOver', function ($compile, $templateCache) {
var itemsTemplate = "<ul class='popper'><li ng-repeat='thing in items track by $index'>";
itemsTemplate+=" <i class='fa fa-square' ng-class=";
itemsTemplate+="{purple:thing.color==='purple','orange':thing.color==='orange','white':thing.color==='white','pink':thing.color==='pink','red':thing.color==='red','green':thing.color==='green','yellow':thing.color==='yellow'}";
itemsTemplate+="></i> {{thing.model}} - {{thing.qty}}</li></ul>";
var getTemplate = function () {
var template=itemsTemplate;
return template;
}
return {
restrict: "A",
transclude: true,
template: "<span ng-transclude></span>",
link: function (scope, element, attrs) {
var popOverContent;
if (scope.items) {
var html = getTemplate();
popOverContent = $compile(html)(scope);
var options = {
trigger:'click',
content: popOverContent,
placement: "right",
html: true,
title:scope.title
};
if (scope.$last) {
}
$(element).popover(options);
}
},
scope: {
items: '=',
title:'#'
}
};
});
Explanation:
To fill up my popover with a list of items, with some fancy color square to indicate different types of items, I created a chunk of HTML code. This chunk named itemsTemplate will be used by $compile to create the popover content with scope. I think the usage is $compile(htmlstring)(scope).
Next comes the standard stuff for options settings for the popover.
For the interpolation of scope with title, I specified the option title: with scope.title. Then, in the scope: option way down at the end, the two scope elements are specified with '=' and '#'.
So, the title:scope.title refers or binds to scope: {items:'=', title:'#' }, and the title:'#' is the attributes of the title in the HTML, which is ng-attrs-title={{week.week}}, where week is from the outer ng-repeat loop.
Hope it helps.
After finding this answer at https://stackoverflow.com/a/17864868/2379762, I tried data-original-title={{i.title}}, and it works.
http://jsfiddle.net/f1tjj8x5/
<li ng-repeat="i in items" data-toggle="popover" data-original-title="{{ i.title }}"
data-content="Popover Content"
init-popover>{{ i.data }}</li>
Related
I'm pretty new to Angular, so I think template is the wrong word. Here's what I mean:
We have a Rails backend serving up data to an Angular frontend. I need to use an ng-repeat to show off this data. The problem is, the desired template comes as part of the data that Rails serves us. Here's an example:
JSON served by Rails
[
{
data: {
title: "example title",
body: "example body"
},
template: "<h1>{{title}}</h1><p>{{body}}</p>"
},
{
data: {
body: "example body two"
},
template: "<div>{{body}}</div>"
}
]
We don't know how many records there will be, what the templates will look like, or what bindings they call for (body, title, caption, etc). I cannot store the templates locally, so all the data has to come from the Rails backend.
Here is pseudocode of what I'd like to accomplish:
<span ng-repeat="block in blocks">
<template src="block.template" data="block.data"></template>
</span>
This would use the specified template and bind the data object to it.
Again, I'm new to angular so if I need to clarify let me know. Thank you!
EDIT: Here's my attempt at a directive. It doesn't work since it appears that template doesn't have access to the desired template represented by block.template. This wouldn't cover binding the data, so I'd have to work that in once I fixed the directive.
app.directive("template", function() {
return {
template: block.template,
scope: {
block: '=data'
},
restrict: 'E'
};
});
EDIT 2:
Here's another attempt that doesn't work. This one shows the template on screen, but in the console it errors that scope.$watch is not a function
app.directive('template', function ($compile) {
return {
restrict: 'A',
replace: true,
scope: {
tmpl: '=template'
},
link: function postLink(scope, element, attrs) {
scope.$watch('tmpl', function (block) {
element.html(block.template.blank_template);
$compile(element.contents())(block.payload);
});
}
};
});
You can do it with a directive:
basically something along the lines of
//Assuming App is your Angular App Instance and Angular at least 1.4ish
app.directive('template',['$compile', function($c) {
return {
link: function (scope, el, attrs, controller) {
el.html(scope.template);
$c(el.contents())(scope);
},
scope: {
template: "=tpl",
data: "=data"
}
}
});
Then you can use it like so:
<span ng-repeat="block in blocks">
<template tpl="block.template" data="block.data"></template>
</span>
I just started working with AngularJS but I found a small problem that I cant find out, I hope you peeps can help me.
I have imported AngularJS Gridster, a simple way to add a dynamic grid to your webpage. Now everything works and the element get succesfully loaded from the database and imported in Gridster but now I want to do the following thing. In the JSON that is retrieved from the database there is also an attribute called "directive". Now, when everything is loaded I want to add in each Gridster element the directive that is returned from the database.
<ul>
<li gridster-item="item" ng-repeat="item in gridsterItems">
{{ item.directive }} // Returns <clock-widget></clock-widget> and print it to the screen, but it dont run the directive and doesn't display.
</li>
</ul>
Now it returns the right value and display the string on the screen but I want to run it directive clockWidget.
app.directive('clockWidget', function() {
return {
replace: true,
template: 'Yups, I am the clockwidget',
};
});
On the internet I read something about $compile but I can't find out. I hope you peeps can help me.
Thanks!
Yes, you need use $compile. See documentation.
Live example on jsfiddle.
angular.module('ExampleApp', [])
.controller('ExampleController', function($scope) {
$scope.directives = ["<directive-one></directive-one>", "<directive-two val='inputVal'></directive-two>"];
})
.directive('compileDirective', function($compile) {
return {
restrict: "E",
replace: true,
link: function(scope, element, attr) {
scope.$watch(function() {
return attr.directive;
}, function(val) {
element.html("");
if (val) {
var directive = $compile(angular.element(val))(scope);
element.append(directive);
}
});
}
};
})
//Directives for example
.directive('directiveOne', function($compile) {
return {
replace: true,
template: "<div>i'm directive one</div>"
};
})
.directive('directiveTwo', function($compile) {
return {
replace: true,
scope:{val:"="},
template: "<div>i'm directive two with val={{val}}</div>"
};
})
.directive('directiveThree', function($compile) {
return {
replace: true,
scope:{val:"="},
template: "<div>i'm directive three</div>"
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController">
<select ng-model="selectDirective" ng-options="dir for dir in directives">
</select>
<input ng-model="inputVal">
<compile-directive directive="{{selectDirective}}"></compile-directive>
<compile-directive directive="<directive-three></directive-three>"></compile-directive>
</div>
</div>
I am wrapping the HTML "select" element with my own directive. This directive should create a Dropdown menu representing it's options.
I tag the select element with my custom "arBootstrapSelect" directive/attribute.
This directive appends a Dropdown menu with the options inside it repeated by ng-repeat.
The "option" elements in the "select" element are tagged with "arBootstrapSelectOption". They should have a "content" attribute, representing a dynamic directive. This dynamic directive should be compiled and shown in the Dropdown menu.
Basiclly, each option(tagged by "arBootstrapSelectOption") compiles it's "content" attribute using the $compile service and injects it into a list living in the arBootstrapSelect directive. After that arBootstrapSelect should show the compiled options using ng-repeat. Hope it's not too complicated.
I am getting Error Link
HTML:
<select ar-bootstrap-select class="form-control">
<option ar-bootstrap-select-option value={{$index}} ng-repeat="c in countries" content="<ar-country country-id='{{$index}}'></ar-country>">
</option>
</select>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Dropdown
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li ng-repeat="option in arBootstrapSelectOptions">
{{option[1]}}
</li>
</ul>
</div>
My directives:
(function () {
'use strict';
var app = angular.module('mainApp');
app.directive('arBootstrapSelect', function ($interval, $compile) {
$scope.arBootstrapSelectOptions = [];
function link(scope, element, attrs) {
//element.hide();
}
return {
restrict: 'A',
link: link,
scope: true
}
});
app.directive('arBootstrapSelectOption', function ($compile) {
function link(scope, element, attrs) {
scope.arBootstrapSelectOptions.push([attrs.value, $compile(attrs.content)(scope)]);
}
return {
scope: true,
restrict: 'A',
link: link,
}
});
})();
This works, but it's ugly and slow:
(function () {
'use strict';
var app = angular.module('mainApp');
app.directive('arBootstrapSelect', function ($interval, $compile) {
//$scope.arBootstrapSelectOptions = [];
function link(scope, element, attrs) {
//element.hide();
scope.$on('arBootstrapSelectNewItem', function (event, data) {
var test = $('<li></li>').append(data);
element.parent().find('.dropdown-menu').append(test);
});
}
return {
restrict: 'A',
link: link,
scope: true,
transclude: true
}
});
app.directive('arBootstrapSelectOption', function ($compile) {
function link(scope, element, attrs) {
//scope.arBootstrapSelectOptions.push([attrs.value, $compile(attrs.content)(scope)]);
scope.$emit('arBootstrapSelectNewItem', $compile(attrs.content)(scope));
}
return {
scope: true,
restrict: 'A',
link: link
}
});
})();
I don't fully understand your specific example, so I'll answer at a conceptual level.
It seems that at a high level all you want to is to specify a user-provided template for ng-repeat (e.g. <ar-country country-id='{{$index}}'>) and place it within a directive-provided template (e.g. within <li>).
It would be easier and more user-friendly to provide the template as content, rather than as an attribute. Then, all you'd need is to transclude the content. (I'm avoiding here the use of <option> since it is a directive on its own and I don't understand exactly what you are trying to do - I'll use <my-option> instead):
<my-option ng-repeat="c in countries">
<ar-country country-id='{{$index}}'></ar-country>
<my-option>
The directive would look like so:
.directive("myOption", function(){
return {
scope: true,
transclude: true,
template: '<li ng-transclude></li>'
}
});
This will create conceptually the following HTML:
<my-option>
<li><ar-country country-id="0"></ar-country></li>
</my-option>
<my-option>
<li><ar-country country-id="1"></ar-country></li>
</my-option>
..
(The directive arCountry will also be compiled and linked, and may generate its own content)
Additionally, if the parent needs to register each child, it's better to use require to communicate rather than $emit/$on. So, the parent needs to define a controller with a function to register children. And, again, if you need to supply an additional template, then transclude the content:
<my-select>
<my-option ng-repeat="c in countries>
<ar-country country-id='{{$index}}'></ar-country>
</my-option>
</my-select>
.directive("mySelect", function(){
return {
scope: true,
transclude: true,
template: '<h1>heading</h1><div ng-transclude></div>',
controller: function(){
// this can be called by the child
this.registerChild = function(childElement, childController){
// store the children, if needed
}
}
}
});
To make use of this.registerChild we'd need to make myOption use require:
.directive("myOption", function(){
return {
scope: true,
transclude: true,
template: '<li ng-transclude></li>'
require: ['myOption', '^mySelect'],
controller: function(){
// you can define some functions here
},
link: function(scope, element, attrs, ctrls){
var me = ctrls[0],
selectCtrl = ctrls[1];
selectCtrl.registerChild(element, me);
}
});
I made a custom directive ("wizard") based on the JQuery Steps plugin. But im facing problems when using ngRepeat inside my directive. The diretive uses ngTransclude so i can provide the steps content inside the page markup.
Im nowhere near an Angular expert, but what i can find is that it's a bug/freakaccident in Angular where an ngRepeat is used inside a directive with isolated scope and transclusion. Like mentioned here
https://github.com/angular/angular.js/issues/7874 but i can't seem to get any of the suggestions working :\
The directive looks like this:
Directives.wizard = ['$compile', function ($compile) {
return {
restrict: 'E',
scope: {
cancel: '&',
finish: '&',
stepChanged: '&'
},
template: '<div class="wizard" data-ng-transclude></div>',
replace: true,
transclude: true,
link: function ($scope, element, attrs, ctrl) {
// directive has to be wrapped in order to ngModel on sections to work with transclude: true
element.wrapInner('<div class="steps-wrapper">');
var opts = {
headerTag: "h3",
bodyTag: "section",
transitionEffect: "slideLeft",
onCanceled: function (event, currentIndex) {
$scope.cancel();
},
onFinished: function (event, currentIndex) {
$scope.finish();
},
onStepChanged: function (event, currentIndex) {
$scope.stepChanged();
}
};
var stepsEl = element.children('.steps-wrapper').steps(opts);
// wrap 'n compile..
$compile(stepsEl)($scope);
}
};}];
markup
<wizard>
<h3>Title</h3>
<section>
<p><DG:TextControl runat="server" Key="MineSuccesHistorier.Step1"/></p>
<input data-ng-model="$parent.newData.Title"></input> <!-- outputs array just fine -->
<!-- test stuff here -->
<!-- 1. regular ng-repeat - doesnt work (also tried with $parent.$parent which Batarang says is the correct path -->
<ul>
<li data-ng-repeat="item in $parent.newData.Competences">{{item.Title}}</li>
</ul>
<!-- 2. try with ng-init - doesnt work -->
<ul data-ng-init="comp = $parent.newData.Competences">
<li data-ng-repeat="item in comp">{{item.Title}}</li>
</ul>
</section>
<h3>Tab</h3>
<section>
<!-- next wizard tab here -->
</section>
How data is set in controller
$scope.newData = {
"Competences": [
{
"IsSelected": true,
"Title": "Hest"
},
{
"IsSelected": false,
"Title": "Kat"
},
{
"IsSelected": true,
"Title": "Ko"
},
{
"IsSelected": false,
"Title": "Ged"
}
],
"Id": "905c1285-d58b-4f65-8df5-52986c70a820",
"Situation": null,
"Title": null}
any ideas are much appreciated, thanks in advance!
UPDATE 25/3:
Added plnkr here http://plnkr.co/edit/kNl4UEoUa7zU4CgWaGSa?p=preview
When i add isolated scope in the directive, the repeaters stop working. If i leave out the isolated scope, it seems as if the ngRepeats are compiled multiple times.
UPDATE 2 25/3:
Added new plunkr with Vinays compiling suggestion - ng-repeat is now compiled only once. But two-way binding with ngModel in controller scope isn't working http://plnkr.co/edit/TR3XxvV4IYI66h4pY5hx?p=preview
Content is getting repeated because, ng-repeat will compile the contents once and $compile will compile it once again.
Directive
app.directive('wizard', function($timeout, $compile) {
return {
restrict: 'E',
replace: true,
template: '<div class="mywizard"></div>',
compile: function (tEl) {
return function(scope, element, attrs) {
var stepsEl;
element.wrapInner('<div class="steps-wrapper">');
element.children('.steps-wrapper').append(tEl.context.innerHTML);
var content = $compile(element.contents())(scope);
$timeout(function () {
element.children('.steps-wrapper').steps(opts);
})
}
}
};
});
Working Plnkr
I have these nested directives the very inner of which has an 'X' sign, which is, when clicked, is supposed to delete an item (classic problem). Basically, the whole thing is a menu.
I have added an ng-click to the 'X' sign/button of the item, but i am very confused on how to link this whole thing back to the controller, so that i can call a deleteItem() function and actually remove the item. I also want to have scope for the sidebar-item separated (the original version of this code is slightly more elaborated and has conditional statements)
Here's what i have so far
The full working - i.e. not really working - version can be found in this jsfiddle
Here's the relevant part of HTML:
<div ng-app="demoApp">
<div ng-controller="sidebarController">
<div sidebar>
<div sidebar-item ng-repeat="item in items" item="item"></div>
</div>
<button ng-click="addItem();">Add Item</button>
</div>
</div>
And here's the JavaScript:
var demoApp = angular.module('demoApp', []);
demoApp.controller("sidebarController", ["$scope", function($scope) {
$scope.items = [
];
$scope.itemId = 1;
$scope.addItem = function() {
var inx = $scope.itemId++;
$scope.items.push( { id: inx, title: "Item " + inx, subTitle: "Extra content " + inx } );
};
$scope.deleteItem = function(item) {
console.log("Delete this!");
console.log(item);
};
}]);
demoApp.directive("sidebar", function() {
return {
restrict: "A",
transclude: true,
template: '<div><div ng-transclude></div></div>',
controller: ["$scope", function($scope) {
this.deleteItem = function(item) {
$scope.deleteItem(item);
$scope.$apply();
};
}]
};
});
demoApp.directive("sidebarItem", function() {
return {
restrict: "A",
scope: {
item: "="
},
require: "^sidebar",
template: '<div><span>{{ item.title }}</span><br /><span>{{ item.subTitle }}</span><br /><span ng-click="deleteItem(item)">X</span></div>',
};
});
Im sure the answer is simple, but I just cant find it.
If you want to use a required controller function you need to inject it into the link function
in sidebar-item add
link : function (scope, element, attrs, sidebar) {
scope.deleteItem = function (item) {
sidebar.deleteItem(item);
}
}