From e8bd1fee4f919b4c2ef0b0c5fc8115c8da44a048 Mon Sep 17 00:00:00 2001 From: Valeryia Shyrko Date: Tue, 15 Apr 2025 15:21:45 +0200 Subject: [PATCH 1/5] Add tagify --- app/components/ui/tag_input/component.rb | 28 +++++++++++++++++++ .../ui/tag_input/tagify_controller.js | 26 +++++++++++++++++ app/views/home/ui.html.erb | 4 +++ package.json | 1 + yarn.lock | 5 ++++ 5 files changed, 64 insertions(+) create mode 100644 app/components/ui/tag_input/component.rb create mode 100644 app/components/ui/tag_input/tagify_controller.js diff --git a/app/components/ui/tag_input/component.rb b/app/components/ui/tag_input/component.rb new file mode 100644 index 00000000..0bfa8ca9 --- /dev/null +++ b/app/components/ui/tag_input/component.rb @@ -0,0 +1,28 @@ +class Ui::TagInput::Component < ApplicationComponent + def initialize(placeholder: nil, value: nil, whitelist: [], **options) + @placeholder = placeholder + @value = value + @whitelist = whitelist + @options = options + end + + def call + tag.input( + type: "text", + value: @value, + placeholder: @placeholder, + data: { + tagify_target: "input" + }, + class: "tagify-input w-full border border-gray-300 rounded px-3 py-2", + **attrs + ) + end + + private + + def attrs + data_attributes = ({ controller: "tagify", whitelist: @whitelist.join(",") }).deep_merge(@options.fetch(:data, {})) + @options.merge(data: data_attributes) + end +end diff --git a/app/components/ui/tag_input/tagify_controller.js b/app/components/ui/tag_input/tagify_controller.js new file mode 100644 index 00000000..c25dd704 --- /dev/null +++ b/app/components/ui/tag_input/tagify_controller.js @@ -0,0 +1,26 @@ +import { Controller } from '@hotwired/stimulus' +import Tagify from '@yaireo/tagify' +import { stimulus } from '~/init' +import '@yaireo/tagify/dist/tagify.css' + +export default class TagifyController extends Controller { + connect () { + const { whitelist } = this.element.dataset + + this.tagify = new Tagify(this.element, { + whitelist: whitelist?.split(',') || [], + dropdown: { + enabled: 0, + maxItems: 10, + classname: 'tags-look', + closeOnSelect: false + } + }) + } + + disconnect () { + this.tagify.destroy() + } +} + +stimulus.register('tagify', TagifyController) diff --git a/app/views/home/ui.html.erb b/app/views/home/ui.html.erb index 2e9d46e9..0ef7eda8 100644 --- a/app/views/home/ui.html.erb +++ b/app/views/home/ui.html.erb @@ -399,6 +399,10 @@ <% end %> <% end %> + <%= accordion.with_item(title: "Tags", id: "tags") do %> + <%= render Ui::TagInput::Component.new(placeholder: "Choose your stack", value: "", whitelist: %w[Rails Ruby Hotwire Turbo Stimulus Tailwind CSS HTML React JS Python Svelte C C# C++]) %> + <% end %> + <%= accordion.with_item(title: "Tooltip", id: "tooltip", class: "flex gap-4 items-center") do %> <%= render Ui::Tooltip::Component.new(tooltip_content: "Tooltip content", data: { placement: "bottom" }) do %> <%= render Ui::Btn::Component.new(variant: :primary).with_content "Tooltip Button" %> diff --git a/package.json b/package.json index 842225c5..4141cb0d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^8.0.2", "@rails/request.js": "^0.0.9", + "@yaireo/tagify": "^4.34.0", "flowbite": "^3.1.2", "mjml": "^4.15.3", "stimulus-rails-autosave": "^5.1.0", diff --git a/yarn.lock b/yarn.lock index 3cecc66e..e533ec2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -499,6 +499,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@yaireo/tagify@^4.34.0": + version "4.34.0" + resolved "https://registry.yarnpkg.com/@yaireo/tagify/-/tagify-4.34.0.tgz#6f058612c9b8d8470a08d24f4072ba9f8cbe8d3e" + integrity sha512-+IckJUhvjii37n/YFX3YUTJvcp8rUU0CnjAqa87/psSKjiF5xwoK7Mo0rsadzdrPxycrcn6Uwzl8zPO6/8CXOQ== + abbrev@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" From c4852806dd3e1a5801e7a5bd5996c3e1f1e976a2 Mon Sep 17 00:00:00 2001 From: Valeryia Shyrko Date: Tue, 15 Apr 2025 16:08:02 +0200 Subject: [PATCH 2/5] Add tag-input-lookbook component --- app/components/ui/tag_input/component.rb | 23 ++++++++----------- app/views/home/ui.html.erb | 2 +- .../ui/tag_input/component_preview.rb | 9 ++++++++ 3 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 test/components/previews/ui/tag_input/component_preview.rb diff --git a/app/components/ui/tag_input/component.rb b/app/components/ui/tag_input/component.rb index 0bfa8ca9..2d4ce91c 100644 --- a/app/components/ui/tag_input/component.rb +++ b/app/components/ui/tag_input/component.rb @@ -1,28 +1,25 @@ class Ui::TagInput::Component < ApplicationComponent - def initialize(placeholder: nil, value: nil, whitelist: [], **options) + def initialize(placeholder: nil, value: nil, **options) @placeholder = placeholder @value = value - @whitelist = whitelist @options = options end def call - tag.input( - type: "text", - value: @value, - placeholder: @placeholder, - data: { - tagify_target: "input" - }, - class: "tagify-input w-full border border-gray-300 rounded px-3 py-2", - **attrs - ) + tag.input(type: "text", value: @value, placeholder: @placeholder, data: { tagify_target: "input" }, class: input_classes, **attrs) end private def attrs - data_attributes = ({ controller: "tagify", whitelist: @whitelist.join(",") }).deep_merge(@options.fetch(:data, {})) + data_attributes = ({ controller: "tagify" }).deep_merge(@options.fetch(:data, {})) @options.merge(data: data_attributes) end + + def input_classes + class_names( + "tagify-input w-1/2 border border-gray-300 rounded px-3 py-2", + @options.delete(:class) + ) + end end diff --git a/app/views/home/ui.html.erb b/app/views/home/ui.html.erb index 0ef7eda8..90d0481f 100644 --- a/app/views/home/ui.html.erb +++ b/app/views/home/ui.html.erb @@ -400,7 +400,7 @@ <% end %> <%= accordion.with_item(title: "Tags", id: "tags") do %> - <%= render Ui::TagInput::Component.new(placeholder: "Choose your stack", value: "", whitelist: %w[Rails Ruby Hotwire Turbo Stimulus Tailwind CSS HTML React JS Python Svelte C C# C++]) %> + <%= render Ui::TagInput::Component.new(placeholder: "Choose your stack", value: "", data: { whitelist: "Rails,Hotwire,Turbo,Stimulus,Tailwind,CSS,HTML,React,JS,Python,Svelte,C,C#,C++" }) %> <% end %> <%= accordion.with_item(title: "Tooltip", id: "tooltip", class: "flex gap-4 items-center") do %> diff --git a/test/components/previews/ui/tag_input/component_preview.rb b/test/components/previews/ui/tag_input/component_preview.rb new file mode 100644 index 00000000..18250062 --- /dev/null +++ b/test/components/previews/ui/tag_input/component_preview.rb @@ -0,0 +1,9 @@ +class Ui::TagInput::ComponentPreview < ViewComponent::Preview + # @param placeholder + # @param default_value + # @param whitelist + + def default(placeholder: "Choose your stack", default_value: "Ruby", whitelist: "Rails,Hotwire,Turbo,Stimulus,Tailwind,CSS,HTML,React,JS,Python,Svelte") + render(Ui::TagInput::Component.new(placeholder: placeholder, value: default_value, data: { whitelist: whitelist })) + end +end From 894d353563ddb354ca579747adda0d3c517e42c4 Mon Sep 17 00:00:00 2001 From: Valeryia Shyrko Date: Tue, 15 Apr 2025 16:34:12 +0200 Subject: [PATCH 3/5] Tune data options --- .../ui/tag_input/tagify_controller.js | 22 +++++++++++-------- .../ui/tag_input/component_preview.rb | 6 +++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/components/ui/tag_input/tagify_controller.js b/app/components/ui/tag_input/tagify_controller.js index c25dd704..e16952d7 100644 --- a/app/components/ui/tag_input/tagify_controller.js +++ b/app/components/ui/tag_input/tagify_controller.js @@ -5,21 +5,25 @@ import '@yaireo/tagify/dist/tagify.css' export default class TagifyController extends Controller { connect () { - const { whitelist } = this.element.dataset + this.tagify = new Tagify(this.element, this.#options()) + } + + disconnect () { + this.tagify.destroy() + } - this.tagify = new Tagify(this.element, { + #options () { + const { whitelist, maxitems, closeonselect } = this.element.dataset + + return { whitelist: whitelist?.split(',') || [], dropdown: { enabled: 0, - maxItems: 10, + maxItems: maxitems || 10, classname: 'tags-look', - closeOnSelect: false + closeOnSelect: closeonselect || false } - }) - } - - disconnect () { - this.tagify.destroy() + } } } diff --git a/test/components/previews/ui/tag_input/component_preview.rb b/test/components/previews/ui/tag_input/component_preview.rb index 18250062..82563e4e 100644 --- a/test/components/previews/ui/tag_input/component_preview.rb +++ b/test/components/previews/ui/tag_input/component_preview.rb @@ -2,8 +2,10 @@ class Ui::TagInput::ComponentPreview < ViewComponent::Preview # @param placeholder # @param default_value # @param whitelist + # @param maxItems number + # @param closeOnSelect toggle - def default(placeholder: "Choose your stack", default_value: "Ruby", whitelist: "Rails,Hotwire,Turbo,Stimulus,Tailwind,CSS,HTML,React,JS,Python,Svelte") - render(Ui::TagInput::Component.new(placeholder: placeholder, value: default_value, data: { whitelist: whitelist })) + def default(placeholder: "Choose your stack", default_value: "Ruby", whitelist: "Rails,Hotwire,Turbo,Stimulus,Tailwind,CSS,HTML,React,JS,Python,Svelte", maxItems: 10, closeOnSelect: false) + render(Ui::TagInput::Component.new(placeholder: placeholder, value: default_value, data: { whitelist: whitelist, maxItems: maxItems, closeOnSelect: closeOnSelect })) end end From 18317bb6e30b0d2c1b8910980351a15eecdbc93c Mon Sep 17 00:00:00 2001 From: Valeryia Shyrko Date: Wed, 16 Apr 2025 13:21:00 +0200 Subject: [PATCH 4/5] Fix inaccuracies --- app/components/ui/tag_input/component.rb | 2 +- app/components/ui/tag_input/tagify_controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/ui/tag_input/component.rb b/app/components/ui/tag_input/component.rb index 2d4ce91c..2000ad35 100644 --- a/app/components/ui/tag_input/component.rb +++ b/app/components/ui/tag_input/component.rb @@ -6,7 +6,7 @@ def initialize(placeholder: nil, value: nil, **options) end def call - tag.input(type: "text", value: @value, placeholder: @placeholder, data: { tagify_target: "input" }, class: input_classes, **attrs) + tag.input(type: "text", value: @value, placeholder: @placeholder, class: input_classes, **attrs) end private diff --git a/app/components/ui/tag_input/tagify_controller.js b/app/components/ui/tag_input/tagify_controller.js index e16952d7..c3d631fe 100644 --- a/app/components/ui/tag_input/tagify_controller.js +++ b/app/components/ui/tag_input/tagify_controller.js @@ -21,7 +21,7 @@ export default class TagifyController extends Controller { enabled: 0, maxItems: maxitems || 10, classname: 'tags-look', - closeOnSelect: closeonselect || false + closeOnSelect: (/true/).test(closeonselect) || false } } } From f87469db4740d625ded38544b7a89e200b42d404 Mon Sep 17 00:00:00 2001 From: Valeryia Shyrko Date: Mon, 21 Apr 2025 12:57:11 +0200 Subject: [PATCH 5/5] Tune controller --- .../ui/tag_input/tagify_controller.js | 69 ++++++++++++++++--- .../ui/tag_input/component_preview.rb | 6 +- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/app/components/ui/tag_input/tagify_controller.js b/app/components/ui/tag_input/tagify_controller.js index c3d631fe..e54542a4 100644 --- a/app/components/ui/tag_input/tagify_controller.js +++ b/app/components/ui/tag_input/tagify_controller.js @@ -4,25 +4,72 @@ import { stimulus } from '~/init' import '@yaireo/tagify/dist/tagify.css' export default class TagifyController extends Controller { + get tagifyInstance () { + const { mode, whitelist, maxtags } = this.element.dataset + + return new Tagify(this.element, { + tagTextProp: 'name', + editTags: false, + mode: mode || null, + dropdown: { + enabled: 0, + maxItems: 100, + placeAbove: false, + fuzzySearch: false + }, + whitelist: whitelist?.split(',') || '[]', + maxTags: maxtags || 100 + }) + } + connect () { - this.tagify = new Tagify(this.element, this.#options()) + this.tagify = this.tagifyInstance + this.#setupInputListener() + this.tagify.on('dropdown:select', this.#onSelectSuggestion.bind(this)) + this.tagify.setContentEditable(!this.tagify.hasMaxTags()) + this.tagify.on('change', () => { + this.tagify.setContentEditable(!this.tagify.hasMaxTags()) + this.dispatch('tagifyChange', { detail: { tagify: this.tagify } }) + }) } disconnect () { - this.tagify.destroy() + this.tagify.off('input') } - #options () { - const { whitelist, maxitems, closeonselect } = this.element.dataset + reload () { + this.disconnect() + this.connect() + } - return { - whitelist: whitelist?.split(',') || [], - dropdown: { - enabled: 0, - maxItems: maxitems || 10, - classname: 'tags-look', - closeOnSelect: (/true/).test(closeonselect) || false + #setupInputListener () { + let controller = new AbortController() + + this.tagify.on('input', (event) => { + const path = this.element.dataset.tagsPath + + if (path) { + const { value } = event.detail + this.tagify.settings.whitelist.length = 0 + + controller.abort() + controller = new AbortController() + this.tagify.loading(true) + + fetch(`${path}?query=${value}`, { signal: controller.signal }) + .then((response) => response.json()) + .then((whitelist) => { + this.tagify.whitelist = whitelist + this.tagify.loading(false).dropdown.show(value) + }) } + }) + } + + #onSelectSuggestion (event) { + if (event.detail.elm.classList.contains('tagify__dropdown__item__addNew')) { + this.tagify.addTags(this.tagify.state.inputText) + this.tagify.DOM.input.innerHTML = '' } } } diff --git a/test/components/previews/ui/tag_input/component_preview.rb b/test/components/previews/ui/tag_input/component_preview.rb index 82563e4e..a787d3f7 100644 --- a/test/components/previews/ui/tag_input/component_preview.rb +++ b/test/components/previews/ui/tag_input/component_preview.rb @@ -2,10 +2,10 @@ class Ui::TagInput::ComponentPreview < ViewComponent::Preview # @param placeholder # @param default_value # @param whitelist - # @param maxItems number + # @param maxTags number # @param closeOnSelect toggle - def default(placeholder: "Choose your stack", default_value: "Ruby", whitelist: "Rails,Hotwire,Turbo,Stimulus,Tailwind,CSS,HTML,React,JS,Python,Svelte", maxItems: 10, closeOnSelect: false) - render(Ui::TagInput::Component.new(placeholder: placeholder, value: default_value, data: { whitelist: whitelist, maxItems: maxItems, closeOnSelect: closeOnSelect })) + def default(placeholder: "Choose your stack", default_value: "Ruby", whitelist: "Rails,Hotwire,Turbo,Stimulus,Tailwind,CSS,HTML,React,JS,Python,Svelte", maxTags: 100, closeOnSelect: false) + render(Ui::TagInput::Component.new(placeholder: placeholder, value: default_value, data: { whitelist: whitelist, maxTags: maxTags, closeOnSelect: closeOnSelect })) end end