-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Refactor address form component (properly this time) #6225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
b977733
6d096ba
8169b9f
3fc263e
e45cec2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,64 +1,7 @@ | ||
| <fieldset class="<%= stimulus_id %>" | ||
| data-controller="<%= stimulus_id %>" | ||
| <%= :disabled if @disabled %> | ||
| > | ||
| <div class="<%= stimulus_id %>--address-form flex flex-wrap gap-4 pb-4"> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :name, object: @addressable) if @include_name_field %> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :address1, object: @addressable) %> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :address2, object: @addressable) %> | ||
| <div class="flex gap-4 w-full"> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :city, object: @addressable) %> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :zipcode, object: @addressable) %> | ||
| </div> | ||
|
|
||
| <%= render component("ui/forms/field").select( | ||
| @form_field_name, | ||
| :country_id, | ||
| Spree::Country.all.map { |c| [c.name, c.id] }, | ||
| object: @addressable, | ||
| value: @addressable.try(:country_id), | ||
| "data-#{stimulus_id}-target": "country", | ||
| "data-action": "change->#{stimulus_id}#loadStates" | ||
| ) %> | ||
|
|
||
| <%= content_tag(:div, | ||
| data: { "#{stimulus_id}-target": "stateNameWrapper" }, | ||
| class: (@addressable.country&.states&.empty? ? "flex flex-col gap-2 w-full" : "hidden flex flex-col gap-2 w-full") | ||
| ) do %> | ||
| <%= render component("ui/forms/field").text_field( | ||
| @form_field_name, | ||
| :state_name, | ||
| object: @addressable, | ||
| value: @addressable.try(:state_name), | ||
| "data-#{stimulus_id}-target": "stateName" | ||
| ) %> | ||
| <% end %> | ||
| <input autocomplete="off" type="hidden" name=<%= "#{@form_field_name}[state_id]" %>> | ||
|
|
||
| <%= content_tag(:div, | ||
| data: { "#{stimulus_id}-target": "stateWrapper" }, | ||
| class: (@addressable.country&.states&.empty? ? "hidden flex flex-col gap-2 w-full" : "flex flex-col gap-2 w-full") | ||
| ) do %> | ||
| <%= render component("ui/forms/field").select( | ||
| @form_field_name, | ||
| :state_id, | ||
| state_options, | ||
| object: @addressable, | ||
| value: @addressable.try(:state_id), | ||
| "data-#{stimulus_id}-target": "state" | ||
| ) %> | ||
| <% end %> | ||
|
|
||
| <%= render component("ui/forms/field").text_field(@form_field_name, :phone, object: @addressable) %> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :email, object: @addressable) %> | ||
| <% if Spree::Backend::Config.show_reverse_charge_fields %> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :vat_id, object: @addressable) %> | ||
| <%= render component("ui/forms/field").select( | ||
| @form_field_name, | ||
| :reverse_charge_status, | ||
| Spree::Address.reverse_charge_statuses.keys.map { |key| [I18n.t("spree.reverse_charge_statuses.#{key}"), key] }, | ||
| object: @addressable | ||
| ) %> | ||
| <% end %> | ||
| <%= fieldset %> | ||
| </div> | ||
| </fieldset> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,37 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class SolidusAdmin::UI::Forms::Address::Component < SolidusAdmin::BaseComponent | ||
| def initialize(addressable:, form_field_name:, disabled: false, include_name_field: true) | ||
| @addressable = addressable | ||
| @form_field_name = form_field_name | ||
| DefaultNamedFieldsetNotFound = Class.new(NameError) | ||
|
|
||
| include ViewComponent::SlotableDefault | ||
|
|
||
| renders_one :fieldset | ||
|
|
||
| # @param fieldset [Symbol] use a default named fieldset, component of the same name must be defined | ||
| # in "ui/forms/address/fieldsets" | ||
| # @param extends [Array<Symbol, Hash{Symbol => #call}>] extend default fieldset, | ||
| # see +SolidusAdmin::UI::Forms::Address::Fieldsets::Base+ | ||
| # @param excludes [Array<Symbol>, Symbol] optionally exclude fields that are present in a default fieldset | ||
| # @raise [DefaultNamedFieldsetNotFound] if the provided +:fieldset+ option does not correspond to a defined component | ||
| # in "ui/forms/address/fieldsets" | ||
| def initialize(addressable:, form_field_name:, disabled: false, fieldset: :contact, extends: [], excludes: []) | ||
| @disabled = disabled | ||
| @include_name_field = include_name_field | ||
| @default_fieldset = fieldset_component(fieldset).new( | ||
| addressable:, | ||
| form_field_name:, | ||
| extends:, | ||
| excludes:, | ||
| ) | ||
| end | ||
|
|
||
| def state_options | ||
| return [] unless @addressable.country | ||
| @addressable.country.states.map { |s| [s.name, s.id] } | ||
| attr_reader :default_fieldset | ||
|
|
||
| private | ||
|
|
||
| def fieldset_component(fieldset) | ||
| component("ui/forms/address/fieldsets/#{fieldset}") | ||
| rescue SolidusAdmin::ComponentRegistry::ComponentNotFoundError | ||
| raise DefaultNamedFieldsetNotFound, | ||
| "to use a default named fieldset `#{fieldset}` you must implement a component in 'ui/forms/address/fieldsets/#{fieldset}'" | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <div class="flex gap-4 w-full"> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :city, object: @addressable) %> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :zipcode, object: @addressable) %> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class SolidusAdmin::UI::Forms::Address::Fields::CityAndZipcode::Component < SolidusAdmin::BaseComponent | ||
| def initialize(addressable:, form_field_name:) | ||
| @addressable = addressable | ||
| @form_field_name = form_field_name | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| <div class="flex flex-col gap-4 w-full" data-controller="<%= stimulus_id %>"> | ||
| <%= render component("ui/forms/field").select( | ||
| @form_field_name, | ||
| :country_id, | ||
| Spree::Country.all.map { |c| [c.name, c.id] }, | ||
| object: @addressable, | ||
| value: @addressable.try(:country_id), | ||
| "data-#{stimulus_id}-target": "country", | ||
| "data-action": "change->#{stimulus_id}#loadStates" | ||
| ) %> | ||
|
|
||
| <%= content_tag(:div, | ||
| data: { "#{stimulus_id}-target": "stateNameWrapper" }, | ||
| class: (@addressable.country&.states&.empty? ? "flex flex-col gap-2 w-full" : "hidden flex flex-col gap-2 w-full") | ||
| ) do %> | ||
| <%= render component("ui/forms/field").text_field( | ||
| @form_field_name, | ||
| :state_name, | ||
| object: @addressable, | ||
| value: @addressable.try(:state_name), | ||
| "data-#{stimulus_id}-target": "stateName" | ||
| ) %> | ||
| <% end %> | ||
| <input autocomplete="off" type="hidden" name=<%= "#{@form_field_name}[state_id]" %>> | ||
|
|
||
| <%= content_tag(:div, | ||
| data: { "#{stimulus_id}-target": "stateWrapper" }, | ||
| class: (@addressable.country&.states&.empty? ? "hidden flex flex-col gap-2 w-full" : "flex flex-col gap-2 w-full") | ||
| ) do %> | ||
| <%= render component("ui/forms/field").select( | ||
| @form_field_name, | ||
| :state_id, | ||
| state_options, | ||
| object: @addressable, | ||
| value: @addressable.try(:state_id), | ||
| "data-#{stimulus_id}-target": "state" | ||
| ) %> | ||
| <% end %> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class SolidusAdmin::UI::Forms::Address::Fields::CountryAndState::Component < SolidusAdmin::BaseComponent | ||
| def initialize(addressable:, form_field_name:) | ||
| @addressable = addressable | ||
| @form_field_name = form_field_name | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def state_options | ||
| return [] unless @addressable.country | ||
| @addressable.country.states.map { |s| [s.name, s.id] } | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <% if Spree::Backend::Config.show_reverse_charge_fields %> | ||
| <%= render component("ui/forms/field").text_field(@form_field_name, :vat_id, object: @addressable) %> | ||
| <%= render component("ui/forms/field").select( | ||
| @form_field_name, | ||
| :reverse_charge_status, | ||
| Spree::Address.reverse_charge_statuses.keys.map { |key| [I18n.t("spree.reverse_charge_statuses.#{key}"), key] }, | ||
| object: @addressable | ||
| ) %> | ||
| <% end %> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class SolidusAdmin::UI::Forms::Address::Fields::ReverseChargeFields::Component < SolidusAdmin::BaseComponent | ||
| def initialize(addressable:, form_field_name:) | ||
| @addressable = addressable | ||
| @form_field_name = form_field_name | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class SolidusAdmin::UI::Forms::Address::Fieldsets::Base < SolidusAdmin::BaseComponent | ||
| renders_many :fields | ||
|
|
||
| # @param extends [Array<Hash{Symbol => #call}, Symbol>] Pass an array of extensions to modify existing default | ||
| # fieldset with custom fields or override existing fields. | ||
| # If extension is a Hash, its key should be the name of the field and its value should be an object that responds | ||
| # to #call (e.g. proc or lambda) and returns a ViewComponent instance (or any object that responds to #render_in). | ||
| # | ||
| # Since text inputs are often used as form fields, pass your field name as a Symbol and the component will render | ||
| # a text input for that field. | ||
| # @example | ||
| # component("ui/forms/address/fieldsets/contact").new( | ||
| # extends: [ | ||
| # title: -> { component("ui/forms/field").select(...) }, # this will add a custom :title select field | ||
| # name: -> { component("path/to/component").new }, # this will override existing default :name field | ||
| # :company, # this will add a text field for :company | ||
| # ], | ||
| # excludes: %i[phone reverse_charge], # this will exclude :phone and :reverse_charge from the fieldset | ||
| # ) | ||
| def initialize(addressable:, form_field_name:, extends: [], excludes: []) | ||
| @addressable = addressable | ||
| @form_field_name = form_field_name | ||
| excludes = Array.wrap(excludes).map(&:to_sym) | ||
|
|
||
| extended_fields_map = extends.reduce({}) do |acc, extension| | ||
| if extension.is_a?(Hash) | ||
| acc.merge!(extension) | ||
| else | ||
| acc[extension.to_sym] = -> { text_field_component(extension) } | ||
| acc | ||
| end | ||
| end | ||
|
|
||
| fields_map.merge(extended_fields_map).each do |field_name, renderable| | ||
| with_field { render renderable.call } unless field_name.in?(excludes) | ||
| end | ||
| end | ||
|
|
||
| def fields_map | ||
| raise NotImplementedError, "fields_map must be implemented in #{self.class}" | ||
| end | ||
|
|
||
| def call | ||
| safe_join(fields) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def text_field_component(field_name) | ||
| component("ui/forms/field").text_field(@form_field_name, field_name.to_sym, object: @addressable) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class SolidusAdmin::UI::Forms::Address::Fieldsets::Contact::Component < SolidusAdmin::UI::Forms::Address::Fieldsets::Base | ||
| def fields_map | ||
| { | ||
| name: -> { component("ui/forms/field").text_field(@form_field_name, :name, object: @addressable) }, | ||
| street: -> { component("ui/forms/field").text_field(@form_field_name, :address1, object: @addressable) }, | ||
| street_contd: -> { component("ui/forms/field").text_field(@form_field_name, :address2, object: @addressable) }, | ||
| city_and_zipcode: -> { component("ui/forms/address/fields/city_and_zipcode").new(form_field_name: @form_field_name, addressable: @addressable) }, | ||
| country_and_state: -> { component("ui/forms/address/fields/country_and_state").new(form_field_name: @form_field_name, addressable: @addressable) }, | ||
| phone: -> { component("ui/forms/field").text_field(@form_field_name, :phone, object: @addressable) }, | ||
| email: -> { component("ui/forms/field").text_field(@form_field_name, :email, object: @addressable) }, | ||
| reverse_charge: -> { component("ui/forms/address/fields/reverse_charge_fields").new(form_field_name: @form_field_name, addressable: @addressable) }, | ||
| } | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class SolidusAdmin::UI::Forms::Address::Fieldsets::Location::Component < SolidusAdmin::UI::Forms::Address::Fieldsets::Base | ||
| def fields_map | ||
| { | ||
| street: -> { component("ui/forms/field").text_field(@form_field_name, :address1, object: @addressable) }, | ||
| street_contd: -> { component("ui/forms/field").text_field(@form_field_name, :address2, object: @addressable) }, | ||
| city_and_zipcode: -> { component("ui/forms/address/fields/city_and_zipcode").new(form_field_name: @form_field_name, addressable: @addressable) }, | ||
| country_and_state: -> { component("ui/forms/address/fields/country_and_state").new(form_field_name: @form_field_name, addressable: @addressable) }, | ||
| phone: -> { component("ui/forms/field").text_field(@form_field_name, :phone, object: @addressable) }, | ||
| email: -> { component("ui/forms/field").text_field(@form_field_name, :email, object: @addressable) }, | ||
| } | ||
| end | ||
| end | ||
|
Check warning on line 14 in admin/app/components/solidus_admin/ui/forms/address/fieldsets/location/component.rb
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,5 +69,11 @@ class Engine < ::Rails::Engine | |
| SolidusAdmin::BaseComponent.include app.routes.mounted_helpers | ||
| end | ||
| end | ||
|
|
||
| initializer "solidus_admin.load_ext" do | ||
| Dir[root.join("lib/solidus_admin/ext/**/*.rb")].sort.each do |file| | ||
|
||
| require file | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # frozen_string_literal: true | ||
|
||
|
|
||
| # ViewComponent provides experimental functionality to define default content for slots | ||
| # https://viewcomponent.org/guide/slots.html#default_slot_name, but unfortunately | ||
| # it does not quite work: https://github.com/ViewComponent/view_component/issues/2169. | ||
| # Here we use a suggested fix from the issue, until it is fixed in ViewComponent. | ||
|
|
||
| module ViewComponent | ||
| module SlotableDefault | ||
| def get_slot(slot_name) | ||
| content unless content_evaluated? # ensure content is loaded so slots will be defined | ||
|
|
||
| @__vc_set_slots ||= {} | ||
|
|
||
| return super unless !@__vc_set_slots[slot_name] && (default_method = registered_slots[slot_name][:default_method]) | ||
|
|
||
| renderable_value = send(default_method) | ||
| slot = ViewComponent::Slot.new(self) | ||
|
|
||
| if renderable_value.respond_to?(:render_in) | ||
| slot.__vc_component_instance = renderable_value | ||
| else | ||
| slot.__vc_content = renderable_value | ||
| end | ||
|
|
||
| slot | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| <%= render current_component.new(form_field_name: "", addressable:) %> | ||
| <%= render current_component.new(form_field_name: "", addressable:, fieldset:) %> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <%= render current_component.new(form_field_name: "", addressable:) do |component| %> | ||
| <% component.with_fieldset do %> | ||
| <div class="flex gap-4 w-full"> | ||
| <%= render component("ui/forms/field").text_field("", :firstname, object: addressable) %> | ||
| <%= render component("ui/forms/field").text_field("", :lastname, object: addressable) %> | ||
| </div> | ||
| <%= render component("ui/forms/field").select("", :company, ["Rockstar Games", "CD Projekt Red", "Bethesda Softworks"], object: addressable) %> | ||
| <%= render component("ui/forms/field").text_field("", :vat_id, object: addressable) %> | ||
| <% end %> | ||
| <% end %> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <%= render current_component.new( | ||
| form_field_name: "", | ||
| addressable:, | ||
| fieldset:, | ||
| extends: [ | ||
| { street_contd: -> { component("ui/forms/field").text_area("", :address2, object: addressable) } }, | ||
| { company: -> { component("ui/forms/field").select("", :company, ["Rockstar Games", "CD Projekt Red", "Bethesda Softworks"], object: addressable) } }, | ||
| :alternative_phone, | ||
| ], | ||
| excludes: %i[phone email reverse_charge] | ||
| ) %> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a huge friend of having surprising behaviour like this in a template. We could make use of the
render?callback in the component class instead.