div.container(ng-app="MainApp", ng-controller="MainCtrl")
  h1 MultiSelect
  p.lead Custom directive for Angular + Bootstrap.
  p Allows user to select multiple choices with the option of marking all available. Fully customizable via directive options.
    em.text-info  Show controls for more toys.

  form.form(name="form")
    dl
      dd
        multi-select(items="selectItems", name="selectedItems", item-delimiter="itemDelim", all-value="allValue", all-label="allLabel", selected-prefix="selectedPrefixStr", none-selected="noSelectionStr", ng-model="value", ng-required="isRequired")

    div(ng-show="showControls")
      hr
      div.row
        dl.col-sm-6
          label
            input(type="checkbox", name="isRequired", ng-model="isRequired")
            |  Field is required
      div.row
        dl.col-xs-12
          dt Disable Items
          dd
            multi-select(items="ctrlItems", item-delimiter="itemDelim", all-value="allValue", all-label="allLabel", ng-model="disabledItems")

      div.row
        dl.col-xs-12
          dt Hide Items
          dd 
            multi-select(items="ctrlItems", item-delimiter="itemDelim", all-value="allValue", all-label="allLabel", ng-model="hiddenItems")
      div.row
        dl.col-sm-2
          dt Delimiter
          dd.input-group
            input.form-control(type="text", name="itemDelim", ng-model="itemDelim", ng-trim="false")
            div.input-group-btn
              button.btn.btn-default(ng-click="resetField('itemDelim', form)", ng-disabled="isClean('itemDelim', form)")
                span.glyphicon.glyphicon-remove
        dl.col-sm-4
          dt All Value
          dd.input-group
            input.form-control(type="text", name="allValue", ng-model="allValue", ng-trim="false")
            div.input-group-btn
              button.btn.btn-default(ng-click="resetField('allValue', form)", ng-disabled="isClean('allValue', form)")
                span.glyphicon.glyphicon-remove
        dl.col-sm-6
          dt All Items Label
          dd.input-group
            input.form-control(type="text", name="allLabel", ng-model="allLabel", ng-trim="false")
            div.input-group-btn
              button.btn.btn-default(ng-click="resetField('allLabel', form)", ng-disabled="isClean('allLabel', form)")
                span.glyphicon.glyphicon-remove
      div.row
        dl.col-xs-6
          dt Selection Label Prefix
          dd.input-group
            input.form-control(type="text", name="selectedPrefixStr", ng-model="selectedPrefixStr", ng-trim="false")
            div.input-group-btn
              button.btn.btn-default(ng-click="resetField('selectedPrefixStr', form)", ng-disabled="isClean('selectedPrefixStr', form)")
                span.glyphicon.glyphicon-remove
        dl.col-xs-6
          dt No Selection Label
          dd.input-group
            input.form-control(type="text", name="noSelectionStr", ng-model="noSelectionStr", ng-trim="false")
            div.input-group-btn
              button.btn.btn-default(ng-click="resetField('noSelectionStr', form)", ng-disabled="isClean('noSelectionStr', form)")
                span.glyphicon.glyphicon-remove

    dl
      button.btn.btn-primary(ng-click="showControls = !showControls")
        span.glyphicon(ng-class="{'glyphicon-minus': showControls, 'glyphicon-plus': !showControls}")
        |  {{showControls ? 'Hide Controls' : 'Show Controls'}}
      button.btn.btn-danger.pull-right(ng-click="resetAll(form)", ng-show="showControls")
        span.glyphicon.glyphicon-trash
        |  Reset Defaults

    p.text-muted Current Value
    pre
      | {{value|json}}
    
  script(type="text/ng-template", id="/multi-select.tpl")
    div.multi-select
      div.input-group(ng-class="{expanded: isExpanded}")
        div.form-control(ng-click="toggleSelector()", ng-class="{'ng-required': required, 'ng-touched': isTouched(), 'ng-invalid': !isValid(), 'ng-valid': isValid() }")
          span.caption(ng-bind-html="currentLabel()")
        div.input-group-addon(ng-click="toggleSelector()")
          span.glyphicon(ng-class="{'glyphicon-chevron-down': !isExpanded, 'glyphicon-chevron-up': isExpanded}")
      div.well(ng-if="isExpanded")
        div.list-group.all-item-group(ng-if="allValue")
          label.list-group-item(ng-class="{active: enabledItems[allValue]}")
            input(type="checkbox", ng-model="enabledItems[allValue]")
            |  {{getAllLabel()}}

        div.list-group
          label.list-group-item(ng-repeat="item in items", ng-class="{active: itemIsActive(item), disabled: itemDisabled(item)}", ng-show="itemVisible(item)")
            input(type="checkbox", ng-model="enabledItems[item.value]", ng-disabled="itemDisabled(item)")
            |  {{item.label}}

        div.clearfix
View Compiled
@import "bourbon";

