Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

feat(popover): support templates #1848

Merged
merged 2 commits into from
Mar 28, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/popover/docs/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@
<h4>Dynamic</h4>
<div class="form-group">
<label>Popup Text:</label>
<input type="text" ng-model="dynamicPopover" class="form-control">
<input type="text" ng-model="dynamicPopover.content" class="form-control">
</div>
<div class="form-group">
<label>Popup Title:</label>
<input type="text" ng-model="dynamicPopoverTitle" class="form-control">
<input type="text" ng-model="dynamicPopover.title" class="form-control">
</div>
<button popover="{{dynamicPopover}}" popover-title="{{dynamicPopoverTitle}}" class="btn btn-default">Dynamic Popover</button>

<div class="form-group">
<label>Popup Template:</label>
<input type="text" ng-model="dynamicPopover.templateUrl" class="form-control">
</div>
<button popover="{{dynamicPopover.content}}" popover-title="{{dynamicPopover.title}}" class="btn btn-default">Dynamic Popover</button>

<button popover-template="{{dynamicPopover.templateUrl}}" popover-template-title="{{dynamicPopover.title}}" class="btn btn-default">Popover With Template</button>

<script type="text/ng-template" id="myPopoverTemplate.html">
<div>{{dynamicPopover.content}}</div>
<div class="form-group">
<label>Popup Title:</label>
<input type="text" ng-model="dynamicPopover.title" class="form-control">
</div>
</script>
<hr />
<h4>Positional</h4>
<button popover-placement="top" popover="On the Top!" class="btn btn-default">Top</button>
Expand Down
7 changes: 5 additions & 2 deletions src/popover/docs/demo.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
angular.module('ui.bootstrap.demo').controller('PopoverDemoCtrl', function ($scope) {
$scope.dynamicPopover = 'Hello, World!';
$scope.dynamicPopoverTitle = 'Title';
$scope.dynamicPopover = {
content: 'Hello, World!',
templateUrl: 'myTemplatePopover.html',
title: 'Title'
};
});
7 changes: 7 additions & 0 deletions src/popover/docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ directive supports multiple placements, optional transition animation, and more.
Like the Bootstrap jQuery plugin, the popover **requires** the tooltip
module.

There are two versions of the popover: `popover` and `popover-template`:

- `popover` takes text only and will escape any HTML provided for the popover
body.
- `popover-template` takes text that specifies the location of a template to
use for the popover body.

The popover directives provides several optional attributes to control how it
will display:

Expand Down
14 changes: 14 additions & 0 deletions src/popover/popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
*/
angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )

.directive( 'popoverTemplatePopup', function () {
return {
restrict: 'EA',
replace: true,
scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&',
originScope: '&' },
templateUrl: 'template/popover/popover-template.html'
};
})

.directive( 'popoverTemplate', [ '$tooltip', function ( $tooltip ) {
return $tooltip( 'popoverTemplate', 'popoverTemplate', 'click' );
}])

