Skip to content

Instantly share code, notes, and snippets.

@eminkel
Last active September 10, 2020 02:17
Show Gist options
  • Save eminkel/5a9194518e2959b24ea21af38309beeb to your computer and use it in GitHub Desktop.
Save eminkel/5a9194518e2959b24ea21af38309beeb to your computer and use it in GitHub Desktop.
StimulusJS Tag Component
<div class="mt-6">
<%= f.label :tags, class: 'form-label' %>
<div class="border border-gray-300 rounded-md flex-1 block w-full transition duration-150 ease-in-out pt-1.5 px-2 mt-1" data-controller="tags" data-tags-tag-collection data-tags-autocomplete="true" data-tags-show-dropdown="" data-target="tags.container">
<div class="flex flex-wrap cursor-text" data-action="click->tags#active">
<div class="flex flex-wrap" data-target="tags.tags">
<div class="text-sm p-2 bg-gray-100 rounded-md flex items-center space-x-1.5 mb-1.5 mr-2">
<p>tag</p>
<div class="inline-flex bg-gray-500 text-gray-100 rounded-full p-1 hover:bg-gray-700 cursor-pointer">
<svg class="h-3 w-3" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"></path></svg>
</div>
</div>
<div class="text-sm p-2 bg-gray-100 rounded-md flex items-center space-x-1.5 mb-1.5 mr-2">
<p>stimulusjs</p>
<div class="inline-flex bg-gray-500 text-gray-100 rounded-full p-1 hover:bg-gray-700 cursor-pointer">
<svg class="h-3 w-3" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"></path></svg>
</div>
</div>
</div>
<div class="mb-1.5 mr-2">
<%= text_field_tag :tags, nil, class: "text-sm appearance-none outline-none p-2", data: { target: "tags.input", action: "keyup->tags#inputTag keydown->tags#backspaceTag" } %>
</div>
<div class="hidden absolute text-gray-900 right-0 inset-y-0 flex items-center">
<svg class="h-4 w-4" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M19 9l-7 7-7-7"></path></svg>
</div>
</div>
<%= f.hidden_field :tag_list, data: { target: "tags.data" } %>
</div>
<p class="input-hint">
Make your submission more discoverable with some tags.
</p>
</div>
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ["input", "outer-input", "container", "tags", "data"];
initialize() {
// If we click outside the tag element we need to unfocus it
document.addEventListener("click", (e) => {
if (!this.containerTarget.contains(e.target)) {
// Outside tags input has been clicked
// If tag input has tag when unfocused make it a pill
if (this.inputTarget.value.length > 0) {
this.setTag(this.inputTarget.value);
}
this.containerTarget.classList.remove(
"border-indigo-700",
"shadow-outline-indigo"
);
} else {
// Inside of tags input has been clicked
this.containerTarget.classList.add(
"border-indigo-700",
"shadow-outline-indigo"
);
}
});
document.addEventListener("focusin", (e) => {
if (this.inputTarget == e.target) {
this.containerTarget.classList.add(
"border-indigo-700",
"shadow-outline-indigo"
);
}
});
document.addEventListener("focusout", (e) => {
if (this.inputTarget == e.target) {
// If tag input has tag when unfocused make it a pill
if (this.inputTarget.value.length > 0) {
this.setTag(this.inputTarget.value);
}
this.containerTarget.classList.remove(
"border-indigo-700",
"shadow-outline-indigo"
);
}
});
}
connect() {
this.renderTags();
}
active() {
this.inputTarget.focus();
}
toggle(event) {
event.preventDefault();
this.formTarget.classList.toggle("hidden");
}
inputTag(event) {
if (event.code === "Space" || event.code === "Comma") {
this.tags = this.inputTarget.value;
this.inputTarget.value = "";
this.renderTags();
}
}
setTag(tag) {
this.tags = tag;
this.inputTarget.value = "";
this.renderTags();
}
backspaceTag(event) {
if (event.code === "Backspace" && this.inputTarget.value === "") {
console.log("backspaced!");
this.removeLastTag();
}
}
renderTags() {
// get template, and render
fetch("/tags/template.html")
.then((response) => response.text())
.then((html) => {
let template = document.createElement("template");
template.innerHTML = html;
this.tagsTarget.innerHTML = "";
this.tags.forEach((tag) => {
let tagElem = template.content.cloneNode(true);
let tagText = tagElem.querySelector("p");
tagText.textContent = tag;
this.tagsTarget.appendChild(tagElem);
});
});
}
removeTag(event) {
// console.log(event.currentTarget.parentElement);
let tag = event.currentTarget.parentElement.querySelector("p").textContent;
if (tag !== null || tag !== undefined) {
let tags = this.tags;
let tagIndex = tags.indexOf(tag);
tags.splice(tagIndex, 1);
this.updateTags = tags;
this.renderTags();
}
}
removeLastTag() {
let tags = this.tags;
tags.splice(tags.length - 1, 1);
this.updateTags = tags;
this.renderTags();
}
set updateTags(tags) {
this.data.set("tagCollection", JSON.stringify(tags));
this.dataTarget.value = tags;
}
set tags(value) {
// Maybe strip non text characters?
let tag = value.toLowerCase().trim().replace(/,/g, "");
let tags = this.tags;
if (tags.indexOf(tag) === -1) {
tags.push(tag);
this.data.set("tagCollection", JSON.stringify(tags));
this.dataTarget.value = tags;
}
}
get tags() {
return this.data.get("tagCollection")
? JSON.parse(this.data.get("tagCollection"))
: [];
}
get dropdownActive() {
return this.data.get("showDropdown") || false;
}
}
<div
class="text-sm p-2 bg-gray-100 rounded-md flex items-center space-x-1.5 mb-1.5 mr-2"
>
<p></p>
<div
class="inline-flex bg-gray-500 text-gray-100 rounded-full p-1 hover:bg-gray-700 cursor-pointer"
data-action="click->tags#removeTag"
>
<svg
class="h-3 w-3"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
</div>
@eminkel
Copy link
Author

eminkel commented Sep 6, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment