From 4082e98433e9d05dcd09c12e4e32748099e168fe Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Sat, 22 Feb 2014 03:05:42 -0600 Subject: [PATCH 1/6] Stabilize specs, lock jquery and select2 versions --- bower.json | 7 ++++--- test/karma.conf.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bower.json b/bower.json index 2cf5041..2783791 100644 --- a/bower.json +++ b/bower.json @@ -11,10 +11,11 @@ "main": "./src/select2.js", "dependencies": { "angular": ">=1.2.0", - "select2": "~3.4", - "jquery": ">=1.6.4" + "select2": ">=3.4.0", + "jquery": ">=1.11.0" }, "devDependencies": { - "angular-mocks": ">=1.0.2" + "angular-mocks": ">=1.2.0", + "bootstrap": ">=3.1.1" } } diff --git a/test/karma.conf.js b/test/karma.conf.js index b5658bc..d5f21b3 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -15,7 +15,7 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ // Dependencies - 'bower_components/jquery/jquery.js', + 'bower_components/jquery/dist/jquery.js', 'bower_components/angular/angular.js', 'bower_components/angular-mocks/angular-mocks.js', 'bower_components/select2/select2.js', From cb9e472b53d3c3feae17c2f4c744f449b34212e1 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Sat, 22 Feb 2014 03:54:14 -0600 Subject: [PATCH 2/6] Run tests with `npm test` and `grunt test` --- Gruntfile.js | 1 + package.json | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index f214842..03b22c7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -54,6 +54,7 @@ module.exports = function (grunt) { // Register tasks grunt.registerTask('default', ['jshint', 'karma:unit']); grunt.registerTask('watch', ['jshint', 'karma:watch']); + grunt.registerTask('test', ['karma:unit']); grunt.initConfig(initConfig); }; diff --git a/package.json b/package.json index 9b83136..0e59852 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,8 @@ "karma": "~0.10.2", "grunt-conventional-changelog": "~1.0.0", "load-grunt-tasks": "~0.2.0" + }, + "scripts": { + "test": "grunt test" } } From 96353a9f0286d5330189c98272ada49e27cbb025 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Sat, 22 Feb 2014 04:14:45 -0600 Subject: [PATCH 3/6] Use `isSelect` computed local --- src/select2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/select2.js b/src/select2.js index 828032d..0caa142 100644 --- a/src/select2.js +++ b/src/select2.js @@ -21,7 +21,7 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec isMultiple = angular.isDefined(tAttrs.multiple); // Enable watching of the options dataset if in use - if (tElm.is('select')) { + if (isSelect) { repeatOption = tElm.find( 'optgroup[ng-repeat], optgroup[data-ng-repeat], option[ng-repeat], option[data-ng-repeat]'); if (repeatOption.length) { From 0d67c9f9cc044078d2897b28fe02223921ace57e Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Sat, 22 Feb 2014 04:29:45 -0600 Subject: [PATCH 4/6] Transclude content into select2 --- src/select2.js | 62 +++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/select2.js b/src/select2.js index 0caa142..43b5dbb 100644 --- a/src/select2.js +++ b/src/select2.js @@ -13,24 +13,14 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec return { require: 'ngModel', priority: 1, - compile: function (tElm, tAttrs) { - var watch, - repeatOption, - repeatAttr, - isSelect = tElm.is('select'), + transclude: true, + compile: function (tElm, tAttrs, transclude) { + var isSelect = tElm.is('select'), isMultiple = angular.isDefined(tAttrs.multiple); - // Enable watching of the options dataset if in use - if (isSelect) { - repeatOption = tElm.find( 'optgroup[ng-repeat], optgroup[data-ng-repeat], 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) { + elm.append(transclude(scope)); + // instance-specific options var opts = angular.extend({}, options, scope.$eval(attrs.uiSelect2)); @@ -114,23 +104,37 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec } }; - // Watch the options dataset for changes - if (watch) { - scope.$watch(watch, function (newVal, oldVal, scope) { - if (angular.equals(newVal, oldVal)) { - return; + $timeout(function () { + var watch, repeatOption, repeatAttr; + + // Enable watching of the options dataset if in use + if (isSelect) { + repeatOption = elm.find( 'optgroup[ng-repeat], optgroup[data-ng-repeat], 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(); } - // 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'); - if(newVal && !oldVal && controller.$setPristine) { - controller.$setPristine(true); + } + + // Watch the options dataset for changes + if (watch) { + scope.$watch(watch, function (newVal, oldVal, scope) { + if (angular.equals(newVal, oldVal)) { + 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'); + if (newVal && !oldVal && controller.$setPristine) { + controller.$setPristine(true); + } + }); }); - }); - } + } + }); // Update valid and dirty statuses controller.$parsers.push(function (value) { From 5613ca5a3999eb3bf090690d58f07ba7880e6fc3 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Sat, 22 Feb 2014 13:00:19 -0600 Subject: [PATCH 5/6] Add support for declarative formatting blocks --- demo/index.html | 194 ++++++++++++++++++++++++++++++++++++++++++++ src/select2.js | 59 +++++++++++++- test/select2Spec.js | 125 ++++++++++++++++++++++++++++ 3 files changed, 376 insertions(+), 2 deletions(-) diff --git a/demo/index.html b/demo/index.html index dc01961..85aaaaf 100644 --- a/demo/index.html +++ b/demo/index.html @@ -220,6 +220,200 @@

