How do I create an AngularJS UI bootstrap popover with HTML content?
Asked Answered
D

8

45

I want to create a bootstrap popover with a pre tag containing a prettified JSON object. The naive implementation,

<span popover='<pre>{[ some_obj | json:"  " ]}</pre>'
      popover-trigger='mouseenter'>

escapes the content before inserting it into the popup. What's the best way of specifying a popover body with html content?

Darwinism answered 23/5, 2013 at 19:36 Comment(2)
My angular popover directive inspector-gadget lets you use HTML in popover content a bit more seemlessly. Might be a better solution if you don't mind the dependency.Holzman
@Andrey Fedorov, you should accept Matthew.Lothian's answer instead of the current one.Demesne
P
72

UPDATE:

As can been seen in this, you should now be able to do this without overriding the default template.

ORIGINAL:

As of angular 1.2+ ng-bind-html-unsafe has been removed. You should be using the $sce service Reference.

Here is a filter for creating trusted HTML.

MyApp.filter('unsafe', ['$sce', function ($sce) {
    return function (val) {
        return $sce.trustAsHtml(val);
    };
}]);

Here is the overwritten Angular Bootstrap 0.11.2 template making use of this filter

// update popover template for binding unsafe html
angular.module("template/popover/popover.html", []).run(["$templateCache", function ($templateCache) {
    $templateCache.put("template/popover/popover.html",
      "<div class=\"popover {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" +
      "  <div class=\"arrow\"></div>\n" +
      "\n" +
      "  <div class=\"popover-inner\">\n" +
      "      <h3 class=\"popover-title\" ng-bind-html=\"title | unsafe\" ng-show=\"title\"></h3>\n" +
      "      <div class=\"popover-content\"ng-bind-html=\"content | unsafe\"></div>\n" +
      "  </div>\n" +
      "</div>\n" +
      "");
}]);

EDIT: Here is a Plunker implementation.

EDIT 2: As this answer keeps getting hits, I'll keep it updated as best I can. As a reference Here is the template from the angular-ui bootstrap repo. If this changes, the override template will require matching updates and the addition of the ng-bind-html=\"title | unsafe\" and ng-bind-html=\"content | unsafe\" attributes to continue working.

For updated conversation check the issue here.

Parasitology answered 24/2, 2014 at 5:4 Comment(3)
@zoidbergi The filter should be added to a module, or any modules used by you main app module. The template override should be included after Angular and Bootstrap UI so it isn't redefined by bootstrap. I'll add an example to make it easier to see it working.Parasitology
@Andrey Fedorov as this has as many upvotes as the current (now incorrect) answer, could you help future viewers by setting the above to the marked answer?Parasitology
Works perfectly. I don't understand why this is not a part of the official code at angular-ui.github.io/bootstrap/#/popoverHelicoid
P
26

Use the popover-template directive

If you are using a version of angular-ui equal or above 0.13.0, your best option is to use the popover-template directive. Here is how to use it:

<button popover-template="'popover.html'">My HTML popover</button>

<script type="text/ng-template" id="popover.html">
    <div>
        Popover content
    </div>
</script>

NB: Do not forget the quotes around the template name in popover-template="'popover.html'".

See demo plunker


As a side note, it is possible to externalize the popover template in a dedicated html file, instead of declaring it in a <script type="text/ng-template> element as above.

See second demo plunker

Pursuer answered 26/9, 2015 at 22:15 Comment(2)
Plus one for the NB. Reminded me what I was missing.Geometry
sorry for commenting 2 years later, but i recieve an angularjs error : Template for directive 'mainmenu' must have exactly one root element. templates/components/shared/menu/mainmenu/menu.view.html what do I must change herE?Ecliptic
A
9

I have posted a solution on the github project: https://github.com/angular-ui/bootstrap/issues/520

I you want to add this functionality to your project, here is a patch.

Add those directives:

angular.module("XXX")
    .directive("popoverHtmlUnsafePopup", function () {
      return {
        restrict: "EA",
        replace: true,
        scope: { title: "@", content: "@", placement: "@", animation: "&", isOpen: "&" },
        templateUrl: "template/popover/popover-html-unsafe-popup.html"
      };
    })

    .directive("popoverHtmlUnsafe", [ "$tooltip", function ($tooltip) {
      return $tooltip("popoverHtmlUnsafe", "popover", "click");
    }]);

And add the template:

<div class="popover {{placement}}" ng-class="{ in: isOpen(), fade: animation() }">
  <div class="arrow"></div>

  <div class="popover-inner">
      <h3 class="popover-title" ng-bind="title" ng-show="title"></h3>
      <div class="popover-content" bind-html-unsafe="content"></div>
  </div>
</div>

Usage: <button popover-placement="top" popover-html-unsafe="On the <b>Top!</b>" class="btn btn-default">Top</button>

View it on plunkr: http://plnkr.co/edit/VhYAD04ETQsJ2dY3Uum3?p=preview

