Skip to content
This repository has been archived by the owner on Nov 22, 2021. It is now read-only.

Commit

Permalink
feat(tagsInput): Add custom template support
Browse files Browse the repository at this point in the history
Add custom template support for the tagsInput directive so it matches
the same functionality of the autoComplete directive. Same rules apply
here: tag data is available to the template via the data property,
tag index is available through $index and a tag can be deleted by
calling the $removeTag function.
  • Loading branch information
mbenford committed Mar 21, 2015
1 parent 8611877 commit 45e5d99
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 47 deletions.
8 changes: 7 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = function(grunt) {
'src/constants.js',
'src/init.js',
'src/tags-input.js',
'src/tag-item.js',
'src/auto-complete.js',
'src/auto-complete-match.js',
'src/transclude-append.js',
Expand All @@ -49,7 +50,12 @@ module.exports = function(grunt) {
}
},
html: {
src: ['templates/tags-input.html', 'templates/auto-complete.html', 'templates/auto-complete-match.html'],
src: [
'templates/tags-input.html',
'templates/tag-item.html',
'templates/auto-complete.html',
'templates/auto-complete-match.html'
],
out: 'tmp/templates.js'
},
zip: {
Expand Down
2 changes: 1 addition & 1 deletion src/auto-complete-match.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* @module ngTagsInput
*
* @description
* Represents an autocomplete match. Used internally by the tagsInput directive.
* Represents an autocomplete match. Used internally by the autoComplete directive.
*/
tagsInput.directive('tiAutocompleteMatch', function($sce, tiUtil) {
return {
Expand Down
36 changes: 36 additions & 0 deletions src/tag-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

/**
* @ngdoc directive
* @name tiTagItem
* @module ngTagsInput
*
* @description
* Represents a tag item. Used internally by the tagsInput directive.
*/
tagsInput.directive('tiTagItem', function(tiUtil) {
return {
restrict: 'E',
require: '^tagsInput',
template: '<ng-include src="$$template"></ng-include>',
scope: { data: '=' },
link: function(scope, element, attrs, tagsInputCtrl) {
var tagsInput = tagsInputCtrl.registerTagItem(),
options = tagsInput.getOptions();

scope.$$template = options.template;
scope.$$removeTagSymbol = options.removeTagSymbol;

scope.$getDisplayText = function() {
return tiUtil.safeToString(scope.data[options.displayProperty]);
};
scope.$removeTag = function() {
tagsInput.removeTag(scope.$index);
};

scope.$watch('$parent.$index', function(value) {
scope.$index = value;
});
}
};
});
22 changes: 18 additions & 4 deletions src/tags-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ tagsInput.directive('tagsInput', function($timeout, $document, $window, tagsInpu
$scope.events = tiUtil.simplePubSub();

tagsInputConfig.load('tagsInput', $scope, $attrs, {
template: [String, 'ngTagsInput/tag-item.html'],
type: [String, 'text', validateType],
placeholder: [String, 'Add a tag'],
tabindex: [Number, null],
Expand Down Expand Up @@ -213,6 +214,20 @@ tagsInput.directive('tagsInput', function($timeout, $document, $window, tagsInpu
}
};
};

this.registerTagItem = function() {
return {
getOptions: function() {
return $scope.options;
},
removeTag: function(index) {
if ($scope.disabled) {
return;
}
$scope.tagList.remove(index);
}
};
};
},
link: function(scope, element, attrs, ngModelCtrl) {
var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right],
Expand Down Expand Up @@ -242,10 +257,6 @@ tagsInput.directive('tagsInput', function($timeout, $document, $window, tagsInpu
}
};

scope.getDisplayText = function(tag) {
return tiUtil.safeToString(tag[options.displayProperty]);
};

scope.track = function(tag) {
return tag[options.keyProperty || options.displayProperty];
};
Expand Down Expand Up @@ -301,6 +312,9 @@ tagsInput.directive('tagsInput', function($timeout, $document, $window, tagsInpu
},
host: {
click: function() {
if (scope.disabled) {
return;
}
input[0].focus();
}
}
Expand Down
2 changes: 2 additions & 0 deletions templates/tag-item.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<span ng-bind="$getDisplayText()"></span>
<a class="remove-button" ng-click="$removeTag()" ng-bind="$$removeTagSymbol"></a>
9 changes: 5 additions & 4 deletions templates/tags-input.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<div class="host" tabindex="-1" ng-click="disabled || eventHandlers.host.click()" ti-transclude-append>
<div class="host" tabindex="-1" ng-click="eventHandlers.host.click()" ti-transclude-append>
<div class="tags" ng-class="{focused: hasFocus}">
<ul class="tag-list">
<li class="tag-item" ng-repeat="tag in tagList.items track by track(tag)" ng-class="{ selected: tag == tagList.selected }">
<span ng-bind="getDisplayText(tag)"></span>
<a class="remove-button" ng-click="disabled || tagList.remove($index)" ng-bind="options.removeTagSymbol"></a>
<li class="tag-item"
ng-repeat="tag in tagList.items track by track(tag)"
ng-class="{ selected: tag == tagList.selected }">
<ti-tag-item data="tag"></ti-tag-item>
</li>
</ul>
<input class="input"
Expand Down
168 changes: 132 additions & 36 deletions test/tags-input.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,12 @@ describe('tags-input directive', function() {

module('ngTagsInput');

$window = { document: document };
module(function($provide) {
$provide.value('$window', $window);
});

inject(function(_$compile_, _$rootScope_, _$document_, _$timeout_) {
inject(function(_$compile_, _$rootScope_, _$document_, _$timeout_, _$window_) {
$compile = _$compile_;
$scope = _$rootScope_;
$document = _$document_;
$timeout = _$timeout_;
$window = _$window_;
});
});

Expand Down Expand Up @@ -55,11 +51,11 @@ describe('tags-input directive', function() {
}

function getTagText(index) {
return getTag(index).find('span').html();
return getTag(index).find('ti-tag-item > ng-include > span').html();
}

function getRemoveButton(index) {
return getTag(index).find('a').first();
return getTag(index).find('ti-tag-item > ng-include > a').first();
}

function getInput() {
Expand Down Expand Up @@ -649,11 +645,13 @@ describe('tags-input directive', function() {
clipboardData: jasmine.createSpyObj('clipboardData', ['getData']),
preventDefault: jasmine.createSpy()
};
windowClipboardData = $window.clipboardData;
windowClipboardData = null;
});

afterEach(function() {
$window.clipboardData = windowClipboardData;
if (windowClipboardData) {
$window.clipboardData = windowClipboardData;
}
});

it('initializes the option to false', function() {
Expand All @@ -664,35 +662,36 @@ describe('tags-input directive', function() {
expect(isolateScope.options.addOnPaste).toBe(false);
});


describe('option is true', function() {
var eventSetups = {
vanillaJS: function() {
jqLite: function(returnValue) {
eventData.clipboardData.getData.and.callFake(function(args) {
return args === 'text/plain' ? 'tag1, tag2, tag3' : null;
});
},
ie: function() {
$window.clipboardData = eventData.clipboardData;
delete eventData.clipboardData;
$window.clipboardData.getData.and.callFake(function(args) {
return args === 'Text' ? 'tag1, tag2, tag3' : null;
return args === 'text/plain' ? returnValue : null;
});
},
jquery: function() {
jQuery: function(returnValue) {
eventData.originalEvent = { clipboardData: eventData.clipboardData };
delete eventData.clipboardData;
eventData.originalEvent.clipboardData.getData.and.callFake(function(args) {
return args === 'text/plain' ? 'tag1, tag2, tag3' : null;
return args === 'text/plain' ? returnValue : null;
});
},
ie: function(returnValue) {
delete eventData.clipboardData;
windowClipboardData = $window.clipboardData;
$window.clipboardData = {
getData: function(args) {
return args === 'Text' ? returnValue : null;
}
};
}
};

angular.forEach(eventSetups, function(setup, name) {
it('splits the pasted text into tags if there is more than one tag (' + name + ')', function() {
// Arrange
compile('add-on-paste="true"');
setup();
setup('tag1, tag2, tag3');

// Act
var event = jQuery.Event('paste', eventData);
Expand All @@ -706,20 +705,20 @@ describe('tags-input directive', function() {
]);
expect(eventData.preventDefault).toHaveBeenCalled();
});
});

it('doesn\'t split the pasted text into tags if there is just one tag', function() {
// Arrange
compile('add-on-paste="true"');
eventData.clipboardData.getData.and.returnValue('tag1');
it('doesn\'t split the pasted text into tags if there is just one tag (' + name + ')', function() {
// Arrange
compile('add-on-paste="true"');
setup('Tag1');

// Act
var event = jQuery.Event('paste', eventData);
getInput().trigger(event);
// Act
var event = jQuery.Event('paste', eventData);
getInput().trigger(event);

// Assert
expect($scope.tags).toEqual([]);
expect(eventData.preventDefault).not.toHaveBeenCalled();
// Assert
expect($scope.tags).toEqual([]);
expect(eventData.preventDefault).not.toHaveBeenCalled();
});
});
});

Expand Down Expand Up @@ -1498,6 +1497,102 @@ describe('tags-input directive', function() {
});
});

describe('template option', function() {
var $templateCache;

function getTagContent(index) {
return getTag(index)
.find('ti-tag-item > ng-include')
.children()
.removeAttr('class')
.parent()
.html();
}

function getTagScope(index) {
return getTag(index)
.find('ti-tag-item > ng-include')
.children()
.scope();
}

beforeEach(function() {
inject(function(_$templateCache_) {
$templateCache = _$templateCache_;
});
});

it('initializes the option to the default template file', function() {
expect(isolateScope.options.template).toBe('ngTagsInput/tag-item.html');
});

it('loads and uses the provided template', function() {
// Arrange
$templateCache.put('customTemplate', '<span>{{data.id}}</span><span>{{data.text}}</span>');
compile('template="customTemplate"');

// Act
$scope.tags = [
{ id: 1, text: 'Item1' },
{ id: 2, text: 'Item2' },
{ id: 3, text: 'Item3' }
];
$scope.$digest();

// Assert
expect(getTagContent(0)).toBe('<span>1</span><span>Item1</span>');
expect(getTagContent(1)).toBe('<span>2</span><span>Item2</span>');
expect(getTagContent(2)).toBe('<span>3</span><span>Item3</span>');
});

it('makes the tag data available to the template', function() {
// Arrange
compile();

// Act
$scope.tags = [
{ id: 1, text: 'Item1', image: 'item1.jpg' },
{ id: 2, text: 'Item2', image: 'item2.jpg' },
{ id: 3, text: 'Item3', image: 'item3.jpg' }
];
$scope.$digest();

// Assert
expect(getTagScope(0).data).toEqual({ id: 1, text: 'Item1', image: 'item1.jpg' });
expect(getTagScope(1).data).toEqual({ id: 2, text: 'Item2', image: 'item2.jpg' });
expect(getTagScope(2).data).toEqual({ id: 3, text: 'Item3', image: 'item3.jpg' });
});

it('makes tags\' indexes available to the template', function() {
// Arrange
compile();

// Act
$scope.tags = generateTags(3);
$scope.$digest();

// Assert
expect(getTagScope(0).$index).toBe(0);
expect(getTagScope(1).$index).toBe(1);
expect(getTagScope(2).$index).toBe(2);
});

it('makes helper functions available to the template', function() {
// Arrange
compile();

// Act
$scope.tags = generateTags(1);
$scope.$digest();

// Assert
var scope = getTagScope(0);
expect(scope.$getDisplayText).not.toBeUndefined();
expect(scope.$removeTag).not.toBeUndefined();
});
});


describe('navigation through tags', function() {
describe('navigation is enabled', function() {
beforeEach(function() {
Expand Down Expand Up @@ -1638,11 +1733,11 @@ describe('tags-input directive', function() {

it('doesn\'t focus the input field when the container div is clicked', function() {
// Arrange
compile('ng-disabled="true"');

var input = getInput()[0];
spyOn(input, 'focus');

compile('ng-disabled="true"');

// Act
element.find('div').click();

Expand All @@ -1654,6 +1749,7 @@ describe('tags-input directive', function() {
// Arrange
compile('ng-disabled="true"');
$scope.tags = generateTags(1);
$scope.$digest();

// Act
getRemoveButton(0).click();
Expand Down
Loading

1 comment on commit 45e5d99

@felippenardi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just learned how to dynamically load templates into a component with TDD thanks to that commit :)
Thank you! @mbenford 👍

Please sign in to comment.