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

6

29

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

Geosphere answered 5/1, 2014 at 22:1 Comment(5)
possible duplicate of Enable angular-ui tooltip on custom eventsOfay
@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').Geosphere
That's true. I see there's an open github issue for this, and it's begging for a PR.Ofay
@Stewie, can you share the link to the gh issue? I would like to take a look.Geosphere
github.com/angular-ui/bootstrap/issues/590Ofay
G
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;
}
Geosphere answered 6/1, 2014 at 15:11 Comment(6)
Thanks for this, very elegant solution that only hacks into angular-ui's privates a smidge.Voile
Seems like a good solution, would love to see the code in actual JavaScript :)Approachable
@PetrPeller javascript provided.Geosphere
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.Uther
When using popover-template this doesn't work. It doesn't fire an error either. Any ideas?Breeding
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.Ridgeling
E
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);
Edition answered 30/4, 2014 at 0:8 Comment(4)
Can you give a fiddle as example? My popovers doesn't show up when I extend the triggers..Upstroke
I don't understand this: { show: hide } -- what is that doing?Denotation
@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.Eclogue
This has been broken by: github.com/angular-ui/bootstrap/commit/…Cathiecathleen
G
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;
}
Geosphere answered 6/1, 2014 at 15:11 Comment(6)
Thanks for this, very elegant solution that only hacks into angular-ui's privates a smidge.Voile
Seems like a good solution, would love to see the code in actual JavaScript :)Approachable
@PetrPeller javascript provided.Geosphere
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.Uther
When using popover-template this doesn't work. It doesn't fire an error either. Any ideas?Breeding
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.Ridgeling
A
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

Approachable answered 12/7, 2015 at 20:55 Comment(2)
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.Maritzamariupol
+1, for most uses, this seems to be the library maintainer's intended and most natural way of 'manually' opening and closing out a popoverWellestablished
M
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
    }

}
Marlea answered 5/1, 2014 at 22:45 Comment(1)
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.Haggadist
C
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.

Convergent answered 23/10, 2015 at 15:28 Comment(0)
G
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>
Generalship answered 18/9, 2015 at 21:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.