28

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').

danludwig
  • 46,965
  • 25
  • 159
  • 237
  • 1
    possible duplicate of [Enable angular-ui tooltip on custom events](http://stackoverflow.com/questions/16651227/enable-angular-ui-tooltip-on-custom-events) – Stewie Jan 05 '14 at 22:35
  • 1
    @Stewie, that solution still only displays the popover when the element is mouseentered. I want to force it to display, as if doing `$('#popover_id').popover('show')`. – danludwig Jan 06 '14 at 00:26
  • That's true. I see there's an open github issue for this, and it's begging for a PR. – Stewie Jan 06 '14 at 12:37
  • @Stewie, can you share the link to the gh issue? I would like to take a look. – danludwig Jan 06 '14 at 15:08
  • 1
    https://github.com/angular-ui/bootstrap/issues/590 – Stewie Jan 06 '14 at 16:36

6 Answers6

29

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);
PeteGO
  • 5,597
  • 3
  • 39
  • 70
Kim Ras
  • 972
  • 8
  • 14
  • 2
    Can you give a fiddle as example? My popovers doesn't show up when I extend the triggers.. – Betty St May 21 '15 at 10:01
  • Very simple to implement. Thanks – Sal Jun 09 '15 at 09:26
  • 6
    I don't understand this: `{ show: hide }` -- what is that doing? – chovy Aug 03 '15 at 18:13
  • 3
    @chovy, that bit is registering a new mapping of events that will show/hide the tooltip/popover. If you take a look at the source, there is a `triggerMap` that puts `mouseenter` with `mouseleave`, `click` with `click`, and `focus` with `blur` by default. The key in this object is the event to listen for to show the popover/toolip, and the value is the event to listen for to hide the popover/tooltip. – kevin Oct 16 '15 at 22:20
  • 3
    This has been broken by: https://github.com/angular-ui/bootstrap/commit/7556beda486f26b40fb860448316e8a32457e9e9 – Joseph Carroll Nov 24 '15 at 01:43
20

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;
}
danludwig
  • 46,965
  • 25
  • 159
  • 237
  • 2
    Thanks for this, very elegant solution that only hacks into angular-ui's privates a smidge. – Matt Greer Mar 04 '14 at 02:38
  • 1
    Seems like a good solution, would love to see the code in actual JavaScript :) – Petr Peller Oct 13 '14 at 11:10
  • 2
    @PetrPeller javascript provided. – danludwig Nov 18 '14 at 15:17
  • 1
    First of all I like this solution alot! A just have one problem, my popover wont detoggle. Do i understand correctly that `"formName.fieldName.$error.required"` is a boolean in the model? Weird thing is that the popover appears when the boolean changes to true, but it doesn't seem to detoggle when it's changed back to false. – Benjamin Hammer Nørgaard Feb 26 '15 at 01:42
  • 1
    When using popover-template this doesn't work. It doesn't fire an error either. Any ideas? – Matthias Max Jun 24 '15 at 08:16
  • Yes, you have to rewrite it a bit in order to get it working with popover-template, as now it binds the directive to the name you pass to the function. – Aron Lorincz Sep 24 '15 at 09:29
17

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

Petr Peller
  • 8,581
  • 10
  • 49
  • 66
  • this is what should be in the core. current implementation is written for angular but seems to be designed to work old school. angular uses watches, jquery (and stuff) is all event driven. – Sam Jan 05 '16 at 06:34
  • +1, for most uses, this seems to be the library maintainer's intended and most natural way of 'manually' opening and closing out a popover – chinnychinchin Sep 14 '16 at 08:12
5

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
    }

}
Ehimen
  • 66
  • 2
  • 2
    I think theres is some merit to this approach and it works well, but using a component to do this is just a lot cleaner, more flexible and reusable. – Michael Yagudaev Jan 30 '14 at 20:40
4

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.

icfantv
  • 4,523
  • 7
  • 36
  • 53
3

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>
Shawn Dotey
  • 616
  • 8
  • 11