Bootstrap popover inside angular repeats form content whenever opened - angularjs

I am showing a form inside of a popover. The popover opens on the click of a button. The issue is that every time I click the button to open the popover, the same form gets appended to the existing popover. Here's my code:
$('.filter-node-btn').popover({
trigger:'click',
html : true,
placement: 'bottom',
content: function() {
return $compile($(".filterNodeDialog").html())($scope);
}
});
update
I found the issue while writing a jsfiddle for this problem. It worked in the fiddle but not my local environment. I am using bootstrap js 3.1.1 while for fiddle I tried bootstrap js 3.0. heres the line thats giving me issue in boostrap.js
bootstrap 3.1.1
Popover.prototype.setContent = function () {
....
$tip.find('.popover-content')[ // we use append for html objects to maintain js events
this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' ](content)
...
}
bootstrap 3.0 the code says
Popover.prototype.setContent = function () {
....
$tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content)
...
}
If I tried replacing the older code it works in my local environment too. http://jsfiddle.net/46Z59/6/
My question is how do I fix it with the bootstrap 3.1.1 ?

Updated Answer
Anything having to do with DOM should go into a directive and not a controller. In fact, by transitioning to a directive, your problem can be easily solved.
Placing your popover HTML into a template that is included in a directive will cause its content will be compiled by Angular automatically. In this case, a manual $compile using content extracted from the DOM by jQuery -- which seems to be at the root of the problem -- is unnecessary.
Such a directive might look like this:
.directive('myPopover', function($compile){
return {
restrict: 'EA',
controller: function($scope){
$scope.clearFilter = function(){
$scope.filterOption = "";
};
$scope.setFilterOption = function(){
console.log("filter option :",$scope.filterOption);
if($scope.filterOption == 'hdnodes'){ }
else if($scope.filterOption == 'gnodes'){ }
else if($scope.filterOption == 'clusters'){ }
else if($scope.filterOption == 'standalones'){ }
};
},
link: function(scope, elem, attrs) {
elem.popover({
trigger:'click',
html : true,
placement: 'bottom',
content: function() {
return elem.find('div');
}
});
},
templateUrl: 'fn-dialog.html'
}
})
... where fn-dialog.html looks like this:
<!-- Button which opens popover when clicked -->
<button title="Filter" type="button" name="filter-node-btn" class="filter-node-btn">open</button>
<!-- Popover content -->
<div class="filterNodeDialog">
<div class="filterCriteriaContent">
<form name="filterCriteriaForm">
<input type="radio" name="filterOption" value="hdnodes" ng-model="filterOption" ng-change="setFilterOption();">
<span>All HD nodes</span>
[ the rest of the content for the popover ... ]
Plunker Demo

Related

angularjs textarea with colors (with html5 contenteditable)

