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
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);
}
};
});