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
Table of Contents
-
Table of Contents
-
Installation
-
Option 1 - import from CDN:
-
option 2 - import as a Node module :
-
Usage (in your bundle):
-
Features
-
Building the project
-
Adding tags dynamically
-
Output value
-
Modify original input value format
-
Ajax whitelist
-
Persisted data
-
Edit tags
-
Validations
-
Drag & Sort
-
Integration example:
-
DOM Templates
-
Example of overriding the tag template:
-
Suggestions list
-
Example for a suggestion item alias
-
Example whitelist:
-
Mixed-Content
-
Single-Value
-
React
-
Update regarding onChange prop:
-
Updating the component's state
-
jQuery version
-
CSS Variables
-
Full list of Tagify's SCSS variables
-
Methods
-
Events
-
Hooks
-
Settings
Installation
$3
Place these lines before any other code which is (or will be) using
Tagify (
Example here )
``
html
`
Tagify
will then be available globally.
To load specific version use @
- for example: unpkg.com/@yaireo/tagify@3.1.0
$3
`
sh
npm i @yaireo/tagify --save
`
#### Usage (in your bundle):
live demo using Parcel as bundler
`
js
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
Features
* Can be applied on input & textarea elements
* Supports mix content (text and tags together)
* Supports single-value mode (like )
* Supports whitelist/blacklist
* Supports Templates for: component wrapper , tag items , suggestion list & suggestion items
Shows suggestions list (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* or by function
* Tags may be editable (double-click)
* 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 giving max-width to the tag element in your CSS
* Easily change direction to RTL (via the SCSS file)
Internet Explorer - A polyfill script should be used: tagify.polyfills.min.js (in /dist) (IE support has been dropped) *
* Many useful custom events
* Original input/textarea element values kept in sync with Tagify
Building the project
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.
Adding tags dynamically
`javascript
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"}])
`
Output value
There are two possible ways to get the value of the tags:
1. Access the tagify's instance's value prop: tagify.value (Array of tags)
2. 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
`javascript
var inputElm = document.querySelector,
tagify = new Tagify (inputElm);
inputElm.addEventListener('change', onChange)
function onChange(e){
// outputs a String
console.log(e.target.value)
}
`
$3
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 change the format, assuming your tags have no commas and are fairly simple:
`js
var tagify = new Tagify(inputElm, {
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(',')
})
`
Output:
"cat,dog"
Ajax whitelist
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:
`javascript
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.whitelist = null // 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()
fetch('http://get_suggestions.com?value=' + value, {signal:controller.signal})
.then(RES => RES.json())
.then(function(newWhitelist){
tagify.whitelist = newWhitelist // update whitelist Array in-place
tagify.loading(false).dropdown.show(value) // render the suggestions dropdown
})
}
`
Persisted data
Sometimes the whitelist might be loaded asynchronously, and so any pre-filled value in the original input field
will be removed if the enforceWhitelist is set to true.
Tagify can automatically restore the last used whitelist by setting a unique id to the Tagify instance,
by using the localstorage to persist the whitelist & value data:
`js
var input = document.querySelector('input'),
tagify = new Tagify(input, {
id: 'test1', // must be unique (per-tagify instance)
enforceWhitelist: true,
}),
`
Edit tags
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.
ctrl z 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:
`html
`
Validations
For "regular" tags (not mix-mode or select-mode ) the easiest way is to use the pattern setting and use a Regex, or
apply the pattern attribute directly on the input which will be "transformed" into a Tagify component (for vanilla code where the input tag is fully accessible to develops).
If the pattern setting does not meet your needs, use the validate setting, which recieves a tag data object as an argument and should return true if validaiton is passing, or false/string of not.
A string may be returned as the reason of the validation failure so it would be printed as the title attribute of the invalid tag.
Note - there is a setting to keep invalid tags (keepInvalidTags) and if it's set to true, the user can see the reason for the invalidation by
hovering the tag and see the browser's native tooltip via the title attribute:
`js
{
empty : "empty",
exceed : "number of tags exceeded",
pattern : "pattern mismatch",
duplicate : "already exists",
notAllowed : "not allowed"
}
`
The texts for those (invalid tags) titles can be customized from the settings:
`js
new Tagify(inputElement, {
texts: {
duplicate: "Duplicates are not allowed"
}
})
`
Or by directly manipulating the Tagify function prototype :
`js
Tagify.prototype.TEXTS = {...Tagify.prototype.TEXTS, {duplicate: "Duplicates are not allowed"}}
`
Drag & Sort
To be able to sort tags by draging, a 3rd-party script is needed.
I have made a very simple drag & drop (~11kb unminified ) script which uses HTML5 native API and
it is available to download via NPM or Github
but any other drag & drop script may possibly work. I could not find in the whole internet a decent lightweight script.
$3
`js
var tagify = new Tagify(inputElement)
// bind "DragSort" to Tagify's main element and tell
// it that all the items with the below "selector" are "draggable"
var dragsort = new DragSort(tagify.DOM.scope, {
selector: '.'+tagify.settings.classNames.tag,
callbacks: {
dragEnd: onDragEnd
}
})
// must update Tagify's value according to the re-ordered nodes in the DOM
function onDragEnd(elm){
tagify.updateValueByDOMTags()
}
`
DOM Templates
It's possible to control the templates for some of the HTML elements tagify is using by
modifying the settings.templates Object with your own custom functions which must return an HTML string .
Available templates are: wrapper, tag, dropdown, dropdownItem and the optional dropdownItemNoMatch
which is a special template for rendering a suggestion item (in the dropdown list) only if there were no matches found for the typed input.
View templates
$3
Each template function automaticaly gets binded with this pointing to the current Tagify instance.
It is imperative to preserve the class names and also the this.getAttributes(tagData) for proper functionality.
`js
new Tagify(inputElem, {
templates: {
tag(tagData, tagify){
return contenteditable='false'
spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ""}"
${this.getAttributes(tagData)}>
${tagData[this.settings.tagTextProp] || tagData.value}
}
})
`
Suggestions list
The suggestions list is a whitelist Array of Strings or Objects which was set in the settings Object when the Tagify instance was created, and can be set latet directly on the instance: tagifyInstance.whitelist = ["tag1", "tag2", ...].
The suggestions dropdown will be appended to the document's 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 list 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 list will render
`javascript
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
`html
aaab
aaabb
aaabc
aaabd
aaabe
`
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, typing a part of a string which is included in the searchBy property, for example land midd" -
the suggested item which match the value "Israel" will be rendered in the suggestions (dropdown) list.
$3
`javascript
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 ).
$3
`javascript
[
{
value : 123456,
nickname : "foo",
email : "foo@mail.com"
},
{
value : 987654,
nickname : "bar",
email : "bar@mail.com"
},
...more..
]
`
// setting to search in other keys:
`javascript
{
dropdown: {
searchKeys: ["nickname", "email"] // fuzzy-search matching for those whitelist items' properties
}
}
`
Mixed-Content
See demo here
This feature must be toggled using these settings :
`js
{
// mixTagsInterpolator: ["{{", "}}"], // optional: interpolation before & after string
mode: 'mix', // <-- Enable mixed-content
pattern: /@|#/ // <-- Text starting with @ or # (if single, String can be used here instead of Regex)
}
`
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
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.
Single-Value
Similar to native element, but allows typing text as value.
React
See live demo for React integration examples.
⚠️ Tagify is not a controlled component .
A Tagify React component is exported from react.tagify.js:
---
$3
I have changed how the onChange works internally within the Wrapper of Tagify
so as of March 30, 2021 the e argument will include a detail parameter with the value as string.
There is no more e.target, and to access the original DOM input element, do this: e.detail.tagify.DOM.originalInput.
----
> Note: You will need to import Tagify's CSS also, either by JavaScript or by SCSS @import (which is preferable)
> Also note that you will need to use dart-sass and not node-sass in order to compile the file.
`javascript
import Tags from "@yaireo/tagify/dist/react.tagify" // React-wrapper file
import "@yaireo/tagify/dist/tagify.css" // Tagify CSS
// on tag add/edit/remove
const onChange = useCallback((e) => {
console.log("CHANGED:"
, e.detail.tagify.value // Array where each tag includes tagify's (needed) extra properties
, e.detail.tagify.getCleanValue()) // Same as above, without the extra properties
, e.detail.value // a string representing the tags
)
}, [])
const App = () => {
return (
tagifyRef={tagifyRef} // optional Ref object for the Tagify instance itself, to get access to inner-methods
settings={settings} // tagify settings object
defaultValue="a,b,c"
{...tagifyProps} // dynamic props such as "loading", "showDropdown:'abc'", "value"
onChange={onChange}
/>
)
})
`
To gain full access to Tagify's (instance) inner methods, A custom ref can be used:
`jsx
...
const tagifyRef = useRef()
...
// or mix-mode
settings={...}
onChange={...}
defaultValue={This is a textarea which mixes text with [[{"value":"tags"}]].}
/>
`
component is a shorthand for
#### Updating the component's state
The settings prop is only used once in the initialization process, please do not update it afterwards.
---
📖 List of (React) props for the <Tags/> component
Prop | Type | Updatable | Info
----------------------- | ------------------------- |:---------:| -----------------------------------------------------------
settings | Object | | See settings section
name | String | ✔ | 's element name attribute
value | String/Array | ✔ | Initial value.
defaultValue | String/Array | | Same as value prop
placeholder | String | ✔ | placeholder text for the component
readOnly | Boolean | ✔ | Toggles readonly state. With capital O.
tagifyRef | Object | | useRef hook refference for the component inner instance of vailla Tagify (for methods access)
showFilteredDropdown | Boolean/String | ✔ | if true shows the suggestions dropdown. if assigned a String, show the dropdown pre-filtered.
loading | Boolean | ✔ | Toggles loading state for the whole component
whitelist | Array | ✔ | Sets the whitelist which is the basis for the suggestions dropdown & autocomplete
className | String | | Component's optional class name to be added
InputMode | String | | "textarea" will create a (hidden) element instead of the default and automatically make Tagify act as "mix mode"
autoFocus | Boolean | | Should the component have focus on mount. Must be unique, per-page.
children | String/Array | | value/defaultValue props are prefered
onChange | Function | | See events section
onInput | Function | | See events section
onAdd | Function | | See events section
onRemove | Function | | See events section
onInvalid | Function | | See events section
onClick | Function | | See events section
onKeydown | Function | | See events section
onFocus | Function | | See events section
onBlur | Function | | See events section
onEditInput | Function | | See events section
onEditBeforeUpdate | Function | | See events section
onEditUpdated | Function | | See events section
onEditStart | Function | | See events section
onEditKeydown | Function | | See events section
onDropdownShow | Function | | See events section
onDropdownHide | Function | | See events section
onDropdownSelect | Function | | See events section
onDropdownScroll | Function | | See events section
onDropdownNoMatch | Function | | See events section
onDropdownUpdated | Function | | See events section
---
jQuery version
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')
``javascript
$('[name=tags]')
.tagify()
.on('add', function(e, tagData){
console.log('added', ...tagData) // data, index, and DOM node
});
`
Accessing methods can be done via the .data('tagify'):
`javascript
$('[name=tags]').tagify();
// get tags from the server (ajax) and add them:
$('[name=tags]').data('tagify').addTags('aaa, bbb, ccc')
``
HTML input & textarea attributes
The below list of attributes affect Tagify .
These can also be set by Tagify settings Object manually, and not declerativly (via attributes).
Attribute | Example | Info
----------------- | ----------------------------------------------------- | --------------------
pattern | | Tag Regex pattern which tag input is validated by.
placeholder | | This attribute's value will be used as a constant placeholder, which is visible unless something is being typed.
readOnly | | No user-interaction (add/remove/edit) allowed.
autofocus | | Automatically focus the the Tagify component when the component is loaded
required | | Adds a required attribute to the Tagify wrapper element. Does nothing more.
FAQ
List of questions & scenarios which might come up during development with Tagify:
Dynamic whitelist
The whitelist initial value is set like so:
`javascript
const tagify = new Tagify(tagNode, {
whitelist: ["a", "b", "c"]
})
`
If changes to the whitelist are needed, they should be done like so:
Incorrect:
`js
tagify.settings.whitelist = ["foo", "bar"]
`
Correct:
`js
// set the whitelist directly on the instance and not on the "settings" property
tagify.whitelist = ["foo", "bar"]
`
---
tags/whitelist data structure
Tagify does not accept just any kind of data structure.
If a tag data is represented as an Object, it must contain a unique property value
which Tagify uses to check if a tag already exists, among other things, so make sure it is present.
Incorrect:
`javascript
[{ "id":1, "name":"foo bar" }]
`
Correct:
`javascript
[{ "id":1, "value": 1, "name":"foo bar" }]
`
`javascript
[{ "value":1, "name":"foo bar" }]
`
`javascript
[{ "value":"foo bar" }]
`
`javascript
// ad a simple array of Strings
["foo bar"]
`
---
Save changes (Ex. to a server)
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).
`javascript
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 /