I'm trying to create an editor which does "syntax highlighting",
it is rather simple:
yellow -> <span style="color:yellow">yellow</span>
I'm also using <code contenteditable> html5 tag to replace <textarea>, and have color output.
I started from angularjs documentation, and created the following simple directive. It does work, except it do not update the contenteditable area with the generated html.
If I use a element.html(htmlTrusted) instead of ngModel.$setViewValue(htmlTrusted), everything works, except the cursor jumps to the beginning at each keypress.
directive:
app.directive("contenteditable", function($sce) {
return {
restrict: "A", // only activate on element attribute
require: "?ngModel", // get ng-model, if not provided in html, then null
link: function(scope, element, attrs, ngModel) {
if (!ngModel) {return;} // do nothing if no ng-model
element.on('blur keyup change', function() {
console.log('app.directive->contenteditable->link->element.on()');
//runs at each event inside <div contenteditable>
scope.$evalAsync(read);
});
function read() {
console.log('app.directive->contenteditable->link->read()');
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
html = html.replace(/</, '<');
html = html.replace(/>/, '>');
html = html.replace(/<span\ style=\"color:\w+\">(.*?)<\/span>/g, "$1");
html = html.replace('yellow', '<span style="color:yellow">yellow</span>');
html = html.replace('green', '<span style="color:green">green</span>');
html = html.replace('purple', '<span style="color:purple">purple</span>');
html = html.replace('blue', '<span style="color:yellow">blue</span>');
console.log('read()-> html:', html);
var htmlTrusted = $sce.trustAsHtml(html);
ngModel.$setViewValue(htmlTrusted);
}
read(); // INITIALIZATION, run read() when initializing
}
};
});
html:
<body ng-app="MyApp">
<code contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>This <span style="color:purple">text is purple.</span> Change me!</code>
<hr>
<pre>{{userContent}}</pre>
</body>
plunkr: demo (type yellow, green or blue into the change me input area)
I tried scope.$apply(), ngModel.$render() but has no effect. I must miss something really obvious...
The links I already read through:
others' plunker demo 1
others' plunker demo 2
angularjs documentation's example
$sce.trustAsHtml stackoverflow question
setViewValue stackoverflow question
setViewValue not updating stackoverflow question
Any help is much appreciated. Please see the plunker demo above.
After almost a year, I finally settled to Codemirror, and I was never happier.
I'm doing side-by-side markdown source editing with live update (with syntax highlighting, so even a bit more advanced than stackoverflow's editing page.)
I created a simple codeEditor angular directive, which requires codeMirror, and uses it.
For completeness, here is the component sourcecode:
$ cat components/codeEditor/code-editor.html
<div class="code-editor"></div>
$ cat codeEditor.js
'use strict';
angular.module('myApp')
.directive('codeEditor', function($timeout, TextUtils){
return {
restrict: 'E',
replace: true,
require: '?ngModel',
transclude: true,
scope: {
syntax: '#',
theme: '#'
},
templateUrl: 'components/codeEditor/code-editor.html',
link: function(scope, element, attrs, ngModelCtrl, transclude){
// Initialize Codemirror
var option = {
mode: scope.syntax || 'xml',
theme: scope.theme || 'default',
lineNumbers: true
};
if (option.mode === 'xml') {
option.htmlMode = true;
}
scope.$on('toedit', function () { //event
//This is required to correctly refresh the codemirror view.
// otherwise the view stuck with 'Both <code...empty.' initial text.
$timeout(function() {
editor.refresh();
});
});
// Require CodeMirror
if (angular.isUndefined(window.CodeMirror)) {
throw new Error('codeEditor.js needs CodeMirror to work... (o rly?)');
}
var editor = window.CodeMirror(element[0], option);
// Handle setting the editor when the model changes if ngModel exists
if(ngModelCtrl) {
// Timeout is required here to give ngModel a chance to setup. This prevents
// a value of undefined getting passed as the view is rendered for the first
// time, which causes CodeMirror to throw an error.
$timeout(function(){
ngModelCtrl.$render = function() {
if (!!ngModelCtrl.$viewValue) {
// overwrite <code-editor>SOMETHING</code-editor>
// if the $scope.content.code (ngModelCtrl.$viewValue) is not empty.
editor.setValue(ngModelCtrl.$viewValue); //THIRD happening
}
};
ngModelCtrl.$render();
});
}
transclude(scope, function(clonedEl){
var initialText = clonedEl.text();
if (!!initialText) {
initialText = TextUtils.normalizeWhitespace(initialText);
} else {
initialText = 'Both <code-editor> tag and $scope.content.code is empty.';
}
editor.setValue(initialText); // FIRST happening
// Handle setting the model if ngModel exists
if(ngModelCtrl){
// Wrap these initial setting calls in a $timeout to give angular a chance
// to setup the view and set any initial model values that may be set in the view
$timeout(function(){
// Populate the initial ng-model if it exists and is empty.
// Prioritize the value in ngModel.
if(initialText && !ngModelCtrl.$viewValue){
ngModelCtrl.$setViewValue(initialText); //SECOND happening
}
// Whenever the editor emits any change events, update the value
// of the model.
editor.on('change', function(){
ngModelCtrl.$setViewValue(editor.getValue());
});
});
}
});
// Clean up the CodeMirror change event whenever the directive is destroyed
scope.$on('$destroy', function(){
editor.off('change');
});
}
};
});
There is also inside the components/codeEditor/vendor directory the full codemirror sourcecode.
I can highly recommend codeMirror. It is a rocksolid component, works in
every browser combination (firefox, firefox for android, chromium).

