Skip to content

Instantly share code, notes, and snippets.

@dtheodor
Last active December 23, 2015 20:18
Show Gist options
  • Save dtheodor/6688152 to your computer and use it in GitHub Desktop.
Save dtheodor/6688152 to your computer and use it in GitHub Desktop.

need to support

  • bi-directional model to select2 selected object changes
  • select2 data-set changes
  • select2 object to ng-model mapping that supports attributes, not necessarily the whole object, similar to <option ng-repeat value={{obj.attribute}}>{{obj.humanReadableAttribute}}</option>
  • should work for both multi and single-value selects
  • should support optgroups
/**
* Enhanced Select2 Dropmenus
*
* @AJAX Mode - When in this mode, your value will be an object (or array of objects) of the data used by Select2
* This change is so that you do not have to do an additional query yourself on top of Select2's own query
* @params [options] {object} The configuration options passed to $.fn.select2(). Refer to the documentation
*/
angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelect2', ['uiSelect2Config', '$timeout', function (uiSelect2Config, $timeout) {
var options = {};
if (uiSelect2Config) {
angular.extend(options, uiSelect2Config);
}
return {
require: 'ngModel',
compile: function (tElm, tAttrs) {
var watch,
repeatOption,
repeatAttr,
isSelect = tElm.is('select'),
isMultiple = angular.isDefined(tAttrs.multiple);
// Enable watching of the options dataset if in use
if (tElm.is('select')) {
repeatOption = tElm.find('option[ng-repeat], option[data-ng-repeat]');
if (repeatOption.length) {
repeatAttr = repeatOption.attr('ng-repeat') || repeatOption.attr('data-ng-repeat');
watch = jQuery.trim(repeatAttr.split('|')[0]).split(' ').pop();
}
}
return function (scope, elm, attrs, controller) {
// instance-specific options
var opts = angular.extend({}, options, scope.$eval(attrs.uiSelect2));
/*
Convert from Select2 view-model to Angular view-model.
*/
var convertToAngularModel = function(select2_data) {
var model;
if (opts.bindTo){
if (select2_data){
if (select2_data instanceof Array){
model = [];
angular.forEach(select2_data, function(d){
model.push(d[opts.bindTo]);
console.log('pushed ' + d[opts.bindTo])
})
} else{
model = select2_data[opts.bindTo];
}
}
}
else {
if (opts.simple_tags) {
model = [];
angular.forEach(select2_data, function(value, index) {
model.push(value.id);
});
} else {
model = select2_data;
}
}
return model;
};
/*
Convert from Angular view-model to Select2 view-model.
*/
var convertToSelect2Model = function(angular_data) {
var model = [];
if (!angular_data) {
return model;
}
if (opts.bindTo){
if (angular_data instanceof Array){
angular.forEach(angular_data, function(d){
model.push(opts.reverseBinding(d));
console.log('pushed ' + opts.reverseBinding(d));
})
} else{
model = opts.reverseBinding(angular_data);
};
}
else{
if (opts.simple_tags) {
model = [];
angular.forEach(
angular_data,
function(value, index) {
model.push({'id': value, 'text': value});
});
} else {
model = angular_data;
}
};
return model;
};
if (isSelect) {
// Use <select multiple> instead
delete opts.multiple;
delete opts.initSelection;
} else if (isMultiple) {
opts.multiple = true;
}
if (controller) {
// Watch the model for programmatic changes
scope.$watch(tAttrs.ngModel, function(current, old) {
if (!current) {
return;
}
if (current === old) {
return;
}
controller.$render();
}, true);
controller.$render = function () {
if (isSelect) {
elm.select2('val', controller.$viewValue);
} else {
if (opts.multiple) {
elm.select2(
'data', convertToSelect2Model(controller.$viewValue));
} else {
if (angular.isObject(controller.$viewValue)) {
elm.select2('data', controller.$viewValue);
} else if (!controller.$viewValue) {
elm.select2('data', null);
} else {
elm.select2('val', controller.$viewValue);
}
}
}
};
// Watch the options dataset for changes
if (watch) {
scope.$watch(watch, function (newVal, oldVal, scope) {
if (!newVal) {
return;
}
// Delayed so that the options have time to be rendered
$timeout(function () {
elm.select2('val', controller.$viewValue);
// Refresh angular to remove the superfluous option
elm.trigger('change');
});
});
}
// Update valid and dirty statuses
controller.$parsers.push(function (value) {
var div = elm.prev();
div
.toggleClass('ng-invalid', !controller.$valid)
.toggleClass('ng-valid', controller.$valid)
.toggleClass('ng-invalid-required', !controller.$valid)
.toggleClass('ng-valid-required', controller.$valid)
.toggleClass('ng-dirty', controller.$dirty)
.toggleClass('ng-pristine', controller.$pristine);
return value;
});
if (!isSelect) {
// Set the view and model value and update the angular template manually for the ajax/multiple select2.
elm.bind("change", function () {
if (scope.$$phase) {
return;
}
scope.$apply(function () {
controller.$setViewValue(
convertToAngularModel(elm.select2('data')));
});
});
if (opts.initSelection) {
var initSelection = opts.initSelection;
opts.initSelection = function (element, callback) {
initSelection(element, function (value) {
controller.$setViewValue(convertToAngularModel(value));
callback(value);
});
};
}
}
}
elm.bind("$destroy", function() {
elm.select2("destroy");
});
attrs.$observe('disabled', function (value) {
elm.select2('enable', !value);
});
attrs.$observe('readonly', function (value) {
elm.select2('readonly', !!value);
});
if (attrs.ngMultiple) {
scope.$watch(attrs.ngMultiple, function(newVal) {
elm.select2(opts);
});
}
// Initialize the plugin late so that the injected DOM does not disrupt the template compiler
$timeout(function () {
elm.select2(opts);
// Set initial value - I'm not sure about this but it seems to need to be there
elm.val(controller.$viewValue);
// important!
controller.$render();
// Not sure if I should just check for !isSelect OR if I should check for 'tags' key
if (!opts.initSelection && !isSelect) {
controller.$setViewValue(
convertToAngularModel(elm.select2('data'))
);
}
});
};
}
};
}]);
var app = angular.module('plunker', ['ui.select2']);
app.controller('MainCtrl', function($scope, $http) {
var items;
$scope.version1model = 'notset';
$scope.version2model = 'notset';
// $scope.version3model = 'notset';
$scope.version4model = 'notset';
$http.get('data.json').success(function(response){
// Version 1
$scope.items = response;
angular.extend($scope.version15.data, response);
angular.extend($scope.version1b.data, response);
angular.extend($scope.version1c.data, response);
// Multi
angular.extend($scope.multi15.data, response);
angular.extend($scope.multi1b.data, response);
angular.extend($scope.multi1c.data, response);
// Version 2
items = response;
// Version 3
angular.extend($scope.version3.data, response);
// $scope.version1model = items[0];
// $scope.version2model = items[1];
// $scope.version3model = {id:3, text: 'ehehe', lala:4};
// $scope.version4model = items[3];
});
$scope.version1 = {allowClear:true};
$scope.version15 = {allowClear:true, data:[]};
$scope.version1b = {allowClear:true, data:[], bindTo:'id',
reverseBinding: function(data){return {id:data, text:data}}};
$scope.version1c = {allowClear:true, data:[],
id:'text'};
$scope.multi1 = {allowClear:true, multiple:true};
$scope.multi15 = {allowClear:true, multiple:true, data:[]};
$scope.multi1b = {allowClear:true, multiple:true, data:[], bindTo:'id',
reverseBinding: function(data){return {id:data, text:data}}};
$scope.multi1c
$scope.updatemulti = function(){
$scope.multimodelStr
}
$scope.$watch('multimodelStr', function(newVal, oldVal){
$scope.multimodel = new
})
// Requires us to write comparison code ourselves :(
$scope.version2 = {
allowClear:true,
query: function (query) {
var data = {results: []};
angular.forEach(items, function(item, key){
if (query.term.toUpperCase() === item.text.substring(0, query.term.length).toUpperCase()) {
data.results.push(item);
}
});
query.callback(data);
}
};
$scope.version3 = {
allowClear:true,
data: []
};
// Simply updating an existing reference :) (refer to $http.get() above)
$scope.update3a = function(){
angular.extend($scope.version3.data, $scope.items.slice(0,2));
}
$scope.update3b = function(){
$scope.version3.data = $scope.items.slice(3,5);
// angular.extend($scope.version3.data, $scope.items.slice(3,5));
}
$scope.setselected3 = function(text_){
$scope.version3model = {id:text_, text:text_};
}
// Built-in support for ajax
$scope.version4 = {
allowClear:true,
ajax: {
url: "data.json",
data: function (term, page) {
return {}; // query params go here
},
results: function (data, page) { // parse the results into the format expected by Select2.
// since we are using custom formatting functions we do not need to alter remote JSON data
return {results: data};
}
}
}
});
[
{
"id" : 1,
"text" : "First"
},
{
"id" : 2,
"text" : "Second",
"color" : "red"
},
{
"id" : 3,
"text" : "Third",
"color" : "orange"
},
{
"id" : 4,
"text" : "Fourth",
"color" : "red"
},
{
"id" : 5,
"text" : "Fifth",
"color" : "pink"
},
{
"id" : 6,
"text" : "Sixth",
"color" : "yellow"
},
{
"id" : 7,
"text" : "Seventh",
"color" : "blue"
},
{
"id" : 8,
"text" : "Eighth",
"color" : "blue"
},
{
"id" : 9,
"text" : "Ninth",
"color" : "purple"
},
{
"id" : 10,
"text" : "Tenth",
"color" : "green"
}
]
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet" />
<link href="https://rawgithub.com/ivaynberg/select2/3.4.3/select2.css" rel="stylesheet" />
<!-- link rel="stylesheet" href="style.css" / -->
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script src="http://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js"></script>
<script src="https://rawgithub.com/ivaynberg/select2/3.4.3/select2.js"></script>
<script src="angular-ui.js"></script>
<!-- script src="https://rawgithub.com/angular-ui/ui-select2/master/src/select2.js"></script -->
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>
<a href="https://gist.github.com/4279651">Fork the Original Gist</a>
</p>
<h3>Single Value</h3>
<p>Use a regular select tag and ng-repeat
</p>
<pre>Selected: {{version1model|json}}</pre>
<select ui-select2="version1" ng-model="version1model"
data-placeholder="-- value=item.id --" style="width:200px">
<option></option>
<option ng-repeat="item in items" value="{{item.id}}">{{item.text}}</option>
</select>
<input ui-select2="version15" ng-model="version1model"
data-placeholder="-- select2 object --" style="width:200px" />
<p>
<input ui-select2="version1b" ng-model="version1model"
data-placeholder="-- bindTo:id --" style="width:200px" />
<input ui-select2="version1c" ng-model="version1model"
data-placeholder="-- id:text --" style="width:200px" />
</p>
<p>
<input ng-model="version1model" />
<button ng-click="update1()">update selected</button>
</p>
<h3>Multi Value</h3>
<pre>Selected: {{multimodel|json}}</pre>
<select ui-select2="multi1" ng-model="multimodel"
data-placeholder="-- value=item.id --" style="width:200px">
<option></option>
<option ng-repeat="item in items" value="{{item.id}}">{{item.text}}</option>
</select>
<input ui-select2="multi15" ng-model="multimodel"
data-placeholder="-- select2 object --" style="width:200px" />
<p>
<input ui-select2="multi1b" ng-model="multimodel"
data-placeholder="-- bindTo:id --" style="width:200px" />
<input ui-select2="multi1c" ng-model="multimodel"
data-placeholder="-- id:text --" style="width:200px" />
</p>
<p>
<input ng-model="multimodelStr" />
<button ng-click="updatemulti()">update selected</button>
</p>
<!--<h3>Version 2</h3>-->
<!--<p>Write your own Select2 query function</p>-->
<!--<pre>Selected: {{version2model|json}}</pre>-->
<!--<input ui-select2="version2" ng-model="version2model" style="width:200px" />-->
<!--<h3>Version 3</h3>-->
<!--<p>Use the Select2 data property, and preserve the reference</p>-->
<!--<pre>Selected: {{version3model|json}}</pre>-->
<!--<input ui-select2="version3" ng-model="version3model" style="width:200px" />-->
<!--<button ng-click="update3a()">update 3a</button>-->
<!--<button ng-click="update3b()">update 3b</button>-->
<!--<br />-->
<!--<input ng-model="selected3" />-->
<!--<button ng-click="setselected3(selected3)">set selected</button>-->
<!--<h3>Version 4</h3>-->
<!--<p>Use the Select2 ajax property</p>-->
<!--<pre>Selected: {{version4model|json}}</pre>-->
<!--<input ui-select2="version4" ng-model="version4model" style="width:200px" />-->
</body>
</html>
/**
* Enhanced Select2 Dropmenus
*
* @AJAX Mode - When in this mode, your value will be an object (or array of objects) of the data used by Select2
* This change is so that you do not have to do an additional query yourself on top of Select2's own query
* @params [options] {object} The configuration options passed to $.fn.select2(). Refer to the documentation
*/
angular.module('ui.select2.v2', []).value('uiSelect2Config', {}).directive('uiSelect2v2', ['uiSelect2Config', '$timeout', function (uiSelect2Config, $timeout) {
var options = {};
if (uiSelect2Config) {
angular.extend(options, uiSelect2Config);
}
return {
require: 'ngModel',
compile: function (tElm, tAttrs) {
var watch,
repeatOption,
repeatAttr,
isSelect = tElm.is('select'),
isMultiple = angular.isDefined(tAttrs.multiple);
// Enable watching of the options dataset if in use
if (tElm.is('select')) {
repeatOption = tElm.find('option[ng-repeat], option[data-ng-repeat]');
if (repeatOption.length) {
repeatAttr = repeatOption.attr('ng-repeat') || repeatOption.attr('data-ng-repeat');
watch = jQuery.trim(repeatAttr.split('|')[0]).split(' ').pop();
}
}
return function (scope, elm, attrs, controller) {
// instance-specific options
var opts = angular.extend({}, options, scope.$eval(attrs.uiSelect2));
/*
Convert from Select2 view-model to Angular view-model.
*/
var convertToAngularModel = function(select2_data) {
var model;
if (opts.simple_tags) {
model = [];
angular.forEach(select2_data, function(value, index) {
model.push(value.id);
});
} else if (opts.bindTo) {
console.log(opts.bindTo);
if (select2_data)
model = select2_data[opts.bindTo];
} else {
model = select2_data;
}
return model;
};
/*
Convert from Angular view-model to Select2 view-model.
*/
var convertToSelect2Model = function(angular_data) {
var model = [];
if (!angular_data) {
return model;
}
if (opts.simple_tags) {
model = [];
angular.forEach(
angular_data,
function(value, index) {
model.push({'id': value, 'text': value});
});
} else if (opts.bindTo) {
console.log(opts.bindTo);
model.push({'id': angular_data, 'text': angular_data})
} else {
model = angular_data;
}
return model;
};
if (isSelect) {
// Use <select multiple> instead
delete opts.multiple;
delete opts.initSelection;
} else if (isMultiple) {
opts.multiple = true;
}
if (controller) {
// Watch the model for programmatic changes
scope.$watch(tAttrs.ngModel, function(current, old) {
if (!current) {
return;
}
if (current === old) {
return;
}
controller.$render();
}, true);
controller.$render = function () {
if (isSelect) {
elm.select2('val', controller.$viewValue);
} else {
if (opts.multiple) {
elm.select2(
'data', convertToSelect2Model(controller.$viewValue));
} else {
if (angular.isObject(controller.$viewValue)) {
elm.select2('data', controller.$viewValue);
} else if (!controller.$viewValue) {
elm.select2('data', null);
} else {
elm.select2('val', controller.$viewValue);
}
}
}
};
// Watch the options dataset for changes
if (watch) {
scope.$watch(watch, function (newVal, oldVal, scope) {
if (!newVal) {
return;
}
// Delayed so that the options have time to be rendered
$timeout(function () {
elm.select2('val', controller.$viewValue);
// Refresh angular to remove the superfluous option
elm.trigger('change');
});
});
}
// Update valid and dirty statuses
controller.$parsers.push(function (value) {
var div = elm.prev();
div
.toggleClass('ng-invalid', !controller.$valid)
.toggleClass('ng-valid', controller.$valid)
.toggleClass('ng-invalid-required', !controller.$valid)
.toggleClass('ng-valid-required', controller.$valid)
.toggleClass('ng-dirty', controller.$dirty)
.toggleClass('ng-pristine', controller.$pristine);
return value;
});
if (!isSelect) {
// Set the view and model value and update the angular template manually for the ajax/multiple select2.
elm.bind("change", function () {
if (scope.$$phase) {
return;
}
scope.$apply(function () {
controller.$setViewValue(
convertToAngularModel(elm.select2('data')));
});
});
if (opts.initSelection) {
var initSelection = opts.initSelection;
opts.initSelection = function (element, callback) {
initSelection(element, function (value) {
controller.$setViewValue(convertToAngularModel(value));
callback(value);
});
};
}
}
}
elm.bind("$destroy", function() {
elm.select2("destroy");
});
attrs.$observe('disabled', function (value) {
elm.select2('enable', !value);
});
attrs.$observe('readonly', function (value) {
elm.select2('readonly', !!value);
});
if (attrs.ngMultiple) {
scope.$watch(attrs.ngMultiple, function(newVal) {
elm.select2(opts);
});
}
// Initialize the plugin late so that the injected DOM does not disrupt the template compiler
$timeout(function () {
elm.select2(opts);
// Set initial value - I'm not sure about this but it seems to need to be there
elm.val(controller.$viewValue);
// important!
controller.$render();
// Not sure if I should just check for !isSelect OR if I should check for 'tags' key
if (!opts.initSelection && !isSelect) {
controller.$setViewValue(
convertToAngularModel(elm.select2('data'))
);
}
});
};
}
};
}]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment