An AngularJS Bootstrap Tabs Example

March 07, 2017

An AngularJS Bootstrap Tabs Example

Since the whole point of the Fair Offer Project was to allow me to compare job offers in an “apples-to-apples” way, I envisioned a user being able to “tab” through different offers.

Angular JS shield logo

This meant when a user logged in, they should see tabs representing different offers. I wasn’t sure how to do this at first, but this post represents the step-by-step approach I took.

Step 1 – Find the plugins

I started searching Google for a plugin to cobble this together. I came across this post by K. Allen.

The post suggested using UI Bootstrap, a library specifically written to work with the Twitter Bootstrap CSS framework.

This was perfect as that’s exactly what I was using on the frontend. Thank you, Internet!

Step 2 – Install UI Bootstrap

In bower.json, I had to add these lines of code to add angular-bootstrap:

"dependencies": {
    "angular": "~1.5.0",
    "angular-mocks": "~1.5.0",
    "angular-ui-router": "~0.2.18",
    "lodash": "~4.5.1",
    "angular-local-storage": "~0.2.5",
    "angular-bootstrap": "~1.2.4",
    "bootstrap": "~3.3.6"
  },

Next, I had to require the ui-bootstrap and the associated dependency ui-bootstrap-tpls.

To do this, in src/app/app.js, I had to add the following lines of code:

require('bower/angular-bootstrap/ui-bootstrap');
require('bower/angular-bootstrap/ui-bootstrap-tpls');

module.exports = angular.module('fairOfferApp', [
        'ui.router',
        'LocalStorageModule',
        'ui.bootstrap',
        //common,
        //'greatestApp.catService',

        // modules
        //'greatestApp.showroom'
        'fairOfferApp.fair_offer'
    ])

In the module.exports configuration, I had to add ui.bootstrap. You might even notice this is in such an “MVP” (minimum viable product) format that I didn’t bother to erase the commented lines of code.

Step 3 – Brainstorming the user interface features I wanted

The third step I took was to brainstorm the features I needed in the user interface.

I wanted:

  • Auto-summation: When a user adds a new dollar value (e.g., a one-time bonus), it should be added automatically into the total compensation package amount.
  • Tabbed workspaces: A user should be able to add a “new job offer” by clicking an “add new offer” type tab. This could be represented by a “+” symbol.
  • A user an create different compensation factors: The user can add a row containing a value and the type of compensation it is (401K matching, yearly bonus, etc.)
  • A user can choose between ongoing or one-time compensation features: Sometimes compensation might be a one-time event (e.g., sign-on bonus) or a recurring event (e.g., guaranteed vacation). The user should be able to identify what it is via select dropdown.

Step 4 – Add the UI markup to the view

The fourth step was to add all the necessary Angular markup to the HTML view. Take note of the uib-tabset and uib-tab directives provided by the UI Bootstrap library.

The markup code below is the latest version I released as of Tuesday, 1/17/2017.

.jumbotron
  .container
    %h1 Fair Offer Project
    %p A tool to help you compare job offers
%uib-tabset{:active => "active"}
  %uib-tab{:active => "workspace.active", :heading => "{{workspace.name}}", "ng-repeat" => "workspace in workspaces"}
    .container
      %table.table
        %tr
          %td{:colspan => "2"}
            %button.form-control.btn.btn-info{"ng-click" => "workspace.addFactor()"} Add Row
        %tr
          %th Factor Name
          %th Value
        %tr{"ng-repeat" => "factor in workspace.factors"}
          %td
            %input.form-control{"ng-model" => "factor.name"}/
          %td
            %input.form-control{"ng-change" => "workspace.addTotals()", "ng-model" => "factor.value"}
          %td
            %select.form-control{"ng-change" => "workspace.addTotals()", "ng-init" => "ongoing", "ng-model" => "factor.repeat_status"}
              %option{"ng-repeat" => "option in workspace.repeat_status_options", :value => "{{option}}"} {{option}}
        %tr
          %td One Time Total +
          %td Ongoing Total
          %td Total Compensation
        %tr
          %td
            %input.form-control{"ng-model" => "workspace.one_time_total"}
          %td
            %input.form-control{"ng-model" => "workspace.ongoing_total"}
          %td
            %input.form-control{"ng-model" => "workspace.compensation_total"}
        %tr
          %td{:colspan => "2"}
            %button.btn.btn-primary{"ng-click" => "workspace.saveLocal()"} Save Local
          %td{:colspan => "2"}
            %button.btn.btn-primary{"ng-click" => "workspace.saveBackend()"} Save Backend
  %uib-tab{:select => "addWorkspace()"}
    %uib-tab-heading
      <i class="icon-plus-sign"></i>
      %button.btn.btn-primary +

Step 5 – Add the controller code

The controller code I’m showing below is an edited example to show the setup to make it work for saving the job offer information locally. You might notice that the saveBackend() function in the UI markup from Step 4 is not defined.

This is because we’re not at the point yet where we have a database backend to save the information too. To illustrate the concepts, I’ve chosen to show only a local storage implementation.

fairOfferApp.controller('fairOfferCtrl', fairOfferCtrl);
fairOfferCtrl.$inject = [
  '$scope',
  'localStorageService',
  'Restangular',
  'AuthFactory',
];
function fairOfferCtrl($scope, localStorageService, Restangular, AuthFactory) {
  $scope.workspaces = [
    {
      id: 1,
      name: 'Job 1',
      active: true,
      ongoing_total: 0,
      one_time_total: 0,
      compensation_total: 0,
      repeat_status_options: [],
      factors: [],
    },
  ];

  var setAllInactive = function() {
    angular.forEach($scope.workspaces, function(workspace, key) {
      workspace.active = false;
      workspace.repeat_status_options = ['ongoing', 'one-time'];
      workspace.addFactor = function() {
        workspace.factors.push({name: '', value: '', repeat_status: 'ongoing'});
      };
      workspace.addTotals = function() {
        workspace.factors.forEach(function(factor) {
          if (factor.repeat_status == 'ongoing') {
            workspace.ongoing_total += parseInt(factor.value);
          } else {
            workspace.one_time_total += parseInt(factor.value);
          }
        });
        workspace.compensation_total =
          workspace.ongoing_total + workspace.one_time_total;
      };

      workspace.saveLocal = function() {
        localStorageService.set('factors' + key, workspace.factors);
        localStorageService.set('compensation' + key, {
          ongoing_total: workspace.ongoing_total,
          one_time_total: workspace.one_time_total,
          compensation_total: workspace.compensation_total,
        });
      };
    });
  };
  var addNewWorkspace = function() {
    var id = $scope.workspaces.length + 1;
    $scope.workspaces.push({
      id: id,
      name: 'Job ' + id,
      active: true,
      ongoing_total: 0,
      one_time_total: 0,
      compensation_total: 0,
      repeat_status_options: [],
      factors: [],
    });
  };

  $scope.addWorkspace = function() {
    addNewWorkspace();
    setAllInactive();
  };
} //end fair offer controller

Summary

This was the 5 step approach I followed to create a tabbed workspace implementation to help compare job offers fairly.


Profile picture

Written by Bruce Park who lives and works in the USA building useful things. He is sometimes around on Twitter.