-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5350 from nebulab/waiting-for-dev/admin/text_fiel…
…d_component [Admin] Add text_field component
- Loading branch information
Showing
5 changed files
with
348 additions
and
0 deletions.
There are no files selected for viewing
156 changes: 156 additions & 0 deletions
156
admin/app/components/solidus_admin/ui/forms/text_field/component.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
# frozen_string_literal: true | ||
|
||
class SolidusAdmin::UI::Forms::TextField::Component < SolidusAdmin::BaseComponent | ||
SIZES = { | ||
s: %w[leading-4 body-small], | ||
m: %w[leading-6 body-small], | ||
l: %w[leading-9 body-text] | ||
}.freeze | ||
|
||
TYPES = { | ||
color: :color_field, | ||
date: :date_field, | ||
datetime: :datetime_field, | ||
email: :email_field, | ||
month: :month_field, | ||
number: :number_field, | ||
password: :password_field, | ||
phone: :phone_field, | ||
range: :range_field, | ||
search: :search_field, | ||
text: :text_field, | ||
time: :time_field, | ||
url: :url_field, | ||
week: :week_field | ||
}.freeze | ||
|
||
# @param field [Symbol] the name of the field. Usually a model attribute. | ||
# @param form [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 | ||
# 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:, type: :text, size: :m, hint: nil, errors: nil, **attributes) | ||
@field = field | ||
@form = form | ||
@type = type | ||
@size = size | ||
@hint = hint | ||
@type = type | ||
@attributes = attributes | ||
@errors = errors || @form.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 | ||
) | ||
end | ||
|
||
def call | ||
tag.div(class: "mb-6") do | ||
label_tag + field_tag + info_wrapper | ||
end | ||
end | ||
|
||
def info_wrapper | ||
tag.div(class: "mt-2") do | ||
hint_tag + error_tag | ||
end | ||
end | ||
|
||
def label_tag | ||
@form.label(@field, class: "block mb-0.5 body-tiny-bold") | ||
end | ||
|
||
def field_tag | ||
@form.send( | ||
field_helper, | ||
@field, | ||
class: field_classes, | ||
**field_aria_describedby_attribute, | ||
**field_error_attributes, | ||
**@attributes.except(:class) | ||
) | ||
end | ||
|
||
def field_classes | ||
%w[ | ||
peer | ||
block px-3 py-1.5 w-full | ||
text-black text-black | ||
bg-white border border-gray-300 rounded-sm | ||
hover:border-gray-500 | ||
placeholder:text-gray-400 | ||
focus:border-gray-500 focus:shadow-[0_0_0_2px_#bbb] focus-visible:outline-none | ||
disabled:bg-gray-50 disabled:text-gray-300 | ||
] + field_size_classes + field_error_classes + Array(@attributes[:class]).compact | ||
end | ||
|
||
def field_helper | ||
TYPES.fetch(@type) | ||
end | ||
|
||
def field_size_classes | ||
SIZES.fetch(@size) | ||
end | ||
|
||
def field_error_classes | ||
return [] unless errors? | ||
|
||
%w[border-red-400 text-red-400] | ||
end | ||
|
||
def field_aria_describedby_attribute | ||
return {} unless @hint || errors? | ||
|
||
{ | ||
"aria-describedby": "#{hint_id if @hint} #{error_id if errors?}" | ||
} | ||
end | ||
|
||
def field_error_attributes | ||
return {} unless errors? | ||
|
||
{ | ||
"aria-invalid": true | ||
} | ||
end | ||
|
||
def hint_tag | ||
return "".html_safe unless @hint | ||
|
||
tag.p(id: hint_id, class: "body-tiny text-gray-500 peer-disabled:text-gray-300") do | ||
@hint | ||
end | ||
end | ||
|
||
def hint_id | ||
"#{id_prefix}_hint" | ||
end | ||
|
||
def error_tag | ||
return "".html_safe unless errors? | ||
|
||
tag.p(id: error_id, class: "body-tiny text-red-400") do | ||
@errors[@field].map do |error| | ||
tag.span(class: "block") { error.capitalize } | ||
end.reduce(&:+) | ||
end | ||
end | ||
|
||
def errors? | ||
@errors[@field].present? | ||
end | ||
|
||
def error_id | ||
"#{id_prefix}_error" | ||
end | ||
|
||
def id_prefix | ||
"#{@form.object_name}_#{@field}" | ||
end | ||
end |
101 changes: 101 additions & 0 deletions
101
admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# frozen_string_literal: true | ||
|
||
# @component "ui/forms/text_field" | ||
class SolidusAdmin::UI::Forms::TextField::ComponentPreview < ViewComponent::Preview | ||
include SolidusAdmin::Preview | ||
|
||
# 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` | ||
# ](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) | ||
# helpers. | ||
# | ||
# When the form builder is not bound to a model instance, you must pass an | ||
# errors Hash to the component. For example: | ||
# | ||
# ```erb | ||
# <%= form_with(url: search_path, method: :get) do |form| %> | ||
# <%= render components('ui/forms/text_field').new( | ||
# form: form, | ||
# field: :q, | ||
# errors: params[:q].present? ? {} : { | ||
# q: ["can't be blank"] | ||
# } | ||
# ) %> | ||
# <%= form.submit "Search" %> | ||
# <% end %> | ||
# ``` | ||
# | ||
# When the form builder is bound to a model instance, the component will | ||
# automatically fetch the errors from the model. | ||
# | ||
# ```erb | ||
# <%= form_with(model: @user) do |form| %> | ||
# <%= render components('ui/forms/text_field').new( | ||
# form: form, | ||
# field: :name | ||
# ) %> | ||
# <%= form.submit "Save" %> | ||
# <% end %> | ||
def overview | ||
render_with_template( | ||
locals: { | ||
sizes: current_component::SIZES.keys, | ||
variants: { | ||
"empty" => { | ||
value: nil, disabled: false, hint: nil, errors: {} | ||
}, | ||
"filled" => { | ||
value: "Alice", disabled: false, hint: nil, errors: {} | ||
}, | ||
"with_hint" => { | ||
value: "Alice", disabled: false, hint: "No special characters", errors: {} | ||
}, | ||
"empty_with_error" => { | ||
value: nil, disabled: false, hint: nil, errors: { "empty_with_error" => ["can't be blank"] } | ||
}, | ||
"filled_with_error" => { | ||
value: "Alice", disabled: false, hint: nil, errors: { "filled_with_error" => ["is invalid"] } | ||
}, | ||
"with_hint_and_error" => { | ||
value: "Alice", disabled: false, hint: "No special characters", errors: { "with_hint_and_error" => ["is invalid"] } | ||
}, | ||
"empty_disabled" => { | ||
value: nil, disabled: true, hint: nil, errors: {} | ||
}, | ||
"filled_disabled" => { | ||
value: "Alice", disabled: true, hint: nil, errors: {} | ||
}, | ||
"with_hint_disabled" => { | ||
value: "Alice", disabled: true, hint: "No special characters", errors: {} | ||
} | ||
} | ||
} | ||
) | ||
end | ||
|
||
# @param size select { choices: [s, m, l] } | ||
# @param type select { choices: [color, date, datetime, email, month, number, password, phone, range, search, text, time, url, week] } | ||
# @param label text | ||
# @param value text | ||
# @param hint text | ||
# @param errors text (comma separated) | ||
# @param placeholder text | ||
# @param disabled toggle | ||
def playground(size: :m, type: :text, label: "Name", value: nil, hint: nil, errors: "", placeholder: "Placeholder", disabled: false) | ||
render_with_template( | ||
locals: { | ||
size: size.to_sym, | ||
type: type.to_sym, | ||
field: label, | ||
value: value, | ||
hint: hint, | ||
errors: { label.dasherize => (errors.blank? ? [] : errors.split(",").map(&:strip)) }, | ||
placeholder: placeholder, | ||
disabled: disabled | ||
} | ||
) | ||
end | ||
end |
34 changes: 34 additions & 0 deletions
34
...components/previews/solidus_admin/ui/forms/text_field/component_preview/overview.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<%= form_with(url: "#", scope: :overview, method: :get) do |form| %> | ||
<table> | ||
<thead> | ||
<tr> | ||
<% sizes.each do |size| %> | ||
<td class="px-3 py-1 text-gray-500 text-center body-text"><%= size.to_s.humanize %></td> | ||
<% end %> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<% | ||
variants.each_pair do |name, definition| %> | ||
<tr> | ||
<% sizes.each do |size| %> | ||
<td class="px-3 py-1"> | ||
<%= | ||
render current_component.new( | ||
form: form, | ||
field: name, | ||
size: size, | ||
errors: definition[:errors], | ||
hint: definition[:hint], | ||
disabled: definition[:disabled], | ||
placeholder: "Placeholder", | ||
value: definition[:value] | ||
) | ||
%> | ||
</td> | ||
<% end %> | ||
</tr> | ||
<% end %> | ||
</tbody> | ||
</table> | ||
<% end %> |
15 changes: 15 additions & 0 deletions
15
...mponents/previews/solidus_admin/ui/forms/text_field/component_preview/playground.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<%= form_with(url: "#", scope: :playground, method: :get, class: "w-56") do |form| %> | ||
<%= | ||
render current_component.new( | ||
form: form, | ||
size: size, | ||
type: type, | ||
field: field, | ||
value: value, | ||
hint: hint, | ||
errors: errors, | ||
placeholder: placeholder, | ||
disabled: disabled | ||
) | ||
%> | ||
<% end %> |
42 changes: 42 additions & 0 deletions
42
admin/spec/components/solidus_admin/ui/forms/text_field/component_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# frozen_string_literal: true | ||
|
||
require "spec_helper" | ||
|
||
RSpec.describe SolidusAdmin::UI::Forms::TextField::Component, type: :component do | ||
it "renders the overview preview" do | ||
render_preview(:overview) | ||
render_preview(:playground) | ||
end | ||
|
||
describe "#initialize" do | ||
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, errors: { name: ["can't be blank"] }) | ||
|
||
expect(component.errors?).to be(true) | ||
end | ||
|
||
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) | ||
|
||
expect(component.errors?).to be(true) | ||
end | ||
|
||
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, errors: { name: ["can't be blank"] }) | ||
|
||
expect(component.errors?).to be(true) | ||
end | ||
|
||
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) }.to raise_error(ArgumentError) | ||
end | ||
end | ||
end |