mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-31 21:37:16 +00:00
Add stimulus tag list input controller
It handles the UI to display the list of tags, and lets you add and remove tags from the list.
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
= f.hidden_field method.to_sym, value: tags.join(",")
|
||||
.tags-input
|
||||
.tags
|
||||
%ul.tag-list
|
||||
- tags.each do |tag|
|
||||
%li.tag-item
|
||||
.tag-template
|
||||
%span=tag
|
||||
%a.remove-button
|
||||
✖
|
||||
= text_field_tag :variant_add_tag, nil, class: "input", placeholder: placeholder
|
||||
- #%div{ "data-controller": "tag-list-input-component--tag-list-input" }
|
||||
%div{ "data-controller": "tag-list-input-component--tag-list-input" }
|
||||
= f.hidden_field method.to_sym, value: tags.join(","), "data-tag-list-input-component--tag-list-input-target": "tagList"
|
||||
.tags-input{ }
|
||||
.tags
|
||||
%ul.tag-list{"data-tag-list-input-component--tag-list-input-target": "list"}
|
||||
%template{"data-tag-list-input-component--tag-list-input-target": "template"}
|
||||
%li.tag-item
|
||||
.tag-template
|
||||
%span
|
||||
%a.remove-button{ "data-action": "click->tag-list-input-component--tag-list-input#removeTag" }
|
||||
✖
|
||||
- tags.each do |tag|
|
||||
%li.tag-item
|
||||
.tag-template
|
||||
%span=tag
|
||||
%a.remove-button{ "data-action": "click->tag-list-input-component--tag-list-input#removeTag" }
|
||||
✖
|
||||
= text_field_tag "variant_add_tag_#{f.object.id}".to_sym, nil, class: "input", placeholder: placeholder, "data-action": "keydown.enter->tag-list-input-component--tag-list-input#addTag keyup->tag-list-input-component--tag-list-input#filterInput", "data-tag-list-input-component--tag-list-input-target": "newTag"
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-size: 85%;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
border: none;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Controller } from "stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["tagList", "newTag", "template", "list"];
|
||||
|
||||
addTag() {
|
||||
// add to tagList
|
||||
this.tagListTarget.value = this.tagListTarget.value.concat(`,${this.newTagTarget.value}`);
|
||||
|
||||
// Create new li component with value
|
||||
const newTagElement = this.templateTarget.content.cloneNode(true);
|
||||
const spanElement = newTagElement.querySelector("span");
|
||||
spanElement.innerText = this.newTagTarget.value;
|
||||
this.listTarget.appendChild(newTagElement);
|
||||
|
||||
// Clear new tag value
|
||||
this.newTagTarget.value = "";
|
||||
}
|
||||
|
||||
removeTag(event) {
|
||||
// Text to remove
|
||||
const tagName = event.srcElement.previousElementSibling.textContent;
|
||||
|
||||
// Remove tag from list
|
||||
const tags = this.tagListTarget.value.split(",");
|
||||
const index = tags.indexOf(tagName);
|
||||
tags.splice(index, 1);
|
||||
this.tagListTarget.value = tags.join(",");
|
||||
|
||||
// Remove HTML element from the list
|
||||
event.srcElement.parentElement.parentElement.remove();
|
||||
}
|
||||
|
||||
// Strip comma from tag name
|
||||
filterInput(event) {
|
||||
if (event.key === ",") {
|
||||
event.srcElement.value = event.srcElement.value.replace(",","");
|
||||
}
|
||||
}
|
||||
}
|
||||
126
spec/javascripts/stimulus/tag_list_input_controller_test.js
Normal file
126
spec/javascripts/stimulus/tag_list_input_controller_test.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { Application } from "stimulus";
|
||||
import tag_list_input_controller from "../../../app/components/tag_list_input_component/tag_list_input_controller";
|
||||
|
||||
describe("TagListInputController", () => {
|
||||
beforeAll(() => {
|
||||
const application = Application.start();
|
||||
application.register("tag-list-input-component--tag-list-input", tag_list_input_controller);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div data-controller="tag-list-input-component--tag-list-input">
|
||||
<input
|
||||
value="tag 1,tag 2,tag 3"
|
||||
data-tag-list-input-component--tag-list-input-target="tagList"
|
||||
type="hidden"
|
||||
name="variant_tag_list" id="variant_tag_list"
|
||||
>
|
||||
<div class="tags-input">
|
||||
<div class="tags">
|
||||
<ul class="tag-list" data-tag-list-input-component--tag-list-input-target="list">
|
||||
<template data-tag-list-input-component--tag-list-input-target="template">
|
||||
<li class="tag-item">
|
||||
<div class="tag-template">
|
||||
<span></span>
|
||||
<a
|
||||
class="remove-button"
|
||||
data-action="click->tag-list-input-component--tag-list-input#removeTag"
|
||||
>✖</a>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
<li class="tag-item">
|
||||
<div class="tag-template">
|
||||
<span>tag 1</span>
|
||||
<a
|
||||
class="remove-button"
|
||||
data-action="click->tag-list-input-component--tag-list-input#removeTag"
|
||||
>✖</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tag-item">
|
||||
<div class="tag-template">
|
||||
<span>tag 2</span>
|
||||
<a
|
||||
class="remove-button"
|
||||
data-action="click->tag-list-input-component--tag-list-input#removeTag"
|
||||
>✖</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tag-item">
|
||||
<div class="tag-template">
|
||||
<span>tag 3</span>
|
||||
<a
|
||||
class="remove-button"
|
||||
data-action="click->tag-list-input-component--tag-list-input#removeTag"
|
||||
>✖</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<input
|
||||
type="text"
|
||||
name="variant_add_tag"
|
||||
id="variant_add_tag"
|
||||
placeholder="Add a tag"
|
||||
data-action="keydown.enter->tag-list-input-component--tag-list-input#addTag keyup->tag-list-input-component--tag-list-input#filterInput" data-tag-list-input-component--tag-list-input-target="newTag"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
describe("addTag", () => {
|
||||
beforeEach(() => {
|
||||
variant_add_tag.value = "new_tag";
|
||||
// { key: "Enter" }
|
||||
variant_add_tag.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
|
||||
});
|
||||
|
||||
it("updates the hidden input tag list", () => {
|
||||
expect(variant_tag_list.value).toBe("tag 1,tag 2,tag 3,new_tag");
|
||||
});
|
||||
|
||||
it("adds the new tag to the HTML tag list", () => {
|
||||
const tagList = document.getElementsByClassName("tag-list")[0];
|
||||
|
||||
// 1 template + 3 tags + 1 new tag
|
||||
expect(tagList.childElementCount).toBe(5);
|
||||
});
|
||||
|
||||
it("clears the tag input", () => {
|
||||
expect(variant_add_tag.value).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeTag", () => {
|
||||
beforeEach(() => {
|
||||
const removeButtons = document.getElementsByClassName("remove-button");
|
||||
// Click on tag 2
|
||||
removeButtons[1].click();
|
||||
});
|
||||
|
||||
it("updates the hidden input tag list", () => {
|
||||
expect(variant_tag_list.value).toBe("tag 1,tag 3");
|
||||
});
|
||||
|
||||
it("removes the tag from the HTML tag list", () => {
|
||||
const tagList = document.getElementsByClassName("tag-list")[0];
|
||||
// 1 template + 2 tags
|
||||
expect(tagList.childElementCount).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterInput", () => {
|
||||
it("removes comma from the tag input", () => {
|
||||
variant_add_tag.value = "text"
|
||||
variant_add_tag.dispatchEvent(new KeyboardEvent("keyup", { key: "," }));
|
||||
|
||||
expect(variant_add_tag.value).toBe("text");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user