diff --git a/admin/app/components/solidus_admin/ui/forms/form/component.erb b/admin/app/components/solidus_admin/ui/forms/form/component.erb new file mode 100644 index 00000000000..7ea4458e700 --- /dev/null +++ b/admin/app/components/solidus_admin/ui/forms/form/component.erb @@ -0,0 +1,3 @@ +<%= form_with(**@attributes) do |builder| %> + <%= render_elements(@elements, builder) %> +<% end %> diff --git a/admin/app/components/solidus_admin/ui/forms/form/component.rb b/admin/app/components/solidus_admin/ui/forms/form/component.rb new file mode 100644 index 00000000000..121b1397f78 --- /dev/null +++ b/admin/app/components/solidus_admin/ui/forms/form/component.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class SolidusAdmin::UI::Forms::Form::Component < SolidusAdmin::BaseComponent + # @param elements [Array<#call(form, builder)>] Builders of renderable + # elements within a form context. They need to implement `#call(form, + # builder)`, where the arguments are an instance of this class and an + # instance of `ActionView::Helpers::FormBuilder`. The method needs to return + # something responding to `#render_in(view_context)`. See the following + # classes for examples: + # - {SolidusAdmin::Form::Elements::Field} + # - {SolidusAdmin::Form::Elements::Fieldset} + # - {SolidusAdmin::Form::Elements::Component} + # - {SolidusAdmin::Form::Elements::HTML} + # @param attributes [Hash] Attributes to pass to the Rails `form_with` helper, + # which is used to render the form. + def initialize( + elements:, + fieldset_component: component("ui/forms/fieldset"), + text_field_component: component("ui/forms/text_field"), + text_area_component: component("ui/forms/text_area"), + **attributes + ) + @elements = elements + @fieldset_component = fieldset_component + @text_field_component = text_field_component + @text_area_component = text_area_component + @attributes = attributes + end + + # @return [Hash{Symbol => SolidusAdmin::BaseComponent}] Hash of component + # classes dependencies given on initialization. + def dependencies + { + fieldset: @fieldset_component, + text_field: @text_field_component, + text_area: @text_area_component + } + end + + # @api private + def render_elements(elements, builder) + safe_join( + elements.map do |element| + render_element(element, builder) + end + ) + end + + # @api private + def render_element(element, builder) + render element.call(self, builder) + end +end diff --git a/admin/app/components/solidus_admin/ui/forms/guidance/component.rb b/admin/app/components/solidus_admin/ui/forms/guidance/component.rb index 3756a83b38d..69bf8ecb840 100644 --- a/admin/app/components/solidus_admin/ui/forms/guidance/component.rb +++ b/admin/app/components/solidus_admin/ui/forms/guidance/component.rb @@ -2,11 +2,11 @@ # @api private class SolidusAdmin::UI::Forms::Guidance::Component < SolidusAdmin::BaseComponent - def initialize(field:, form:, hint:, errors:) + def initialize(field:, builder:, hint:, errors:) @field = field - @form = form + @builder = builder @hint = hint - @errors = errors || @form.object&.errors || raise(ArgumentError, <<~MSG + @errors = errors || @builder.object&.errors || raise(ArgumentError, <<~MSG When the form builder is not bound to a model instance, you must pass an errors Hash (`{ field_name: [errors] }`) to the component. MSG @@ -52,7 +52,7 @@ def error_id end def prefix - "#{@form.object_name}_#{@field}" + "#{@builder.object_name}_#{@field}" end def aria_describedby diff --git a/admin/app/components/solidus_admin/ui/forms/label/component.rb b/admin/app/components/solidus_admin/ui/forms/label/component.rb index cab416168b0..072042cc811 100644 --- a/admin/app/components/solidus_admin/ui/forms/label/component.rb +++ b/admin/app/components/solidus_admin/ui/forms/label/component.rb @@ -2,12 +2,12 @@ # @api private class SolidusAdmin::UI::Forms::Label::Component < SolidusAdmin::BaseComponent - def initialize(field:, form:) + def initialize(field:, builder:) @field = field - @form = form + @builder = builder end def call - @form.label(@field, class: "block mb-0.5 body-tiny-bold") + @builder.label(@field, class: "block mb-0.5 body-tiny-bold") end end diff --git a/admin/app/components/solidus_admin/ui/forms/text_area/component.rb b/admin/app/components/solidus_admin/ui/forms/text_area/component.rb index a79b441be2c..2f334c2349b 100644 --- a/admin/app/components/solidus_admin/ui/forms/text_area/component.rb +++ b/admin/app/components/solidus_admin/ui/forms/text_area/component.rb @@ -8,18 +8,18 @@ class SolidusAdmin::UI::Forms::TextArea::Component < SolidusAdmin::BaseComponent }.freeze # @param field [Symbol] the name of the field. Usually a model attribute. - # @param form [ActionView::Helpers::FormBuilder] the form builder instance. + # @param builder [ActionView::Helpers::FormBuilder] the form builder instance. # @param size [Symbol] the size of the field: `:s`, `:m` or `:l`. # @param hint [String, null] helper text to display below the field. # @param errors [Hash, nil] a Hash of errors for the field. If `nil` and the - # form is bound to a model instance, the component will automatically fetch + # builder is bound to a model instance, the component will automatically fetch # the errors from the model. # @param attributes [Hash] additional HTML attributes to add to the field. # @raise [ArgumentError] when the form builder is not bound to a model # instance and no `errors` Hash is passed to the component. def initialize( field:, - form:, + builder:, size: :m, hint: nil, errors: nil, @@ -28,7 +28,7 @@ def initialize( **attributes ) @field = field - @form = form + @builder = builder @size = size @hint = hint @attributes = attributes @@ -40,7 +40,7 @@ def initialize( def call guidance = @guidance_component.new( field: @field, - form: @form, + builder: @builder, hint: @hint, errors: @errors ) @@ -51,7 +51,7 @@ def call end def field_tag(guidance) - @form.text_area( + @builder.text_area( @field, class: field_classes(guidance), **field_aria_describedby_attribute(guidance), @@ -100,7 +100,7 @@ def field_error_attributes(guidance) end def label_tag - render @label_component.new(field: @field, form: @form) + render @label_component.new(field: @field, builder: @builder) end def guidance_tag(guidance) diff --git a/admin/app/components/solidus_admin/ui/forms/text_field/component.rb b/admin/app/components/solidus_admin/ui/forms/text_field/component.rb index 164bd7f8063..44375b59c63 100644 --- a/admin/app/components/solidus_admin/ui/forms/text_field/component.rb +++ b/admin/app/components/solidus_admin/ui/forms/text_field/component.rb @@ -25,19 +25,19 @@ class SolidusAdmin::UI::Forms::TextField::Component < SolidusAdmin::BaseComponen }.freeze # @param field [Symbol] the name of the field. Usually a model attribute. - # @param form [ActionView::Helpers::FormBuilder] the form builder instance. + # @param builder [ActionView::Helpers::FormBuilder] the form builder instance. # @param type [Symbol] the type of the field. Defaults to `:text`. # @param size [Symbol] the size of the field: `:s`, `:m` or `:l`. # @param hint [String, null] helper text to display below the field. # @param errors [Hash, nil] a Hash of errors for the field. If `nil` and the - # form is bound to a model instance, the component will automatically fetch + # builder is bound to a model instance, the component will automatically fetch # the errors from the model. # @param attributes [Hash] additional HTML attributes to add to the field. # @raise [ArgumentError] when the form builder is not bound to a model # instance and no `errors` Hash is passed to the component. def initialize( field:, - form:, + builder:, type: :text, size: :m, hint: nil, @@ -47,7 +47,7 @@ def initialize( **attributes ) @field = field - @form = form + @builder = builder @type = type @size = size @hint = hint @@ -61,7 +61,7 @@ def initialize( def call guidance = @guidance_component.new( field: @field, - form: @form, + builder: @builder, hint: @hint, errors: @errors ) @@ -72,7 +72,7 @@ def call end def field_tag(guidance) - @form.send( + @builder.send( field_helper, @field, class: field_classes(guidance), @@ -126,7 +126,7 @@ def field_error_attributes(guidance) end def label_tag - render @label_component.new(field: @field, form: @form) + render @label_component.new(field: @field, builder: @builder) end def guidance_tag(guidance) diff --git a/admin/lib/solidus_admin/configuration.rb b/admin/lib/solidus_admin/configuration.rb index 8a25052951f..cc8fadab439 100644 --- a/admin/lib/solidus_admin/configuration.rb +++ b/admin/lib/solidus_admin/configuration.rb @@ -29,7 +29,7 @@ class Configuration < Spree::Preferences::Configuration SolidusAdmin::Engine.root.join("app/assets/javascripts/**/*.js"), SolidusAdmin::Engine.root.join("app/views/**/*.erb"), SolidusAdmin::Engine.root.join("app/components/**/*.{rb,erb,js}"), - SolidusAdmin::Engine.root.join("spec/components/previews/**/*.erb"), + SolidusAdmin::Engine.root.join("spec/components/previews/**/*.{erb,rb}"), Rails.root.join("public/solidus_admin/*.html"), Rails.root.join("app/helpers/solidus_admin/**/*.rb"), diff --git a/admin/lib/solidus_admin/form/element/component.rb b/admin/lib/solidus_admin/form/element/component.rb new file mode 100644 index 00000000000..c46751808ab --- /dev/null +++ b/admin/lib/solidus_admin/form/element/component.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module SolidusAdmin + module Form + module Element + # Builds an arbitrary component in a form context. + # + # This class can be used to render an arbitrary components in a form. + # + # This is useful when there's the need to render a component that's not + # strictly related to a form definition, but still needs to be within the + # form tags. + class Component + # @param component [ViewComponent::Base] the component instance to + # render. + def initialize(component:) + @component = component + end + + # @api private + def call(_form, _builder) + @component + end + end + end + end +end diff --git a/admin/lib/solidus_admin/form/element/field.rb b/admin/lib/solidus_admin/form/element/field.rb new file mode 100644 index 00000000000..9b54f22679e --- /dev/null +++ b/admin/lib/solidus_admin/form/element/field.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module SolidusAdmin + module Form + module Element + # Builds a form field component. + # + # This class encapsulates a form field definition and its resolution to a + # component. + class Field + # @param component [Symbol, ViewComponent::Base] the component to be + # used when rendering. It can be a component class (which needs to + # accept the `builder:` parameter on initialization) or a Symbol. When + # the latter, it's used to infer the one configured in the form + # instance. For instance, for a `:text_field` type, the component used + # will be the one given to the form component as the + # `text_field_component` keyword argument on initialization. + # @param attributes [Hash] attributes to pass to the field component. + def initialize(component:, **attributes) + @component = component + @attributes = attributes + end + + # @api private + def call(form, builder) + component_class(form).new( + builder: builder, + **@attributes + ) + end + + private + + def component_class(form) + case @component + when Symbol + form.dependencies[@component] + else + @component + end + end + end + end + end +end diff --git a/admin/lib/solidus_admin/form/element/fieldset.rb b/admin/lib/solidus_admin/form/element/fieldset.rb new file mode 100644 index 00000000000..08f01e4674d --- /dev/null +++ b/admin/lib/solidus_admin/form/element/fieldset.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module SolidusAdmin + module Form + module Element + # Builds a form fieldset component. + # + # This class encapsulates a form fieldset definition and its resolution to + # a component. + class Fieldset + # @param elements [Array<#call(form, builder)>] See + # {SolidusAdmin::UI::Forms::Form::Component#initialize}. + # @param component [ViewComponent::Base, nil] the component to be + # used when rendering. When `nil`, the component configured in the form + # `fieldset_component` keyword argument on initialization is used. + # @param attributes [Hash] Attributes to pass to the fieldset + # component. + def initialize(elements:, component: nil, **attributes) + @elements = elements + @component = component + @attributes = attributes + end + + # @api private + def call(form, builder) + component_class(form).new( + **@attributes + ).with_content( + render_elements(form, builder) + ) + end + + private + + def component_class(form) + @component || form.dependencies[:fieldset] + end + + def render_elements(form, builder) + return "" if @elements.empty? + + form.render_elements(@elements, builder) + end + end + end + end +end diff --git a/admin/lib/solidus_admin/form/element/html.rb b/admin/lib/solidus_admin/form/element/html.rb new file mode 100644 index 00000000000..e9c681507b2 --- /dev/null +++ b/admin/lib/solidus_admin/form/element/html.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module SolidusAdmin + module Form + module Element + # Builds arbitrary HTML in a form. + # + # This class can be used to render arbitrary content in a form. + # + # This is useful when there's the need to render content that's not + # strictly related to a form definition, but still needs to be within the + # form tags. If the content is a component, it's better to use + # {SolidusAdmin::Form::Element::Component} instead. + class HTML + # @param html [String] the HTML to render. + def initialize(html:) + @html = html + end + + # @api private + def call(_form, _builder) + self + end + + # @api private + def render_in(_view_context) + @html + end + end + end + end +end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview.rb index 3298c4e0024..798e05de3a6 100644 --- a/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview.rb +++ b/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview.rb @@ -6,7 +6,10 @@ class SolidusAdmin::UI::Forms::Fieldset::ComponentPreview < ViewComponent::Previ # The fieldset component is used to render a set of fields in a form. # - # In its most basic form, it wraps the yielded content in a fieldset tag: + # Most commonly, it'll be used indirectly through the definition given to a + # [form component](../form/overview). + # + # For standalone usage, it wraps the yielded content in a fieldset tag: # # ```erb # <%= render components('ui/forms/fieldset').new do %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview/overview.html.erb index 836381be8a6..187c07c52f2 100644 --- a/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview/overview.html.erb +++ b/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview/overview.html.erb @@ -5,7 +5,7 @@ <%= render component('ui/forms/text_field').new( field: :name, - form: form, + builder: form, errors: {} ) %> @@ -16,7 +16,7 @@ <%= render component('ui/forms/text_field').new( field: :name, - form: form, + builder: form, errors: {} ) %> @@ -27,7 +27,7 @@ <%= render component('ui/forms/text_field').new( field: :name, - form: form, + builder: form, errors: {} ) %> @@ -38,7 +38,7 @@ <%= render component('ui/forms/text_field').new( field: :name, - form: form, + builder: form, errors: {} ) %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview.rb new file mode 100644 index 00000000000..5674ee89ba4 --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +require "solidus_admin/form/element/field" +require "solidus_admin/form/element/fieldset" +require "solidus_admin/form/element/component" +require "solidus_admin/form/element/html" + +# @component "ui/forms/form" +class SolidusAdmin::UI::Forms::Form::ComponentPreview < ViewComponent::Preview + include SolidusAdmin::Preview + + # The form component is used to render a form tag along with its content, most + # commonly form fields. + # + # Internally, the + # [`form_with`](https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) + # Rails helper is used to render the form tag, and the component will dispatch + # given arguments to it. + # + # The definition of the form is provided from the outside through the + # `elements` parameter. This parameter is an array of builders of renderable + # elements, and Solidus Admin provides all the necessary ones to build a form + # following its UI: + # + # ## SolidusAdmin::Form::Element::Field + # + # This element renders a form field: + # + # ```erb + # <%= + # render components('ui/forms/form', + # model: Spree::Product.new, + # elements: [ + # SolidusAdmin::Form::Element::Field.new( + # component: :text_field, + # field: :name + # ) + # ] + # ) + # %> + # ``` + # + # The previous example will use the [`text_field` + # component](../text_field/overview), but you can use any of the available + # field component. + # + # ## SolidusAdmin::Form::Element::Fieldset + # + # Wraps a set of fields in a fieldset. + # + # You need to provide the inner fields akin to how it's done with the form + # component. [The fieldet component](../fieldset/overview) is used under the + # hood, and you can pass any of its attributes through the `attributes` + # parameter. + # + # ```erb + # <%= + # render components('ui/forms/form', + # model: Spree::Product.new, + # elements: [ + # SolidusAdmin::Form::Element::Fieldset.new( + # elements: [ + # SolidusAdmin::Form::Element::Field.new( + # component: :text_field, + # field: :name + # ) + # ], + # legend: "Product details", + # toggletip_attributes: { guide: "Minimal info", position: :right } + # ) + # ] + # ) + # %> + # ``` + # + # ## SolidusAdmin::Form::Element::Component + # + # This element allows you to render any component inside the form. + # + # ```erb + # <%= + # render components('ui/forms/form', + # model: Spree::Product.new, + # elements: [ + # SolidusAdmin::Form::Element::Component.new( + # component: MyCustomComponent.new + # ) + # ] + # ) + # %> + # ``` + # + # ## SolidusAdmin::Form::Element::HTML + # + # This element allows you to render any HTML inside the form. + # + # ```erb + # <%= + # render components('ui/forms/form', + # model: Spree::Product.new, + # elements: [ + # SolidusAdmin::Form::Element::HTML.new( + # html: "