Alberta answered 24/7, 2014 at 9:49 Comment(1)
Since the template used is defined directly in directive,is there any way we can choose template directly from HTML ? Since we may need different template at different times.Comptometer
I
6

You need to alter the default popover template to specify you want to allow Html content. Look the popover-content div, it now has its binding done to the content property allowing unsafe html:

 angular.module("template/popover/popover.html", []).run(["$templateCache", function ($templateCache) {
        $templateCache.put("template/popover/popover.html",
            "<div class='popover {{placement}}' ng-class='{ in: isOpen(), fade: animation() }'>" + 
            "<div class='arrow'></div><div class='popover-inner'>" + 
            "<h3 class='popover-title' ng-bind='title' ng-show='title'></h3>" + 
            "<div class='popover-content' ng-bind-html-unsafe='content'></div>" +
            "<button class='btn btn-cancel' ng-click='manualHide()'>Cancel</button>" +
            "<button class='btn btn-apply' ng-click='manualHide()'>Apply</button></div></div>");
    }]);
Interplead answered 20/12, 2013 at 0:56 Comment(2)
This is no longer correct as of angular 1.2+ as ng-bind-html-unsafe has been removed.Parasitology
See the correct answer for Angular UI 1.2, below. https://mcmap.net/q/366550/-how-do-i-create-an-angularjs-ui-bootstrap-popover-with-html-contentHartill
L
6

For all your conventional Bootstrap popover needs you could utilize the following angular directive. It removes clutter from the HTML template and is very easy to use.

You can configure popover's title, content, placement, fade in/out delay, trigger event and whether content should be treated as html. It also prevents content overflow & clipping.

Related plunker with all teh codes here http://plnkr.co/edit/MOqhJi

Screencap

imgur

Usage

<!-- HTML -->
<div ng-model="popup.content" popup="popup.options">Some element</div>

/* JavaScript */
this.popup = {
  content: 'Popup content here',
  options: {
    title: null,
    placement: 'right', 
    delay: { show: 800, hide: 100 }
  }
}; 

JavaScript

/**
 * Popup, a Bootstrap popover wrapper.
 *
 * Usage: 
 *  <div ng-model="model" popup="options"></div>
 * 
 * Remarks: 
 *  To prevent content overflow and clipping, use CSS
 *  .popover { word-wrap: break-word; }
 *  Popup without title and content will not be shown.
 *
 * @param {String}  ngModel           popup content
 * @param {Object}  options           popup options
 * @param {String}  options.title     title
 * @param {Boolean} options.html      content should be treated as html markup
 * @param {String}  options.placement placement (top, bottom, left or right)
 * @param {String}  options.trigger   trigger event, default is hover
 * @param {Object}  options.delay     milliseconds or { show:<ms>, hide:<ms> }
 */
app.directive('popup', function() {
  return {
    restrict: 'A',
    require: 'ngModel',
    scope: {
      ngModel: '=',
      options: '=popup'
    },
    link: function(scope, element) {
      scope.$watch('ngModel', function(val) {
        element.attr('data-content', val);
      });

      var options = scope.options || {} ; 

      var title = options.title || null;
      var placement = options.placement || 'right';
      var html = options.html || false;
      var delay = options.delay ? angular.toJson(options.delay) : null;
      var trigger = options.trigger || 'hover';

      element.attr('title', title);
      element.attr('data-placement', placement);
      element.attr('data-html', html);
      element.attr('data-delay', delay);
      element.popover({ trigger: trigger });
    }
  };
});
Longsome answered 28/1, 2015 at 10:17 Comment(1)
This isn't using angular UI bootstrap like OP asked, and instead relies on jQuery and bootstrap.js not sure how conventional that is. Other wise good answer.Parasitology
M
3

See https://github.com/jbruni/bootstrap-bower-jbruni, which allow to use a popover-template

Matsuyama answered 4/7, 2014 at 10:8 Comment(0)
D
1

The following CSS styling seems to have done what I wanted in my specific case:

.popover-content {
  white-space: pre;
  font-family: monospace;
}

The general question still remains open.

Darwinism answered 24/5, 2013 at 19:11 Comment(0)
J
0

Here is a fiddle of my solution that:

  • Is accessible (you can use tab keys to activate/deactivate).
  • Allows a user to hover the popover and for the popover to remain open.
  • Allows multiple popovers on the page, but only a single popover to be activated at any given time.
  • Doesn't rely on any third party, though the bootstrap popover styles have been borrowed.

The way this works is that we instantiate however many popovers we will have on the page in a popover array (see the TODO in the comments on how to wire this up).

Then anytime a user tabs into or hovers into an element that should trigger a popover we activate that specific popover in the popover array. When the user is no longer hovering the element we set a timeout for that specific popover in the array. If that timeout has elapsed it does a quick check to see if the user has re-hovered or re-focused (via tabbing) the element. If so then we keep the popover alive. If not we hide the popover.

