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
The problem is the slider on the 2nd tab is being rendered and hitting the multislider.js - updateCaclulations (or whatever function is calculating the space between the slider handles) function before the tab content area is visible. So there is no parent space to calc from. This plunk demonstrates using an ng-if to only render the multi-slider when the tab is active. And since SO won't let you post an answer with a plunk without code, here it is:
<uib-tabset>
<uib-tab heading="Tab 1" active="activeTabs[0]">
<multi-slider name="mySlider"
floor="0"
step="1"
precision="2"
ceiling="365"
bubbles="true"
ng-model="app.sliders"
ng-if="activeTabs[0]">
</multi-slider>
</uib-tab>
<uib-tab heading="Tab 2" active="activeTabs[1]">
<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"
ng-if="activeTabs[1]">
</multi-slider>
</section>
</uib-tab>
</uib-tabset>
controller:
$scope.activeTabs = [true, false];
I've been looking at this for over an hour and have tried a bunch of different things and none of them have worked. It would be easy to blame the third party library but I suspect it's also due to how the tabs are rendered (i.e., via replace: true
). Yours would not be the first problem we've experienced with users having issues with tab contents. We need to come up with a best practices for tab contents - especially when users put complex directives in the contents.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With