diff --git a/Gruntfile.js b/Gruntfile.js index 1b8cd626..767f95eb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,6 +10,7 @@ module.exports = function(grunt) { js: { src: [ 'src/keycodes.js', + 'src/init.js', 'src/tags-input.js', 'src/auto-complete.js', 'src/transclude-append.js', diff --git a/karma.conf.js b/karma.conf.js index 0020027b..e041d47d 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -13,7 +13,7 @@ module.exports = function(config) { 'test/lib/angular.js', 'test/lib/angular-mocks.js', 'test/*.spec.js', - 'src/tags-input.js', + 'src/init.js', 'src/*.js', 'templates/*.html' ], diff --git a/src/auto-complete.js b/src/auto-complete.js index 6dd58bcb..607e3206 100644 --- a/src/auto-complete.js +++ b/src/auto-complete.js @@ -18,7 +18,7 @@ * suggestions list. * @param {number=} [maxResultsToShow=10] Maximum number of results to be displayed at a time. */ -tagsInput.directive('autoComplete', function($document, $timeout, $sce, tiConfiguration) { +tagsInput.directive('autoComplete', function($document, $timeout, $sce, tagsInputConfig) { function SuggestionList(loadFn, options) { var self = {}, debouncedLoadId, getDifference, lastPromise; @@ -114,11 +114,11 @@ tagsInput.directive('autoComplete', function($document, $timeout, $sce, tiConfig var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down], suggestionList, tagsInput, markdown; - tiConfiguration.load(scope, attrs, { - debounceDelay: { type: Number, defaultValue: 100 }, - minLength: { type: Number, defaultValue: 3 }, - highlightMatchedText: { type: Boolean, defaultValue: true }, - maxResultsToShow: { type: Number, defaultValue: 10 } + tagsInputConfig.load('autoComplete', scope, attrs, { + debounceDelay: [Number, 100], + minLength: [Number, 3], + highlightMatchedText: [Boolean, true], + maxResultsToShow: [Number, 10] }); tagsInput = tagsInputCtrl.registerAutocomplete(); diff --git a/src/configuration.js b/src/configuration.js index 187c6c60..30177d73 100644 --- a/src/configuration.js +++ b/src/configuration.js @@ -1,27 +1,48 @@ 'use strict'; /** - * @ngdoc service - * @name tagsInput.service:tiConfiguration + * @ngdoc provider + * @name tagsInput.provider:tagsInputConfig * * @description - * Loads and initializes options from HTML attributes. Used internally by tagsInput and autoComplete directives. + * Sets global default configuration options for tagsInput and autoComplete directives. It's also used internally to parse and + * initialize options from HTML attributes. */ -tagsInput.service('tiConfiguration', function($interpolate) { - this.load = function(scope, attrs, options) { +tagsInput.provider('tagsInputConfig', function() { + var globalDefaults = {}; + + /** + * @ngdoc function + * @name setDefaults + * @description Sets the default configuration option for a directive. + * + * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'. + * @param {object} defaults Object containing options and their values. + */ + this.setDefaults = function(directive, defaults) { + globalDefaults[directive] = defaults; + return this; + }; + + this.$get = function($interpolate) { var converters = {}; converters[String] = function(value) { return value; }; converters[Number] = function(value) { return parseInt(value, 10); }; - converters[Boolean] = function(value) { return value === 'true'; }; + converters[Boolean] = function(value) { return value.toLowerCase() === 'true'; }; converters[RegExp] = function(value) { return new RegExp(value); }; - scope.options = {}; + return { + load: function(directive, scope, attrs, options) { + scope.options = {}; - angular.forEach(options, function(value, key) { - var interpolatedValue = attrs[key] && $interpolate(attrs[key])(scope.$parent), - converter = converters[options[key].type]; + angular.forEach(options, function(value, key) { + var interpolatedValue = attrs[key] && $interpolate(attrs[key])(scope.$parent), + converter = converters[value[0]], + getDefault = function(key) { return globalDefaults[directive] ? globalDefaults[directive][key] : value[1]; }; - scope.options[key] = interpolatedValue ? converter(interpolatedValue) : options[key].defaultValue; - }); + scope.options[key] = interpolatedValue ? converter(interpolatedValue) : getDefault(key); + }); + } + }; }; }); diff --git a/src/init.js b/src/init.js new file mode 100644 index 00000000..e44ca133 --- /dev/null +++ b/src/init.js @@ -0,0 +1,3 @@ +'use strict'; + +var tagsInput = angular.module('ngTagsInput', []); \ No newline at end of file diff --git a/src/tags-input.js b/src/tags-input.js index 7ef51c92..4a657c82 100644 --- a/src/tags-input.js +++ b/src/tags-input.js @@ -1,7 +1,5 @@ 'use strict'; -var tagsInput = angular.module('ngTagsInput', []); - /** * @ngdoc directive * @name tagsInput.directive:tagsInput @@ -30,7 +28,7 @@ var tagsInput = angular.module('ngTagsInput', []); * @param {expression} onTagAdded Expression to evaluate upon adding a new tag. The new tag is available as $tag. * @param {expression} onTagRemoved Expression to evaluate upon removing an existing tag. The removed tag is available as $tag. */ -tagsInput.directive('tagsInput', function($timeout, $document, tiConfiguration) { +tagsInput.directive('tagsInput', function($timeout, $document, tagsInputConfig) { function SimplePubSub() { var events = {}; @@ -63,22 +61,22 @@ tagsInput.directive('tagsInput', function($timeout, $document, tiConfiguration) controller: function($scope, $attrs, $element) { var shouldRemoveLastTag; - tiConfiguration.load($scope, $attrs, { - customClass: { type: String, defaultValue: '' }, - placeholder: { type: String, defaultValue: 'Add a tag' }, - tabindex: { type: Number }, - removeTagSymbol: { type: String, defaultValue: String.fromCharCode(215) }, - replaceSpacesWithDashes: { type: Boolean, defaultValue: true }, - minLength: { type: Number, defaultValue: 3 }, - maxLength: { type: Number }, - addOnEnter: { type: Boolean, defaultValue: true }, - addOnSpace: { type: Boolean, defaultValue: false }, - addOnComma: { type: Boolean, defaultValue: true }, - addOnBlur: { type: Boolean, defaultValue: true }, - allowedTagsPattern: { type: RegExp, defaultValue: /^[a-zA-Z0-9\s]+$/ }, - enableEditingLastTag: { type: Boolean, defaultValue: false }, - minTags: { type: Number }, - maxTags: { type: Number } + tagsInputConfig.load('tagsInput', $scope, $attrs, { + customClass: [String], + placeholder: [String, 'Add a tag'], + tabindex: [Number], + removeTagSymbol: [String, String.fromCharCode(215)], + replaceSpacesWithDashes: [Boolean, true], + minLength: [Number, 3], + maxLength: [Number], + addOnEnter: [Boolean, true], + addOnSpace: [Boolean, false], + addOnComma: [Boolean, true], + addOnBlur: [Boolean, true], + allowedTagsPattern: [RegExp, /^[a-zA-Z0-9\s]+$/], + enableEditingLastTag: [Boolean, false], + minTags: [Number], + maxTags: [Number] }); $scope.events = new SimplePubSub(); diff --git a/test/auto-complete.spec.js b/test/auto-complete.spec.js index 4defbfbb..b0edcbac 100644 --- a/test/auto-complete.spec.js +++ b/test/auto-complete.spec.js @@ -590,23 +590,6 @@ describe('autocomplete-directive', function() { expect(isolateScope.options.debounceDelay).toBe(100); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('debounce-delay="1000"'); - - // Assert - expect(isolateScope.options.debounceDelay).toBe(1000); - }); - - it('sets the option given an interpolated string', function() { - // Arrange/Act - $scope.value = 1000; - compile('debounce-delay="{{ value }}"'); - - // Assert - expect(isolateScope.options.debounceDelay).toBe(1000); - }); - it('doesn\'t call the load function immediately', function() { // Arrange compile('debounce-delay="100"'); @@ -658,23 +641,6 @@ describe('autocomplete-directive', function() { expect(isolateScope.options.minLength).toBe(3); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('min-length="5"'); - - // Assert - expect(isolateScope.options.minLength).toBe(5); - }); - - it('sets the option given an interpolated string', function() { - // Arrange/Act - $scope.value = 5; - compile('min-length="{{ value }}"'); - - // Assert - expect(isolateScope.options.minLength).toBe(5); - }); - it('calls the load function only after the minimum amount of characters has been entered', function() { // Arrange compile('min-length="3"'); @@ -727,23 +693,6 @@ describe('autocomplete-directive', function() { expect(isolateScope.options.highlightMatchedText).toBe(true); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('highlight-matched-text="false"'); - - // Assert - expect(isolateScope.options.highlightMatchedText).toBe(false); - }); - - it('sets the option given an interpolated string', function() { - // Arrange/Act - $scope.value = false; - compile('highlight-matched-text="{{ value }}"'); - - // Assert - expect(isolateScope.options.highlightMatchedText).toBe(false); - }); - it('highlights the matched text in the suggestions list', function() { // Arrange compile('highlight-matched-text="true"', 'min-length="1"'); @@ -808,23 +757,6 @@ describe('autocomplete-directive', function() { expect(isolateScope.options.maxResultsToShow).toBe(10); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('max-results-to-show="5"'); - - // Assert - expect(isolateScope.options.maxResultsToShow).toBe(5); - }); - - it('sets the option given an interpolated string', function() { - // Arrange/Act - $scope.value = 5; - compile('max-results-to-show="{{ value }}"'); - - // Assert - expect(isolateScope.options.maxResultsToShow).toBe(5); - }); - it('limits the number of results to be displayed at a time', function() { // Arrange compile('max-results-to-show="3"'); diff --git a/test/configuration.spec.js b/test/configuration.spec.js new file mode 100644 index 00000000..9093ab43 --- /dev/null +++ b/test/configuration.spec.js @@ -0,0 +1,152 @@ +'use strict'; + +describe('configuration service', function() { + var $scope, + attrs, provider, service; + + beforeEach(function() { + module('ngTagsInput', function(tagsInputConfigProvider) { + provider = tagsInputConfigProvider; + }); + + inject(function($rootScope, tagsInputConfig) { + $scope = $rootScope.$new(); + service = tagsInputConfig; + }); + + attrs = {}; + $scope.options = {}; + }); + + it('loads literal values from attributes', function() { + // Arrange + attrs.prop1 = 'foobar'; + attrs.prop2 = '42'; + attrs.prop3 = 'true'; + attrs.prop4 = '.*'; + + // Act + service.load('foo', $scope, attrs, { + prop1: [String], + prop2: [Number], + prop3: [Boolean], + prop4: [RegExp] + }); + + // Assert + expect($scope.options).toEqual({ + prop1: 'foobar', + prop2: 42, + prop3: true, + prop4: /.*/ + }); + }); + + it('loads interpolated values from attributes', function() { + // Arrange + $scope.$parent.prop1 = 'barfoo'; + $scope.$parent.prop2 = 24; + $scope.$parent.prop3 = false; + $scope.$parent.prop4 = '.+'; + + attrs.prop1 = '{{ prop1 }}'; + attrs.prop2 = '{{ prop2 }}'; + attrs.prop3 = '{{ prop3 }}'; + attrs.prop4 = '{{ prop4 }}'; + + // Act + service.load('foo', $scope, attrs, { + prop1: [String], + prop2: [Number], + prop3: [Boolean], + prop4: [RegExp] + }); + + // Assert + expect($scope.options).toEqual({ + prop1: 'barfoo', + prop2: 24, + prop3: false, + prop4: /.+/ + }); + }); + + it('loads default values when attributes are missing', function() { + // Act + service.load('foo', $scope, attrs, { + prop1: [String, 'foobaz'], + prop2: [Number, 84], + prop3: [Boolean, true], + prop4: [RegExp, /.?/] + }); + + // Assert + expect($scope.options).toEqual({ + prop1: 'foobaz', + prop2: 84, + prop3: true, + prop4: /.?/ + }); + }); + + it('overrides default values with global ones', function() { + // Arrange + provider.setDefaults('foo', { + prop1: 'foobar', + prop2: 42, + prop3: false, + prop4: /.*/ + }); + + // Act + service.load('foo', $scope, attrs, { + prop1: [String, 'foobaz'], + prop2: [Number, 84], + prop3: [Boolean, true], + prop4: [RegExp, /.?/] + }); + + // Assert + expect($scope.options).toEqual({ + prop1: 'foobar', + prop2: 42, + prop3: false, + prop4: /.*/ + }); + }); + + it('overrides global configuration with local values', function() { + // Arrange + provider.setDefaults('foo', { + prop1: 'foobar', + prop2: 42, + prop3: true, + prop4: /.*/ + }); + + attrs.prop1 = 'foobaz'; + attrs.prop2 = '84'; + attrs.prop3 = 'false'; + attrs.prop4 = '.?'; + + // Act + service.load('foo', $scope, attrs, { + prop1: [String], + prop2: [Number], + prop3: [Boolean], + prop4: [RegExp] + }); + + // Assert + expect($scope.options).toEqual({ + prop1: 'foobaz', + prop2: 84, + prop3: false, + prop4: /.?/ + }); + }); + + it('returns the same object so calls can be chained', function() { + expect(provider.setDefaults('foo', {})).toBe(provider); + }); +}); diff --git a/test/tags-input.spec.js b/test/tags-input.spec.js index 27490518..390754ed 100644 --- a/test/tags-input.spec.js +++ b/test/tags-input.spec.js @@ -239,25 +239,6 @@ describe('tags-input-directive', function() { // Assert expect(getInput().attr('tabindex')).toBe('1'); }); - - it('sets the option given a static string', function() { - // Arrange/Act - compile('tabindex="1"'); - - // Assert - expect(isolateScope.options.tabindex).toBe(1); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = 1; - - // Act - compile('tabindex="{{ value }}"'); - - // Assert - expect(isolateScope.options.tabindex).toBe(1); - }); }); describe('add-on-enter option', function() { @@ -290,25 +271,6 @@ describe('tags-input-directive', function() { // Assert expect(isolateScope.options.addOnEnter).toBe(true); }); - - it('sets the option given a static string', function() { - // Arrange/Act - compile('add-on-enter="true"'); - - // Assert - expect(isolateScope.options.addOnEnter).toBe(true); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = true; - - // Act - compile('add-on-enter="{{ value }}"'); - - // Assert - expect(isolateScope.options.addOnEnter).toBe(true); - }); }); describe('add-on-space option', function() { @@ -341,25 +303,6 @@ describe('tags-input-directive', function() { // Assert expect(isolateScope.options.addOnSpace).toBe(false); }); - - it('sets the option given a static string', function() { - // Arrange/Act - compile('add-on-space="true"'); - - // Assert - expect(isolateScope.options.addOnSpace).toBe(true); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = true; - - // Act - compile('add-on-space="{{ value }}"'); - - // Assert - expect(isolateScope.options.addOnSpace).toBe(true); - }); }); describe('add-on-comma option', function() { @@ -392,25 +335,6 @@ describe('tags-input-directive', function() { // Assert expect(isolateScope.options.addOnComma).toBe(true); }); - - it('sets the option given a static string', function() { - // Arrange/Act - compile('add-on-comma="true"'); - - // Assert - expect(isolateScope.options.addOnComma).toBe(true); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = true; - - // Act - compile('add-on-comma="{{ value }}"'); - - // Assert - expect(isolateScope.options.addOnComma).toBe(true); - }); }); describe('add-on-blur option', function() { @@ -422,25 +346,6 @@ describe('tags-input-directive', function() { expect(isolateScope.options.addOnBlur).toBe(true); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('add-on-blur="false"'); - - // Assert - expect(isolateScope.options.addOnBlur).toBe(false); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = false; - - // Act - compile('add-on-blur="{{ value }}"'); - - // Assert - expect(isolateScope.options.addOnBlur).toBe(false); - }); - it('ensures the outermost div element has a tabindex attribute set to -1', function() { // Arrange/Act compile(); @@ -510,25 +415,6 @@ describe('tags-input-directive', function() { expect(getInput().attr('placeholder')).toBe('New tag'); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('placeholder="New tag"'); - - // Assert - expect(isolateScope.options.placeholder).toBe('New tag'); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = 'New tag'; - - // Act - compile('placeholder="{{ value }}"'); - - // Assert - expect(isolateScope.options.placeholder).toBe('New tag'); - }); - it('initializes the option to "Add a tag"', function() { // Arrange/Act compile(); @@ -550,25 +436,6 @@ describe('tags-input-directive', function() { expect(element.find('button').html()).toBe('X'); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('remove-tag-symbol="X"'); - - // Assert - expect(isolateScope.options.removeTagSymbol).toBe('X'); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = 'X'; - - // Act - compile('remove-tag-symbol="{{ value }}"'); - - // Assert - expect(isolateScope.options.removeTagSymbol).toBe('X'); - }); - it('initializes the option to charcode 215 (×)', function() { // Arrange/Act compile(); @@ -608,25 +475,6 @@ describe('tags-input-directive', function() { // Assert expect(isolateScope.options.replaceSpacesWithDashes).toBe(true); }); - - it('sets the option given a static string', function() { - // Arrange/Act - compile('replace-spaces-with-dashes="true"'); - - // Assert - expect(isolateScope.options.replaceSpacesWithDashes).toBe(true); - }); - - it('sets the option given a interpolated string', function() { - // Arrange - $scope.value = true; - - // Act - compile('replace-spaces-with-dashes="{{ value }}"'); - - // Assert - expect(isolateScope.options.replaceSpacesWithDashes).toBe(true); - }); }); describe('allowed-tags-pattern option', function() { @@ -659,25 +507,6 @@ describe('tags-input-directive', function() { // Assert expect(isolateScope.options.allowedTagsPattern.toString()).toBe('/^[a-zA-Z0-9\\s]+$/'); }); - - it('sets the option given a static string', function() { - // Arrange/Act - compile('allowed-tags-pattern=".*"'); - - // Assert - expect(isolateScope.options.allowedTagsPattern.toString()).toBe('/.*/'); - }); - - it('sets the option given a interpolated string', function() { - // Arrange - $scope.value = '.*'; - - // Act - compile('allowed-tags-pattern="{{ value }}"'); - - // Assert - expect(isolateScope.options.allowedTagsPattern.toString()).toBe('/.*/'); - }); }); describe('min-length option', function() { @@ -699,25 +528,6 @@ describe('tags-input-directive', function() { // Assert expect(isolateScope.options.minLength).toBe(3); }); - - it('sets the option given a static string', function() { - // Arrange/Act - compile('min-length="5"'); - - // Assert - expect(isolateScope.options.minLength).toBe(5); - }); - - it('sets the option given a interpolated string', function() { - // Arrange - $scope.value = 5; - - // Act - compile('min-length="{{ value }}"'); - - // Assert - expect(isolateScope.options.minLength).toBe(5); - }); }); describe('max-length option', function() { @@ -736,25 +546,6 @@ describe('tags-input-directive', function() { // Assert expect(getInput().attr('maxlength')).toBe(''); }); - - it('sets the option given a static string', function() { - // Arrange/Act - compile('max-length="5"'); - - // Assert - expect(isolateScope.options.maxLength).toBe(5); - }); - - it('sets the option given a interpolated string', function() { - // Arrange - $scope.value = 5; - - // Act - compile('max-length="{{ value }}"'); - - // Assert - expect(isolateScope.options.maxLength).toBe(5); - }); }); describe('enable-editing-last-tag option', function() { @@ -770,25 +561,6 @@ describe('tags-input-directive', function() { expect(isolateScope.options.enableEditingLastTag).toBe(false); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('enable-editing-last-tag="true"'); - - // Assert - expect(isolateScope.options.enableEditingLastTag).toBe(true); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = true; - - // Act - compile('enable-editing-last-tag="{{ value }}"'); - - // Assert - expect(isolateScope.options.enableEditingLastTag).toBe(true); - }); - describe('option is on', function() { beforeEach(function() { compile('enable-editing-last-tag="true"'); @@ -881,25 +653,6 @@ describe('tags-input-directive', function() { expect(isolateScope.options.minTags).toBeUndefined(); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('min-tags="3"'); - - // Assert - expect(isolateScope.options.minTags).toBe(3); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = 5; - - // Act - compile('min-tags="{{ value }}"'); - - // Assert - expect(isolateScope.options.minTags).toBe(5); - }); - it('makes the element invalid when the number of tags is less than the min-tags option', function() { // Arrange compileWithForm('min-tags="3"', 'name="tags"'); @@ -950,25 +703,6 @@ describe('tags-input-directive', function() { expect(isolateScope.options.maxTags).toBeUndefined(); }); - it('sets the option given a static string', function() { - // Arrange/Act - compile('max-tags="3"'); - - // Assert - expect(isolateScope.options.maxTags).toBe(3); - }); - - it('sets the option given an interpolated string', function() { - // Arrange - $scope.value = 5; - - // Act - compile('max-tags="{{ value }}"'); - - // Assert - expect(isolateScope.options.maxTags).toBe(5); - }); - it('makes the element invalid when the number of tags is greater than the max-tags option', function() { // Arrange compileWithForm('max-tags="2"', 'name="tags"'); diff --git a/test/test-page.html b/test/test-page.html index bd03d002..b8fae522 100644 --- a/test/test-page.html +++ b/test/test-page.html @@ -18,7 +18,6 @@ + @@ -55,6 +54,16 @@ }, 2000); return deferred.promise; }; + }) + .config(function(tagsInputConfigProvider) { + tagsInputConfigProvider + .setDefaults('tagsInput', { + placeholder: 'new tag', + removeTagSymbol: 'x' + }) + .setDefaults('autoComplete', { + highlightMatchedText: true + }); });