Value

+
+

Custom Element

+
+
+

+ The ui-select2 directive can also be used as an element to allow for declarative configuration of some of select2's behaviors. +

+

Value

+
{{ customElementValue }}
+
+
+
<ui-select2 style="width:300px" options="customElementConfig" ng-model="customElementValue">
+</ui-select2>
+ + +
+
+ +
+
+ +

+ Results within the dropdown can be displayed with custom markup. +

+
+
+
<ui-select2 style="width:300px" options="customElementConfig" ng-model="customElementValue">
+    <format-result>
+        <strong>{{ id }}</strong>
+        <i>{{ text }}</i>
+    </format-result>
+</ui-select2>
+ + + + {{ id }} + {{ text }} + + + +
+
+ +
+
+ +

+ Selected results within the control can be displayed with custom markup. +

+
+
+
<ui-select2 style="width:300px" options="customElementConfig" ng-model="customElementValue">
+    <format-selection>
+        <strong>{{ id }}</strong>
+        <i>{{ text }}</i>
+    </format-selection>
+</ui-select2>
+ + + + {{ id }} + {{ text }} + + + +
+
+ +
+
+ +

+ The message shown when no results match the query can be displayed with custom markup. The template will be rendered with one local. +

    +
  • input (String) the current search term
  • +
+

+
+
+
<ui-select2 style="width:300px" options="customElementConfig" ng-model="customElementValue">
+    <format-no-matches>
+        <svg style="width: 16px; height: 16px">
+            <circle fill="red" stroke-width="10" cx="8" cy="10" r="6" />
+        </svg>
+        No matches found for term
+        <i>{{ input }}</i>
+    </format-no-matches>
+</ui-select2>
+ + + + + + No matches found for input + {{ input }} + + +
+
+ +
+
+ +

+ The message shown when retrieving query results asynchronously can be displayed with custom markup. +

+
+
+
<ui-select2 style="width:300px" options="customElementConfig" ng-model="customElementValue">
+    <format-searching>
+        The
+        <strong>SEARCH</strong>
+        continues.
+    </format-searching>
+</ui-select2>
+ + + + The + SEARCH + continues. + + + +
+
+ + +
+
+ +

+ The message shown when the search term entered is too short can be displayed with custom markup. The template will be rendered with two locals. +

    +
  • input (String) the current search term
  • +
  • minimumInputLength (Number) the configured minimum input length
  • +
+

+
+
+
<ui-select2 style="width:300px" options="customElementConfig" ng-model="customElementValue">
+    <format-input-too-short>
+        Thy Input
+        <strong>{{ input }}</strong>
+        Be Too Short By
+        <i>{{ minimumInputLength - input.length }}</i>
+    </format-input-too-short>
+</ui-select2>
+ + + + Thy Input + {{ term }} + Be Too Short By + {{ minimumInputLength - input.length }} + + + +
+
+ + +
+
+ +

+ The messaging shown when attempting to add another selection to an already-full multi-select can be displayed with custom markup. The template will be rendered with one local. +

    +
  • maximumSelectionSize (Number) the configured maximum number of selected values
  • +
