Skip to content

Commit

Permalink
Implement customizable elements (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefarias authored Mar 9, 2024
1 parent 88b82fc commit 65366dd
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 21 deletions.
34 changes: 22 additions & 12 deletions app/presenters/hotwire_combobox/component.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require "securerandom"

class HotwireCombobox::Component
include Customizable

attr_reader :options, :dialog_label

def initialize \
Expand All @@ -24,20 +26,26 @@ def initialize \
view, autocomplete, id, name.to_s, value, form, async_src,
name_when_new, open, data, mobile_at, options, dialog_label

@combobox_attrs = input.reverse_merge(rest).with_indifferent_access
@combobox_attrs = input.reverse_merge(rest).deep_symbolize_keys
@association_name = association_name || infer_association_name
end

def render_in(view_context, &block)
block.call(self) if block_given?
view_context.render partial: "hotwire_combobox/component", locals: { component: self }
end


def fieldset_attrs
{
apply_customizations_to :fieldset, base: {
class: "hw-combobox",
data: fieldset_data
}
end


def hidden_field_attrs
{
apply_customizations_to :hidden_field, base: {
id: hidden_field_id,
name: hidden_field_name,
data: hidden_field_data,
Expand All @@ -49,28 +57,30 @@ def hidden_field_attrs
def input_attrs
nested_attrs = %i[ data aria ]

{
base = {
id: input_id,
role: :combobox,
class: "hw-combobox__input",
type: input_type,
data: input_data,
aria: input_aria,
autocomplete: :off
}.with_indifferent_access.merge combobox_attrs.except(*nested_attrs)
}.merge combobox_attrs.except(*nested_attrs)

apply_customizations_to :input, base: base
end


def handle_attrs
{
apply_customizations_to :handle, base: {
class: "hw-combobox__handle",
data: handle_data
}
end


def listbox_attrs
{
apply_customizations_to :listbox, base: {
id: listbox_id,
role: :listbox,
class: "hw-combobox__listbox",
Expand All @@ -81,28 +91,28 @@ def listbox_attrs


def dialog_wrapper_attrs
{
apply_customizations_to :dialog_wrapper, base: {
class: "hw-combobox__dialog__wrapper"
}
end

def dialog_attrs
{
apply_customizations_to :dialog, base: {
class: "hw-combobox__dialog",
role: :dialog,
data: dialog_data
}
end

def dialog_label_attrs
{
apply_customizations_to :dialog_label, base: {
class: "hw-combobox__dialog__label",
for: dialog_input_id
}
end

def dialog_input_attrs
{
apply_customizations_to :dialog_input, base: {
id: dialog_input_id,
role: :combobox,
class: "hw-combobox__dialog__input",
Expand All @@ -114,7 +124,7 @@ def dialog_input_attrs
end

def dialog_listbox_attrs
{
apply_customizations_to :dialog_listbox, base: {
id: dialog_listbox_id,
class: "hw-combobox__dialog__listbox",
role: :listbox,
Expand Down
49 changes: 49 additions & 0 deletions app/presenters/hotwire_combobox/component/customizable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module HotwireCombobox::Component::Customizable
CUSTOMIZABLE_ELEMENTS = %i[
fieldset
hidden_field
input
handle
listbox
dialog
dialog_wrapper
dialog_label
dialog_input
dialog_listbox
].freeze

PROTECTED_ATTRS = %i[
id
name
value
open
role
hidden
].freeze

CUSTOMIZABLE_ELEMENTS.each do |element|
define_method "customize_#{element}" do |**attrs|
customize element, **attrs
end
end

private
def custom_attrs
@custom_attrs ||= Hash.new { |h, k| h[k] = {} }
end

def customize(element, **attrs)
element = element.to_sym.presence_in(CUSTOMIZABLE_ELEMENTS)
sanitized_attrs = attrs.deep_symbolize_keys.except(*PROTECTED_ATTRS)

custom_attrs.store element, sanitized_attrs
end

def apply_customizations_to(element, base: {})
custom = custom_attrs[element]
coalesce = ->(k, v) { v.is_a?(String) ? view.token_list(v, custom.delete(k)) : v }
default = base.map { |k, v| [ k, coalesce.(k, v) ] }.to_h

custom.deep_merge default
end
end
5 changes: 2 additions & 3 deletions lib/hotwire_combobox/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ def hw_combobox_style_tag(*args, **kwargs)
end
hw_alias :hw_combobox_style_tag

def hw_combobox_tag(name, options_or_src = [], render_in: {}, include_blank: nil, **kwargs)
def hw_combobox_tag(name, options_or_src = [], render_in: {}, include_blank: nil, **kwargs, &block)
options, src = hw_extract_options_and_src(options_or_src, render_in, include_blank)
component = HotwireCombobox::Component.new self, name, options: options, async_src: src, **kwargs

render "hotwire_combobox/combobox", component: component
render component, &block
end
hw_alias :hw_combobox_tag

Expand Down
14 changes: 10 additions & 4 deletions test/application_view_test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ class ApplicationViewTestCase < ActionView::TestCase

# `#assert_attrs` expects attrs to appear in the order they are passed.
def assert_attrs(tag, tag_name: :input, **attrs)
attrs = attrs.map do |k, v|
%Q(#{escape_specials(k)}="#{escape_specials(v)}".*)
end.join(" ")
assert_match(/<#{tag_name}.* #{escaped_attrs(attrs)}/, tag)
end

assert_match /<#{tag_name}.* #{attrs}/, tag
def assert_no_attrs(tag, tag_name: :input, **attrs)
assert_no_match(/<#{tag_name}.* #{escaped_attrs(attrs)}/, tag)
end

private
def escaped_attrs(attrs)
attrs.map do |k, v|
%Q(#{escape_specials(k)}="#{escape_specials(v)}".*)
end.join(" ")
end

def escape_specials(value)
special_characters = Regexp.union "[]".chars
value.to_s.gsub(special_characters) { |char| "\\#{char}" }
Expand Down
3 changes: 3 additions & 0 deletions test/dummy/app/controllers/comboboxes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def include_blank
def custom_events
end

def custom_attrs
end

private
delegate :combobox_options, :html_combobox_options, to: "ApplicationController.helpers", private: true

Expand Down
12 changes: 12 additions & 0 deletions test/dummy/app/views/comboboxes/custom_attrs.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%= combobox_tag "state", State.all do |combobox| %>
<% combobox.customize_fieldset class: "custom-class--fieldset", data: { customized_fieldset: "" } %>
<% combobox.customize_hidden_field class: "custom-class--hidden_field", data: { customized_hidden_field: "" } %>
<% combobox.customize_input class: "custom-class--input", data: { customized_input: "" } %>
<% combobox.customize_handle class: "custom-class--handle", data: { customized_handle: "" } %>
<% combobox.customize_listbox class: "custom-class--listbox", data: { customized_listbox: "" } %>
<% combobox.customize_dialog class: "custom-class--dialog", data: { customized_dialog: "" } %>
<% combobox.customize_dialog_wrapper class: "custom-class--dialog_wrapper", data: { customized_dialog_wrapper: "" } %>
<% combobox.customize_dialog_label class: "custom-class--dialog_label", data: { customized_dialog_label: "" } %>
<% combobox.customize_dialog_input class: "custom-class--dialog_input", data: { customized_dialog_input: "" } %>
<% combobox.customize_dialog_listbox class: "custom-class--dialog_listbox", data: { customized_dialog_listbox: "" } %>
<% end %>
1 change: 1 addition & 0 deletions test/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
get "enum", to: "comboboxes#enum"
get "include_blank", to: "comboboxes#include_blank"
get "custom_events", to: "comboboxes#custom_events"
get "custom_attrs", to: "comboboxes#custom_attrs"

resources :movies, only: %i[ index update ]
get "movies_html", to: "movies#index_html"
Expand Down
27 changes: 25 additions & 2 deletions test/presenters/hotwire_combobox/component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,36 @@
class HotwireCombobox::ComponentTest < ApplicationViewTestCase
test "native html autocomplete is off by default" do
HotwireCombobox::Component.new(view, :foo).tap do |component|
assert_equal :off, component.input_attrs[:autocomplete]
assert_equal "off", component.input_attrs[:autocomplete].to_s
end
end

test "native html autocomplete can be turned on" do
HotwireCombobox::Component.new(view, :foo, input: { autocomplete: :on }).tap do |component|
assert_equal :on, component.input_attrs[:autocomplete]
assert_equal "on", component.input_attrs[:autocomplete].to_s
end
end

test "attributes can be customized" do
component = HotwireCombobox::Component.new(view, "field-name", id: "id-string")
component.customize_input class: "my-custom-class", data: { my_custom_attr: "value" }
html = render component

assert_attrs html, tag_name: :input, class: "hw-combobox__input my-custom-class"
assert_attrs html, tag_name: :input, "data-my-custom-attr": "value"
end

test "protected attributes cannot be overridden" do
component = HotwireCombobox::Component.new(view, "field-name", id: "id-string")
component.customize_input id: "foo", name: "bar", role: "baz", value: "qux", aria: { haspopup: "foobar" }, data: { hw_combobox_target: "thud" }
html = render component

assert_attrs html, tag_name: :input, id: "id-string"
assert_attrs html, tag_name: :input, name: "field-name"
assert_attrs html, tag_name: :input, role: "combobox"
assert_attrs html, tag_name: :input, "aria-haspopup": "listbox"
assert_attrs html, tag_name: :input, "data-hw-combobox-target": "combobox"

assert_no_attrs html, tag_name: :input, value: ""
end
end
36 changes: 36 additions & 0 deletions test/system/hotwire_combobox_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,42 @@ class HotwireComboboxTest < ApplicationSystemTestCase
assert_text "isValid: false"
end

test "customized elements" do
visit custom_attrs_path

assert_selector ".custom-class--fieldset"
assert_selector ".custom-class--input"
assert_selector ".custom-class--handle"
assert_selector ".custom-class--hidden_field", visible: :hidden
assert_selector ".custom-class--listbox", visible: :hidden
assert_selector ".custom-class--dialog", visible: :hidden
assert_selector ".custom-class--dialog_wrapper", visible: :hidden
assert_selector ".custom-class--dialog_label", visible: :hidden
assert_selector ".custom-class--dialog_input", visible: :hidden
assert_selector ".custom-class--dialog_listbox", visible: :hidden

assert_selector ".hw-combobox"
assert_selector ".hw-combobox__input"
assert_selector ".hw-combobox__handle"
assert_selector ".hw-combobox__listbox", visible: :hidden
assert_selector ".hw-combobox__option", visible: :hidden
assert_selector ".hw-combobox__dialog", visible: :hidden
assert_selector ".hw-combobox__dialog__label", visible: :hidden
assert_selector ".hw-combobox__dialog__input", visible: :hidden
assert_selector ".hw-combobox__dialog__listbox", visible: :hidden

assert_selector "[data-customized-fieldset]"
assert_selector "[data-customized-input]"
assert_selector "[data-customized-handle]"
assert_selector "[data-customized-hidden-field]", visible: :hidden
assert_selector "[data-customized-listbox]", visible: :hidden
assert_selector "[data-customized-dialog]", visible: :hidden
assert_selector "[data-customized-dialog-wrapper]", visible: :hidden
assert_selector "[data-customized-dialog-label]", visible: :hidden
assert_selector "[data-customized-dialog-input]", visible: :hidden
assert_selector "[data-customized-dialog-listbox]", visible: :hidden
end

private
def open_combobox(selector)
find(selector).click
Expand Down

0 comments on commit 65366dd

Please sign in to comment.