Is there a callback function for uib-tab directives I can use to refresh the inner directives after a tab has been rendered?
I am trying to find the source of an issue with a third-party directive that appears when I use that directive inside the uib-tab directive provided by angular-bootstrap. The third-party directive is angular-multi-slider and the issue was first reported in that repository.
A use case is available in plnkr. Click on the second tab, and you will see that the inner slider has all its handles one on top of the others (i.e., widths=0px). Then click on one of the handles and it will appear correctly. The issue persists even after following your recommendation regarding scopes in the FAQ.
Angular App
'use strict';
angular.module('multiSliderDemo', ['angularMultiSlider', 'ngAnimate', 'ui.bootstrap']);
angular.module('multiSliderDemo')
.controller('DemoCtrl', function ($rootScope, $scope, $sce, $uibModal) {
var s = [
{value: 2, title:"Brainstorming", component: "Proposal Making",
symbol: $sce.trustAsHtml("1")},
{value: 50, title:"Working groups formation", component: "Proposal Making",
symbol: $sce.trustAsHtml("2")},
{value: 100, title:"Proposal drafting",component:"Proposal Making",
symbol: $sce.trustAsHtml("3")},
{value: 130, title:"Proposal editing", component: "Versioning",
symbol: $sce.trustAsHtml("4")},
{value: 160, title:"Proposal selection", component: "Versioning",
symbol: $sce.trustAsHtml("5")},
{value: 200, title:"Discussion of proposals", component: "Deliberation",
symbol: $sce.trustAsHtml("6")},
{value: 250, title:"Technical assessment", component: "Deliberation",
symbol: $sce.trustAsHtml("7")},
{value: 300, title:"Voting on proposals", component: "Voting",
symbol: $sce.trustAsHtml("8")}
];
$scope.app = {sliders:s}
});
index.html
<html ng-app="multiSliderDemo">
<head>
<meta charset="UTF-8">
<title>Multi Slider</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="multislider.css">
</head>
<body>
<div ng-controller="DemoCtrl" class="container">
<article>
<h2>Multi-Slider Issue with uib-tabs</h2>
<form name="sliderForm" id="sliderForm" novalidate autocomplete="off">
<fieldset class="row">
<uib-tabset>
<uib-tab heading="Tab 1" active="true">
<multi-slider name="mySlider"
floor="0"
step="1"
precision="2"
ceiling="365"
bubbles="true"
ng-model="app.sliders">
</multi-slider>
</uib-tab>
<uib-tab heading="Tab 2" active="false">
<section class="col-sm-6 padding-10">
<multi-slider name="mySlider"
floor="0"
step="1"
precision="2"
ceiling="365"
bubbles="true"
ng-model="app.sliders">
</multi-slider>
</section>
</uib-tab>
</uib-tabset>
</fieldset>
</form>
</article>
</div>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular-animate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.14.3/ui-bootstrap-tpls.min.js"></script>
<script src="multislider.js"></script>
<script src="script.js"></script>
</body>
</html>
CSS
.angular-multi-slider {
display: inline-block;
position: relative;
height: 5px;
width: 100%;
margin: 25px 5px 25px 5px;
vertical-align: middle;
}
.angular-multi-slider div {
white-space: nowrap;
position: absolute;
}
.angular-multi-slider div.bar {
width: 100%;
height: 100%;
border-radius: 6px;
background: #999;
overflow: hidden;
}
.angular-multi-slider div.handle {
cursor: pointer;
width: 10px;
height: 30px;
top: -15px;
background-color: #13b6ff; /*can override with color in slider object*/
border: 2px solid #000;
z-index: 2;
border-radius: 4px;
-o-transition: .3s;
-ms-transition: .3s;
-moz-transition: .3s;
-webkit-transition: .3s;
-webkit-transition-property: background-color;
transition-property: background-color;
}
.angular-multi-slider div.handle:hover,
.angular-multi-slider div.handle:focus,
.angular-multi-slider div.handle:active,
.angular-multi-slider div.handle.active {
-webkit-filter: brightness(70%);
filter: brightness(70%);
}
.angular-multi-slider div.handle:hover + .bubble,
.angular-multi-slider div.handle:focus + .bubble,
.angular-multi-slider div.handle.grab + .bubble,
.angular-multi-slider div.handle:hover,
.angular-multi-slider div.handle:focus,
.angular-multi-slider div.handle.grab {
-webkit-transform: scale(1.1);
transform: scale(1.1);
z-index: 9999;
}
.angular-multi-slider div.handle.grab + .bubble,
.angular-multi-slider div.handle.grab{
background-color: rgba(0,0,0,1);
}
.angular-multi-slider div.bubble {
display: none;
cursor: default;
top: -36px;
padding: 1px 3px 1px 3px;
font-size: 0.7em;
font-family: sans-serif;
-o-transition: .1s;
-ms-transition: .1s;
-moz-transition: .1s;
-webkit-transition: .1s;
-webkit-transition-property: top;
transition-property: top;
}
.angular-multi-slider div.bubble:nth-child(2) {
top: 34px !important;
z-index:9999;
}
.angular-multi-slider div.bubble.active {
display: inline-block;
color: #fff;
font-size:12px;
font-family: 'Arial', sans-serif;
text-align: center;
background-color: rgba(0,0,0,0.75);
border-radius: 8px;
padding: 3px 8px;
}
.angular-multi-slider div.limit {
margin-top: 12px;
color: #000;
font-weight: bold;
}
Multislider.js
'use strict';
angular.module('angularMultiSlider', [])
.directive('multiSlider', function($compile, $timeout) {
var events = {
mouse: {
start: 'mousedown',
move: 'mousemove',
end: 'mouseup'
},
touch: {
start: 'touchstart',
move: 'touchmove',
end: 'touchend'
}
};
function roundStep(value, precision, step, floor) {
var remainder = (value - floor) % step;
var steppedValue = remainder > (step / 2) ?
value + step - remainder : value - remainder;
var decimals = Math.pow(10, precision);
var roundedValue = steppedValue * decimals / decimals;
return parseFloat(roundedValue.toFixed(precision));
}
function offset(element, position) {
return element.css({
left: position
});
}
function pixelize(position) {
return parseInt(position) + "px";
}
function contain(value) {
if (isNaN(value)) return value;
return Math.min(Math.max(0, value), 100);
}
return {
restrict: 'EA',
require: '?ngModel',
scope: {
floor : '@',
ceiling : '@',
step : '@',
precision : '@',
bubbles : '@',
sliders : '=ngModel'
},
template :
'<div class="bar"></div>' +
'<div class="limit floor">{{ floor }}</div>' +
'<div class="limit ceiling">{{ ceiling }}</div>',
link : function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
//base copy to see if sliders returned to original
var original;
ngModel.$render = function() {
original = angular.copy(scope.sliders);
};
element.addClass('angular-multi-slider');
// DOM Components
var sliderStr = '';
angular.forEach(scope.sliders, function(slider, key){
sliderStr += ('<div class="handle">
</div>
<div class="bubble">{{ sliders[' + key.toString()
+ '].title }}{{ sliders[' + key.toString()
+ '].value}}
</div>');
});
var sliderControls = angular.element(sliderStr);
element.append(sliderControls);
$compile(sliderControls)(scope);
var children = element.children();
var bar = angular.element(children[0]),
ngDocument = angular.element(document),
floorBubble = angular.element(children[1]),
ceilBubble = angular.element(children[2]),
bubbles = [],
handles = [];
//var sliderChildren = sliderControls.children();
angular.forEach(scope.sliders, function(slider, key) {
handles.push(angular.element(children[(key * 2) + 3]));
bubbles.push(angular.element(children[(key * 2) + 4]));
});
// Control Dimensions Used for Calculations
var handleHalfWidth = 0,
barWidth = 0,
minOffset = 0,
maxOffset = 0,
minValue = 0,
maxValue = 0,
valueRange = 0,
offsetRange = 0;
if (scope.step === undefined) scope.step = 1;
if (scope.floor === undefined) scope.floor = 0;
if (scope.ceiling === undefined) scope.ceiling = 500;
if (scope.precision === undefined) scope.precision = 0;
if (scope.bubbles === undefined) scope.bubbles = false;
var bindingsSet = false;
var updateCalculations = function() {
scope.floor = roundStep(parseFloat(scope.floor), parseInt(scope.precision),
parseFloat(scope.step), parseFloat(scope.floor));
scope.ceiling = roundStep(parseFloat(scope.ceiling), parseInt(scope.precision),
parseFloat(scope.step), parseFloat(scope.floor));
angular.forEach(scope.sliders, function(slider) {
slider.value = roundStep(parseFloat(slider.value), parseInt(scope.precision),
parseFloat(scope.step), parseFloat(scope.floor));
});
handleHalfWidth = handles[0][0].offsetWidth / 2;
barWidth = bar[0].offsetWidth;
minOffset = 0;
maxOffset = barWidth - handles[0][0].offsetWidth;
minValue = parseFloat(scope.floor);
maxValue = parseFloat(scope.ceiling);
valueRange = maxValue - minValue;
offsetRange = maxOffset - minOffset;
};
var updateDOM = function () {
updateCalculations();
var percentOffset = function (offset) {
return contain(((offset - minOffset) / offsetRange) * 100);
};
var percentValue = function (value) {
return contain(((value - minValue) / valueRange) * 100);
};
var pixelsToOffset = function (percent) {
return pixelize(percent * offsetRange / 100);
};
var setHandles = function () {
offset(ceilBubble, pixelize(barWidth - ceilBubble[0].offsetWidth));
angular.forEach(scope.sliders, function(slider,key){
if (slider.color) {
handles[key].css({ "background-color": slider.color });
}
offset( handles[key],
pixelsToOffset(percentValue(slider.value)));
offset( bubbles[key],
pixelize(handles[key][0].offsetLeft
- (bubbles[key][0].offsetWidth / 2) + handleHalfWidth));
});
};
var bind = function (handle, bubble, currentRef, events) {
var onEnd = function () {
handle.removeClass('grab');
bubble.removeClass('grab');
if (!(''+scope.bubbles === 'true')) {
bubble.removeClass('active');
}
ngDocument.unbind(events.move);
ngDocument.unbind(events.end);
if (angular.equals(scope.sliders, original)) {
ngModel.$setPristine();
}
scope.$apply();
};
var onMove = function (event) {
// Suss out which event type we are capturing and get the x value
var eventX = 0;
if (event.clientX !== undefined) {
eventX = event.clientX;
}
else if ( event.touches !== undefined && event.touches.length) {
eventX = event.touches[0].clientX;
}
else if ( event.originalEvent !== undefined &&
event.originalEvent.changedTouches !== undefined &&
event.originalEvent.changedTouches.length) {
eventX = event.originalEvent.changedTouches[0].clientX;
}
var newOffset =
Math.max( Math.min(
(eventX - element[0].getBoundingClientRect().left
- handleHalfWidth), maxOffset), minOffset),
newPercent = percentOffset(newOffset),
newValue = minValue
+ (valueRange * newPercent / 100.0);
newValue = roundStep(newValue, parseInt(scope.precision), parseFloat(scope.step),
parseFloat(scope.floor));
scope.sliders[currentRef].value = newValue;
setHandles();
ngModel.$setDirty();
scope.$apply();
};
var onStart = function (event) {
updateCalculations();
bubble.addClass('active grab');
handle.addClass('active grab');
setHandles();
event.stopPropagation();
event.preventDefault();
ngDocument.bind(events.move, onMove);
return ngDocument.bind(events.end, onEnd);
};
handle.bind(events.start, onStart);
};
var setBindings = function () {
var method, i;
var inputTypes = ['touch', 'mouse'];
for (i = 0; i < inputTypes.length; i++) {
method = inputTypes[i];
angular.forEach(scope.sliders, function(slider, key){
bind(handles[key], bubbles[key], key, events[method]);
});
}
bindingsSet = true;
};
if (!bindingsSet) {
setBindings();
// Timeout needed because bubbles offsetWidth is incorrect
// during initial rendering of html elements
setTimeout( function() {
if (''+scope.bubbles === 'true') {
angular.forEach(bubbles, function(bubble) {
bubble.addClass('active');
});
}
//added this for tab 1...
updateCalculations();
setHandles();
}, 1);
}
};
// Watch Models based on mode
scope.$watch('sliders', updateDOM);
// Update on Window resize
window.addEventListener('resize', updateDOM);
}
}
});
Notes: The version of AngularJS I am using is 1.4.7. The version of angular-bootstrap is 0.14.3. The version of angular-multi-slider is 0.1.1