$breakpoint-mobile: 480px;
$breakpoint-tablet: 720px;
$border-radius: 5px;

.form {
  .ng-invalid.ng-touched {
    background-color: #FAC8C8;
    border-color: #F16B6B;
  }
  .ng-valid.ng-touched {
    background-color: #C0F0C0;
    border-color: #6EDC6E;
  }
}

.multi-select {
  .input-group.expanded {
    .form-control {
      border-radius: $border-radius 0 0 0;
    }
    .input-group-addon {
      border-radius: 0 $border-radius 0 0;
    }
  }

  .well {
    padding: 5px;
    border-radius: 0 0 $border-radius $border-radius;
    border-top: none;
  }

  .form-control {
    position: relative;

    .caption {
      display: block;
      position: absolute;
      margin: 0 10px;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      line-height: 30px;
    }
  }

  .form-control,
  .input-group-addon {
    cursor: pointer;
  }

  .form-control .caption,
  .list-group .list-group-item {
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }

  .list-group {
    max-height: 240px;
    overflow: auto;
    margin-bottom: 0;
    border-radius: 0;

    .list-group-item {
      border-radius: 0;
      padding: 0 10px;
      line-height: 30px;
      border-width: 0;
    }

    &.all-item-group {
      margin-bottom: 5px;

      .list-group-item {
      }
    }
  }

  @media (min-width: $breakpoint-mobile) {
    .list-group {
      .list-group-item {
        box-sizing: border-box;
        float: left;
        width: 50%;
      }

      &.all-item-group {
        .list-group-item {
          float: none;
          width: auto;
        }
      }
    }
  }

  @media (min-width: $breakpoint-tablet) {
    .list-group {
      .list-group-item {
        box-sizing: border-box;
        float: left;
        width: 33.3333%;
      }
    }
  }
}
View Compiled
angular
.module('MainApp', ['ngSanitize'])

// demo controller
.controller('MainCtrl', function ($scope) {

  $scope.master = {
    isRequired: false,
    itemDelim: undefined,
    allValue: 'all',
    allLabel: undefined,
    selectedPrefixStr: undefined,
    noSelectionStr: undefined,
    hiddenItems: [],
    disabledItems: []
  };
  
  function isDisabled(itm) {
    return $scope.disabledItems === 'all' || $scope.disabledItems.indexOf(itm.value) > -1;
  }
  function isVisible(itm) {
    return $scope.hiddenItems !== 'all' && $scope.hiddenItems.indexOf(itm.value) === -1;
  }

  $scope.showControls = false;

  var
  testValues = [
    ['item0',  'Item With really long freaking text.'],
    ['item1',  'Item One'],
    ['item2',  'Item Two'],
    ['item3',  'Item Three'],
    ['item4',  'Item Four'],
    ['item5',  'Item Five'],
    ['item6',  'Item Six'],
    ['item7',  'Item Seven'],
    ['item8',  'Item Eight'],
    ['item9',  'Item Nine'],
    ['item10', 'Item Ten'],
    ['item11', 'Item Eleven'],
    ['item12', 'Item Twelve'],
    ['item13', 'Item Thirteen'],
    ['item14', 'Item Fourteen']
  ];

  $scope.value = [testValues[1][0], testValues[2][0]];

  $scope.selectItems = testValues
    .map(function (entry) {
   	  return {
        value: entry[0],
        label: entry[1],
        showIf: isVisible,
        disableIf: isDisabled
      };
  	});

  $scope.ctrlItems = testValues
    .map(function (entry) {
   	  return {
        value: entry[0],
        label: entry[1]
      };
  	});

  $scope.resetField = function(modelVal, form) {
    $scope[modelVal] = $scope.master[modelVal];

    if(form && form[modelVal] !== undefined) {
      form[modelVal].$setUntouched();
    }
  };
  ($scope.resetAll = function(form) {
    Object.keys($scope.master).forEach(function(key) {
      $scope.resetField(key, form);
    });
    if(form) form.$setUntouched();
  })();
  $scope.isClean = function(modelVal, form) {
    var matches = $scope[modelVal] === $scope.master[modelVal];

    if(matches && form && form[modelVal]) {
      matches = !form[modelVal].$touched;
    }

    return matches;
  };
})