.directive( 'popoverPopup', function () {
return {
restrict: 'EA',
Expand Down
66 changes: 66 additions & 0 deletions src/popover/test/popover-template.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
describe('popover template', function() {
var elm,
elmBody,
scope,
elmScope,
tooltipScope;

// load the popover code
beforeEach(module('ui.bootstrap.popover'));

// load the template
beforeEach(module('template/popover/popover.html'));
beforeEach(module('template/popover/popover-template.html'));

beforeEach(inject(function ($templateCache) {
$templateCache.put('myUrl', [200, '<span>{{ myTemplateText }}</span>', {}]);
}));

beforeEach(inject(function($rootScope, $compile) {
elmBody = angular.element(
'<div><span popover-template="{{ templateUrl }}">Selector Text</span></div>'
);

scope = $rootScope;
$compile(elmBody)(scope);
scope.templateUrl = 'myUrl';

scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));

it('should open on click', inject(function() {
elm.trigger( 'click' );
expect( tooltipScope.isOpen ).toBe( true );

expect( elmBody.children().length ).toBe( 2 );
}));

it('should not open on click if templateUrl is empty', inject(function() {
scope.templateUrl = null;
scope.$digest();

elm.trigger( 'click' );
expect( tooltipScope.isOpen ).toBe( false );

expect( elmBody.children().length ).toBe( 1 );
}));

it('should show updated text', inject(function() {
scope.myTemplateText = 'some text';
scope.$digest();

elm.trigger( 'click' );
expect( tooltipScope.isOpen ).toBe( true );

expect( elmBody.children().eq(1).text().trim() ).toBe( 'some text' );

scope.myTemplateText = 'new text';
scope.$digest();

expect( elmBody.children().eq(1).text().trim() ).toBe( 'new text' );
}));
});

6 changes: 6 additions & 0 deletions src/tooltip/docs/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<a href="#" tooltip-animation="false" tooltip="I don't fade. :-(">fading</a>
at elementum eu, facilisis sed odio morbi quis commodo odio. In cursus
<a href="#" tooltip-popup-delay='1000' tooltip='appears with delay'>delayed</a> turpis massa tincidunt dui ut.
<a href="#" tooltip-template="myTooltipTemplate.html">Custom template</a>
nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas
</p>

<p>
Expand Down Expand Up @@ -58,4 +60,8 @@
tooltip-enable="!inputModel" />
</div>
</form>

<script type="text/ng-template" id="myTooltipTemplate.html">
<span>Special Tooltip with <strong>markup</strong> and {{ dynamicTooltipText }}</span>
</script>
</div>
15 changes: 10 additions & 5 deletions src/tooltip/docs/readme.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
A lightweight, extensible directive for fancy tooltip creation. The tooltip
directive supports multiple placements, optional transition animation, and more.

There are two versions of the tooltip: `tooltip` and `tooltip-html-unsafe`. The
former takes text only and will escape any HTML provided. The latter takes
whatever HTML is provided and displays it in a tooltip; it's called "unsafe"
because the HTML is not sanitized. *The user is responsible for ensuring the
content is safe to put into the DOM!*
There are three versions of the tooltip: `tooltip`, `tooltip-template`, and
`tooltip-html-unsafe`:

- `tooltip` takes text only and will escape any HTML provided.
- `tooltip-template` takes text that specifies the location of a template to
use for the tooltip.
- `tooltip-html-unsafe` takes
whatever HTML is provided and displays it in a tooltip; it's called "unsafe"
because the HTML is not sanitized. *The user is responsible for ensuring the
content is safe to put into the DOM!*

The tooltip directives provide several optional attributes to control how they
will display:
Expand Down
65 changes: 65 additions & 0 deletions src/tooltip/test/tooltip-template.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
describe('tooltip template', function() {
var elm,
elmBody,
scope,
elmScope,
tooltipScope;

// load the popover code
beforeEach(module('ui.bootstrap.tooltip'));

// load the template
beforeEach(module('template/tooltip/tooltip-template-popup.html'));

beforeEach(inject(function ($templateCache) {
$templateCache.put('myUrl', [200, '<span>{{ myTemplateText }}</span>', {}]);
}));

beforeEach(inject(function($rootScope, $compile) {
elmBody = angular.element(
'<div><span tooltip-template="{{ templateUrl }}">Selector Text</span></div>'
);

scope = $rootScope;
$compile(elmBody)(scope);
scope.templateUrl = 'myUrl';

scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));

it('should open on mouseenter', inject(function() {
elm.trigger( 'mouseenter' );
expect( tooltipScope.isOpen ).toBe( true );

expect( elmBody.children().length ).toBe( 2 );
}));

it('should not open on mouseenter if templateUrl is empty', inject(function() {
scope.templateUrl = null;
scope.$digest();

elm.trigger( 'mouseenter' );
expect( tooltipScope.isOpen ).toBe( false );

expect( elmBody.children().length ).toBe( 1 );
}));

it('should show updated text', inject(function() {
scope.myTemplateText = 'some text';
scope.$digest();

elm.trigger( 'mouseenter' );
expect( tooltipScope.isOpen ).toBe( true );

expect( elmBody.children().eq(1).text().trim() ).toBe( 'some text' );

scope.myTemplateText = 'new text';
scope.$digest();

expect( elmBody.children().eq(1).text().trim() ).toBe( 'new text' );
}));
});

93 changes: 91 additions & 2 deletions src/tooltip/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
'class="'+startSym+'class'+endSym+'" '+
'animation="animation" '+
'is-open="isOpen"'+
'origin-scope="origScope" '+
'>'+
'</div>';

Expand All @@ -111,7 +112,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
compile: function (tElem, tAttrs) {
var tooltipLinker = $compile( template );

return function link ( scope, element, attrs ) {
return function link ( scope, element, attrs, tooltipCtrl ) {
var tooltip;
var tooltipLinkedScope;
var transitionTimeout;
Expand All @@ -132,6 +133,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
tooltip.css( ttPosition );
};

// Set up the correct scope to allow transclusion later
ttScope.origScope = scope;

// By default, the tooltip is not open.
// TODO add ability to start tooltip opened
ttScope.isOpen = false;
Expand Down Expand Up @@ -197,7 +201,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap

// And show the tooltip.
ttScope.isOpen = true;
ttScope.$digest(); // digest required as $apply is not called
ttScope.$apply(); // digest required as $apply is not called

// Return positioning function as promise callback for correct
// positioning after draw.
Expand Down Expand Up @@ -349,6 +353,74 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
}];
})

// This is mostly ngInclude code but with a custom scope
.directive( 'tooltipTemplateTransclude', [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be split off into its own component as a helper directive, similar to the dateParser service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little hard to generalize though, because I don't want to create another scope here (notice the directive only has a link function), and it specifically looks for origScope and content to be present on the scope.

'$animate', '$sce', '$compile', '$templateRequest',
function ($animate , $sce , $compile , $templateRequest) {
return {
link: function ( scope, elem, attrs ) {
var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);

var changeCounter = 0,
currentScope,
previousElement,
currentElement;

var cleanupLastIncludeContent = function() {
if (previousElement) {
previousElement.remove();
previousElement = null;
}
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if (currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
});
previousElement = currentElement;
currentElement = null;
}
};

scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function (src) {
var thisChangeId = ++changeCounter;

if (src) {
//set the 2nd param to true to ignore the template request error so that the inner
//contents and scope can be cleaned up.
$templateRequest(src, true).then(function(response) {
if (thisChangeId !== changeCounter) { return; }
var newScope = origScope.$new();
var template = response;

var clone = $compile(template)(newScope, function(clone) {
cleanupLastIncludeContent();
$animate.enter(clone, elem);
});

currentScope = newScope;
currentElement = clone;

currentScope.$emit('$includeContentLoaded', src);
}, function() {
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError', src);
}
});
scope.$emit('$includeContentRequested', src);
} else {
cleanupLastIncludeContent();
}
});

scope.$on('$destroy', cleanupLastIncludeContent);
}
};
}])

.directive( 'tooltipPopup', function () {
return {
restrict: 'EA',
Expand All @@ -362,6 +434,23 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
}])

.directive( 'tooltipTemplatePopup', function () {
return {
restrict: 'EA',
replace: true,
scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&',
originScope: '&' },
templateUrl: 'template/tooltip/tooltip-template-popup.html'
};
})

.directive( 'tooltipTemplate', [ '$tooltip', function ( $tooltip ) {
return $tooltip( 'tooltipTemplate', 'tooltipTemplate', 'mouseenter' );
}])

/*
Deprecated
*/
.directive( 'tooltipHtmlUnsafePopup', function () {
return {
restrict: 'EA',
Expand Down
10 changes: 10 additions & 0 deletions template/popover/popover-template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<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"
tooltip-template-transclude="content"
tooltip-template-transclude-scope="originScope()"></div>
</div>
</div>
Loading