twitter bootstrap and angularjs directives

The past few days, I’ve been experimenting with angularjs. It’s an awesome javascript MVC framework and I recommend checking it out if you haven’t!
One of its features are directives which, among other things, allow you to replace html elements with a specific tag name or attribute name with a template and adjust their behaviour. This is nice if you have complex HTML structures in a website which repeat themselves.
Another awesome thing I’ve already posted about is twitter bootstrap, so I thought I would make some angularjs directives for bootstrap syntax that I have difficulty remembering.

Note: I recommend having some knowledge on twitter bootstrap and angularjs before reading this. Of course, the docs are always wonderful ;)

To begin, let’s make sure we have all the needed things. Your page’s head should look something like this:

<!DOCTYPE html>
<html ng-app="directives">
    <head>
        <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css" />
        <link rel="stylesheet" type="text/css" 
        href="css/bootstrap-responsive.min.css" />
    </head>
    <body ng-controller="myController">

We’re loading the stylesheets for bootstrap here, as well as having the ‘directives’ module handle our app and the ‘myController’ controller throughout the whole page.

And right before </body>, let’s load the scripts:

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js">
</script>
<script src="js/directives.js" type="text/javascript"></script>     
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js">
</script>
<script src="js/bootstrap.js"></script>

The scripts are Angularjs, our custom angularjs code (‘directives.js’) where we will code our directives, jquery, and bootstrap.

Let’s also define our module in directives.js:

function myController ($scope) {
}
var module = angular.module('directives', []);

Now with that out of the way, let’s get on with the directives.

Tabs

The first one is an example from the angularjs website: tabs.

This allows you to use the following syntax in your page:

<tabs>
    <pane title="First pane">    
         Content here
    </pane>
    <pane title="Second pane">
         Second pane content here
    </pane>
</tabs>

The directive is as follows:

module.directive('tabs', function() {
    return {
        restrict: 'E',
        transclude: true,
        scope: {},
        controller: function($scope, $element) {
            var panes = $scope.panes = [];

            $scope.select = function(pane) {
                angular.forEach(panes, function(pane) {
                    pane.selected = false;
                });

                pane.selected = true;
            }

            this.addPane = function(pane) {
                if (panes.length == 0)
                    $scope.select(pane);

                panes.push(pane);
            }
        },
        template:
        '<div class="tabbable">' +
        '<ul class="nav nav-tabs">' +
        '<li ng-repeat="pane in panes" ng-class="{active:pane.selected}">'+
        '<a href="" ng-click="select(pane)">{{pane.title}}</a>' +
        '</li>' +
        '</ul>' +
        '<div class="tab-content" ng-transclude></div>' +
        '</div>',
        replace: true
    };
});

module.directive('pane', function() {
    return {
        require: '^tabs',
        restrict: 'E',
        transclude: true,
        scope: { title: '@' },
        link: function(scope, element, attrs, tabsCtrl) {
            tabsCtrl.addPane(scope);
        },
        template:
        '<div class="tab-pane" ng-class="{active: selected}" ng-transclude>' +
        '</div>',
        replace: true
    };
});

What’s special about these directives is that the pane children let the parent know when they are added (by calling addPane), and the tab controller has some code to give the ‘selected’ property to the selected pane every time.

Media

<div class="media">
<a class="pull-left" href="#">
<img class="media-object" src="image">
</a>
<div class="media-body">
<h4 class="media-heading">heading</h4>
<p>
Content
</p>
</div>
</div>

This is not too much trouble, but I like this more:

<media image="img/sample.png" heading="Heading here">
   Content
</media>

And this little directive….

module.directive ('media', function() {
    return {
        restrict: 'E',
        transclude: true,
        scope: { heading: '@', image: '@' },
        template:
        '<div class="media">' +
            '<a class="pull-left" href="#">' +
                '<img class="media-object" ng-src="{{ image }}">' +
            '</a>' +
            '<div class="media-body">' +
                '<h4 class="media-heading">{{ heading }}</h4>' +
                '<p ng-transclude>' +
                '</p>' +
            '</div>' +
        '</div>',
        replace: true
    }
});

