Tag input
Token field. Type and press Enter or , to commit a tag. Backspace on an empty input removes the last one. Paste splits on commas. Duplicates are de-duped on commit.
Live preview
Markup
<div data-component="tag-input-demo">
<label class="tag-field" @click="focusInput">
<template data-each="tags" data-key="id">
<span class="tag">
<span data-text="item.text"></span>
<button class="tag-remove" type="button"
@click="remove" data-bind="data-id:item.id">×</button>
</span>
</template>
<input class="tag-input-inner"
data-ref="input"
data-model="draft"
@keydown="onKey"
@paste="onPaste"
@blur="commitDraft"
placeholder="Add a tag…" />
</label>
</div>
Definition
Micra.define('tag-input-demo', {
state: { tags: [], draft: '' },
serialize() {
return this.state.tags.map(t => t.text).join(', ') || '—'
},
focusInput() { this.refs.input?.focus() },
onKey(e) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault()
this.commitDraft()
} else if (e.key === 'Backspace' && this.state.draft === '' && this.state.tags.length) {
this.state.tags = this.state.tags.slice(0, -1)
}
},
onPaste(e) {
const text = (e.clipboardData || window.clipboardData).getData('text')
if (!text.includes(',')) return // single token, let it land in the input
e.preventDefault()
const fresh = text.split(',').map(s => s.trim()).filter(Boolean)
this.state.tags = this._dedupe([...this.state.tags, ...fresh.map(this._make)])
},
commitDraft() {
const text = this.state.draft.trim()
if (!text) return
this.state.tags = this._dedupe([...this.state.tags, this._make(text)])
this.state.draft = ''
},
remove(e) {
const id = e.currentTarget.dataset.id
this.state.tags = this.state.tags.filter(t => t.id !== id)
},
_make(text) {
return {
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
text,
}
},
_dedupe(list) {
const seen = new Set()
return list.filter(t => {
const k = t.text.toLowerCase()
if (seen.has(k)) return false
seen.add(k)
return true
})
},
})
Integration
- Tags are objects with ids. The keyed data-each needs a stable identifier — using the tag text as the key would break the moment two tags with the same text coexist.
- Refs for focus. data-ref="input" exposes the inner input on this.refs.input so the wrapping label can forward focus.
- Paste split handles "alpha, beta, gamma" from a spreadsheet. Single-token paste falls through to the browser's default input behavior.
- Submit-safe. e.preventDefault() in onKey stops Enter from submitting a wrapping form. Add type="button" on the tag-remove for the same reason.
Pitfalls
- Don't store a separate tagCount state field — derive (data-text="tags.length") so it can't drift.
- Don't use @keydown.enter — you need to differentiate Enter, comma, and Backspace, all in one handler. Branch on e.key.
- Don't forget commitDraft() on @blur. Otherwise a half-typed tag is discarded when the user clicks "Save" outside the input.