Module: Basecoat::FormHelper

Defined in:
lib/basecoat/form_helper.rb

Instance Method Summary collapse

Instance Method Details

#basecoat_country_select_tag(name, options = {}) ⇒ Object

Renders a country select component with flag emojis.

Parameters

  • name - The name attribute for the hidden input field (required)
  • options - Hash of optional parameters:
    • :selected - The initially selected country code (e.g., "US")
    • :countries - Array of country codes to include (defaults to all countries)
    • :priority - Array of priority country codes to show at the top
    • :except - Array of country codes to exclude
    • All other basecoat_select_tag options (placeholder, scrollable, etc.)

Examples

<%= basecoat_country_select_tag :country %> <%= basecoat_country_select_tag :country, selected: "US" %> <%= basecoat_country_select_tag :country, priority: ["US", "CA", "GB"] %> <%= basecoat_country_select_tag :country, countries: ["US", "CA", "MX"] %>



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/basecoat/form_helper.rb', line 120

def basecoat_country_select_tag(name, options = {})
  require 'countries'

  # Extract country-specific options
  country_codes = options.delete(:countries)
  priority_codes = options.delete(:priority) || []
  except_codes = options.delete(:except) || []

  # Get all countries or filtered list
  all_countries = if country_codes
    country_codes.map { |code| ISO3166::Country[code] }.compact
  else
    ISO3166::Country.all
  end

  # Remove excluded countries
  all_countries.reject! { |country| except_codes.include?(country.alpha2) } if except_codes.any?

  # Build choices with flag emoji
  choices = all_countries.map do |country|
    ["#{country.emoji_flag} #{country.iso_short_name}", country.alpha2]
  end

  # Sort alphabetically by country name
  choices.sort_by! { |label, _| label }

  # Add priority countries at the top
  if priority_codes.any?
    priority_choices = priority_codes.map do |code|
      country = ISO3166::Country[code]
      next unless country
      ["#{country.emoji_flag} #{country.iso_short_name}", country.alpha2]
    end.compact

    choices.reject! { |_, code| priority_codes.include?(code) }
    choices = priority_choices + [["---", nil]] + choices
  end

  # Update selected_label if a country is selected
  if options[:selected]
    country = ISO3166::Country[options[:selected]]
    options[:selected_label] = "#{country.emoji_flag} #{country.iso_short_name}" if country
  end

  # Set defaults
  options[:placeholder] ||= "Search countries..."
  options[:group_label] ||= "Countries"
  options[:scrollable] = true unless options.key?(:scrollable)

  basecoat_select_tag(name, choices, options)

rescue LoadError
   :div, class: "alert", "data-variant": "destructive" do
    lucide_icon("circle-alert") + tag.section("gem 'countries' required")
  end
end

#basecoat_remote_search_tag(url, options = {}) ⇒ Object

Renders a remote search component with Turbo Stream support.

Parameters

  • url - The URL to fetch search results from (required)
  • options - Hash of optional parameters:
    • :turbo_frame - Custom turbo frame name (defaults to underscored URL)
    • :placeholder - Custom placeholder text (defaults to "Type a command or search...")
    • :data_empty - Message shown when no results found (defaults to "No results found.")
    • :classes - Additional CSS classes to apply to the component

Examples

<%= basecoat_remote_search_tag("/posts/search") %> <%= basecoat_remote_search_tag("/posts/search", turbo_frame: "custom_frame") %> <%= basecoat_remote_search_tag("/posts/search", placeholder: "Search posts...", classes: "rounded-lg border shadow-md") %>



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/basecoat/form_helper.rb', line 18

def basecoat_remote_search_tag(url, options = {})
  turbo_frame = options[:turbo_frame] || url.gsub("/", "_")
  placeholder = options[:placeholder] || "Type a command or search..."
  data_empty = options[:data_empty] || "No results found."
  classes = options[:classes] || ""
  search_id = "search-#{SecureRandom.random_number(1000000)}"

  (:div, id: search_id, data: { controller: "search", search_url_value: url }, class: "command #{classes}", "aria-label": "Command menu") do
    (:header) do
      lucide_icon("search").html_safe +
      tag(:input,
        type: "search",
        id: "#{search_id}-input",
        placeholder: placeholder,
        autocomplete: "off",
        autocorrect: "off",
        spellcheck: "false",
        "aria-autocomplete": "list",
        role: "combobox",
        "aria-expanded": "true",
        "aria-controls": "#{search_id}-menu",
        data: {
          search_target: "input",
          action: "input->search#search"
        }
      )
    end +
    (:div, role: "menu", id: "#{search_id}-menu", "aria-orientation": "vertical", data: { empty: data_empty }, class: "scrollbar") do
      turbo_frame_tag(turbo_frame)
    end
  end
end

#basecoat_select_tag(name, choices, options = {}) ⇒ Object

Renders a select component outside of a form context.

Parameters

  • name - The name attribute for the hidden input field (required)
  • choices - Array of [label, value] pairs for select options (required)
  • options - Hash of optional parameters:
    • :selected - The initially selected value (defaults to first choice)
    • :group_label - Label for the option group (defaults to titleized and pluralized name)
    • :placeholder - Placeholder text for the search input (defaults to "Search entries...")
    • :url - URL for remote search via Turbo Stream
    • :turbo_frame - Custom turbo frame name when using :url (defaults to underscored URL)
    • :scrollable - Whether to make the listbox scrollable with max height (defaults to false)

Examples

<%= basecoat_select_tag "fruit", [["Apple", 1], ["Pear", 2]] %> <%= basecoat_select_tag "fruit", [["Apple", 1], ["Pear", 2]], selected: 2, placeholder: "Search fruits..." %> <%= basecoat_select_tag "fruit", [], url: "/fruits/search", turbo_frame: "custom_frame", scrollable: true %>



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/basecoat/form_helper.rb', line 69

def basecoat_select_tag(name, choices, options = {})
  select_id = "select-#{SecureRandom.random_number(1000000)}"
  url = options[:url]

  if url || choices.empty?
    selected_value = options[:selected]
    selected_label = options[:selected_label] || "Select..."
  else
    selected_value = options[:selected] || choices.first[1]
    selected_choice = choices.find { |label, val| val.to_s == selected_value.to_s }
    selected_label = selected_choice ? selected_choice[0] : choices.first[0]
  end

  group_label = options[:group_label] || name.to_s.titleize.pluralize
  placeholder = options[:placeholder] || "Search entries..."
  scrollable = options[:scrollable] || false
  turbo_frame = options[:turbo_frame] || (url ? url.gsub("/", "_") : nil)

  select_attrs = { id: select_id, class: "combobox" }
  if url
    select_attrs[:data] = {
      controller: "search",
      search_url_value: url,
      filter: "manual" # filtering happens server-side, tell basecoat not to filter
    }
  end

  (:div, select_attrs) do
    basecoat_select_button(select_id, selected_label) +
    basecoat_select_popover(select_id, choices, group_label, placeholder, selected_value, url, scrollable, turbo_frame) +
    tag(:input, type: "hidden", name: name, value: selected_value)
  end
end