Skip to content

Commit bc7ef6a

Browse files
committed
Update address form component to allow more customization
An updated iteration of address component: - now using default slots (https://viewcomponent.org/guide/slots.html#default_slot_name) and allow overriding them; Note: `SlotableDefault` had to be patched as it has a bug: ViewComponent/view_component#2169 - default address fields are contained in separate components (currently "contact" and "location" with a set of relevant fields for both); - it is possible to add a new default named fieldset by creating new fieldset component and placing it along with two other default ones; - if user wants to include more fields to the default fieldset or redefine a default field, they can pass extension definitions in `extends` parameter; - to opt out of rendering certain fields one can pass `excludes` array, and those fields will not be included in the view; - users can pass their own fieldsets if default ones do not fit their needs, defining fieldset slot `#with_fieldset &block` and `block` evaluated content will render instead of a default fieldset;
1 parent c3011c9 commit bc7ef6a

File tree

18 files changed

+219
-100
lines changed

18 files changed

+219
-100
lines changed

admin/app/components/solidus_admin/ui/forms/address/component.html.erb

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,6 @@
22
<%= :disabled if @disabled %>
33
>
44
<div class="<%= stimulus_id %>--address-form flex flex-wrap gap-4 pb-4">
5-
<%= name %>
6-
<%= street %>
7-
<%= street_contd %>
8-
9-
<% if city? || zipcode? %>
10-
<div class="flex gap-4 w-full">
11-
<%= city %>
12-
<%= zipcode %>
13-
</div>
14-
<% end %>
15-
16-
<%= country_and_state %>
17-
18-
<%= phone %>
19-
<%= email %>
20-
<% if Spree::Backend::Config.show_reverse_charge_fields %>
21-
<%= vat_id %>
22-
<%= reverse_charge_status %>
23-
<% end %>
5+
<%= fieldset %>
246
</div>
257
</fieldset>
Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,37 @@
11
# frozen_string_literal: true
22

33
class SolidusAdmin::UI::Forms::Address::Component < SolidusAdmin::BaseComponent
4-
DEFAULT_FIELDS = %i[
5-
street
6-
street_contd
7-
city
8-
zipcode
9-
country_and_state
10-
phone
11-
email
12-
].freeze
4+
DefaultNamedFieldsetNotFound = Class.new(NameError)
135

14-
FIELDS_PRESETS = {
15-
contact: DEFAULT_FIELDS + %i[name vat_id reverse_charge_status],
16-
location: DEFAULT_FIELDS,
17-
}.freeze
6+
include ViewComponent::SlotableDefault
187

19-
renders_one :name, -> do
20-
component("ui/forms/field").text_field(@form_field_name, :name, object: @addressable)
21-
end
22-
23-
renders_one :street, -> do
24-
component("ui/forms/field").text_field(@form_field_name, :address1, object: @addressable)
25-
end
26-
27-
renders_one :street_contd, -> do
28-
component("ui/forms/field").text_field(@form_field_name, :address2, object: @addressable)
29-
end
30-
31-
renders_one :city, -> do
32-
component("ui/forms/field").text_field(@form_field_name, :city, object: @addressable)
33-
end
34-
35-
renders_one :zipcode, -> do
36-
component("ui/forms/field").text_field(@form_field_name, :zipcode, object: @addressable)
37-
end
8+
renders_one :fieldset
389

39-
renders_one :country_and_state, SolidusAdmin::UI::Forms::Address::CountryAndState::Component
40-
renders_one :phone, -> do
41-
component("ui/forms/field").text_field(@form_field_name, :phone, object: @addressable)
42-
end
43-
44-
renders_one :email, -> do
45-
component("ui/forms/field").text_field(@form_field_name, :email, object: @addressable)
46-
end
47-
48-
renders_one :vat_id, -> do
49-
component("ui/forms/field").text_field(@form_field_name, :vat_id, object: @addressable)
50-
end
51-
52-
renders_one :reverse_charge_status, -> do
53-
component("ui/forms/field").select(
54-
@form_field_name,
55-
:reverse_charge_status,
56-
Spree::Address.reverse_charge_statuses.keys.map { |key| [I18n.t("spree.reverse_charge_statuses.#{key}"), key] },
57-
object: @addressable
10+
# @param fieldset [Symbol] use a default named fieldset, component of the same name must be defined
11+
# in "ui/forms/address/fieldsets"
12+
# @param extends [Array<Symbol, Hash{Symbol => #call}>] extend default fieldset,
13+
# see +SolidusAdmin::UI::Forms::Address::Fieldsets::Base+
14+
# @param excludes [Array<Symbol>, Symbol] optionally exclude fields that are present in a default fieldset
15+
# @raise [DefaultNamedFieldsetNotFound] if the provided +:fieldset+ option does not correspond to a defined component
16+
# in "ui/forms/address/fieldsets"
17+
def initialize(addressable:, form_field_name:, disabled: false, fieldset: :contact, extends: [], excludes: [])
18+
@disabled = disabled
19+
@default_fieldset = fieldset_component(fieldset).new(
20+
addressable:,
21+
form_field_name:,
22+
extends:,
23+
excludes:,
5824
)
5925
end
6026

61-
# @param fields_preset [Symbol] decides which set of fields to render, accepted values: [:contact, :location]
62-
# @param include_fields [Symbol] optionally include fields that are not present in default/chosen field preset
63-
# @param exclude_fields [Symbol] optionally exclude fields that are present in default/chosen field preset
64-
def initialize(addressable:, form_field_name:, disabled: false, fields_preset: :contact, include_fields: [], exclude_fields: [])
65-
@addressable = addressable
66-
@form_field_name = form_field_name
67-
@disabled = disabled
27+
attr_reader :default_fieldset
6828

69-
fields = FIELDS_PRESETS[fields_preset] || []
70-
fields = (fields + Array.wrap(include_fields.map(&:to_sym)) - Array.wrap(exclude_fields.map(&:to_sym))).uniq
29+
private
7130

72-
with_name if fields.include?(:name)
73-
with_street if fields.include?(:street)
74-
with_street_contd if fields.include?(:street_contd)
75-
with_city if fields.include?(:city)
76-
with_zipcode if fields.include?(:zipcode)
77-
with_country_and_state(addressable:, form_field_name:) if fields.include?(:country_and_state)
78-
with_phone if fields.include?(:phone)
79-
with_email if fields.include?(:email)
80-
with_vat_id if fields.include?(:vat_id)
81-
with_reverse_charge_status if fields.include?(:reverse_charge_status)
31+
def fieldset_component(fieldset)
32+
component("ui/forms/address/fieldsets/#{fieldset}")
33+
rescue SolidusAdmin::ComponentRegistry::ComponentNotFoundError
34+
raise DefaultNamedFieldsetNotFound,
35+
"to use a default named fieldset `#{fieldset}` you must implement a component in 'ui/forms/address/fieldsets/#{fieldset}'"
8236
end
8337
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="flex gap-4 w-full">
2+
<%= render component("ui/forms/field").text_field(@form_field_name, :city, object: @addressable) %>
3+
<%= render component("ui/forms/field").text_field(@form_field_name, :zipcode, object: @addressable) %>
4+
</div>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
class SolidusAdmin::UI::Forms::Address::Fields::CityAndZipcode::Component < SolidusAdmin::BaseComponent
4+
def initialize(addressable:, form_field_name:)
5+
@addressable = addressable
6+
@form_field_name = form_field_name
7+
end
8+
end

admin/app/components/solidus_admin/ui/forms/address/country_and_state/component.html.erb renamed to admin/app/components/solidus_admin/ui/forms/address/fields/country_and_state/component.html.erb

File renamed without changes.

admin/app/components/solidus_admin/ui/forms/address/country_and_state/component.js renamed to admin/app/components/solidus_admin/ui/forms/address/fields/country_and_state/component.js

File renamed without changes.

admin/app/components/solidus_admin/ui/forms/address/country_and_state/component.rb renamed to admin/app/components/solidus_admin/ui/forms/address/fields/country_and_state/component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
class SolidusAdmin::UI::Forms::Address::CountryAndState::Component < SolidusAdmin::BaseComponent
3+
class SolidusAdmin::UI::Forms::Address::Fields::CountryAndState::Component < SolidusAdmin::BaseComponent
44
def initialize(addressable:, form_field_name:)
55
@addressable = addressable
66
@form_field_name = form_field_name
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<% if Spree::Backend::Config.show_reverse_charge_fields %>
2+
<%= render component("ui/forms/field").text_field(@form_field_name, :vat_id, object: @addressable) %>
3+
<%= render component("ui/forms/field").select(
4+
@form_field_name,
5+
:reverse_charge_status,
6+
Spree::Address.reverse_charge_statuses.keys.map { |key| [I18n.t("spree.reverse_charge_statuses.#{key}"), key] },
7+
object: @addressable
8+
) %>
9+
<% end %>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
class SolidusAdmin::UI::Forms::Address::Fields::ReverseChargeFields::Component < SolidusAdmin::BaseComponent
4+
def initialize(addressable:, form_field_name:)
5+
@addressable = addressable
6+
@form_field_name = form_field_name
7+
end
8+
end
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
class SolidusAdmin::UI::Forms::Address::Fieldsets::Base < SolidusAdmin::BaseComponent
4+
renders_many :fields
5+
6+
# @param extends [Array<Hash{Symbol => #call}, Symbol>] Pass an array of extensions to modify existing default
7+
# fieldset with custom fields or override existing fields.
8+
# If extension is a Hash, its key should be the name of the field and its value should be an object that responds
9+
# to #call (e.g. proc or lambda) and returns a ViewComponent instance (or any object that responds to #render_in).
10+
#
11+
# Since text inputs are often used as form fields, pass your field name as a Symbol and the component will render
12+
# a text input for that field.
13+
# @example
14+
# component("ui/forms/address/fieldsets/contact").new(
15+
# extends: [
16+
# title: -> { component("ui/forms/field").select(...) }, # this will add a custom :title select field
17+
# name: -> { component("path/to/component").new }, # this will override existing default :name field
18+
# :company, # this will add a text field for :company
19+
# ],
20+
# excludes: %i[phone reverse_charge], # this will exclude :phone and :reverse_charge from the fieldset
21+
# )
22+
def initialize(addressable:, form_field_name:, extends: [], excludes: [])
23+
@addressable = addressable
24+
@form_field_name = form_field_name
25+
excludes = Array.wrap(excludes).map(&:to_sym)
26+
27+
extended_fields_map = extends.reduce({}) do |acc, extension|
28+
if extension.is_a?(Hash)
29+
acc.merge!(extension)
30+
else
31+
acc[extension.to_sym] = -> { text_field_component(extension) }
32+
acc
33+
end
34+
end
35+
36+
fields_map.merge(extended_fields_map).each do |field_name, renderable|
37+
with_field { render renderable.call } unless field_name.in?(excludes)
38+
end
39+
end
40+
41+
def fields_map
42+
raise NotImplementedError, "fields_map must be implemented in #{self.class}"
43+
end
44+
45+
def call
46+
safe_join(fields)
47+
end
48+
49+
private
50+
51+
def text_field_component(field_name)
52+
component("ui/forms/field").text_field(@form_field_name, field_name.to_sym, object: @addressable)
53+
end
54+
end

0 commit comments

Comments
 (0)