..Will save time in the long run!

The special things I did here was use ng-src= for {{ image }} instead of just src= to prevent the browser from trying to download {{ src }}. Also, ng-transclude will copy the content of my <media> tag into the element that has that attribute, so the content can be anything you wish.

<media image="img/sample.png" heading="Heading here">
Content
<media image="img/sample.png" heading="Another heading">
Hello!
</media>
</media>

Works as expected!

Modals

Let’s look at what code for a modal might look like:

<div class="modal hide fade">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
&times;
</button>
<h3>Modal title</h3>
</div>
<div id="modal-body" class="modal-body">
<p>Content</p>
</div>
<div class="modal-footer">
<a href="#" onclick="cancelFn()" class="btn">Cancel</a>
<a href="#" onclick="okFn()" class="btn btn-primary">Save changes</a>
</div>
</div>

That’s long to type every time you need it. Thankfully, We can make it look more like this:

<modal ok-fn="destroyDialog()" cancel-fn="destroyDialog()"
title="Modal Title" id="modal">
    Modal Content
</modal>

You can change ok-fn and cancel-fn of course.

The directive is:

module.directive('modal', function() {
    return {
        restrict: 'E',
        transclude: true,
        scope: { title:'@', okFn: '&', cancelFn: '&' },
        template:
        '<div class="modal hide fade">'
        +'<div class="modal-header">'
        +'<button type="button" class="close" '
        + 'data-dismiss="modal" aria-hidden="true">&times;</button>'
        +'<h3>{{ title }} </h3>'
        +'</div>'
        + '<div id="modal-body" class="modal-body">'
        + '<p ng-transclude></p>'
        + '</div>'
        + '<div class="modal-footer">'
        + '<a href="#" ng-click="cancelFn()" class="btn">Cancel</a>'
        + '<a href="#" ng-click="okFn()" class="btn btn-primary">Save changes</a>'
        + '</div>'
        + '</div>',
        replace: true
    }
});

The only ‘special’ things here are including &t;p ng-transclude> which will copy the innerHTML of our into that element, and ‘okFn: “&”, cancelFn: “&”‘ which means that these two functions will be called in the context of the parent controller.

DestroyDialog is defined in the parent:

function myController ($scope) {
    $scope.destroyDialog = function() {
        $("#modal").modal("hide");
    };
}

Navbars