+

+
+
+
<ui-select2 style="width:300px" options="customElementConfig" ng-model="customElementValue">
+    <format-selection-too-big>
+        Surely,
+        <strong>{{ maximumSelectionSize }}</strong>
+        is sufficient.
+    </format-selection-too-big>
+</ui-select2>
+ + + + Surely, + {{ maximumSelectionSize }} + is sufficient. + + + +
+
+ +
+ diff --git a/src/select2.js b/src/select2.js index 43b5dbb..8670539 100644 --- a/src/select2.js +++ b/src/select2.js @@ -5,7 +5,7 @@ * 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) { +angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelect2', ['uiSelect2Config', '$timeout', '$interpolate', function (uiSelect2Config, $timeout, $interpolate) { var options = {}; if (uiSelect2Config) { angular.extend(options, uiSelect2Config); @@ -13,16 +13,21 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec return { require: 'ngModel', priority: 1, + restrict: 'AE', transclude: true, compile: function (tElm, tAttrs, transclude) { var isSelect = tElm.is('select'), isMultiple = angular.isDefined(tAttrs.multiple); return function (scope, elm, attrs, controller) { + if (elm.is('ui-select2')) { + elm.append(''); + } + elm.append(transclude(scope)); // instance-specific options - var opts = angular.extend({}, options, scope.$eval(attrs.uiSelect2)); + var opts = angular.extend({}, options, scope.$eval(attrs.uiSelect2 || attrs.options)); /* Convert from Select2 view-model to Angular view-model. @@ -81,6 +86,7 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec } controller.$render(); }, true); + controller.$render = function () { if (isSelect) { elm.select2('val', controller.$viewValue); @@ -180,6 +186,55 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec } } + var formatResultElm = elm.find('format-result').remove(), + formatSelectionElm = elm.find('format-selection').remove(), + formatNoMatchesElm = elm.find('format-no-matches').remove(), + formatSearchingElm = elm.find('format-searching').remove(), + formatInputTooShortElm = elm.find('format-input-too-short').remove(), + formatSelectionTooBigElm = elm.find('format-selection-too-big').remove(); + + if (formatResultElm.length) { + formatResultElm = formatResultElm[0].outerHTML; + opts.formatResult = function (model, eh___someLabel, query, escape) { + return $interpolate(formatResultElm)(model); + }; + } + + if (formatSelectionElm.length) { + formatSelectionElm = formatSelectionElm[0].outerHTML; + opts.formatSelection = function (model, eh___someLabel, escape) { + return $interpolate(formatSelectionElm)(model); + }; + } + + if (formatNoMatchesElm.length) { + formatNoMatchesElm = formatNoMatchesElm[0].outerHTML; + opts.formatNoMatches = function (input) { + return $interpolate(formatNoMatchesElm)({ input: input }); + }; + } + + if (formatSearchingElm.length) { + formatSearchingElm = formatSearchingElm[0].outerHTML; + opts.formatSearching = function () { + return $interpolate(formatSearchingElm)({}); + }; + } + + if (formatInputTooShortElm.length) { + formatInputTooShortElm = formatInputTooShortElm[0].outerHTML; + opts.formatInputTooShort = function (input, minimumInputLength) { + return $interpolate(formatInputTooShortElm)({ input: input, minimumInputLength: minimumInputLength }); + }; + } + + if (formatSelectionTooBigElm.length) { + formatSelectionTooBigElm = formatSelectionTooBigElm[0].outerHTML; + opts.formatSelectionTooBig = function (maximumSelectionSize) { + return $interpolate(formatSelectionTooBigElm)({ maximumSelectionSize: maximumSelectionSize }); + }; + } + elm.bind("$destroy", function() { elm.select2("destroy"); }); diff --git a/test/select2Spec.js b/test/select2Spec.js index 8c5c27d..12443d0 100644 --- a/test/select2Spec.js +++ b/test/select2Spec.js @@ -189,6 +189,14 @@ describe('uiSelect2', function () { }); describe('with an element', function () { describe('compiling this directive', function () { + function configFromSpy (spy) { + for (var i = 0, len = spy.callCount; i < len; i++) { + if (spy.argsForCall[i][0].query) { + return spy.argsForCall[i][0]; + } + } + } + it('should throw an error if we have no model defined', function () { expect(function() { compile(''); @@ -201,6 +209,123 @@ describe('uiSelect2', function () { it('should not modify the model if there is no initial value', function(){ //TODO }); + it('should compile to input as element', function () { + var element = compile(''); + expect(element.children().is('input[type=hidden]')).toBe(true); + }); + it('should compile format-result', function () { + spyOn($.fn, 'select2'); + + var element = compile( + '' + + '' + + '{{text}}' + + '' + + ''); + + // Find the `select2` call made with the configuration object + var fn = configFromSpy(element.select2).formatResult; + expect(fn({ text: 'Congratulations' })) + .toBe('Congratulations'); + + // format-result block is removed + expect(element.find('format-result').length).toBe(0); + }); + it('should compile format-selection', function () { + spyOn($.fn, 'select2'); + + var element = compile( + '' + + '' + + '{{text}}' + + '' + + ''); + + // Find the `select2` call made with the configuration object + var fn = configFromSpy(element.select2).formatSelection; + expect(fn({ text: 'Impressive' })) + .toBe('Impressive'); + + // format-selection block is removed + expect(element.find('format-selection').length).toBe(0); + }); + it('should compile format-no-matches', function () { + spyOn($.fn, 'select2'); + + var element = compile( + '' + + '' + + '{{input}}' + + '' + + ''); + + // Find the `select2` call made with the configuration object + var fn = configFromSpy(element.select2).formatNoMatches; + expect(fn('Dag')) + .toBe('Dag'); + + // format-no-matches block is removed + expect(element.find('format-no-matches').length).toBe(0); + }); + it('should compile format-searching', function () { + spyOn($.fn, 'select2'); + + var element = compile( + '' + + '' + + '?' + + '' + + ''); + + // Find the `select2` call made with the configuration object + var fn = configFromSpy(element.select2).formatSearching; + expect(fn('Dag')) + .toBe('?'); + + // format-searching block is removed + expect(element.find('format-searching').length).toBe(0); + }); + it('should compile format-input-too-short', function () { + spyOn($.fn, 'select2'); + + var element = compile( + '' + + '' + + '{{ input }}' + + ' is ' + + '{{ minimumInputLength - input.length }}' + + ' character(s) too short' + + '' + + ''); + + // Find the `select2` call made with the configuration object + var fn = configFromSpy(element.select2).formatInputTooShort; + expect(fn('bin', 5)) + .toBe('bin is 2 character(s) too short'); + + // format-input-too-short block is removed + expect(element.find('format-input-too-short').length).toBe(0); + }); + it('should compile format-selection-too-big', function () { + spyOn($.fn, 'select2'); + + var element = compile( + '' + + '' + + 'No more than ' + + '{{ maximumSelectionSize }}' + + ' selections please.' + + '' + + ''); + + // Find the `select2` call made with the configuration object + var fn = configFromSpy(element.select2).formatSelectionTooBig; + expect(fn(5)) + .toBe('No more than 5 selections please.'); + + // format-selection-too-big block is removed + expect(element.find('format-selection-too-big').length).toBe(0); + }); }); describe('when model is changed programmatically', function(){ describe('for single-select', function(){ From a4a5f9434eb5251d5fabcced7cbb4b5676e1a218 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Sat, 22 Feb 2014 13:00:44 -0600 Subject: [PATCH 6/6] Add demos of declarative formatting blocks --- demo/app.js | 38 ++++++++++++++++++++++++++++++++++++++ demo/bower.json | 11 ----------- demo/index.html | 16 ++++++++-------- 3 files changed, 46 insertions(+), 19 deletions(-) delete mode 100644 demo/bower.json diff --git a/demo/app.js b/demo/app.js index 7530204..fead199 100644 --- a/demo/app.js +++ b/demo/app.js @@ -76,6 +76,36 @@ app.controller('MainCtrl', function ($scope, $element) { } } + function forEachState (fn) { + for (var i=0; i=1.2.0", - "select2": "~3.4", - "jquery": ">=1.6.4", - "bootstrap": "~3.0.3", - "angular-ui-select2": "~0.0.5" - } -} diff --git a/demo/index.html b/demo/index.html index 85aaaaf..9607e49 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,8 +1,8 @@ - - + + - - - - - + + + + +