From d780d07603b9ca90074eeb402b98c310bfe89b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Wollse=CC=81n?= Date: Tue, 1 Sep 2015 22:24:58 +0300 Subject: [PATCH 1/9] Stripped down angular-select2 to the bare minimum. Supports select2 4.0 in the case where ajax search is not required --- src/select2.js | 231 +++++-------------------------------------------- 1 file changed, 20 insertions(+), 211 deletions(-) diff --git a/src/select2.js b/src/select2.js index 1e3d200..f7fa27d 100644 --- a/src/select2.js +++ b/src/select2.js @@ -23,21 +23,7 @@ angular.module("rt.select2", []) .directive("select2", function ($rootScope, $timeout, $parse, $filter, select2Config, select2Stack) { "use strict"; - var filter = $filter("filter"); - - function sortedKeys(obj) { - var keys = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - keys.push(key); - } - } - return keys.sort(); - } - var defaultOptions = {}; - //0000111110000000000022220000000000000000000000333300000000000000444444444444444000000000555555555555555000000066666666666666600000000000000007777000000000000000000088888 - var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/; if (select2Config) { angular.extend(defaultOptions, select2Config); @@ -47,11 +33,10 @@ angular.module("rt.select2", []) require: "ngModel", priority: 1, restrict: "E", - template: "", + transclude: true, // transclusion instructs angular to embed the original content from the DOM into the resultant output + template: "", replace: true, link: function (scope, element, attrs, controller) { - var getOptions; - var opts = angular.extend({}, defaultOptions, scope.$eval(attrs.options)); var isMultiple = angular.isDefined(attrs.multiple) || opts.multiple; @@ -61,178 +46,6 @@ angular.module("rt.select2", []) opts.placeholder = attrs.placeholder; } - var filterOptions = $parse(attrs.optionsFilter); - - // All values returned from Select2 are strings. This is a - // problem if you supply integer indexes: they'll become - // strings once passing through this directive. We keep a - // mapping between string keys and values through the - // optionItems object, to be able to return the correctly typed - // value. - var optionItems = {}; - - function filterValues(values) { - if (filterOptions) { - var filterParams = filterOptions(scope); - if (filterParams) { - return filter(values, filterParams); - } - } - - return values; - } - - if (attrs.s2Options) { - var match; - if (!(match = attrs.s2Options.match(NG_OPTIONS_REGEXP))) { - throw new Error("Invalid s2Options encountered!"); - } - - var displayFn = $parse(match[2] || match[1]); - var valuesFn = $parse(match[7]); - var valueName = match[4] || match[6]; - var valueFn = $parse(match[2] ? match[1] : valueName); - var keyName = match[5]; - - getOptions = function (callback) { - optionItems = {}; - var values = filterValues(valuesFn(scope)); - var keys = (keyName ? sortedKeys(values) : values) || []; - - var options = []; - for (var i = 0; i < keys.length; i++) { - var locals = {}; - var key = i; - if (keyName) { - key = keys[i]; - locals[keyName] = key; - } - locals[valueName] = values[key]; - - var value = valueFn(scope, locals); - var label = displayFn(scope, locals) || ""; - - // Select2 returns strings, we use a dictionary to get - // back to the original value. - optionItems[value] = { - id: value, - text: label, - obj: values[key] - }; - - options.push(optionItems[value]); - } - - callback(options); - }; - - opts.query = function (query) { - var values = filterValues(valuesFn(scope)); - var keys = (keyName ? sortedKeys(values) : values) || []; - - var options = []; - for (var i = 0; i < keys.length; i++) { - var locals = {}; - var key = i; - if (keyName) { - key = keys[i]; - locals[keyName] = key; - } - locals[valueName] = values[key]; - - var value = valueFn(scope, locals); - var label = displayFn(scope, locals) || ""; - - if (label.toLowerCase().indexOf(query.term.toLowerCase()) > -1) { - options.push({ - id: value, - text: label, - obj: values[key] - }); - } - } - - query.callback({ - results: options - }); - }; - - // Make sure changes to the options get filled in - scope.$watch(match[7], function () { - controller.$render(); - }); - } else { - if (!opts.query) { - throw new Error("You need to supply a query function!"); - } - - var queryFn = opts.query; - opts.query = function (query) { - var cb = query.callback; - query.callback = function (data) { - for (var i = 0; i < data.results.length; i++) { - var result = data.results[i]; - optionItems[result.id] = result; - } - cb(data); - }; - queryFn(query); - }; - - getOptions = function (callback) { - opts.query({ - term: "", - callback: function (query) { - callback(query.results); - } - }); - }; - } - - function getSelection(callback) { - if (isMultiple) { - getOptions(function (options) { - var selection = []; - for (var i = 0; i < options.length; i++) { - var option = options[i]; - var viewValue = controller.$viewValue || []; - if (viewValue.indexOf(option.id) > -1) { - selection.push(option); - } - } - callback(selection); - }); - } else { - getOptions(function () { - callback(optionItems[controller.$viewValue] || { obj: {} }); - }); - } - } - - controller.$render = function () { - getSelection(function (selection) { - if (isMultiple) { - element.select2("data", selection); - } else { - element.select2("val", selection.id); - } - }); - }; - - if (!opts.initSelection) { - opts.initSelection = function (element, callback) { - getSelection(callback); - }; - } else { - var _initSelection = opts.initSelection; - opts.initSelection = function (element, callback) { - _initSelection(element, function (result) { - optionItems[result.id] = result; - callback(result); - }); - }; - } - // register with the select2stack var controlObj = { close: function () { @@ -244,29 +57,9 @@ angular.module("rt.select2", []) select2Stack.$unregister(controlObj); }); + // initiate select2 $timeout(function () { element.select2(opts); - element.on("change", function (e) { - scope.$apply(function () { - var val; - if (isMultiple) { - var vals = []; - for (var i = 0; i < e.val.length; i++) { - val = optionItems[e.val[i]]; - if (val) { - vals.push(val.id); - } - } - controller.$setViewValue(vals); - } else { - val = optionItems[e.val]; - controller.$setViewValue(val ? val.id : null); - } - - controller.$render(); - - }); - }); element.on("select2-blur", function () { if (controller.$touched) { @@ -276,8 +69,24 @@ angular.module("rt.select2", []) scope.$apply(controller.$setTouched); }); - controller.$render(); }); + + // Make sure that changes to the value is reflected in the select2 input + scope.$watch( + function () { + return controller.$viewValue + }, + function (newVal, oldVal) { + if (newVal === oldVal) { + return; + } + $timeout(function () { + element.val(newVal).trigger("change"); + //element.trigger("change"); + }); + } + ); + } }; }); From 1ea93d1d051f97d16bbb90593dc4c65e058dbc8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Wollse=CC=81n?= Date: Wed, 2 Sep 2015 08:16:39 +0300 Subject: [PATCH 2/9] Updated documentation to match select2 4.0 syntax --- README.md | 59 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 40dfab5..7e011cc 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,19 @@ angular.module('myApp', ['rt.select2']); ``` ## Usage -Usage a similar to a normal select with `ngOptions`: +Usage similar to a normal select: ```html - + + + + ``` -*Note: using `ng-options` was supported until Angular 1.4 made this impossible. When upgrading to Angular.JS 1.4, be sure to replace all instances of `ng-options` to `s2-options`.* - In fact, you can replace any `", + transclude: true, // transclusion instructs angular to embed the original content from the DOM into the resultant output + template: "", replace: true, link: function (scope, element, attrs, controller) { - var getOptions; - var opts = angular.extend({}, defaultOptions, scope.$eval(attrs.options)); var isMultiple = angular.isDefined(attrs.multiple) || opts.multiple; @@ -61,178 +46,6 @@ angular.module("rt.select2", []) opts.placeholder = attrs.placeholder; } - var filterOptions = $parse(attrs.optionsFilter); - - // All values returned from Select2 are strings. This is a - // problem if you supply integer indexes: they'll become - // strings once passing through this directive. We keep a - // mapping between string keys and values through the - // optionItems object, to be able to return the correctly typed - // value. - var optionItems = {}; - - function filterValues(values) { - if (filterOptions) { - var filterParams = filterOptions(scope); - if (filterParams) { - return filter(values, filterParams); - } - } - - return values; - } - - if (attrs.s2Options) { - var match; - if (!(match = attrs.s2Options.match(NG_OPTIONS_REGEXP))) { - throw new Error("Invalid s2Options encountered!"); - } - - var displayFn = $parse(match[2] || match[1]); - var valuesFn = $parse(match[7]); - var valueName = match[4] || match[6]; - var valueFn = $parse(match[2] ? match[1] : valueName); - var keyName = match[5]; - - getOptions = function (callback) { - optionItems = {}; - var values = filterValues(valuesFn(scope)); - var keys = (keyName ? sortedKeys(values) : values) || []; - - var options = []; - for (var i = 0; i < keys.length; i++) { - var locals = {}; - var key = i; - if (keyName) { - key = keys[i]; - locals[keyName] = key; - } - locals[valueName] = values[key]; - - var value = valueFn(scope, locals); - var label = displayFn(scope, locals) || ""; - - // Select2 returns strings, we use a dictionary to get - // back to the original value. - optionItems[value] = { - id: value, - text: label, - obj: values[key] - }; - - options.push(optionItems[value]); - } - - callback(options); - }; - - opts.query = function (query) { - var values = filterValues(valuesFn(scope)); - var keys = (keyName ? sortedKeys(values) : values) || []; - - var options = []; - for (var i = 0; i < keys.length; i++) { - var locals = {}; - var key = i; - if (keyName) { - key = keys[i]; - locals[keyName] = key; - } - locals[valueName] = values[key]; - - var value = valueFn(scope, locals); - var label = displayFn(scope, locals) || ""; - - if (label.toLowerCase().indexOf(query.term.toLowerCase()) > -1) { - options.push({ - id: value, - text: label, - obj: values[key] - }); - } - } - - query.callback({ - results: options - }); - }; - - // Make sure changes to the options get filled in - scope.$watch(match[7], function () { - controller.$render(); - }); - } else { - if (!opts.query) { - throw new Error("You need to supply a query function!"); - } - - var queryFn = opts.query; - opts.query = function (query) { - var cb = query.callback; - query.callback = function (data) { - for (var i = 0; i < data.results.length; i++) { - var result = data.results[i]; - optionItems[result.id] = result; - } - cb(data); - }; - queryFn(query); - }; - - getOptions = function (callback) { - opts.query({ - term: "", - callback: function (query) { - callback(query.results); - } - }); - }; - } - - function getSelection(callback) { - if (isMultiple) { - getOptions(function (options) { - var selection = []; - for (var i = 0; i < options.length; i++) { - var option = options[i]; - var viewValue = controller.$viewValue || []; - if (viewValue.indexOf(option.id) > -1) { - selection.push(option); - } - } - callback(selection); - }); - } else { - getOptions(function () { - callback(optionItems[controller.$viewValue] || { obj: {} }); - }); - } - } - - controller.$render = function () { - getSelection(function (selection) { - if (isMultiple) { - element.select2("data", selection); - } else { - element.select2("val", selection.id); - } - }); - }; - - if (!opts.initSelection) { - opts.initSelection = function (element, callback) { - getSelection(callback); - }; - } else { - var _initSelection = opts.initSelection; - opts.initSelection = function (element, callback) { - _initSelection(element, function (result) { - optionItems[result.id] = result; - callback(result); - }); - }; - } - // register with the select2stack var controlObj = { close: function () { @@ -244,29 +57,9 @@ angular.module("rt.select2", []) select2Stack.$unregister(controlObj); }); + // initiate select2 $timeout(function () { element.select2(opts); - element.on("change", function (e) { - scope.$apply(function () { - var val; - if (isMultiple) { - var vals = []; - for (var i = 0; i < e.val.length; i++) { - val = optionItems[e.val[i]]; - if (val) { - vals.push(val.id); - } - } - controller.$setViewValue(vals); - } else { - val = optionItems[e.val]; - controller.$setViewValue(val ? val.id : null); - } - - controller.$render(); - - }); - }); element.on("select2-blur", function () { if (controller.$touched) { @@ -276,8 +69,23 @@ angular.module("rt.select2", []) scope.$apply(controller.$setTouched); }); - controller.$render(); }); + + // Make sure that changes to the value is reflected in the select2 input + scope.$watch( + function () { + return controller.$viewValue; + }, + function (newVal, oldVal) { + if (newVal === oldVal) { + return; + } + $timeout(function () { + element.val(newVal).trigger("change"); + }); + } + ); + } }; }]); diff --git a/dist/angular-select2.min.js b/dist/angular-select2.min.js index ae4ba2b..70a6d8c 100644 --- a/dist/angular-select2.min.js +++ b/dist/angular-select2.min.js @@ -1 +1 @@ -angular.module("rt.select2",[]).value("select2Config",{}).factory("select2Stack",function(){var a=[];return{$register:function(b){a.push(b)},$unregister:function(b){var c=a.indexOf(b);-1!==c&&a.splice(c,1)},closeAll:function(){a.forEach(function(a){a.close()})}}}).directive("select2",["$rootScope","$timeout","$parse","$filter","select2Config","select2Stack",function(a,b,c,d,e,f){"use strict";function g(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c);return b.sort()}var h=d("filter"),i={},j=/^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/;return e&&angular.extend(i,e),{require:"ngModel",priority:1,restrict:"E",template:'',replace:!0,link:function(a,d,e,k){function l(b){if(q){var c=q(a);if(c)return h(b,c)}return b}function m(a){n(p?function(b){for(var c=[],d=0;d-1&&c.push(e)}a(c)}:function(){a(r[k.$viewValue]||{obj:{}})})}var n,o=angular.extend({},i,a.$eval(e.options)),p=angular.isDefined(e.multiple)||o.multiple;o.multiple=p,e.placeholder&&(o.placeholder=e.placeholder);var q=c(e.optionsFilter),r={};if(e.s2Options){var s;if(!(s=e.s2Options.match(j)))throw new Error("Invalid s2Options encountered!");var t=c(s[2]||s[1]),u=c(s[7]),v=s[4]||s[6],w=c(s[2]?s[1]:v),x=s[5];n=function(b){r={};for(var c=l(u(a)),d=(x?g(c):c)||[],e=[],f=0;f-1&&e.push({id:j,text:k,obj:c[i]})}b.callback({results:e})},a.$watch(s[7],function(){k.$render()})}else{if(!o.query)throw new Error("You need to supply a query function!");var y=o.query;o.query=function(a){var b=a.callback;a.callback=function(a){for(var c=0;c",replace:!0,link:function(a,c,d,e){var h=angular.extend({},g,a.$eval(d.options)),i=angular.isDefined(d.multiple)||h.multiple;h.multiple=i,d.placeholder&&(h.placeholder=d.placeholder);var j={close:function(){c.select2("close")}};f.$register(j),a.$on("destroy",function(){f.$unregister(j)}),b(function(){c.select2(h),c.on("select2-blur",function(){e.$touched||a.$apply(e.$setTouched)})}),a.$watch(function(){return e.$viewValue},function(a,d){a!==d&&b(function(){c.val(a).trigger("change")})})}}}]); \ No newline at end of file From 0d7e647df43254a5bab792a8689b50c03a05c3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Wollse=CC=81n?= Date: Sun, 20 Sep 2015 01:02:59 +0100 Subject: [PATCH 8/9] Bug fixes and updated documentation --- README.md | 23 +++++++++++++++-------- dist/angular-select2.js | 30 ++++++++++++++---------------- dist/angular-select2.min.js | 2 +- src/select2.js | 30 ++++++++++++++---------------- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 2546909..25e8f70 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,13 @@ angular.module('myApp', ['rt.select2']); ## Usage -A JS Bin demo showing working usage examples are available [here](https://jsbin.com/gipezidemi/edit?html,js,output). +A JS Bin demo showing working usage examples are available [here](https://jsbin.com/fifaqu/edit?html,js,output). Usage similar to a normal select: ```html - - + + ``` @@ -57,6 +53,15 @@ angular.module('myApp').controller('MyAsyncController', function ($scope) { /* ... omitted for brevity, see the JS bin ... */ + $scope.selected = "3620194"; + + $scope.values = [ + { + id: "3620194", + name: "select2/select2" + } + ]; + $scope.queryOptions = { ajax: { url: "https://api.github.com/search/repositories", @@ -87,7 +92,9 @@ angular.module('myApp').controller('MyAsyncController', function ($scope) { ``` ```html - + + + ``` ## Custom formatting, restrictions, tokenization, ... diff --git a/dist/angular-select2.js b/dist/angular-select2.js index c071657..d4bedc3 100644 --- a/dist/angular-select2.js +++ b/dist/angular-select2.js @@ -31,13 +31,17 @@ angular.module("rt.select2", []) return { require: "ngModel", + scope: { + ngModel: "=", + options: "=" + }, priority: 1, restrict: "E", transclude: true, // transclusion instructs angular to embed the original content from the DOM into the resultant output template: "", replace: true, link: function (scope, element, attrs, controller) { - var opts = angular.extend({}, defaultOptions, scope.$eval(attrs.options)); + var opts = angular.extend({}, defaultOptions, scope.options); var isMultiple = angular.isDefined(attrs.multiple) || opts.multiple; opts.multiple = isMultiple; @@ -65,26 +69,20 @@ angular.module("rt.select2", []) if (controller.$touched) { return; } - - scope.$apply(controller.$setTouched); + controller.$setTouched(); }); }); - // Make sure that changes to the value is reflected in the select2 input - scope.$watch( - function () { - return controller.$viewValue; - }, - function (newVal, oldVal) { - if (newVal === oldVal) { - return; - } - $timeout(function () { - element.val(newVal).trigger("change"); - }); + // make sure that changes to the value is reflected in the select2 input + scope.$watch("ngModel", function (newVal, oldVal) { + if (newVal === oldVal) { + return; } - ); + $timeout(function () { + element.trigger("change"); + }); + }); } }; diff --git a/dist/angular-select2.min.js b/dist/angular-select2.min.js index 70a6d8c..61b55f4 100644 --- a/dist/angular-select2.min.js +++ b/dist/angular-select2.min.js @@ -1 +1 @@ -angular.module("rt.select2",[]).value("select2Config",{}).factory("select2Stack",function(){var a=[];return{$register:function(b){a.push(b)},$unregister:function(b){var c=a.indexOf(b);-1!==c&&a.splice(c,1)},closeAll:function(){a.forEach(function(a){a.close()})}}}).directive("select2",["$rootScope","$timeout","$parse","$filter","select2Config","select2Stack",function(a,b,c,d,e,f){"use strict";var g={};return e&&angular.extend(g,e),{require:"ngModel",priority:1,restrict:"E",transclude:!0,template:"",replace:!0,link:function(a,c,d,e){var h=angular.extend({},g,a.$eval(d.options)),i=angular.isDefined(d.multiple)||h.multiple;h.multiple=i,d.placeholder&&(h.placeholder=d.placeholder);var j={close:function(){c.select2("close")}};f.$register(j),a.$on("destroy",function(){f.$unregister(j)}),b(function(){c.select2(h),c.on("select2-blur",function(){e.$touched||a.$apply(e.$setTouched)})}),a.$watch(function(){return e.$viewValue},function(a,d){a!==d&&b(function(){c.val(a).trigger("change")})})}}}]); \ No newline at end of file +angular.module("rt.select2",[]).value("select2Config",{}).factory("select2Stack",function(){var a=[];return{$register:function(b){a.push(b)},$unregister:function(b){var c=a.indexOf(b);-1!==c&&a.splice(c,1)},closeAll:function(){a.forEach(function(a){a.close()})}}}).directive("select2",["$rootScope","$timeout","$parse","$filter","select2Config","select2Stack",function(a,b,c,d,e,f){"use strict";var g={};return e&&angular.extend(g,e),{require:"ngModel",scope:{ngModel:"=",options:"="},priority:1,restrict:"E",transclude:!0,template:"",replace:!0,link:function(a,c,d,e){var h=angular.extend({},g,a.options),i=angular.isDefined(d.multiple)||h.multiple;h.multiple=i,d.placeholder&&(h.placeholder=d.placeholder);var j={close:function(){c.select2("close")}};f.$register(j),a.$on("destroy",function(){f.$unregister(j)}),b(function(){c.select2(h),c.on("select2-blur",function(){e.$touched||e.$setTouched()})}),a.$watch("ngModel",function(a,d){a!==d&&b(function(){c.trigger("change")})})}}}]); \ No newline at end of file diff --git a/src/select2.js b/src/select2.js index 942d781..5d2417f 100644 --- a/src/select2.js +++ b/src/select2.js @@ -31,13 +31,17 @@ angular.module("rt.select2", []) return { require: "ngModel", + scope: { + ngModel: "=", + options: "=" + }, priority: 1, restrict: "E", transclude: true, // transclusion instructs angular to embed the original content from the DOM into the resultant output template: "", replace: true, link: function (scope, element, attrs, controller) { - var opts = angular.extend({}, defaultOptions, scope.$eval(attrs.options)); + var opts = angular.extend({}, defaultOptions, scope.options); var isMultiple = angular.isDefined(attrs.multiple) || opts.multiple; opts.multiple = isMultiple; @@ -65,26 +69,20 @@ angular.module("rt.select2", []) if (controller.$touched) { return; } - - scope.$apply(controller.$setTouched); + controller.$setTouched(); }); }); - // Make sure that changes to the value is reflected in the select2 input - scope.$watch( - function () { - return controller.$viewValue; - }, - function (newVal, oldVal) { - if (newVal === oldVal) { - return; - } - $timeout(function () { - element.val(newVal).trigger("change"); - }); + // make sure that changes to the value is reflected in the select2 input + scope.$watch("ngModel", function (newVal, oldVal) { + if (newVal === oldVal) { + return; } - ); + $timeout(function () { + element.trigger("change"); + }); + }); } }; From d7e28335d3ccdf54efb7a4e50e69304e9fd5dfbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Wollse=CC=81n?= Date: Sun, 20 Sep 2015 23:39:48 +0100 Subject: [PATCH 9/9] Updated readme code examples to match the latest working jsbin --- README.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 25e8f70..c795a84 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,7 @@ A JS Bin demo showing working usage examples are available [here](https://jsbin. Usage similar to a normal select: ```html - - + ``` @@ -55,13 +54,6 @@ angular.module('myApp').controller('MyAsyncController', function ($scope) { $scope.selected = "3620194"; - $scope.values = [ - { - id: "3620194", - name: "select2/select2" - } - ]; - $scope.queryOptions = { ajax: { url: "https://api.github.com/search/repositories", @@ -92,8 +84,9 @@ angular.module('myApp').controller('MyAsyncController', function ($scope) { ``` ```html - - + + + ```