Form field
UI::FormFieldA label, hint and error message wired to any form control.
# VelvetKit
## Form field (`UI::FormField`)
A label, hint and error message wired to any form control.
Add it: `bin/rails generate velvetkit:add form_field`
Also adds: `label`
### Arguments
```ruby
UI::FormField.new(label: nil, hint: nil, error: nil, required: false, disabled: false, label_size: :default, label_weight: :medium, form: nil, field: nil, for: nil, class: nil, **html_attrs)
```
### Notes
A label, a control, a hint and an error message, wired together: the label
points at the control, and the control is described by the hint and error.
Input, Textarea, NativeSelect and Select render through it, so they take
`hint:` and `error:` directly. Wrap any other control yourself and spread
`field.control_attributes` onto it:
<%= render(UI::FormField.new(label: "Colour", hint: "Used for your avatar")) do |field| %>
<%= tag.input(type: "color", name: "colour", **field.control_attributes) %>
<% end %>
`error:` takes a message, or `true` to mark the control invalid without one.
Bound to a form (`form:` + `field:`), the model's errors on that attribute
are the message (for `team_id`, the `team` association's too, where
belongs_to puts "must exist"), and the attribute's human name is the label
(`label:` overrides it, `label: false` opts out). Nothing to show means no
wrapper: just the control.
### Example: On the built-in controls
```erb
<div class="flex flex-col gap-6 max-w-md">
<%= render(UI::Input.new(type: :email, label: "Email", hint: "We'll only use it to send receipts.", placeholder: "you@example.com")) %>
<%= render(UI::Input.new(label: "Username", value: "a", error: "Use at least 3 characters.")) %>
<%= render(UI::Textarea.new(label: "Bio", hint: "Shown on your public profile.", rows: 3)) %>
<%= render(UI::NativeSelect.new(label: "Team", options: %w[Design Engineering], prompt: "Choose a team", error: "Choose a team to continue.")) %>
<%= render(UI::Select.new(label: "Plan", options: %w[Free Pro Team], hint: "You can change plans at any time.")) %>
</div>
```
### Example: Errors from the model
```erb
<%# In an app, @signup comes back from a failed save. Bound fields show its errors on their own. %>
<% signup = Struct.new(:email, :bio) do
include ActiveModel::Validations
def self.model_name = ActiveModel::Name.new(self, nil, "Signup")
validates :email, presence: true
validates :bio, length: { minimum: 20 }
end.new("", "Hi").tap(&:validate) %>
<%= form_with(model: signup, url: "#", class: "flex flex-col gap-6 max-w-md") do |f| %>
<%= render(UI::Input.new(form: f, field: :email, type: :email)) %>
<%= render(UI::Textarea.new(form: f, field: :bio, hint: "A sentence or two about you.")) %>
<% end %>
```
### Example: Your own control
```erb
<div class="max-w-md">
<%= render(UI::FormField.new(label: "Accent colour", hint: "Used for your avatar and links.")) do |field| %>
<%= tag.input(type: "color", name: "accent", value: "#4d1a7f",
class: "h-9 w-16 rounded-md border border-input bg-background p-1", **field.control_attributes) %>
<% end %>
</div>
```
bin/rails generate velvetkit:add form_field
- Also adds
- Label
- Stimulus
- None, it's server-rendered
- Installs
- Nothing
Examples
On the built-in controls
We'll only use it to send receipts.
Use at least 3 characters.
Shown on your public profile.
Choose a team to continue.
Free
Pro
Team
You can change plans at any time.
default.html.erb
<div class="flex flex-col gap-6 max-w-md">
<%= render(UI::Input.new(type: :email, label: "Email", hint: "We'll only use it to send receipts.", placeholder: "you@example.com")) %>
<%= render(UI::Input.new(label: "Username", value: "a", error: "Use at least 3 characters.")) %>
<%= render(UI::Textarea.new(label: "Bio", hint: "Shown on your public profile.", rows: 3)) %>
<%= render(UI::NativeSelect.new(label: "Team", options: %w[Design Engineering], prompt: "Choose a team", error: "Choose a team to continue.")) %>
<%= render(UI::Select.new(label: "Plan", options: %w[Free Pro Team], hint: "You can change plans at any time.")) %>
</div>
Errors from the model
model_errors.html.erb
<%# In an app, @signup comes back from a failed save. Bound fields show its errors on their own. %>
<% signup = Struct.new(:email, :bio) do
include ActiveModel::Validations
def self.model_name = ActiveModel::Name.new(self, nil, "Signup")
validates :email, presence: true
validates :bio, length: { minimum: 20 }
end.new("", "Hi").tap(&:validate) %>
<%= form_with(model: signup, url: "#", class: "flex flex-col gap-6 max-w-md") do |f| %>
<%= render(UI::Input.new(form: f, field: :email, type: :email)) %>
<%= render(UI::Textarea.new(form: f, field: :bio, hint: "A sentence or two about you.")) %>
<% end %>
Your own control
Used for your avatar and links.
custom_control.html.erb
<div class="max-w-md">
<%= render(UI::FormField.new(label: "Accent colour", hint: "Used for your avatar and links.")) do |field| %>
<%= tag.input(type: "color", name: "accent", value: "#4d1a7f",
class: "h-9 w-16 rounded-md border border-input bg-background p-1", **field.control_attributes) %>
<% end %>
</div>
API
Arguments
UI::FormField.new(label: nil, hint: nil, error: nil, required: false, disabled: false, label_size: :default, label_weight: :medium, form: nil, field: nil, for: nil, class: nil, **html_attrs)
Notes
A label, a control, a hint and an error message, wired together: the label
points at the control, and the control is described by the hint and error.
Input, Textarea, NativeSelect and Select render through it, so they take
`hint:` and `error:` directly. Wrap any other control yourself and spread
`field.control_attributes` onto it:
<%= render(UI::FormField.new(label: "Colour", hint: "Used for your avatar")) do |field| %>
<%= tag.input(type: "color", name: "colour", **field.control_attributes) %>
<% end %>
`error:` takes a message, or `true` to mark the control invalid without one.
Bound to a form (`form:` + `field:`), the model's errors on that attribute
are the message (for `team_id`, the `team` association's too, where
belongs_to puts "must exist"), and the attribute's human name is the label
(`label:` overrides it, `label: false` opts out). Nothing to show means no
wrapper: just the control.