20
votes

I have two Bootstrap tabs on a page and each tab displays a list. I want each list item to be a square and wrote this AngularJS directive to set the height of each list item to be equal to its Bootstrap controlled dynamic width:

app.directive('squareDiv', function () {
    return {
        restrict: "A",
        link: function (scope, element,attr) {
            scope.getWidth = function () {
                return element.width();
            };
            scope.$watch(scope.getWidth, function (width) {
                element.css({ 
                    height: width + 'px'
                });
            }, true);
        }
    };
});

When I load this page, the first tab's list items show up as squares which means the directive is working. But when I click on another tab, the list items there have the default height of 100 px. When I switch back to the first tab, even the list items there have the default height now.

What is going wrong here?

UPDATE 1:

Here is the link to a plunker that illustrates this problem: http://plnkr.co/edit/Pc7keuRXR9R3U6AhZmFE?p=preview

UPDATE 2:

Plunker with Bootstrap replaced by UI-Bootstrap that still has the same problem: http://plnkr.co/edit/rLE1wUAHKzJP6FVnGF6b?p=preview

UPDATE 3:

Plunker with watch for tab switch instead of width change (workaround for the original problem of width watch not working correctly; will need to use a separate directive for adapting to width changes due to window resizing): http://plnkr.co/edit/Y4Goe0cexq979JsIcBxl?p=preview

I would still like to understand why the width watch stops working correctly after a tab switch.

2
It may be because the tab is hidden, so its contents don't have dimensions yet. Anyway, it would help if you made a plunker or something. - George Kagan
Thats right, a plunker could be of great help! - Ignacio Villaverde
@Price - I have absolutely no knowledge about angular, but can't you just use bootstraps shown.bs.tab event to set the height of the .holder elements inside the list items ? Here is an example Plunker. Just like i said i have no clue if this is the correct way to do this with angular, but it works. - DavidDomain
@Price - Sure, no problem. It's probably better to wait and see if someone with a better background on angular can tell you what's going on here. In general i always rely on bootstrap events whenever i want to manipulate content inside bootstraps components, but since i have no angular skills, there may be a totally different solution to your problem, which i am not aware of. ;-) - DavidDomain

2 Answers

8
votes

Your directive's watcher is one digest cycle behind of what's happening. If you look closely enough, you will see that the very first tab clicking does not do much (nothing logged to console). What you see is your active tab's elements being resized to the hidden tabs elements heights. So the hidden tab's heights are correct. I would strongly advise using the angular version of bootstrap instead of the generic one. That way everything (events) will be in the control of angular.

https://angular-ui.github.io/bootstrap/

edit: this maybe useful for understanding digest cycle triggering and callbacks outside angular : http://school.codecraftpro.com/courses/angularjs-1x-fundamentals/lectures/392422

edit2: also useful https://scotch.io/tutorials/how-to-correctly-use-bootstrapjs-and-angularjs-together

0
votes

We know that the digest of bootstrap tabs switching is ahead of the width of your divs. So instead of watching for the width of your divs to change, let's instead watch when your tab changes. When the tab changes, let's wait 1 ms to make sure we are on the next digest cycle. Then get the width and update your box height.

I have no idea why this is the case, but here is the solution that will work.

Here is a plunker example

Javascript

var app = angular.module("app",['ngAnimate', 'ui.bootstrap']);
app.controller('mainController',['$scope',function($scope){
    $scope.tabs = [
        {'title': 'Tab 1'},
        {'title': 'Tab 2'}
    ];
}]);

app.directive('squareDiv', function ($timeout) {
    return {
        restrict: "A",
        link: function (scope, element, attr) {

            scope.active = function() {
                return scope.tabs.filter(function(tab){
                    return tab.active;
                })[0];
            };

            scope.$watch(scope.active, function (tabActive) {
                $timeout(function() {
                    element.css({ 
                        height: element.width()
                    });
                }, 1);

            });

        }
    }
});