Whatever HTML you want

".html_safe + # ) + # ] + # ) + # %> + # ``` + def overview + render_with_template( + locals: { + elements: elements + } + ) + end + + private + + def elements + [ + field_element, + fieldset_element, + component_element, + html_element + ] + end + + def field_element + SolidusAdmin::Form::Element::Field.new( + component: :text_field, + field: :name, + placeholder: "SolidusAdmin::Form::Element::Field", + errors: {} + ) + end + + def fieldset_element + SolidusAdmin::Form::Element::Fieldset.new( + elements: [ + SolidusAdmin::Form::Element::Field.new( + component: :text_field, + field: :name, + placeholder: "SolidusAdmin::Form::Element::Field", + errors: {} + ) + ], + legend: "SolidusAdmin::Form::Element::Fieldset" + ) + end + + def component_element + SolidusAdmin::Form::Element::Component.new( + component: Class.new(SolidusAdmin::BaseComponent) do + def self.name + "MyCustomComponent" + end + + def call + tag.p(class: "body-text-bold mb-2 italic") { "SolidusAdmin::Form::Element::Component" } + end + end.new + ) + end + + def html_element + SolidusAdmin::Form::Element::HTML.new( + html: "

SolidusAdmin::Form::Element::HTML

".html_safe + ) + end +end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview/overview.html.erb new file mode 100644 index 00000000000..40dee9a8220 --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview/overview.html.erb @@ -0,0 +1,5 @@ +
+ <%= + render current_component.new(url: "#", scope: :overview, method: :get, class: "m-auto", elements: elements) + %> +
diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/overview.html.erb index 56bd273fe2a..8ded42897d1 100644 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/overview.html.erb +++ b/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/overview.html.erb @@ -15,7 +15,7 @@ <%= render current_component.new( - form: form, + builder: form, field: name, size: size, errors: definition[:errors], diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/playground.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/playground.html.erb index f2e792fdea0..c12a2684850 100644 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/playground.html.erb +++ b/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/playground.html.erb @@ -1,7 +1,7 @@ <%= form_with(url: "#", scope: :playground, method: :get, class: "w-60") do |form| %> <%= render current_component.new( - form: form, + builder: form, size: size, field: field, value: value, diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview.rb index 79a0d95e30a..72d46308dd8 100644 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview.rb +++ b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview.rb @@ -6,7 +6,11 @@ class SolidusAdmin::UI::Forms::TextField::ComponentPreview < ViewComponent::Prev # The text field component is used to render a text field in a form. # - # It must be used within the block context yielded in the [`form_with` + # Most commonly, it'll be used indirectly through the definition given to a + # [form component](../form/overview). + # + # For standalone usage, it must be used within the block context yielded in + # the [`form_with` # ](https://api.rubyonrails.org/v5.1/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) # or # [`form_for`](https://api.rubyonrails.org/v5.1/classes/ActionView/Helpers/FormHelper.html#method-i-form_for) @@ -18,7 +22,7 @@ class SolidusAdmin::UI::Forms::TextField::ComponentPreview < ViewComponent::Prev # ```erb # <%= form_with(url: search_path, method: :get) do |form| %> # <%= render components('ui/forms/text_field').new( - # form: form, + # builder: form, # field: :q, # errors: params[:q].present? ? {} : { # q: ["can't be blank"] @@ -34,7 +38,7 @@ class SolidusAdmin::UI::Forms::TextField::ComponentPreview < ViewComponent::Prev # ```erb # <%= form_with(model: @user) do |form| %> # <%= render components('ui/forms/text_field').new( - # form: form, + # builder: form, # field: :name # ) %> # <%= form.submit "Save" %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/overview.html.erb index 35f0ca4e694..200f131e93b 100644 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/overview.html.erb +++ b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/overview.html.erb @@ -15,7 +15,7 @@ <%= render current_component.new( - form: form, + builder: form, field: name, size: size, errors: definition[:errors], diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/playground.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/playground.html.erb index 83f1ed221e3..76072bee203 100644 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/playground.html.erb +++ b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/playground.html.erb @@ -1,7 +1,7 @@ <%= form_with(url: "#", scope: :playground, method: :get, class: "w-56") do |form| %> <%= render current_component.new( - form: form, + builder: form, size: size, type: type, field: field, diff --git a/admin/spec/components/solidus_admin/ui/forms/form/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/form/component_spec.rb new file mode 100644 index 00000000000..87d5382ba0c --- /dev/null +++ b/admin/spec/components/solidus_admin/ui/forms/form/component_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SolidusAdmin::UI::Forms::Form::Component, type: :component do + it "renders the overview preview" do + render_preview(:overview) + end +end diff --git a/admin/spec/components/solidus_admin/ui/forms/guidance/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/guidance/component_spec.rb index daf45f92d51..f5092e16c0d 100644 --- a/admin/spec/components/solidus_admin/ui/forms/guidance/component_spec.rb +++ b/admin/spec/components/solidus_admin/ui/forms/guidance/component_spec.rb @@ -7,7 +7,7 @@ it "uses given errors when form is bound to a model" do form = double("form", object: double("model", errors: {})) - component = described_class.new(form: form, field: :name, hint: nil, errors: { name: ["can't be blank"] }) + component = described_class.new(builder: form, field: :name, hint: nil, errors: { name: ["can't be blank"] }) expect(component.errors?).to be(true) end @@ -15,7 +15,7 @@ it "uses model errors when form is bound to a model and they are not given" do form = double("form", object: double("model", errors: { name: ["can't be blank"] })) - component = described_class.new(form: form, field: :name, hint: nil, errors: nil) + component = described_class.new(builder: form, field: :name, hint: nil, errors: nil) expect(component.errors?).to be(true) end @@ -23,7 +23,7 @@ it "uses given errors when form is not bound to a model" do form = double("form", object: nil) - component = described_class.new(form: form, field: :name, hint: nil, errors: { name: ["can't be blank"] }) + component = described_class.new(builder: form, field: :name, hint: nil, errors: { name: ["can't be blank"] }) expect(component.errors?).to be(true) end @@ -31,7 +31,7 @@ it "raises an error when form is not bound to a model and errors are not given" do form = double("form", object: nil) - expect { described_class.new(form: form, field: :name, errors: nil) }.to raise_error(ArgumentError) + expect { described_class.new(builder: form, field: :name, errors: nil) }.to raise_error(ArgumentError) end end end diff --git a/admin/spec/solidus_admin/form/element/component_spec.rb b/admin/spec/solidus_admin/form/element/component_spec.rb new file mode 100644 index 00000000000..eda06035b50 --- /dev/null +++ b/admin/spec/solidus_admin/form/element/component_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "spec_helper" +require "solidus_admin/form/element/component" + +RSpec.describe SolidusAdmin::Form::Element::Component do + describe "#call" do + it "returns the given instance component" do + element = described_class.new(component: :component) + + expect( + element.call(double("form"), double("builder")) + ).to be(:component) + end + end +end diff --git a/admin/spec/solidus_admin/form/element/field_spec.rb b/admin/spec/solidus_admin/form/element/field_spec.rb new file mode 100644 index 00000000000..855b2549b6f --- /dev/null +++ b/admin/spec/solidus_admin/form/element/field_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "spec_helper" +require "solidus_admin/form/element/field" + +RSpec.describe SolidusAdmin::Form::Element::Field do + include SolidusAdmin::ComponentHelpers + + describe "#call" do + it "returns an instance of the given component" do + component = mock_component do + def initialize(builder:); end + end + builder = double("builder") + + element = described_class.new(component: component) + + expect( + element.call(double("form"), builder) + ).to be_a(component) + end + + it "initializes the component with the given attributes" do + component = mock_component do + attr_reader :builder, :attributes + + def initialize(builder:, **attributes) + @builder = builder + @attributes = attributes + end + end + attributes = { foo: :bar } + element = described_class.new(component: component, **attributes) + + result = element.call(double("form"), double("builder")) + + expect(result.attributes).to eq(attributes) + end + + it "initializes the component with the given builder" do + component = mock_component do + attr_reader :builder + + def initialize(builder:) + @builder = builder + end + end + builder = double("builder") + element = described_class.new(component: component) + + result = element.call(double("form"), builder) + + expect(result.builder).to be(builder) + end + + it "infers the component class from the form dependencies when given as a Symbol" do + component = mock_component do + def initialize(builder:); end + end + element = described_class.new(component: :text_field) + form = SolidusAdmin::UI::Forms::Form::Component.new( + elements: [element], + text_field_component: component + ) + + result = element.call(form, double("builder")) + + expect(result).to be_a(component) + end + end +end diff --git a/admin/spec/solidus_admin/form/element/fieldset_spec.rb b/admin/spec/solidus_admin/form/element/fieldset_spec.rb new file mode 100644 index 00000000000..20864a08c20 --- /dev/null +++ b/admin/spec/solidus_admin/form/element/fieldset_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "spec_helper" +require "solidus_admin/form/element/fieldset" +require "solidus_admin/form/element/html" + +RSpec.describe SolidusAdmin::Form::Element::Fieldset do + include SolidusAdmin::ComponentHelpers + + describe "#call" do + it "returns an instance of the given component" do + component = mock_component + element = described_class.new(component: component, elements: []) + + expect( + element.call(double("form"), double("builder")) + ).to be_a(component) + end + + it "initializes the component with the given attributes" do + component = mock_component do + attr_reader :attributes + + def initialize(**attributes) + @attributes = attributes + end + end + attributes = { foo: :bar } + element = described_class.new(component: component, elements: [], **attributes) + + result = element.call(double("form"), double("builder")) + + expect(result.attributes).to eq(attributes) + end + + it "gives the concatenation of the rendered elements as the content of the component" do + component = mock_component + elements = [ + SolidusAdmin::Form::Element::HTML.new(html: "foo"), + SolidusAdmin::Form::Element::HTML.new(html: "bar") + ] + element = described_class.new(component: component, elements: elements) + form = SolidusAdmin::UI::Forms::Form::Component.new(elements: [element]) + + # Workaround for view_context not being available in specs + expect(form).to receive(:render_element).with(elements[0], any_args).and_return("foo") + expect(form).to receive(:render_element).with(elements[1], any_args).and_return("bar") + + result = element.call(form, double("builder")) + + expect(result.content).to eq("foobar") + end + + it "uses the fieldset component from the form dependencies when component is not given" do + component = mock_component + element = described_class.new(elements: []) + form = SolidusAdmin::UI::Forms::Form::Component.new( + elements: [element], + fieldset_component: component + ) + + expect(element.call(form, double("builder"))).to be_a(component) + end + end +end diff --git a/admin/spec/solidus_admin/form/element/html_spec.rb b/admin/spec/solidus_admin/form/element/html_spec.rb new file mode 100644 index 00000000000..e6c0a7de0e8 --- /dev/null +++ b/admin/spec/solidus_admin/form/element/html_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "spec_helper" +require "solidus_admin/form/element/html" + +RSpec.describe SolidusAdmin::Form::Element::HTML do + describe "#call" do + it "returns itself" do + element = described_class.new(html: "foo") + + expect( + element.call(double("form"), double("builder")) + ).to be(element) + end + end + + describe "#render_in" do + it "returns the given HTML" do + element = described_class.new(html: "foo") + + expect( + element.render_in(double("view_context")) + ).to eq("foo") + end + end +end diff --git a/admin/spec/support/solidus_admin/component_helpers.rb b/admin/spec/support/solidus_admin/component_helpers.rb index 3b999589da8..e68d7d37c9d 100644 --- a/admin/spec/support/solidus_admin/component_helpers.rb +++ b/admin/spec/support/solidus_admin/component_helpers.rb @@ -18,7 +18,7 @@ def mock_component(&definition) def self.name "Foo" end - end.tap { |klass| klass.class_eval(&definition) } + end.tap { |klass| klass.class_eval(&definition) if definition } end end end