Navbars aren’t very complex, but the syntax can become, depending on what you want. Luckily, you can create a special ‘navbar’ attribute that makes a

    a navbar:

    <ul navbar title='Title here'>
        <li class='active'><a href='javascript:;'>Menu item 1</a></li>
        <li><a href='javascript:;'>Menu Item 2</a></li>
        <li><a href='javascript:;'>Menu item 3</a></li>
    </ul>
    

    The directive:

    module.directive('navbar', function() {
        return {
            restrict: 'A',
            transclude: true,
            scope: { title: '@' },
            template:
            '<div class="navbar">' +
                '<div class="navbar-inner">' +
                    '<a class="brand" href="#">{{ title }} </a>' +
                    '<ul class="nav" ng-transclude>' +
                    '</ul>' +
                '</div>' +
            '</div>',
            replace: true
        }
    });
    

    We’re using ‘A’ in ‘restrict’ for the first time, because we want it to search for an attribute, not element name. Because attributes are transcluded to the resulting element, you can give your navbar other classes, as normal:

    <ul navbar class='navbar-fixed-top navbar-inverse' title='Title here'>
    

    And of course, ng-transclude will copy our <lis> to the resulting element.

    Responsive navbars

    The syntax for responsive navbars is more complex. Let’s make a directive for these, as well.

    The directive to create this:

    <ul navbar-responsive class='navbar-fixed-top navbar-inverse' title='Title here'>
    

    Is as simple as this:

    module.directive ('navbarResponsive', function() {
        return {
            restrict: 'A',
            transclude: true,
            scope: { title: '@' },
            template:
            '<div class="navbar">' +
                '<div class="navbar-inner">' +
                    '<div class="container">' +
                        '<a class="btn btn-navbar" data-toggle="collapse" ' +
                            ' data-target=".nav-collapse">' +
                            '<span class="icon-bar"></span>' +
                            '<span class="icon-bar"></span>' +
                            '<span class="icon-bar"></span>' +
                        '</a>' +
                        '<a class="brand" href="#">{{ title }}</a>' +
                        '<div class="nav-collapse collapse">' +
                            '<ul class="nav" ng-transclude>' +
                            '</ul>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
            '</div>',
            replace: true
        }
    });
    

    Carousel

    The original syntax for carousel is in the source of this page.

    Let’s improve it from that to this:

    <carousel id="carousel">
      <carousel-item>
        <div class="carousel-caption">
           <h4>First Thumbnail label</h4>
           <p>Content</p>
        </div>
        <img src="img/carousel1.png" />
      </carousel-item>
      <carousel-item>
       <div class="carousel-caption">
           <h4>Second Thumbnail label</h4>
           <p>Content</p>
        </div>
        <img src="img/carousel2.png" />
      </carousel-item>
    </carousel>
    

    Directive:

    module.directive ('carousel', function() {
        return {
            restrict: 'E',
            transclude: true,
            scope: { id: '@' },
            controller: function($scope, $element) {
                var items = $scope.items = [];
                $scope.selectedIndex = 0;
    
                $scope.select = function (index) {
                    if ( index >= $scope.items.length || index < 0 ) {
                        return;
                    }
    
                    angular.forEach (items, function(item) {
                        item.selected = false;
                    });
    
                    items[index].selected = true;
                    $scope.selectedIndex = index;
                }
    
                this.addItem = function(item) {
                    items.push(item);
    
                    if (items.length == 1)
                        $scope.select (0);
                }
            },
            template:
            '<div class="carousel slide">' +
                '<ol class="carousel-indicators">' +
                    '<li ng-repeat="item in items" data-target="#{{id}}" '+
                     'data-slide-to="{{$index}}" ng-click="select($index)"'+
                     'ng-class="{active:item.selected}"></li>' +
                '</ol>' +
                '<div class="carousel-inner" ng-transclude>' +
                '</div>' +
                '<a class="carousel-control left" ' +
                'href="#{{id}}" ng-click="select(selectedIndex-1)">&lsaquo;</a>' +
                '<a class="carousel-control right" ' +
                'href="#{{id}}" ng-click="select(selectedIndex+1)" >&rsaquo;</a>' +
            '</div>',
            replace: true
        };
    });
    
    module.directive('carouselItem', function() {
        return {
            require: '^carousel',
            restrict: 'E',
            transclude: true,
            scope: {  },
            link: function(scope, element, attrs, carouselCtrl) {
                carouselCtrl.addItem(scope);
            },
            template:
            '<div class="item" ng-class="{active: selected}" ng-transclude>' +
            '</div>',
            replace: true
        };
    });
    
    
    

    The special magic here (in the carousel controller function) holds the selected carousel item. The indicators are generated for every carousel item, and also call the select function. That function is also called by the next and back buttons. The carousel items call addItem on the parent controller.

    And of course, don’t forget:

    $("#carousel").carousel();
    

    Is still needed for it to work

    Well, that’s all for now. Remember to check back later for more.. magic!

    A live demo of all this is here

One thought on “twitter bootstrap and angularjs directives

  1. Hi
    I am using your carousel directive, but sliding from one slide to another is not smooth. its jumping from one slide to another. Do you have updated code pls.

    thank you

Leave a Comment

Your email address will not be published. Required fields are marked *