├── .editorconfig
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── README.md
├── config
└── webpack.dev.js
├── demo
├── app.component.html
├── app.component.ts
├── app.module.ts
├── index.html
├── main.ts
└── polyfills.ts
├── index.ts
├── lib
├── components
│ ├── tag-input-autocomplete
│ │ └── tag-input-autocomplete.component.ts
│ ├── tag-input-item
│ │ └── tag-input-item.component.ts
│ └── tag-input
│ │ └── tag-input.component.ts
├── shared
│ └── tag-input-keys.ts
└── tag-input.module.ts
├── package.json
├── tsconfig-aot.json
├── tsconfig.json
└── tslint.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = 0
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | typings
3 | dist
4 | lib/**/*.css
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | typings
2 | node_modules
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.2.0
4 | - Made library Ahead-of-Time (AoT) ready, closes [#15](https://github.com/rosslavery/angular2-tag-input/issues/15)
5 | - Added missing rxjs imports, closes [#13](https://github.com/rosslavery/angular2-tag-input/issues/13)
6 |
7 | ## 1.1.0
8 | - Added autocomplete feature
9 | - Greatly-improved styling to adhere to Material spec
10 | - Added numerous new inputs/outputs (see API docs)
11 |
12 | ## 1.0.0
13 | ### Breaking Changes
14 | - Removed delimiterCode @Input. While some flexibility is lost, code is more reliable with a limited number of split patterns
15 | - Updated dependencies to match latest RC6
16 | - Exporting an NgModule instead of a component, so installation instructions have changed
17 | - Namespaced selector to be `rl-tag-input` to protect against conflicts
18 |
19 | ### Other Changes
20 | - New @Input() addOnComma
21 | - New @Input() pasteSplitPattern - defaults to comma
22 | - Fixed input not being emptied out on paste
23 |
24 |
25 | ## 0.1.5
26 | - First version published
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # angular2-tag-input
2 | Tag input component for Angular 2
3 |
4 | ## Demo & Examples
5 | [View Demo](http://www.webpackbin.com/EkDO0p3Ab)
6 |
7 | ## Quick Start
8 | ```
9 | npm install angular2-tag-input --save
10 | ```
11 |
12 | ```
13 | // In one of your application NgModules
14 | import {RlTagInputModule} from 'angular2-tag-input';
15 |
16 | @NgModule({
17 | imports: [
18 | RlTagInputModule
19 | ]
20 | })
21 | export class YourModule {}
22 |
23 | // In one of your component templates
24 |
25 | ```
26 |
27 | ## API
28 | ### Inputs
29 | | Name | Type | Default | Description |
30 | | --- | --- | --- | --- |
31 | | `addOnBlur` | `boolean` | true | Whether to attempt to add a tag when the input loses focus. |
32 | | `addOnComma` | `boolean` | true | Whether to attempt to add a tag when the user presses comma. |
33 | | `addOnEnter` | `boolean` | true | Whether to attempt to add a tag when the user presses enter. |
34 | | `addOnPaste` | `boolean` | true | Whether to attempt to add a tags when the user pastes their clipboard contents. |
35 | | `addOnSpace` | `boolean` | true | Whether to attempt to add a tags when the user presses space. |
36 | | `allowDuplicates` | `boolean` | `false` | Allow duplicate tags. |
37 | | `allowedTagsPattern` | `RegExp` | `/.+/` | RegExp that must match for a tag to be added. |
38 | | `autocomplete` | `boolean` | `false` | Toggle autocomplete mode on/off |
39 | | `autocompleteItems` | `string[]` | `[]` | List of suggestions for autocomplete menu |
40 | | `autocompleteMustMatch` | `boolean` | `true` | Whether a tag must be present in the suggestions list to be valid |
41 | | `autocompleteSelectFirstItem` | `boolean` | `true` | Pre-highlight the first item in the suggestions list |
42 | | `placeholder` | `string` | `'Add a tag'` | Placeholder for the `` tag. |
43 |
44 |
45 | ### Outputs
46 | | Name | Type Emitted | Description |
47 | | --- | --- | --- |
48 | | `addTag` | `string` | Emits the added tag string |
49 | | `removeTag` | `string` | Emits the removed tag string |
50 |
--------------------------------------------------------------------------------
/config/webpack.dev.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | entry: {
6 | app: './demo/main.ts',
7 | },
8 | output: {
9 | path: root('/demo/dist'),
10 | publicPath: '/',
11 | filename: '[name].js'
12 | },
13 | resolve: {
14 | extensions: [
15 | '.ts',
16 | '.js',
17 | '.json',
18 | '.css',
19 | '.scss',
20 | '.html',
21 | ]
22 | },
23 | module: {
24 | loaders: [
25 | {
26 | test: /\.ts$/,
27 | loader: 'awesome-typescript-loader'
28 | },
29 | {
30 | test: /\.html$/,
31 | loader: 'raw-loader'
32 | }
33 | ]
34 | }
35 | };
36 |
37 | function root(__path) {
38 | return path.join(__dirname, __path);
39 | }
40 |
--------------------------------------------------------------------------------
/demo/app.component.html:
--------------------------------------------------------------------------------
1 |
Basic Tag Input:
2 |
3 |
6 |
7 |
8 |
9 |
Bound List:
10 |
11 | {{tags | json}}
12 |
13 |
14 |
15 |
16 |
17 | AutoComplete Tag Input:
18 | Tag must belong to autocomplete list.
19 |
20 |
26 |
27 |
28 |
29 |
Bound List:
30 |
31 | {{autocompleteTags | json}}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/demo/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'rl-demo-app',
5 | template: require('./app.component.html')
6 | })
7 | export class AppComponent {
8 | public tags = ['Car', 'Bus', 'Train'];
9 | public autocompleteTags = [];
10 | public autocompleteItems = [
11 | 'Banana',
12 | 'Orange',
13 | 'Apple',
14 | 'Pear',
15 | 'Grape',
16 | 'Potato',
17 | 'Peach'
18 | ];
19 | }
20 |
--------------------------------------------------------------------------------
/demo/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { FormsModule } from '@angular/forms';
4 | import { AppComponent } from './app.component';
5 | import { RlTagInputModule } from '../lib/tag-input.module';
6 |
7 | @NgModule({
8 | declarations: [
9 | AppComponent
10 | ],
11 | imports: [
12 | BrowserModule,
13 | FormsModule,
14 | RlTagInputModule
15 | ],
16 | bootstrap: [
17 | AppComponent
18 | ]
19 | })
20 | export class AppModule {}
21 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 | Tag Input Demo
10 |
11 |
12 | Loading...
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/main.ts:
--------------------------------------------------------------------------------
1 | import './polyfills';
2 |
3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4 | import { AppModule } from './app.module';
5 |
6 | platformBrowserDynamic().bootstrapModule(AppModule);
7 |
8 |
--------------------------------------------------------------------------------
/demo/polyfills.ts:
--------------------------------------------------------------------------------
1 | // This file includes polyfills needed by Angular 2 and is loaded before
2 | // the app. You can add your own extra polyfills to this file.
3 | import 'core-js/es6/symbol';
4 | import 'core-js/es6/object';
5 | import 'core-js/es6/function';
6 | import 'core-js/es6/parse-int';
7 | import 'core-js/es6/parse-float';
8 | import 'core-js/es6/number';
9 | import 'core-js/es6/math';
10 | import 'core-js/es6/string';
11 | import 'core-js/es6/date';
12 | import 'core-js/es6/array';
13 | import 'core-js/es6/regexp';
14 | import 'core-js/es6/map';
15 | import 'core-js/es6/set';
16 | import 'core-js/es6/reflect';
17 |
18 | import 'core-js/es7/reflect';
19 | import 'zone.js/dist/zone';
20 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/tag-input.module';
2 | export * from './lib/components/tag-input/tag-input.component';
3 |
--------------------------------------------------------------------------------
/lib/components/tag-input-autocomplete/tag-input-autocomplete.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
2 | import { Observable } from 'rxjs/Observable';
3 | import { Subscription } from 'rxjs/Subscription';
4 |
5 | import { KEYS } from '../../shared/tag-input-keys';
6 |
7 | @Component({
8 | selector: 'rl-tag-input-autocomplete',
9 | template: `
10 |
15 | {{item}}
16 |
17 | `,
18 | styles: [`
19 | :host {
20 | box-shadow: 0 1.5px 4px rgba(0, 0, 0, 0.24), 0 1.5px 6px rgba(0, 0, 0, 0.12);
21 | display: block;
22 | position: absolute;
23 | top: 100%;
24 | font-family: "Roboto", "Helvetica Neue", sans-serif;
25 | font-size: 16px;
26 | color: #444444;
27 | background: white;
28 | padding: 8px 0;
29 | }
30 |
31 | :host .rl-autocomplete-item {
32 | padding: 0 16px;
33 | height: 48px;
34 | line-height: 48px;
35 | }
36 |
37 | :host .is-selected {
38 | background: #eeeeee;
39 | }
40 | `]
41 | })
42 | export class TagInputAutocompleteComponent implements OnChanges, OnDestroy, OnInit {
43 | @Input() items: string[];
44 | @Input() selectFirstItem: boolean = false;
45 | @Output() itemSelected: EventEmitter = new EventEmitter();
46 | @Output() enterPressed: EventEmitter = new EventEmitter();
47 | public selectedItemIndex: number = null;
48 | private keySubscription: Subscription;
49 | private get itemsCount(): Number {
50 | return this.items ? this.items.length : 0;
51 | }
52 |
53 | constructor(private elementRef: ElementRef) { }
54 |
55 | ngOnInit() {
56 | this.keySubscription = Observable.fromEvent(window, 'keydown')
57 | .filter(
58 | (event: KeyboardEvent) =>
59 | event.keyCode === KEYS.upArrow ||
60 | event.keyCode === KEYS.downArrow ||
61 | event.keyCode === KEYS.enter ||
62 | event.keyCode === KEYS.esc
63 | )
64 | .do((event: KeyboardEvent) => {
65 | switch (event.keyCode) {
66 | case KEYS.downArrow:
67 | this.handleDownArrow();
68 | break;
69 |
70 | case KEYS.upArrow:
71 | this.handleUpArrow();
72 | break;
73 |
74 | case KEYS.enter:
75 | this.selectItem();
76 | this.enterPressed.emit();
77 | break;
78 |
79 | case KEYS.esc:
80 | break;
81 | }
82 |
83 | event.stopPropagation();
84 | event.preventDefault();
85 | })
86 | .subscribe();
87 | }
88 |
89 | ensureHighlightVisible() {
90 | let container = this.elementRef.nativeElement.querySelector('.sk-select-results__container');
91 | if (!container) {
92 | return;
93 | }
94 | let choices = container.querySelectorAll('.sk-select-results__item');
95 | if (choices.length < 1) {
96 | return;
97 | }
98 | if (this.selectedItemIndex < 0) {
99 | return;
100 | }
101 | let highlighted: any = choices[this.selectedItemIndex];
102 | if (!highlighted) {
103 | return;
104 | }
105 | let posY: number = highlighted.offsetTop + highlighted.clientHeight - container.scrollTop;
106 | let height: number = container.offsetHeight;
107 |
108 | if (posY > height) {
109 | container.scrollTop += posY - height;
110 | } else if (posY < highlighted.clientHeight) {
111 | container.scrollTop -= highlighted.clientHeight - posY;
112 | }
113 | }
114 |
115 | goToTop() {
116 | this.selectedItemIndex = 0;
117 | this.ensureHighlightVisible();
118 | }
119 |
120 | goToBottom(itemsCount) {
121 | this.selectedItemIndex = itemsCount - 1;
122 | this.ensureHighlightVisible();
123 | }
124 |
125 | goToNext() {
126 | if (this.selectedItemIndex + 1 < this.itemsCount) {
127 | this.selectedItemIndex++;
128 | } else {
129 | this.goToTop();
130 | }
131 | this.ensureHighlightVisible();
132 | }
133 |
134 | goToPrevious() {
135 | if (this.selectedItemIndex - 1 >= 0) {
136 | this.selectedItemIndex--;
137 | } else {
138 | this.goToBottom(this.itemsCount);
139 | }
140 | this.ensureHighlightVisible();
141 | }
142 |
143 | handleUpArrow() {
144 | if (this.selectedItemIndex === null) {
145 | this.goToBottom(this.itemsCount);
146 | return false;
147 | }
148 | this.goToPrevious();
149 | }
150 |
151 | handleDownArrow() {
152 | // Initialize to zero if first time results are shown
153 | if (this.selectedItemIndex === null) {
154 | this.goToTop();
155 | return false;
156 | }
157 | this.goToNext();
158 | }
159 |
160 | selectItem(itemIndex?: number): void {
161 | let itemToEmit = itemIndex ? this.items[itemIndex] : this.items[this.selectedItemIndex];
162 | if (itemToEmit) {
163 | this.itemSelected.emit(itemToEmit);
164 | }
165 | }
166 |
167 | ngOnChanges(changes) {
168 | if (this.selectFirstItem && this.itemsCount > 0) {
169 | this.goToTop();
170 | }
171 | }
172 |
173 | ngOnDestroy() {
174 | this.keySubscription.unsubscribe();
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/lib/components/tag-input-item/tag-input-item.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'rl-tag-input-item',
5 | template: `
6 | {{text}}
7 | ×
10 | `,
11 | styles: [`
12 | :host {
13 | font-family: "Roboto", "Helvetica Neue", sans-serif;
14 | font-size: 16px;
15 | height: 32px;
16 | line-height: 32px;
17 | display: inline-block;
18 | background: #e0e0e0;
19 | padding: 0 12px;
20 | border-radius: 90px;
21 | margin-right: 10px;
22 | transition: all 0.12s ease-out;
23 | }
24 |
25 | :host .ng2-tag-input-remove {
26 | background: #a6a6a6;
27 | border-radius: 50%;
28 | color: #e0e0e0;
29 | cursor: pointer;
30 | display: inline-block;
31 | font-size: 17px;
32 | height: 24px;
33 | line-height: 24px;
34 | margin-left: 6px;
35 | margin-right: -6px;
36 | text-align: center;
37 | width: 24px;
38 | }
39 |
40 | :host.ng2-tag-input-item-selected {
41 | color: white;
42 | background: #0d8bff;
43 | }
44 |
45 | :host.ng2-tag-input-item-selected .ng2-tag-input-remove {
46 | background: white;
47 | color: #0d8bff;
48 | }
49 | `]
50 | })
51 | export class TagInputItemComponent {
52 | @Input() selected: boolean;
53 | @Input() text: string;
54 | @Input() index: number;
55 | @Output() tagRemoved: EventEmitter = new EventEmitter();
56 | @HostBinding('class.ng2-tag-input-item-selected') get isSelected() { return !!this.selected; }
57 |
58 | constructor() { }
59 |
60 | removeTag(): void {
61 | this.tagRemoved.emit(this.index);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/components/tag-input/tag-input.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | ElementRef,
4 | EventEmitter,
5 | forwardRef,
6 | HostBinding,
7 | HostListener,
8 | Input,
9 | OnDestroy,
10 | OnInit,
11 | Output,
12 | ViewChild
13 | } from '@angular/core';
14 | import { AbstractControl, ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder, FormGroup } from '@angular/forms';
15 | import { Subscription } from 'rxjs';
16 |
17 | import { KEYS } from '../../shared/tag-input-keys';
18 |
19 | /**
20 | * Taken from @angular/common/src/facade/lang
21 | */
22 | function isBlank(obj: any): boolean {
23 | return obj === undefined || obj === null;
24 | }
25 |
26 | export interface AutoCompleteItem {
27 | [index: string]: any;
28 | }
29 |
30 | @Component({
31 | selector: 'rl-tag-input',
32 | template: `
33 |
39 |
40 |
61 | `,
62 | styles: [`
63 | :host {
64 | font-family: "Roboto", "Helvetica Neue", sans-serif;
65 | font-size: 16px;
66 | display: block;
67 | box-shadow: 0 1px #ccc;
68 | padding: 8px 0 6px 0;
69 | will-change: box-shadow;
70 | transition: box-shadow 0.12s ease-out;
71 | }
72 |
73 | :host .ng2-tag-input-form {
74 | display: inline;
75 | }
76 |
77 | :host .ng2-tag-input-field {
78 | font-family: "Roboto", "Helvetica Neue", sans-serif;
79 | font-size: 16px;
80 | display: inline-block;
81 | width: auto;
82 | box-shadow: none;
83 | border: 0;
84 | padding: 8px 0;
85 | }
86 |
87 | :host .ng2-tag-input-field:focus {
88 | outline: 0;
89 | }
90 |
91 | :host .rl-tag-input-autocomplete-container {
92 | position: relative;
93 | z-index: 10;
94 | }
95 |
96 | :host.ng2-tag-input-focus {
97 | box-shadow: 0 2px #0d8bff;
98 | }
99 | `],
100 | providers: [
101 | {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagInputComponent), multi: true},
102 | ]
103 | })
104 | export class TagInputComponent implements ControlValueAccessor, OnDestroy, OnInit {
105 | @HostBinding('class.ng2-tag-input-focus') isFocused;
106 | @Input() addOnBlur: boolean = true;
107 | @Input() addOnComma: boolean = true;
108 | @Input() addOnEnter: boolean = true;
109 | @Input() addOnPaste: boolean = true;
110 | @Input() addOnSpace: boolean = false;
111 | @Input() allowDuplicates: boolean = false;
112 | @Input() allowedTagsPattern: RegExp = /.+/;
113 | @Input() autocomplete: boolean = false;
114 | @Input() autocompleteItems: string[] = [];
115 | @Input() autocompleteMustMatch: boolean = true;
116 | @Input() autocompleteSelectFirstItem: boolean = true;
117 | @Input() pasteSplitPattern: string = ',';
118 | @Input() placeholder: string = 'Add a tag';
119 | @Output('addTag') addTag: EventEmitter = new EventEmitter();
120 | @Output('removeTag') removeTag: EventEmitter = new EventEmitter();
121 | @ViewChild('tagInputElement') tagInputElement: ElementRef;
122 |
123 | private canShowAutoComplete: boolean = false;
124 | private tagInputSubscription: Subscription;
125 | private splitRegExp: RegExp;
126 | private get tagInputField(): AbstractControl {
127 | return this.tagInputForm.get('tagInputField');
128 | }
129 | private get inputValue(): string {
130 | return this.tagInputField.value;
131 | }
132 |
133 | public tagInputForm: FormGroup;
134 | public autocompleteResults: string[] = [];
135 | public tagsList: string[] = [];
136 | public selectedTag: number;
137 |
138 | @HostListener('document:click', ['$event', '$event.target']) onDocumentClick(event: MouseEvent, target: HTMLElement) {
139 | if (!target) {
140 | return;
141 | }
142 |
143 | if (!this.elementRef.nativeElement.contains(target)) {
144 | this.canShowAutoComplete = false;
145 | }
146 | }
147 |
148 | constructor(
149 | private fb: FormBuilder,
150 | private elementRef: ElementRef) {}
151 |
152 | ngOnInit() {
153 | this.splitRegExp = new RegExp(this.pasteSplitPattern);
154 |
155 | this.tagInputForm = this.fb.group({
156 | tagInputField: ''
157 | });
158 |
159 | this.tagInputSubscription = this.tagInputField.valueChanges
160 | .do(value => {
161 | this.autocompleteResults = this.autocompleteItems.filter(item => {
162 | /**
163 | * _isTagUnique makes sure to remove items from the autocompelte dropdown if they have
164 | * already been added to the model, and allowDuplicates is false
165 | */
166 | return item.toLowerCase().indexOf(value.toLowerCase()) > -1 && this._isTagUnique(item);
167 | });
168 | })
169 | .subscribe();
170 | }
171 |
172 | onKeydown(event: KeyboardEvent): void {
173 | let key = event.keyCode;
174 | switch (key) {
175 | case KEYS.backspace:
176 | this._handleBackspace();
177 | break;
178 |
179 | case KEYS.enter:
180 | if (this.addOnEnter && !this.showAutocomplete()) {
181 | this._addTags([this.inputValue]);
182 | event.preventDefault();
183 | }
184 | break;
185 |
186 | case KEYS.comma:
187 | if (this.addOnComma) {
188 | this._addTags([this.inputValue]);
189 | event.preventDefault();
190 | }
191 | break;
192 |
193 | case KEYS.space:
194 | if (this.addOnSpace) {
195 | this._addTags([this.inputValue]);
196 | event.preventDefault();
197 | }
198 | break;
199 |
200 | default:
201 | break;
202 | }
203 | }
204 |
205 | onInputBlurred(event): void {
206 | if (this.addOnBlur) { this._addTags([this.inputValue]); }
207 | this.isFocused = false;
208 | }
209 |
210 | onInputFocused(): void {
211 | this.isFocused = true;
212 | setTimeout(() => this.canShowAutoComplete = true);
213 | }
214 |
215 | onInputPaste(event): void {
216 | let clipboardData = event.clipboardData || (event.originalEvent && event.originalEvent.clipboardData);
217 | let pastedString = clipboardData.getData('text/plain');
218 | let tags = this._splitString(pastedString);
219 | this._addTags(tags);
220 | setTimeout(() => this._resetInput());
221 | }
222 |
223 | onAutocompleteSelect(selectedItem) {
224 | this._addTags([selectedItem]);
225 | this.tagInputElement.nativeElement.focus();
226 | }
227 |
228 | onAutocompleteEnter() {
229 | if (this.addOnEnter && this.showAutocomplete() && !this.autocompleteMustMatch) {
230 | this._addTags([this.inputValue]);
231 | }
232 | }
233 |
234 | showAutocomplete(): boolean {
235 | return (
236 | this.autocomplete &&
237 | this.autocompleteItems &&
238 | this.autocompleteItems.length > 0 &&
239 | this.canShowAutoComplete &&
240 | this.inputValue.length > 0
241 | );
242 | }
243 |
244 | private _splitString(tagString: string): string[] {
245 | tagString = tagString.trim();
246 | let tags = tagString.split(this.splitRegExp);
247 | return tags.filter((tag) => !!tag);
248 | }
249 |
250 | private _isTagValid(tagString: string): boolean {
251 | return this.allowedTagsPattern.test(tagString) &&
252 | this._isTagUnique(tagString);
253 | }
254 |
255 | private _isTagUnique(tagString: string): boolean {
256 | return this.allowDuplicates ? true : this.tagsList.indexOf(tagString) === -1;
257 | }
258 |
259 | private _isTagAutocompleteItem(tagString: string): boolean {
260 | return this.autocompleteItems.indexOf(tagString) > -1;
261 | }
262 |
263 | private _emitTagAdded(addedTags: string[]): void {
264 | addedTags.forEach(tag => this.addTag.emit(tag));
265 | }
266 |
267 | private _emitTagRemoved(removedTag): void {
268 | this.removeTag.emit(removedTag);
269 | }
270 |
271 | private _addTags(tags: string[]): void {
272 | let validTags = tags.map(tag => tag.trim())
273 | .filter(tag => this._isTagValid(tag))
274 | .filter((tag, index, tagArray) => tagArray.indexOf(tag) === index)
275 | .filter(tag => (this.showAutocomplete() && this.autocompleteMustMatch) ? this._isTagAutocompleteItem(tag) : true);
276 |
277 | this.tagsList = this.tagsList.concat(validTags);
278 | this._resetSelected();
279 | this._resetInput();
280 | this.onChange(this.tagsList);
281 | this._emitTagAdded(validTags);
282 | }
283 |
284 | private _removeTag(tagIndexToRemove: number): void {
285 | let removedTag = this.tagsList[tagIndexToRemove];
286 | this.tagsList.splice(tagIndexToRemove, 1);
287 | this._resetSelected();
288 | this.onChange(this.tagsList);
289 | this._emitTagRemoved(removedTag);
290 | }
291 |
292 | private _handleBackspace(): void {
293 | if (!this.inputValue.length && this.tagsList.length) {
294 | if (!isBlank(this.selectedTag)) {
295 | this._removeTag(this.selectedTag);
296 | } else {
297 | this.selectedTag = this.tagsList.length - 1;
298 | }
299 | }
300 | }
301 |
302 | private _resetSelected(): void {
303 | this.selectedTag = null;
304 | }
305 |
306 | private _resetInput(): void {
307 | this.tagInputField.setValue('');
308 | }
309 |
310 | /** Implemented as part of ControlValueAccessor. */
311 | onChange: (value: any) => any = () => { };
312 |
313 | onTouched: () => any = () => { };
314 |
315 | writeValue(value: any) {
316 | this.tagsList = value;
317 | }
318 |
319 | registerOnChange(fn: any) {
320 | this.onChange = fn;
321 | }
322 |
323 | registerOnTouched(fn: any) {
324 | this.onTouched = fn;
325 | }
326 |
327 | ngOnDestroy() {
328 | this.tagInputSubscription.unsubscribe();
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/lib/shared/tag-input-keys.ts:
--------------------------------------------------------------------------------
1 | export const KEYS = {
2 | backspace: 8,
3 | comma: 188,
4 | downArrow: 40,
5 | enter: 13,
6 | esc: 27,
7 | space: 32,
8 | upArrow: 38
9 | };
10 |
--------------------------------------------------------------------------------
/lib/tag-input.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4 | import 'rxjs/add/observable/fromEvent';
5 | import 'rxjs/add/operator/filter';
6 | import 'rxjs/add/operator/do';
7 |
8 | import { TagInputAutocompleteComponent } from './components/tag-input-autocomplete/tag-input-autocomplete.component';
9 | import { TagInputComponent } from './components/tag-input/tag-input.component';
10 | import { TagInputItemComponent } from './components/tag-input-item/tag-input-item.component';
11 |
12 | @NgModule({
13 | imports: [
14 | CommonModule,
15 | FormsModule,
16 | ReactiveFormsModule
17 | ],
18 | declarations: [
19 | TagInputAutocompleteComponent,
20 | TagInputComponent,
21 | TagInputItemComponent
22 | ],
23 | exports: [
24 | TagInputComponent
25 | ]
26 | })
27 | export class RlTagInputModule {}
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular2-tag-input",
3 | "version": "1.2.3",
4 | "description": "Tag input component for Angular 2",
5 | "main": "dist/index.js",
6 | "keywords": [
7 | "angular2",
8 | "tag input",
9 | "taginput",
10 | "tags",
11 | "chips",
12 | "chips input"
13 | ],
14 | "scripts": {
15 | "clean": "./node_modules/.bin/rimraf ./dist/",
16 | "package:aot": "npm run clean && npm run build:aot",
17 | "build:aot": "./node_modules/.bin/ngc -p tsconfig-aot.json",
18 | "serve": "webpack-dev-server --config config/webpack.dev.js --progress --profile --watch --content-base demo/",
19 | "lint": "tslint lib/**.*.ts"
20 | },
21 | "author": "TEAM COOL",
22 | "license": "ISC",
23 | "peerDependencies": {
24 | "@angular/common": "2.2.4",
25 | "@angular/core": "2.2.4",
26 | "@angular/forms": "2.2.4"
27 | },
28 | "devDependencies": {
29 | "@angular/common": "2.2.4",
30 | "@angular/compiler": "2.2.4",
31 | "@angular/compiler-cli": "2.2.4",
32 | "@angular/core": "2.2.4",
33 | "@angular/forms": "2.2.4",
34 | "@angular/platform-browser": "2.2.4",
35 | "@angular/platform-browser-dynamic": "2.2.4",
36 | "@angular/platform-server": "2.2.4",
37 | "@types/node": "^6.0.45",
38 | "awesome-typescript-loader": "^2.2.3",
39 | "codelyzer": "~0.0.26",
40 | "core-js": "^2.4.1",
41 | "node-sass": "^3.10.1",
42 | "postcss-loader": "^0.13.0",
43 | "raw-loader": "^0.5.1",
44 | "rimraf": "^2.5.4",
45 | "rxjs": "5.0.0-beta.12",
46 | "sass-loader": "^4.0.2",
47 | "source-map-loader": "^0.1.5",
48 | "tslint": "3.13.0",
49 | "typescript": "2.0.10",
50 | "webpack": "^2.1.0-beta.21",
51 | "webpack-dev-server": "^2.1.0-beta.0",
52 | "zone.js": "^0.6.25"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tsconfig-aot.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "declaration": true,
7 | "noEmitHelpers": false,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "sourceMap": true,
11 | "pretty": true,
12 | "allowUnreachableCode": true,
13 | "allowUnusedLabels": true,
14 | "noImplicitAny": false,
15 | "noImplicitReturns": false,
16 | "noImplicitUseStrict": false,
17 | "noFallthroughCasesInSwitch": false,
18 | "allowSyntheticDefaultImports": true,
19 | "suppressExcessPropertyErrors": true,
20 | "suppressImplicitAnyIndexErrors": true,
21 | "outDir": "dist",
22 | "declarationDir": "dist",
23 | "lib": ["es6", "dom"],
24 | "skipLibCheck": true
25 | },
26 | "files": [
27 | "index.ts"
28 | ],
29 | "exclude": [
30 | "node_modules",
31 | "demo"
32 | ],
33 | "compileOnSave": false,
34 | "buildOnSave": false,
35 | "awesomeTypescriptLoaderOptions": {
36 | "forkChecker": false
37 | },
38 | "angularCompilerOptions": {
39 | "genDir": "dist/",
40 | "skipMetadataEmit": false
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "removeComments": true,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "declaration": true,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "lib": ["es6", "dom"],
13 | "typeRoots": [
14 | "./node_modules/@types"
15 | ]
16 | },
17 | "files": [
18 | "./index.ts"
19 | ],
20 | "include": [
21 | "lib"
22 | ],
23 | "exclude": [
24 | "node_modules"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "class-name": true,
7 | "comment-format": [
8 | true,
9 | "check-space"
10 | ],
11 | "curly": true,
12 | "eofline": true,
13 | "forin": true,
14 | "indent": [
15 | true,
16 | "spaces"
17 | ],
18 | "label-position": true,
19 | "label-undefined": true,
20 | "max-line-length": [
21 | true,
22 | 140
23 | ],
24 | "member-access": false,
25 | "member-ordering": [
26 | true,
27 | "static-before-instance",
28 | "variables-before-functions"
29 | ],
30 | "no-arg": true,
31 | "no-bitwise": true,
32 | "no-console": [
33 | true,
34 | "debug",
35 | "info",
36 | "time",
37 | "timeEnd",
38 | "trace"
39 | ],
40 | "no-construct": true,
41 | "no-debugger": true,
42 | "no-duplicate-key": true,
43 | "no-duplicate-variable": true,
44 | "no-empty": false,
45 | "no-eval": true,
46 | "no-inferrable-types": true,
47 | "no-shadowed-variable": true,
48 | "no-string-literal": false,
49 | "no-switch-case-fall-through": true,
50 | "no-trailing-whitespace": true,
51 | "no-unused-expression": true,
52 | "no-unused-variable": true,
53 | "no-unreachable": true,
54 | "no-use-before-declare": true,
55 | "no-var-keyword": true,
56 | "object-literal-sort-keys": false,
57 | "one-line": [
58 | true,
59 | "check-open-brace",
60 | "check-catch",
61 | "check-else",
62 | "check-whitespace"
63 | ],
64 | "quotemark": [
65 | true,
66 | "single"
67 | ],
68 | "radix": true,
69 | "semicolon": [
70 | "always"
71 | ],
72 | "triple-equals": [
73 | true,
74 | "allow-null-check"
75 | ],
76 | "typedef-whitespace": [
77 | true,
78 | {
79 | "call-signature": "nospace",
80 | "index-signature": "nospace",
81 | "parameter": "nospace",
82 | "property-declaration": "nospace",
83 | "variable-declaration": "nospace"
84 | }
85 | ],
86 | "variable-name": false,
87 | "whitespace": [
88 | true,
89 | "check-branch",
90 | "check-decl",
91 | "check-operator",
92 | "check-separator",
93 | "check-type"
94 | ],
95 |
96 | "directive-selector-prefix": [true, "rl"],
97 | "component-selector-prefix": [true, "rl"],
98 | "directive-selector-name": [true, "camelCase"],
99 | "component-selector-name": [true, "kebab-case"],
100 | "directive-selector-type": [true, "attribute"],
101 | "component-selector-type": [true, "element"],
102 | "use-input-property-decorator": true,
103 | "use-output-property-decorator": true,
104 | "use-host-property-decorator": true,
105 | "no-input-rename": true,
106 | "no-output-rename": true,
107 | "use-life-cycle-interface": true,
108 | "use-pipe-transform-interface": true,
109 | "component-class-suffix": true,
110 | "directive-class-suffix": true
111 | }
112 | }
113 |
--------------------------------------------------------------------------------