//
// MultiSelect Directive
// @author Hans Doller
// https://codepen.io/kryo2k/pen/qErJBQ
//
.directive('multiSelect', function ($rootScope) {

  var
  DEFAULT_ALL_ITEMS = 'All Items',
  DEFAULT_SELECTED_ITEMS = 'Selected: ',
  DEFAULT_NO_SELECTION = 'Nothing Selected',
  DEFAULT_ITEM_DELIMITER = ', ';

  return {
    require: 'ngModel',
    restrict: 'E',
    replace: true,
    templateUrl: '/multi-select.tpl',

    scope: {
      items: '=',
      itemDelimiter: '=',
      allValue: '=',
      allLabel: '=',
      selectedPrefix: '=',
      noneSelected: '=',
      modelValue: '=ngModel'
    },

    link: function(scope, el, attrs, ctrls) {
      scope.getAllLabel = function () {
        return scope.allLabel || DEFAULT_ALL_ITEMS;
      };
      scope.toggleSelector = function() {
        scope.isExpanded = !scope.isExpanded;
      };
      scope.isSelectedAll = function() {
        return scope.supportAllValue() && scope.enabledItems[scope.allValue];
      };
      scope.supportAllValue = function() {
        return !!scope.allValue;
      };
      scope.itemVisible = function(item) {
        if(!itemExists(item)) return false;

        if(angular.isFunction(item.showIf)) {
          return !!item.showIf.call(this, item);
        }

        return !item.hidden;
      };
      scope.itemDisabled = function(item) {
        if(scope.isSelectedAll()) return true;

        if(angular.isFunction(item.disableIf)) {
          return !!item.disableIf.call(this, item);
        }

        return false;
      };
      scope.itemIsActive = function(item) {
        return !!scope.enabledItems[item.value];
      };
      scope.isTouched = function() {
        return ctrls.$touched;
      };
      scope.isValid = function() {
        return !!scope.required ? (
          !!scope.modelValue && !!scope.modelValue.length
        ) : true;
      };

      scope.currentLabel = function updateLabel() {
        var
        tag = 'b',
        allItems = scope.items||[],
        allLabel = scope.allLabel || DEFAULT_ALL_ITEMS,
        itemDelim = scope.itemDelimiter|| DEFAULT_ITEM_DELIMITER,
        selPrefix = scope.selectedPrefix || DEFAULT_SELECTED_ITEMS,
        noSelection = scope.noneSelected || DEFAULT_NO_SELECTION,
        items = scope.enabledItems||{},
        label;

        if(!!scope.allValue && items[scope.allValue]) {
          label = wrapTag(allLabel, tag);
        }
        else {
          var
          indexed = allItems.reduce(function(p, c) {
            p[c.value] = c;
            return p;
          }, {}),
          itemStr = Object.keys(items)
            .reduce(function (p, c) {
              if(!!indexed[c] && scope.itemVisible(indexed[c])) {
                p.push(indexed[c].label);
              }
              return p;
            }, []).join(itemDelim);

          if(itemStr) {
            label = wrapTag(selPrefix, tag) + itemStr;
          }
          else {
            label = wrapTag(noSelection, tag);
          }
        }

        return label;
      }

      scope.isExpanded = false;
      scope.enabledItems = null;

      var
      modelLoaded = false;

      function wrapTag(content, tag) {
        return '<' + tag + '>' + content + '</' + tag + '>';
      }

      function itemExists(value) {
        var valueId = (value||{}).value||value;
        return !(scope.items||[]).every(function (itm) {
          return itm.value !== valueId;
        });
      }

      function pushModel() {
        if(!modelLoaded || !scope.enabledItems) return;

        if(!!scope.allValue && scope.enabledItems[scope.allValue]) { // if all value is enabled:
          scope.modelValue = scope.allValue;
          return;
        }

        scope.modelValue = Object.keys(scope.enabledItems)
          .reduce(function (p, c) {

            if(scope.enabledItems[c]) {
              p.push(c);
            }

            return p;
          }, []);
      }

      function pullModel() {
        scope.enabledItems = {}; // reset enabled items

        var isModelArray = angular.isArray(scope.modelValue);

        if(!!scope.allValue && (scope.modelValue === scope.allValue
           || ( isModelArray && scope.modelValue.indexOf(scope.allValue) > -1)) ) { // all is enabled:
          scope.enabledItems[scope.allValue] = true;
          modelLoaded = true;
          return;
        }

        if(angular.isArray(scope.modelValue)) {
          scope.modelValue
            .forEach(function (item) {
              scope.enabledItems[item] = true;
            });
        }
        else if(angular.isString(scope.modelValue)) {
          scope.enabledItems[scope.modelValue] = true;
        }

        modelLoaded = (scope.modelValue !== undefined);
      }

      ctrls.$validators.multiSelect = function(nV, vV) {
        return scope.isValid();
      };

      attrs.$observe('required', function(value) {
        scope.required = !!value;
      });

      scope.$watch('isExpanded', function(nV) {
        if(ctrls.$touched || !nV) return;

        if ($rootScope.$$phase) {
          scope.$evalAsync(ctrls.$setTouched);
        } else {
          scope.$apply(ctrls.$setTouched);
        }
      });

      scope.$watch('enabledItems', pushModel, true);
      scope.$watch('modelValue', pullModel, true);
    }
  };
});
Run Pen

External CSS

  1. //maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css

External JavaScript

  1. //ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.2/angular-sanitize.min.js