For the CSS I did not want to rely on using bootstrap so I borrowed the styles directly from bootstrap. If you try to use bootstrap's popover styles you may run into some weird behavior where bootstrap is running it's own scripts on our custom popover which we do not want.

HTML:

 <section>
    <a href="#" 
       ng-mouseover="showPopover(i)" 
       ng-mouseleave="hidePopover(i)"
       ng-focus="showPopover(i)"
       ng-blur="hidePopover(i)">
        I trigger a popover - {{i}}
    </a>
    <popover popover-show="popover[i].popoverTracker">
      <div class="arrow"></div>
      <div class="custom-popover-content"
           ng-mouseover="showPopover(i)" 
           ng-mouseleave="hidePopover(i)"
           ng-focus="showPopover(i)"
           ng-blur="hidePopover(i)">
        <a href="#"
           ng-focus="showPopover(i)"
           ng-blur="hidePopover(i)">You can tab into me, I'm accessible!</a>
        <br/>
        <a href="#" 
           ng-focus="showPopover(i)"
           ng-blur="hidePopover(i)">You can tab into me, I'm accessible!</a>
      </div>
    </popover>
  </section> 

Angular Controller and Directive:

angular.module('controllers', []);
angular.module('directives', []);
angular.module('myApp', ['ngAnimate', 'controllers', 'directives']);

angular.module('controllers')
    .controller('MainCtrl', function ($scope, $timeout) {

    $scope.popover = [];

    (function init() {
        // TODO: Make this dynamic so that we can pass it a value and it will generate the right amount
        // Initializing the array of popovers on startup
      createPopoverTrackers(20);
        })();

    // Creating an array of popovers equal to the number of popovers on the page
    function createPopoverTrackers(count) {
        for(var i = 0; i < count; i++) {
        $scope.popover.push({
            popoverTracker: false,
          popoverKeepAlive: false,
          timer: null
        })
      }
    }

    // A user has focused on an element that has an associated popover
    $scope.queueOpenPopover = function(index) {
    // Show our specified tracker
    $scope.popover[index].popoverTracker = true;

     // Hide the rest
     Object.keys($scope.popover)
        .filter(function(trackerIndex) {
          return trackerIndex != index
       })
      .forEach(function(trackerIndex) {
          $scope.popover[trackerIndex].popoverTracker = false;
          $scope.popover[trackerIndex].popoverKeepAlive = false;

          const timer = $scope.popover[trackerIndex].timer;
          if(timer) {
             $timeout.cancel(timer);
             $scope.popover[trackerIndex].timer = null;
          }
      })
        };

    // Queuing up the demise of the popover
    $scope.queueKillPopover = function(index) {
      $scope.popover[index].timer = $timeout(function() {
             if (!$scope.popover[index].popoverKeepAlive) {
          // Popover or the popover trigger were not hovered within the time limit, kill it!
          $scope.popover[index].popoverTracker = false;
        }
        }, 700);
    };

    // When a user focuses into the actual popover itself or it's trigger,  we need to keep it alive
    $scope.showPopover = function(index) {
        $scope.popover[index].popoverKeepAlive = true;
      $scope.queueOpenPopover(index);
    };

    // The user has removed focus from the popover or it's trigger, set this to false so the timer knows to kill it
    $scope.hidePopover = function(index) {
        $scope.popover[index].popoverKeepAlive = false;
      $scope.queueKillPopover(index);
    };
});

angular.module('directives')
    .directive('popover', function () {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        scope: {
            'popoverShow': '='
        },
        template: '<div class="custom-popover bottom" ng-show="popoverShow" ng-transclude></div>'
    };
});

CSS borrowed from bootstrap:

.custom-popover {
    position: absolute;
    z-index: 1010;
    max-width: 276px;
    padding: 1px;
    text-align: left;
    white-space: normal;
    background-color: #fff;
    border: 1px solid rgba(0,0,0,0.2);
    border-radius: 6px;
    box-shadow: 0 5px 10px rgba(0,0,0,0.2);
    background-clip: padding-box;
}

.custom-popover .arrow,
.custom-popover .arrow:after {
  position: absolute;
  display: block;
  width: 0;
  height: 0;
  border-color: transparent;
  border-style: solid;
}

.custom-popover .arrow {
  border-width: 11px;
}

.custom-popover .arrow:after {
  border-width: 10px;
  content: "";
}

.custom-popover.bottom {
  margin-top: 10px;
}

.custom-popover.bottom .arrow {
  top: -11px;
  left: 50%;
  margin-left: -11px;
  border-bottom-color: rgba(0, 0, 0, 0.25);
  border-top-width: 0;
}

.custom-popover.bottom .arrow:after {
  top: 1px;
  margin-left: -10px;
  border-bottom-color: #ffffff;
  border-top-width: 0;
  content: " ";
}

.custom-popover-content {
  padding: 9px 14px;
}
Judijudicable answered 13/3, 2020 at 17:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.