diff --git a/docs/app/views/docs/form.rb b/docs/app/views/docs/form.rb index 499ad4b65..2340dad1f 100644 --- a/docs/app/views/docs/form.rb +++ b/docs/app/views/docs/form.rb @@ -170,6 +170,95 @@ def view_template RUBY end + Heading(level: 2) { "Rails Integration" } + + Text do + plain "RubyUI Form components are plain HTML — they work with any form submission strategy. " + plain "The recommended approach for Rails apps is to use " + InlineLink(href: "https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with", target: "_blank") { "form_with" } + plain " to generate the " + code(class: "font-mono text-sm") { "action" } + plain " URL and CSRF token, then pass explicit " + code(class: "font-mono text-sm") { "name" } + plain " / " + code(class: "font-mono text-sm") { "id" } + plain " attributes to each RubyUI input so the browser serialises them correctly. " + plain "Server-side errors can be surfaced by rendering " + code(class: "font-mono text-sm") { "FormFieldError" } + plain " with content from " + code(class: "font-mono text-sm") { "model.errors.full_messages_for(:attr)" } + plain "." + end + + Heading(level: 3) { "Minimal Rails form" } + Codeblock(<<~RUBY, syntax: :ruby) + # In your Phlex view, call form_with via helpers: + # form_with(url: users_path, method: :post) passes action + CSRF automatically. + # + # You can also set action and the CSRF token manually: + Form(action: helpers.users_path, method: "post", class: "w-2/3 space-y-6") do + input(type: "hidden", name: "authenticity_token", value: helpers.form_authenticity_token) + + FormField do + FormFieldLabel(for: "user_email") { "Email" } + Input( + type: "email", + id: "user_email", + name: "user[email]", + placeholder: "you@example.com", + required: true + ) + FormFieldError() + end + + Button(type: "submit") { "Continue" } + end + RUBY + + Heading(level: 3) { "Devise-style login form" } + Codeblock(<<~RUBY, syntax: :ruby) + # Full sign-in form mirroring Devise session[email] / session[password] params. + # Pass backend errors (e.g. "Invalid email or password") into FormFieldError. + Form(action: helpers.user_session_path, method: "post", class: "space-y-6") do + input(type: "hidden", name: "authenticity_token", value: helpers.form_authenticity_token) + + FormField do + FormFieldLabel(for: "session_email") { "Email" } + Input( + type: "email", + id: "session_email", + name: "session[email]", + placeholder: "you@example.com", + autocomplete: "email", + required: true + ) + FormFieldError { @error_message } + end + + FormField do + FormFieldLabel(for: "session_password") { "Password" } + Input( + type: "password", + id: "session_password", + name: "session[password]", + autocomplete: "current-password", + required: true, + minlength: "8" + ) + FormFieldError() + end + + FormField do + div(class: "flex items-center gap-2") do + Checkbox(id: "session_remember_me", name: "session[remember_me]", value: "1") + FormFieldLabel(for: "session_remember_me") { "Remember me" } + end + end + + Button(type: "submit", class: "w-full") { "Sign in" } + end + RUBY + render Components::ComponentSetup::Tabs.new(component_name: component) render Docs::ComponentsTable.new(component_files(component)) diff --git a/gem/lib/ruby_ui/form/form_docs.rb b/gem/lib/ruby_ui/form/form_docs.rb index 499ad4b65..2340dad1f 100644 --- a/gem/lib/ruby_ui/form/form_docs.rb +++ b/gem/lib/ruby_ui/form/form_docs.rb @@ -170,6 +170,95 @@ def view_template RUBY end + Heading(level: 2) { "Rails Integration" } + + Text do + plain "RubyUI Form components are plain HTML — they work with any form submission strategy. " + plain "The recommended approach for Rails apps is to use " + InlineLink(href: "https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with", target: "_blank") { "form_with" } + plain " to generate the " + code(class: "font-mono text-sm") { "action" } + plain " URL and CSRF token, then pass explicit " + code(class: "font-mono text-sm") { "name" } + plain " / " + code(class: "font-mono text-sm") { "id" } + plain " attributes to each RubyUI input so the browser serialises them correctly. " + plain "Server-side errors can be surfaced by rendering " + code(class: "font-mono text-sm") { "FormFieldError" } + plain " with content from " + code(class: "font-mono text-sm") { "model.errors.full_messages_for(:attr)" } + plain "." + end + + Heading(level: 3) { "Minimal Rails form" } + Codeblock(<<~RUBY, syntax: :ruby) + # In your Phlex view, call form_with via helpers: + # form_with(url: users_path, method: :post) passes action + CSRF automatically. + # + # You can also set action and the CSRF token manually: + Form(action: helpers.users_path, method: "post", class: "w-2/3 space-y-6") do + input(type: "hidden", name: "authenticity_token", value: helpers.form_authenticity_token) + + FormField do + FormFieldLabel(for: "user_email") { "Email" } + Input( + type: "email", + id: "user_email", + name: "user[email]", + placeholder: "you@example.com", + required: true + ) + FormFieldError() + end + + Button(type: "submit") { "Continue" } + end + RUBY + + Heading(level: 3) { "Devise-style login form" } + Codeblock(<<~RUBY, syntax: :ruby) + # Full sign-in form mirroring Devise session[email] / session[password] params. + # Pass backend errors (e.g. "Invalid email or password") into FormFieldError. + Form(action: helpers.user_session_path, method: "post", class: "space-y-6") do + input(type: "hidden", name: "authenticity_token", value: helpers.form_authenticity_token) + + FormField do + FormFieldLabel(for: "session_email") { "Email" } + Input( + type: "email", + id: "session_email", + name: "session[email]", + placeholder: "you@example.com", + autocomplete: "email", + required: true + ) + FormFieldError { @error_message } + end + + FormField do + FormFieldLabel(for: "session_password") { "Password" } + Input( + type: "password", + id: "session_password", + name: "session[password]", + autocomplete: "current-password", + required: true, + minlength: "8" + ) + FormFieldError() + end + + FormField do + div(class: "flex items-center gap-2") do + Checkbox(id: "session_remember_me", name: "session[remember_me]", value: "1") + FormFieldLabel(for: "session_remember_me") { "Remember me" } + end + end + + Button(type: "submit", class: "w-full") { "Sign in" } + end + RUBY + render Components::ComponentSetup::Tabs.new(component_name: component) render Docs::ComponentsTable.new(component_files(component)) diff --git a/mcp/data/registry.json b/mcp/data/registry.json index 8fa00b3f8..908f89c1c 100644 --- a/mcp/data/registry.json +++ b/mcp/data/registry.json @@ -1394,7 +1394,7 @@ "gems": [] }, "install_command": "rails g ruby_ui:component Form", - "docs_markdown": "# Form\n\nBuilding forms with built-in client-side validations.\n\n## Usage\n\n### Example\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Default error\" }\n Input(placeholder: \"Joel Drapper\", required: true, minlength: \"3\") { \"Joel Drapper\" }\n FormFieldHint()\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Disabled\n\n```ruby\nFormField do\n FormFieldLabel { \"Disabled\" }\n Input(disabled: true, placeholder: \"Joel Drapper\", required: true, minlength: \"3\") { \"Joel Drapper\" }\nend\n```\n\n### Aria Disabled\n\n```ruby\nFormField do\n FormFieldLabel { \"Aria Disabled\" }\n Input(aria: {disabled: \"true\"}, placeholder: \"Joel Drapper\", required: true, minlength: \"3\") { \"Joel Drapper\" }\nend\n```\n\n### Custom error message\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Custom error message\" }\n Input(placeholder: \"joel@drapper.me\", required: true, data_value_missing: \"Custom error message\")\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Backend error\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Backend error\" }\n Input(placeholder: \"Joel Drapper\", required: true)\n FormFieldError { \"Error from backend\" }\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Checkbox\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n Checkbox(required: true)\n label(\n class:\n \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n ) { \" Accept terms and conditions \" }\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Select\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Select\" }\n Select do\n SelectInput(required: true)\n SelectTrigger do\n SelectValue(placeholder: \"Select a fruit\")\n end\n SelectContent() do\n SelectGroup do\n SelectLabel { \"Fruits\" }\n SelectItem(value: \"apple\") { \"Apple\" }\n SelectItem(value: \"orange\") { \"Orange\" }\n SelectItem(value: \"banana\") { \"Banana\" }\n SelectItem(value: \"watermelon\") { \"Watermelon\" }\n end\n end\n end\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Combobox\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Combobox\" }\n\n Combobox do\n ComboboxTrigger placeholder: \"Pick value\"\n\n ComboboxPopover do\n ComboboxSearchInput(placeholder: \"Pick value or type anything\")\n\n ComboboxList do\n ComboboxEmptyState { \"No result\" }\n\n ComboboxListGroup label: \"Fruits\" do\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"apple\", required: true)\n span { \"Apple\" }\n end\n\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"banana\", required: true)\n span { \"Banana\" }\n end\n end\n\n ComboboxListGroup label: \"Vegetable\" do\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"brocoli\", required: true)\n span { \"Broccoli\" }\n end\n\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"carrot\", required: true)\n span { \"Carrot\" }\n end\n end\n\n ComboboxListGroup label: \"Others\" do\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"chocolate\", required: true)\n span { \"Chocolate\" }\n end\n\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"milk\", required: true)\n span { \"Milk\" }\n end\n end\n end\n end\n end\n\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```", + "docs_markdown": "# Form\n\nBuilding forms with built-in client-side validations.\n\n## Usage\n\n### Example\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Default error\" }\n Input(placeholder: \"Joel Drapper\", required: true, minlength: \"3\") { \"Joel Drapper\" }\n FormFieldHint()\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Disabled\n\n```ruby\nFormField do\n FormFieldLabel { \"Disabled\" }\n Input(disabled: true, placeholder: \"Joel Drapper\", required: true, minlength: \"3\") { \"Joel Drapper\" }\nend\n```\n\n### Aria Disabled\n\n```ruby\nFormField do\n FormFieldLabel { \"Aria Disabled\" }\n Input(aria: {disabled: \"true\"}, placeholder: \"Joel Drapper\", required: true, minlength: \"3\") { \"Joel Drapper\" }\nend\n```\n\n### Custom error message\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Custom error message\" }\n Input(placeholder: \"joel@drapper.me\", required: true, data_value_missing: \"Custom error message\")\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Backend error\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Backend error\" }\n Input(placeholder: \"Joel Drapper\", required: true)\n FormFieldError { \"Error from backend\" }\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Checkbox\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n Checkbox(required: true)\n label(\n class:\n \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n ) { \" Accept terms and conditions \" }\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Select\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Select\" }\n Select do\n SelectInput(required: true)\n SelectTrigger do\n SelectValue(placeholder: \"Select a fruit\")\n end\n SelectContent() do\n SelectGroup do\n SelectLabel { \"Fruits\" }\n SelectItem(value: \"apple\") { \"Apple\" }\n SelectItem(value: \"orange\") { \"Orange\" }\n SelectItem(value: \"banana\") { \"Banana\" }\n SelectItem(value: \"watermelon\") { \"Watermelon\" }\n end\n end\n end\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n### Combobox\n\n```ruby\nForm(class: \"w-2/3 space-y-6\") do\n FormField do\n FormFieldLabel { \"Combobox\" }\n\n Combobox do\n ComboboxTrigger placeholder: \"Pick value\"\n\n ComboboxPopover do\n ComboboxSearchInput(placeholder: \"Pick value or type anything\")\n\n ComboboxList do\n ComboboxEmptyState { \"No result\" }\n\n ComboboxListGroup label: \"Fruits\" do\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"apple\", required: true)\n span { \"Apple\" }\n end\n\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"banana\", required: true)\n span { \"Banana\" }\n end\n end\n\n ComboboxListGroup label: \"Vegetable\" do\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"brocoli\", required: true)\n span { \"Broccoli\" }\n end\n\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"carrot\", required: true)\n span { \"Carrot\" }\n end\n end\n\n ComboboxListGroup label: \"Others\" do\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"chocolate\", required: true)\n span { \"Chocolate\" }\n end\n\n ComboboxItem do\n ComboboxRadio(name: \"food\", value: \"milk\", required: true)\n span { \"Milk\" }\n end\n end\n end\n end\n end\n\n FormFieldError()\n end\n Button(type: \"submit\") { \"Save\" }\nend\n```\n\n## Rails Integration\n\n### Minimal Rails form\n\n### Devise-style login form", "examples": [ { "title": "Example",