Pass in Angular data-bound text to Bootstrap Tooltip title attribute

I am using Bootstrap tooltips on my page and I want to pass in text to the title attribute on it, using {{ }} but it doesn't work.
Here's my HTML:
<a class="questionIcon" data-toggle="tooltip" data-placement="right" title="{{textBundle.tooltip-message}}">?</a>
I am initializing the tooltips in my controller like this:
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
However, when I hover over the ?, the message that is displayed is: {{textBundle.tooltip-message}} and not the actual content. Is there a way to get the dynamic content to be inputted using the standard Bootstrap tooltip?
Agree with the comments... never, ever use jquery inside a controller. And you should use a directive. For example, my directive is called "aiTooltip", and here is how it leverages angular strap (http://mgcrea.github.io/angular-strap/#)
plunkr: http://plnkr.co/edit/DIgj8vnZFyKFtX6CjDHi?p=preview (something is awry with the placement, but you get the idea)
In your template:
<p class="link" ai-tooltip="{{ name }}">{{ name }}</p>
And in the directive we inject the $tooltip service provided by angular-strap:
app.directive('aiTooltip', function aiTooltipDirective($rootScope, $timeout, $tooltip) {
return {
restrict: 'A',
scope: {
aiTooltip: '#', // text to display in caption
},
link: function aiTooltipLink(scope, elem, attrs) {
var tooltip;
$timeout(function() {
tooltip = $tooltip(elem, {
title: scope.aiTooltip,
html: true,
trigger: scope.aiTooltipTrigger|| 'hover',
placement: scope.aiTooltipPlacement || 'top',
container: scope.aiTooltipContainer || 'body'
});
});
}
};
});
And in the $scope of the template we pass in a scope variable called name
$scope.name = 'Foobar';

ng-model not updating in directive

There seems to be an issue with my ng-model binding to filterDetails.value. It will display the value "Test", but when I update the value to "Test1234" and click the button to execute filterColumn(), the alert displays "Test" instead of the updated value. If I then update the value again to "Test12345" and click the filter button it displays "Test1234". It is always one update behind. I am using angular version 1.2.24. This particular pop over is being used inside a header cell template in a ng-grid. I added a popover to something else on the page and it works fine so it does appear to be related to using it in the ng-grid.
app.directive('popOver', function ($compile) {
var filterTemplate = "<div><input type=\"text\" ng-model=\"filterDetails.value\"></input><button style=\"margin-left:5px;\" ng-click=\"filterColumn()\">Filter</button></div>";
var getTemplate = function () {
var template = filterTemplate;
return template;
}
return {
restrict: "A",
link: function (scope, element, attrs) {
scope.filterDetails =
{
value: "Test"
};
scope.filterColumn = function () {
alert(scope.filterDetails.value);
};
var popOverContent;
var html = getTemplate();
popOverContent = $compile(html)(scope);
var options = {
content: popOverContent,
placement: "bottom",
html: true,
title: scope.title,
container: "body"
};
$(element).popover(options);
},
scope: false
};
});
What version of angular are you using? You may need to upgrade your version of angular. I put together a quick jsfiddle with your directive in it, and it seems to be working ok for me using angular 1.3.15 AND 1.2.28
//angular 1.3.15
http://jsfiddle.net/x5o9s27a/
//angular 1.2.28
http://jsfiddle.net/ge7bqh4n/

Directive inside bs-tooltip is not evaluated

I am trying to have a <ul> element with its own directive (checkStrength) inside of an AngularStrap bs-tooltip title property, like this:
$scope.tooltip = {
title: '<ul id="strength" check-strength="pw"></ul>',
checked: false
};
The behavior I want is as follows: when a user clicks on the input textbox, a tooltip will appear showing the strength of the password as they enter it in the textbox.
This does not work, as shown in the two Plunkers below:
Custom "checkStrength" directive outside bs-tooltip works fine: Plunker
Custom "checkStrength" directive inside bs-tooltip does not work: Plunker
Ok, it doesn't appear that this is supported out of the box. You are going to have to create your own binding directive
Directive
.directive('customBindHtml', function($compile) {
return {
link: function(scope, element, attr) {
scope.$watch(attr.customBindHtml, function (value) {
element.html(value);
$compile(element.contents())(scope);
});
}
};
});
This go into Angular Straps code and make the follow modification on line 10 of tooltip.js in the plunker
Template
<div class="tooltip-inner" custom-bind-html="title"></div>
Then set the html property in the config to false.
Config
.config(function($tooltipProvider) {
angular.extend($tooltipProvider.defaults, {
html: false
});
})
Example: Plunker

Good way to dynamically open / close a popover (or tooltip) using angular, based on expression?

I have a form that is wired into angular, using it for validation. I am able to display error messages using ng-show directives like so:
<span ng-show="t3.f.needsAttention(f.fieldName)" ng-cloak>
<span ng-show="f.fieldName.$error.required && !f.fieldName.$viewValue">
This field is required.
</span>
</span>
.. where f is the form, and t3 comes from a custom directive on the form which detects whether a submission was attempted, and contains functions for checking the validity of fields.
What I am trying to accomplish is to display validation message(s) inside a popover instead. Either bootstrap's native popover, or the popover from UI Bootstrap, I have both loaded. I may also consider AngularStrap if it is easier to do it using that lib.
What I'm struggling with right now is the nature of popovers in general -- they autodisplay based on user events like click, mouseenter, blur, etc. What I want to do is show & hide the popover(s) based on the same functions in the ng-show attributes above. So that when the expression returns false hide it, and when it returns true, show it.
I know bootstrap has the .popover('show') for this, but I'm not supposed to tell angular anything about the dom, so I'm not sure how I would get access to $(element).popover() if doing this in a custom form controller function. Am I missing something?
Update
The solution mentioned in the duplicate vote still only shows the popover on mouseenter. I want to force it to display, as if doing $('#popover_id').popover('show').
You can also build your own extended triggers. This will apply to both Tooltip and Popover.
First extend the Tooltip triggers as follows:
// define additional triggers on Tooltip and Popover
app.config(['$tooltipProvider', function($tooltipProvider){
$tooltipProvider.setTriggers({
'show': 'hide'
});
}]);
Then define the trigger on the HTML tag like this:
<div id="RegisterHelp" popover-trigger="show" popover-placement="left" popover="{{ 'Login or register here'}}">
And now you can call hide and show from JavaScript, this is a show in 3 seconds.
$("#RegisterHelp").trigger('show');
//Close the info again
$timeout(function () {
$("#RegisterHelp").trigger('hide');
}, 3000);
As it turns out, it's not very difficult to decorate either the ui-bootstrap tooltip or the popover with a custom directive. This is written in typescript, but the javascript parts of it should be obvious. This single piece of code works to decorate either a tooltip or a popover:
'use strict';
module App.Directives.TooltipToggle {
export interface DirectiveSettings {
directiveName: string;
directive: any[];
directiveConfig?: any[];
}
export function directiveSettings(tooltipOrPopover = 'tooltip'): DirectiveSettings {
var directiveName = tooltipOrPopover;
// events to handle show & hide of the tooltip or popover
var showEvent = 'show-' + directiveName;
var hideEvent = 'hide-' + directiveName;
// set up custom triggers
var directiveConfig = ['$tooltipProvider', ($tooltipProvider: ng.ui.bootstrap.ITooltipProvider): void => {
var trigger = {};
trigger[showEvent] = hideEvent;
$tooltipProvider.setTriggers(trigger);
}];
var directiveFactory = (): any[] => {
return ['$timeout', ($timeout: ng.ITimeoutService): ng.IDirective => {
var d: ng.IDirective = {
name: directiveName,
restrict: 'A',
link: (scope: ng.IScope, element: JQuery, attr: ng.IAttributes) => {
if (angular.isUndefined(attr[directiveName + 'Toggle'])) return;
// set the trigger to the custom show trigger
attr[directiveName + 'Trigger'] = showEvent;
// redraw the popover when responsive UI moves its source
var redrawPromise: ng.IPromise<void>;
$(window).on('resize', (): void => {
if (redrawPromise) $timeout.cancel(redrawPromise);
redrawPromise = $timeout((): void => {
if (!scope['tt_isOpen']) return;
element.triggerHandler(hideEvent);
element.triggerHandler(showEvent);
}, 100);
});
scope.$watch(attr[directiveName + 'Toggle'], (value: boolean): void => {
if (value && !scope['tt_isOpen']) {
// tooltip provider will call scope.$apply, so need to get out of this digest cycle first
$timeout((): void => {
element.triggerHandler(showEvent);
});
}
else if (!value && scope['tt_isOpen']) {
$timeout((): void => {
element.triggerHandler(hideEvent);
});
}
});
}
};
return d;
}];
};
var directive = directiveFactory();
var directiveSettings: DirectiveSettings = {
directiveName: directiveName,
directive: directive,
directiveConfig: directiveConfig,
};
return directiveSettings;
}
}
With this single piece of code, you can set up programmatic hide and show of either a tooltip or popover like so:
var tooltipToggle = App.Directives.TooltipToggle.directiveSettings();
var popoverToggle = App.Directives.TooltipToggle.directiveSettings('popover');
var myModule = angular.module('my-mod', ['ui.bootstrap.popover', 'ui.bootstrap.tpls'])
.directive(tooltipToggle.directiveName, tooltipToggle.directive)
.config(tooltipToggle.directiveConfig)
.directive(popoverToggle.directiveName, popoverToggle.directive)
.config(popoverToggle.directiveConfig);
Usage:
<span tooltip="This field is required."
tooltip-toggle="formName.fieldName.$error.required"
tooltip-animation="false" tooltip-placement="right"></span>
or
<span popover="This field is required."
popover-toggle="formName.fieldName.$error.required"
popover-animation="false" popover-placement="right"></span>
So we are reusing everything else that comes with the ui-bootstrap tooltip or popover, and only implementing the -toggle attribute. The decorative directive watches that attribute, and fires custom events to show or hide, which are then handled by the ui-bootstrap tooltip provider.
Update:
Since this answer seems to be helping others, here is the code written as javascript (the above typescript more or less compiles to this javascript):
'use strict';
function directiveSettings(tooltipOrPopover) {
if (typeof tooltipOrPopover === "undefined") {
tooltipOrPopover = 'tooltip';
}
var directiveName = tooltipOrPopover;
// events to handle show & hide of the tooltip or popover
var showEvent = 'show-' + directiveName;
var hideEvent = 'hide-' + directiveName;
// set up custom triggers
var directiveConfig = ['$tooltipProvider', function ($tooltipProvider) {
var trigger = {};
trigger[showEvent] = hideEvent;
$tooltipProvider.setTriggers(trigger);
}];
var directiveFactory = function() {
return ['$timeout', function($timeout) {
var d = {
name: directiveName,
restrict: 'A',
link: function(scope, element, attr) {
if (angular.isUndefined(attr[directiveName + 'Toggle']))
return;
// set the trigger to the custom show trigger
attr[directiveName + 'Trigger'] = showEvent;
// redraw the popover when responsive UI moves its source
var redrawPromise;
$(window).on('resize', function() {
if (redrawPromise) $timeout.cancel(redrawPromise);
redrawPromise = $timeout(function() {
if (!scope['tt_isOpen']) return;
element.triggerHandler(hideEvent);
element.triggerHandler(showEvent);
}, 100);
});
scope.$watch(attr[directiveName + 'Toggle'], function(value) {
if (value && !scope['tt_isOpen']) {
// tooltip provider will call scope.$apply, so need to get out of this digest cycle first
$timeout(function() {
element.triggerHandler(showEvent);
});
}
else if (!value && scope['tt_isOpen']) {
$timeout(function() {
element.triggerHandler(hideEvent);
});
}
});
}
};
return d;
}];
};
var directive = directiveFactory();
var directiveSettings = {
directiveName: directiveName,
directive: directive,
directiveConfig: directiveConfig,
};
return directiveSettings;
}
For ui.bootstrap 0.13.4 and newer:
A new parameter (popover-is-open) was introduced to control popovers in the official ui.bootstrap repo. This is how you use it in the latest version:
<a uib-popover="Hello world!" popover-is-open="isOpen" ng-click="isOpen = !isOpen">
Click me to show the popover!
</a>
For ui.bootstrap 0.13.3 and older:
I just published a small directive that adds more control over popovers on GitHub: https://github.com/Elijen/angular-popover-toggle
You can use a scope variable to show/hide the popover using popover-toggle="variable" directive like this:
<span popover="Hello world!" popover-toggle="isOpen">
Popover here
</span>
Here is a demo Plunkr: http://plnkr.co/edit/QeQqqEJAu1dCuDtSvomD?p=preview
My approach:
Track the state of the popover in the model
Change this state per element using the appropriate directives.
The idea being to leave the DOM manipulation to the directives.
I have put together a fiddle that I hope gives a better explain, but you'll find much more sophisticated solutions in UI Bootstrap which you mentioned.
jsfiddle
Markup:
<div ng-repeat="element in elements" class="element">
<!-- Only want to show a popup if the element has an error and is being hovered -->
<div class="popover" ng-show="element.hovered && element.error" ng-style>Popover</div>
<div class="popoverable" ng-mouseEnter="popoverShow(element)" ng-mouseLeave="popoverHide(element)">
{{ element.name }}
</div>
</div>
JS:
function DemoCtrl($scope)
{
$scope.elements = [
{name: 'Element1 (Error)', error: true, hovered: false},
{name: 'Element2 (no error)', error: false, hovered: false},
{name: 'Element3 (Error)', error: true, hovered: false},
{name: 'Element4 (no error)', error: false, hovered: false},
{name: 'Element5 (Error)', error: true, hovered: false},
];
$scope.popoverShow = function(element)
{
element.hovered = true;
}
$scope.popoverHide = function(element)
{
element.hovered = false
}
}
For others coming here, as of the 0.13.4 release, we have added the ability to programmatically open and close popovers via the *-is-open attribute on both tooltips and popovers in the Angular UI Bootstrap library. Thus, there is no longer any reason to have to roll your own code/solution.
From Michael Stramel's answer, but with a full angularJS solution:
// define additional triggers on Tooltip and Popover
app.config(['$tooltipProvider', function($tooltipProvider){
$tooltipProvider.setTriggers({
'show': 'hide'
});
}])
Now add this directive:
app.directive('ntTriggerIf', ['$timeout',
function ($timeout) {
/*
Intended use:
<div nt-trigger-if={ 'triggerName':{{someCodition === SomeValue}},'anotherTriggerName':{{someOtherCodition === someOtherValue}} } ></div>
*/
return {
restrict: 'A',
link: function (scope, element, attrs) {
attrs.$observe('ntTriggerIf', function (val) {
try {
var ob_options = JSON.parse(attrs.ntTriggerIf.split("'").join('"') || "");
}
catch (e) {
return
}
$timeout(function () {
for (var st_name in ob_options) {
var condition = ob_options[st_name];
if (condition) {
element.trigger(st_name);
}
}
})
})
}
}
}])
Then in your markup:
<span tooltip-trigger="show" tooltip="Login or register here" nt-trigger-if="{'show':{{ (errorConidtion) }}, 'hide':{{ !(errorConidtion) }} }"></span>

Resources