Tagify - tags input component
Transforms an input field or a textarea into a Tags component, in an easy, customizable way,
with great performance and small code footprint, exploded with features.
Vanilla โก React โก Vue โก Angular
๐ See Demos ๐
- Installation
- What can Tagify do
- Building the project
- Adding tags dynamically
- Ajax whitelist
- Edit tags
- DOM Templates
- Suggestions selectbox
- React wrapper
- Angular wrapper
- Vue Example
- jQuery version
- FAQ
- Methods
- Events
- Settings
npm i @yaireo/tagify --save
import Tagify from '@yaireo/tagify'
var tagify = new Tagify(...)
Don't forget to include
tagify.css
file in your project. CSS location:@yaireo/tagify/dist/tagify.css
SCSS location:@yaireo/tagify/src/tagify.scss
See SCSS usecase & example
- Can be applied on input & textarea elements
- Supports mix content (text and tags together)
- Supports single-value mode (like
<select>
) - Supports whitelist/blacklist
- Supports Templates for: component wrapper, tag items, suggestion list & suggestion items
- Shows suggestions selectbox (flexiable settings & styling) at full (component) width or next to the typed texted (caret)
- Allows setting suggestions' aliases for easier fuzzy-searching
- Auto-suggest input as-you-type with ability to auto-complete
- Can paste in multiple values:
tag 1, tag 2, tag 3
or even newline-separated tags - Tags can be created by Regex delimiter or by pressing the "Enter" key / focusing of the input
- Validate tags by Regex pattern
- Tags are editable
ARIA accessibility support(Component too generic for any meaningful ARIA)- Supports read-only mode to the whole componenet or per-tag
- Each tag can have any properties desired (class, data-whatever, readonly...)
- Automatically disallow duplicate tags (vis "settings" object)
- Has built-in CSS loader, if needed (Ex. AJAX whitelist pulling)
- Tags can be trimmed via
hellip
by givingmax-width
to thetag
element in yourCSS
- Easily change direction to RTL (via the SCSS file)
- Internet Explorer - A polyfill script can be used:
tagify.polyfills.min.js
in/dist
- Many useful custom events
- Original input/textarea element values kept in sync with Tagify
Simply run gulp
in your terminal, from the project's path (Gulp should be installed first).
Source files are this path: /src/
Output files, which are automatically generated using Gulp, are in: /dist/
The rest of the files are most likely irrelevant.
var tagify = new Tagify(...);
tagify.addTags(["banana", "orange", "apple"])
// or add tags with pre-defined propeties
tagify.addTags([{value:"banana", color:"yellow"}, {value:"apple", color:"red"}, {value:"watermelon", color:"green"}])
There are two possible ways to get the value of the tags:
- Access the tagify's instance's
value
prop:tagify.value
(Array of tags) - Access the original input's value:
inputElm.value
(Stringified Array of tags)
The most common way is to simply listen to the change
event on the original input
var inputElm = document.querySelector,
tagify = new Tagify (inputElm);
inputElm.addEventListener('change', onChange)
function onChange(e){
// outputs a String
console.log(e.target.value)
}
Dynamically-loaded suggestions list (whitelist) from the server (as the user types) is a frequent need to many.
Tagify comes with its own loading animation, which is a very lightweight CSS-only code, and the loading
state is controlled by the method tagify.loading
which accepts true
or false
as arguments.
Below is a basic example using the fetch
API. I advise to abort the last request on any input before starting a new request.
Example:
var input = document.querySelector('input'),
tagify = new Tagify(input, {whitelist:[]}),
controller; // for aborting the call
// listen to any keystrokes which modify tagify's input
tagify.on('input', onInput)
function onInput( e ){
var value = e.detail.value;
tagify.settings.whitelist.length = 0; // reset the whitelist
// https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
controller && controller.abort();
controller = new AbortController();
// show loading animation and hide the suggestions dropdown
tagify.loading(true).dropdown.hide.call(tagify)
fetch('http://get_suggestions.com?value=' + value, {signal:controller.signal})
.then(RES => RES.json())
.then(function(whitelist){
// update inwhitelist Array in-place
tagify.settings.whitelist.splice(0, whitelist.length, ...whitelist)
tagify.loading(false).dropdown.show.call(tagify, value); // render the suggestions dropdown
})
}
Tags which aren't read-only
can be edited by double-clicking them (by default)
or by changing the editTags
setting to 1
, making tags editable by single-clicking them.
The value is saved on blur
or by pressing enter
key. Pressing Escape
will revert the change trigger blur
.
ctrlz will revert the change if an edited tag was marked as not valid (perhaps duplicate or blacklisted)
To prevent all tags from being allowed to be editable, set the editTags
setting to false
(or null
).
To do the same but for specific tag(s), set those tags' data with editable
property set to false
:
<input value='[{"value":"foo", "editable":false}, {"value":"bar"}]'>
It's possible to control the templates for the following HTML elements tagify generates by
modying the settings.templates
Object with your own custom functions which should output a string.
Default templates
templates : {
wrapper(input, settings){
return `<tags class="tagify ${settings.mode ? "tagify--" + settings.mode : ""} ${input.className}"
${settings.readonly ? 'readonly aria-readonly="true"' : 'aria-haspopup="listbox" aria-expanded="false"'}
role="tagslist"
tabIndex="-1">
<span contenteditable data-placeholder="${settings.placeholder || '​'}" aria-placeholder="${settings.placeholder || ''}"
class="tagify__input"
role="textbox"
aria-controls="dropdown"
aria-autocomplete="both"
aria-multiline="${settings.mode=='mix'?true:false}"></span>
</tags>`
},
tag(value, tagData){
return `<tag title='${tagData.title || value}'
contenteditable='false'
spellcheck='false'
tabIndex="-1"
class='tagify__tag ${tagData.class ? tagData.class : ""}'
${this.getAttributes(tagData)}>
<x title='' class='tagify__tag__removeBtn' role='button' aria-label='remove tag'></x>
<div>
<span class='tagify__tag-text'>${value}</span>
</div>
</tag>`
},
dropdown(settings){
var _s = settings.dropdown,
className = `${_s.position == 'manual' ? "" : `tagify__dropdown tagify__dropdown--${_s.position}`} ${_s.classname}`.trim();
return `<div class="${className}" role="listbox" aria-labelledby="dropdown">
<div class="tagify__dropdown__wrapper"></div>
<button class="tagify__dropdown__addNewBtn">add new</button>
</div>`
},
dropdownItem( item ){
return `<div ${this.getAttributes(item)}
class='tagify__dropdown__item ${item.class ? item.class : ""}'
tabindex="0"
role="option">${item.value}</div>`
}
}
The suggestions selectbox is shown is a whitelist Array of Strings or Objects was passed in the settings when the Tagify instance was created. Suggestions list will only be rendered if there are at least two matching sugegstions (case-insensetive).
The selectbox dropdown will be appended to the document's <body>
element and will be rendered by default in a position below (bottom of) the Tagify element.
Using the keyboard arrows up/down will highlight an option from the list, and hitting the Enter key to select.
It is possible to tweak the selectbox dropdown via 2 settings:
enabled
- this is a numeral value which tells Tagify when to show the suggestions dropdown, when a minimum of N characters were typed.maxItems
- Limits the number of items the suggestions selectbox will render
var input = document.querySelector('input'),
tagify = new Tagify(input, {
whitelist : ['aaa', 'aaab', 'aaabb', 'aaabc', 'aaabd', 'aaabe', 'aaac', 'aaacc'],
dropdown : {
classname : "color-blue",
enabled : 0, // show the dropdown immediately on focus
maxItems : 5,
position : "text", // place the dropdown near the typed text
closeOnSelect : false, // keep the dropdown open after selecting a suggestion
highlightFirst: true
}
});
Will render
<div class="tagify__dropdown tagify__dropdown--text" style="left:993.5px; top:106.375px; width:616px;">
<div class="tagify__dropdown__wrapper">
<div class="tagify__dropdown__item tagify__dropdown__item--active" value="aaab">aaab</div>
<div class="tagify__dropdown__item" value="aaabb">aaabb</div>
<div class="tagify__dropdown__item" value="aaabc">aaabc</div>
<div class="tagify__dropdown__item" value="aaabd">aaabd</div>
<div class="tagify__dropdown__item" value="aaabe">aaabe</div>
</div>
</div>
By default searching the suggestions is using fuzzy-search (see settings).
If you wish to assign alias to items (in your suggestion list), add the searchBy
property to whitelist items you wish
to have an alias for. In the below example, when typing a part of string which is included in the searchBy
property, the suggested item will
match "Israel" will be rendered in the suggestion list selectbox.
Example for a suggestion item alias
whitelist = [
...
{ value:'Israel', code:'IL', searchBy:'holy land, desert, middle east' },
...
]
Another handy setting is dropdown.searchKeys
which, like the above dropdown.searchBy
setting, allows
expanding the search of any typed terms to more than the value
property of the whitelist items (if items are a Collection).
[
{
value : "foo",
nickname : "bar",
email : "[email protected]"
},
...
]
// setting to search in other keys:
{
dropdown: {
searchKeys: ["nickname", "email"] // "value" & "searchBy" key are searched in, regardless
}
}
To use this feature it must be toggled - see settings.
When mixing text with tags, the original textarea (or input) element will have a value as follows:
[[cartman]]โ and [[kyle]]โ do not know [[Homer simpson]]โ
If the inital value of the textarea or input is formatted as the above example, tagify will try to
automatically convert everything between [[
& ]]
to a tag, if tag exists in the whitelist, so make
sure when the Tagify instance is initialized, that it has tags with the correct value
property that match
the same values that appear between [[
& ]]
.
Applying the setting dropdown.position:"text"
is encouraged for mixed-content tags, because the suggestions list
will be rendered right next to the caret location and not the the bottom of the Tagify componenet, which might look
weird when there is already a lot of content at multiple lines.
If a tag does not exists in the whitelist, it may be created by the user and all you should do is listen to the add
event and update your local/remote state.
Similar to native <Select>
element, but allows typing free text as value.
A Tagify React component is exported as <Tags>
from react.tagify.js
:
import Tags from "@yaireo/tagify/dist/react.tagify" // React-wrapper file
const App = () => {
return (
<Tags
settings={settings} // tagify settings object
value="a,b,c"
{...tagifyProps} // dynamic props such as "loading", "showDropdown:'abc'", "value"
onChange={e => (e.persist(), console.log("CHANGED:", e.target.value))}
/>
)
})
See live demo for React integration examples.
TagifyComponent which will be used by your template as <tagify>
Example:
<div>
testing tagify wrapper
<tagify [settings]="settings"
(add)="onAdd($event)"
(remove)="onRemove($event)">
</tagify>
<button (click)="clearTags()">clear</button>
<button (click)="addTags()">add Tags</button>
</div>
TagifyService
(The tagifyService is a singletone injected by angular, do not create a new instance of it) Remember to add
TagifyService
to your module definition.
Example:
import {Component, OnDestroy} from '@angular/core';
import {TagifyService} from '@yaireo/tagify';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
constructor(private tagifyService: TagifyService) {}
public settings = { blacklist: ['fucking', 'shit']};
onAdd(tagify) {
console.log('added a tag', tagify);
}
onRemove(tags) {
console.log('removed a tag', tags);
}
clearTags() {
this.tagifyService.removeAll();
}
addTags() {
this.tagifyService.addTags(['this', 'is', 'cool']);
}
ngOnDestroy() {
this.tagifyService.destroy();
}
}
jQuery.tagify.js
A jQuery wrapper verison is also available, but I advise not using it because it's basically the exact same as the "normal" script (non-jqueryfied) and all the jQuery's wrapper does is allowing to chain the event listeners for ('add', 'remove', 'invalid')
$('[name=tags]')
.tagify()
.on('add', function(e, tagName){
console.log('added', tagName)
});
Accessing methods can be done via the .data('tagify')
:
$('[name=tags]').tagify();
// get tags from the server (ajax) and add them:
$('[name=tags]').data('tagify').addTags('aaa, bbb, ccc')
List of questions & scenarios which might come up during development with Tagify:
In framework-less projects, the developer should save the state of the Tagify component (somewhere), and
the question is:
when should the state be saved?
On every change made to Tagify's internal state (tagify.value
via the update()
method).
var tagify = new Tagify(...)
// listen to "change" events on the "original" input/textarea element
tagify.DOM.originalInput.addEventListener('change', onTagsChange)
// This example uses async/await but you can use Promises, of course, if you prefer.
async function onTagsChange(e){
const {name, value} = e.target
// "imaginary" async function "saveToServer" should get the field's name & value
await saveToServer(name, value)
}
If you are using React/Vue/Angular or any "modern" framework, then you already know how to
attach "onChange" event listeners to your <input>
/<textarea>
elements, so the above is irrelevant.
Default format is a JSON string:
'[{"value":"cat"}, {"value":"dog"}]'
I recommend keeping this because some situations might have values such as addresses (tags contain commas):
'[{"value":"Apt. 2A, Jacksonville, FL 39404"}, {"value":"Forrest Ray, 191-103 Integer Rd., Corona New Mexico"}]'
Another example for complex tags state might be disabled tags, or ones with custom identifier class:
(tags can be clicked, so delevopers can choose to use this to disable/enable tags)
'[{"value":"cat", "disabled":true}, {"value":"dog"}, {"value":"bird", "class":"color-green"}]'
To chnage the format, assuming your tags have no commas and are fairly simple:
var tagify = new Tagify(inputElm, {
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(',')
})
Output:
"cat,dog"
Stopping tags from wrapping to new lines, add this to your .tagify
CSS Rule:
flex-wrap: nowrap;
- Double-click tag fires both "edit" & "click" custom events
- Manualy open the suggestions dropdown
- Render your own suggestions dropdown
- Allow max length on mix mode
- Always show dropdown
- Limit the length of a tag value (minimum & maximum)
- Mixed mode initial value
- Random colors for each tag
- Format input value for server side
- Writing to tagify textarea
- Scroll all tags within one line, instead of growing vertically
- Insert emoji at caret location when editing a tag
- propagate
change
event - Manually update tag data after it was added
- Ajax Whitelist with "enforceWhitelist" setting enabled
- Custom (mutiple) tag valitation & AJAX
CSS Variables (see MDN docs)
These CSS variables allow easy customization without the need to manually write CSS. If you do wish to heavily style your Tagify components, then you can use these variables within your modified styles.
For example how this can be used, see the demos page.
Name | Info |
---|---|
--tags-border-color | The outer border color which surrounds tagify |
--tags-hover-border-color | hover state |
--tags-focus-border-color | focus state |
--tag-bg | Tag background color |
--tag-hover | Tag background color on hover (mouse) |
--tag-text-color | Tag text color |
--tag-text-color--edit | Tag text color when a Tag is being edited |
--tag-pad | Tag padding, from all sides. Ex. .3em .5em |
--tag--min-width | Minimum Tag width |
--tag--max-width | Maximum tag width, which gets trimmed with hellip after |
--tag-inset-shadow-size | This is the inner shadow size, which dictates the color of the Tags. It's important the size fits exactly to the tag. Change this if you change the --tag-pad or fontsize. |
--tag-invalid-color | For border color of edited tags with invalid value being typed into them |
--tag-invalid-bg | Background color for invalid Tags. |
--tag-remove-bg | Tag background color when hovering the ร button. |
--tag-remove-btn-bg | The remove (ร ) button background color |
--tag-remove-btn-bg--hover | The remove (ร ) button background color on hover |
--loader-size | Loading animation size. 1em is pretty big, default is a bit less. |
--tag-hide-transition | Controls the transition property when a tag is removed. default is '.3s' |
--placeholder-color | Placeholder text color |
Tagify
is prototype based and There are many methods, but I've chosen to list the most relevant ones:
Name | Parameters | Info |
---|---|---|
destroy | Reverts the input element back as it was before Tagify was applied | |
removeAllTags | Removes all tags and resets the original input tag's value property | |
addTags | tagsItems , clearInput , skipInvalid |
Accepts a String (word, single or multiple with a delimiter), an Array of Objects (see above) or Strings |
removeTags | Array/Node/String, silent , tranDuration |
(#502) Remove single/multiple Tags. When nothing passed, removes last tag.
|
loadOriginalValues | String/Array | Converts the input's value into tags. This method gets called automatically when instansiating Tagify. Also works for mixed-tags |
getTagIndexByValue | String | Returns the index of a specific tag, by value |
parseMixTags | String | Converts a String argument ([[foo]]โ and [[bar]]โ are.. ) into HTML with mixed tags & texts |
getTagElms | Returns a DOM nodes list of all the tags | |
getTagElmByValue | String | Returns a specific tag DOM node by value |
editTag | Node | Goes to edit-mode in a specific tag |
replaceTag | tagElm , tagData |
Exit a tag's edit-mode. if "tagData" exists, replace the tag element with new data and update Tagify value |
loading | Boolean | Toogle loading state on/off (Ex. AJAX whitelist pulling) |
All triggered events return the instance's scope (tagify).
See e.detail
for custom event additional data.
See Examples
var tagify = new Tagify(...)
// events are chainable and multiple events may be binded for the same callback
tagify
.on('input', e => console.log(e.detail))
.on('edit:input edit:updated edit:start edit:keydown', e => console.log(e.type, e.detail))
Name | Info |
---|---|
add | A tag has been added |
remove | A tag has been removed (use removeTag intead with jQuery) |
invalid | A tag has been added but did not pass vaildation. See event detail |
input | Input event, when a tag is being typed/edited. e.detail exposes value , inputElm & isValid |
click | Clicking a tag. Exposes the tag element, its index & data |
dblclick | Double-clicking a tag |
keydown | When tagify input has focus and a key was pressed |
focus | The component currently has focus |
blur | The component lost focus |
edit:input | Typing inside an edited tag |
edit:beforeUpdate | Just before a tag has been updated, while still in "edit" mode |
edit:updated | A tag as been updated (changed view editing or by directly calling the replaceTag() method) |
edit:start | A tag is now in "edit mode" |
edit:keydown | keydown event while an edited tag is in focus |
dropdown:show | Suggestions dropdown is to be rendered. The dropdown DOM node is passed in the callback, see demo. |
dropdown:hide | Suggestions dropdown has been removed from the DOM |
dropdown:select | Suggestions dropdown item selected (by mouse/keyboard/touch) |
dropdown:scroll | Tells the percentage scrolled. (event.detail.percentage ) |
Promise-based hooks for async program flow scenarios.
Allows to "hook" (intervene) at certain points of the program, which were selected as a suitable place to pause and wait for further instructions on how to procceed.
For example, if a developer wishes to add a (native) confirmation popup before a tag is removed (by a user action):
var input = document.querySelector('input')
var tagify = new Tagify(input,{
hooks: {
/**
* Removes a tag
* @param {Array} tags [Array of Objects [{node:..., data:...}, {...}, ...]]
*/
beforeRemoveTag : function( tags ){
return new Promise((resolve, reject) => {
confirm("Remove " + tags[0].data.value + "?")
? resolve()
: reject()
})
}
}
})
Name | Parameters | Info |
---|---|---|
beforeRemoveTag | Array (of Objects) | Example |
Name | Type | Default | Info |
---|---|---|---|
placeholder | String | Placeholder text. If this attribute is set on an input/textarea element it will override this setting | |
delimiters | String | , |
[RegEx string] split tags by any of these delimiters. Example: `", |
pattern | String/RegEx | null | Validate input by RegEx pattern (can also be applied on the input itself as an attribute) Ex: /[1-9]/ |
mode | String | null | Use select for single-value dropdown-like select box. See mix as value to allow mixed-content. The 'pattern' setting must be set to some character. |
mixTagsInterpolator | Array | ['[[', ']]'] |
Interpolation for mix mode. Everything between these will become a tag |
mixTagsAllowedAfter | RegEx | /,|\.|\:|\s/ |
Define conditions in which typed mix-tags content is allowing a tag to be created after. |
duplicates | Boolean | false | Should duplicate tags be allowed or not |
enforceWhitelist | Boolean | false | Should ONLY use tags allowed in whitelist. In mix-mode , setting it to false will not allow creating new tags. |
autoComplete.enabled | Boolean | true | Tries to suggest the input's value while typing (match from whitelist) by adding the rest of term as grayed-out text |
autoComplete.rightKey | Boolean | false | If true , when โ is pressed, use the suggested value to create a tag, else just auto-completes the input. In mixed-mode this is ignored and treated as "true" |
whitelist | Array | [] | An array of tags which only they are allowed |
blacklist | Array | [] | An array of tags which aren't allowed |
addTagOnBlur | Boolean | true | Automatically adds the text which was inputed as a tag when blur event happens |
callbacks | Object | {} | Exposed callbacks object to be triggered on events: 'add' / 'remove' tags |
maxTags | Number | Infinity | Maximum number of allowed tags. when reached, adds a class "tagify--hasMaxTags" to <Tags> |
editTags | Number | 2 | Number of clicks on a tag to enter "edit" mode. Only 1 or 2 work. false or null will disallow editing |
templates | Object | wrapper , tag , dropdownItem |
Object consisting of functions which return template strings |
transformTag | Function | undefined | Takes a tag input as argument and returns a transformed value |
keepInvalidTags | Boolean | false | If true , do not remove tags which did not pass validation |
skipInvalid | Boolean | false | If true , do not add invalid, temporary, tags before automatically removing them |
backspace | * | true | On pressing backspace key:true - remove last tag edit - edit last tag |
originalInputValueFormat | Function | If you wish your original input/textarea value property format to other than the default (which I recommend keeping) you may use this and make sure it returns a string. |
|
dropdown.enabled | Number | 2 | Minimum characters input for showing a suggestions list. false will not render a suggestions list. |
dropdown.maxItems | Number | 10 | Maximum items to show in the suggestions list |
dropdown.classname | String | "" |
Custom classname for the dropdown suggestions selectbox |
dropdown.fuzzySearch | Boolean | true | Enables filtering dropdown items values' by string containing and not only beginning |
dropdown.accentedSearch | Boolean | true | Enable searching for accented items in the whitelist without typing exact match (#491) |
dropdown.position | String | null |
|
dropdown.highlightFirst | Boolean | false | When a suggestions list is shown, highlight the first item, and also suggest it in the input (The suggestion can be accepted with โ key) |
dropdown.closeOnSelect | Boolean | true | close the dropdown after selecting an item, if enabled:0 is set (which means always show dropdown on focus) |
dropdown.mapValueTo | Function/String | if whitelist is an Array of Objects: Ex. [{value:'foo', email:'[email protected]'},...] )this setting controlls which data key will be printed in the dropdown. Ex. mapValueTo: data => "To:" + data.email Ex. mapValueTo: "email" |
|
dropdown.searchKeys | Array | ["value", "searchBy"] |
When a user types something and trying to match the whitelist items for suggestions, this setting allows matching other keys of a whitelist objects |