Form layouts
Stacked with server validation
open System
open FSharp.ViewEngine
open Acme.Components.Primitives
open Acme.Components.Application
open type Html
open type Svg
open type Datastar
let private choiceSubmitButton id (label:string) =
Button.create label
|> Button.withVariant ButtonVariant.Primary
|> Button.asSubmit
|> Button.withAttributes [ _id id ]
|> Button.render
type ContactDetails = { name:string; email:string; notes:string }
type ContactFormLayout = Stacked | TwoColumns | Sectioned
let contactFormRegion layout (details:ContactDetails) (errors:Map<string,string>) accepted =
let key, title =
match layout with
| Stacked -> "", "Contact details"
| TwoColumns -> "grid", "Contact details in two columns"
| Sectioned -> "sectioned", "Contact details in sections"
let prefix = if key = "" then "contact-" else "contact-" + key + "-"
let endpoint = "/components/forms/contact" + (if key = "" then "" else "/" + key)
let name =
Input.create "contactName" "Contact name"
|> Input.withId (prefix + "name")
|> Input.withValue details.name
|> Input.required
|> Input.withAttributes [ _autocomplete "name" ]
|> (match Map.tryFind "contactName" errors with Some error -> Input.withValidation error | None -> id)
let email =
Input.create "email" "Email address"
|> Input.withId (prefix + "email")
|> Input.withType InputType.Email
|> Input.withValue details.email
|> Input.required
|> Input.withDescription "Use the address where you receive account correspondence."
|> Input.withAttributes [ _autocomplete "email" ]
|> (match Map.tryFind "email" errors with Some error -> Input.withValidation error | None -> id)
let identityFields =
div {
_class (if layout = Stacked then "grid gap-4" else "grid gap-4 sm:grid-cols-2")
Input.render name
Input.render email
}
let notes =
Textarea.create "notes" "Notes"
|> Textarea.withId (prefix + "notes")
|> Textarea.withValue details.notes
|> Textarea.withDescription "Any additional information we should know."
|> Textarea.render
div {
_id ("components-" + prefix + "region")
_class ("mx-auto grid w-full min-w-0 gap-6 p-6 sm:p-8 " + if layout = Stacked then "max-w-xl" else "max-w-4xl")
if not (Map.isEmpty errors) then
ErrorSummary.create (prefix + "errors") "Check your contact details" [
match Map.tryFind "contactName" errors with
| Some message -> FieldError.create (Input.id name) "Contact name" message
| None -> ()
match Map.tryFind "email" errors with
| Some message -> FieldError.create (Input.id email) "Email address" message
| None -> () ]
|> ErrorSummary.focusOnMount
|> ErrorSummary.render
if accepted then
Notice.create (prefix + "result") "Details are valid" (p { "Validation succeeded. This example does not save your data." })
|> Notice.withTone Tone.Positive
|> Notice.withAnnouncement NoticeAnnouncement.Polite
|> Notice.render
form {
_ariaLabel title
_class "grid gap-6"
_attr ("novalidate", "")
_dataOn ("submit", $"@post('{endpoint}', {{contentType: 'form'}})")
if layout = Sectioned then
Section.create (SectionHeader.create "Contact information" |> SectionHeader.withDescription "Who should we contact?" |> SectionHeader.withDivider) identityFields
|> Section.render (fun (_:unit) -> "")
Section.create (SectionHeader.create "Additional details" |> SectionHeader.withDivider) notes
|> Section.render (fun (_:unit) -> "")
else
identityFields
notes
div {
_class "flex justify-end"
choiceSubmitButton (prefix + "submit") "Validate details"
}
}
}
let private emptyContact = { name = ""; email = ""; notes = "" }
let contactFormExample = contactFormRegion Stacked emptyContact Map.empty falseSubmit to see validation errors. This example does not save contact details. The validation form previously shown on Input now lives here.
Two-column form
open System
open FSharp.ViewEngine
open Acme.Components.Primitives
open Acme.Components.Application
open type Html
open type Svg
open type Datastar
let private choiceSubmitButton id (label:string) =
Button.create label
|> Button.withVariant ButtonVariant.Primary
|> Button.asSubmit
|> Button.withAttributes [ _id id ]
|> Button.render
type ContactDetails = { name:string; email:string; notes:string }
type ContactFormLayout = Stacked | TwoColumns | Sectioned
let contactFormRegion layout (details:ContactDetails) (errors:Map<string,string>) accepted =
let key, title =
match layout with
| Stacked -> "", "Contact details"
| TwoColumns -> "grid", "Contact details in two columns"
| Sectioned -> "sectioned", "Contact details in sections"
let prefix = if key = "" then "contact-" else "contact-" + key + "-"
let endpoint = "/components/forms/contact" + (if key = "" then "" else "/" + key)
let name =
Input.create "contactName" "Contact name"
|> Input.withId (prefix + "name")
|> Input.withValue details.name
|> Input.required
|> Input.withAttributes [ _autocomplete "name" ]
|> (match Map.tryFind "contactName" errors with Some error -> Input.withValidation error | None -> id)
let email =
Input.create "email" "Email address"
|> Input.withId (prefix + "email")
|> Input.withType InputType.Email
|> Input.withValue details.email
|> Input.required
|> Input.withDescription "Use the address where you receive account correspondence."
|> Input.withAttributes [ _autocomplete "email" ]
|> (match Map.tryFind "email" errors with Some error -> Input.withValidation error | None -> id)
let identityFields =
div {
_class (if layout = Stacked then "grid gap-4" else "grid gap-4 sm:grid-cols-2")
Input.render name
Input.render email
}
let notes =
Textarea.create "notes" "Notes"
|> Textarea.withId (prefix + "notes")
|> Textarea.withValue details.notes
|> Textarea.withDescription "Any additional information we should know."
|> Textarea.render
div {
_id ("components-" + prefix + "region")
_class ("mx-auto grid w-full min-w-0 gap-6 p-6 sm:p-8 " + if layout = Stacked then "max-w-xl" else "max-w-4xl")
if not (Map.isEmpty errors) then
ErrorSummary.create (prefix + "errors") "Check your contact details" [
match Map.tryFind "contactName" errors with
| Some message -> FieldError.create (Input.id name) "Contact name" message
| None -> ()
match Map.tryFind "email" errors with
| Some message -> FieldError.create (Input.id email) "Email address" message
| None -> () ]
|> ErrorSummary.focusOnMount
|> ErrorSummary.render
if accepted then
Notice.create (prefix + "result") "Details are valid" (p { "Validation succeeded. This example does not save your data." })
|> Notice.withTone Tone.Positive
|> Notice.withAnnouncement NoticeAnnouncement.Polite
|> Notice.render
form {
_ariaLabel title
_class "grid gap-6"
_attr ("novalidate", "")
_dataOn ("submit", $"@post('{endpoint}', {{contentType: 'form'}})")
if layout = Sectioned then
Section.create (SectionHeader.create "Contact information" |> SectionHeader.withDescription "Who should we contact?" |> SectionHeader.withDivider) identityFields
|> Section.render (fun (_:unit) -> "")
Section.create (SectionHeader.create "Additional details" |> SectionHeader.withDivider) notes
|> Section.render (fun (_:unit) -> "")
else
identityFields
notes
div {
_class "flex justify-end"
choiceSubmitButton (prefix + "submit") "Validate details"
}
}
}
let private emptyContact = { name = ""; email = ""; notes = "" }
let twoColumnFormExample = contactFormRegion TwoColumns emptyContact Map.empty falseSectioned form
open System
open FSharp.ViewEngine
open Acme.Components.Primitives
open Acme.Components.Application
open type Html
open type Svg
open type Datastar
let private choiceSubmitButton id (label:string) =
Button.create label
|> Button.withVariant ButtonVariant.Primary
|> Button.asSubmit
|> Button.withAttributes [ _id id ]
|> Button.render
type ContactDetails = { name:string; email:string; notes:string }
type ContactFormLayout = Stacked | TwoColumns | Sectioned
let contactFormRegion layout (details:ContactDetails) (errors:Map<string,string>) accepted =
let key, title =
match layout with
| Stacked -> "", "Contact details"
| TwoColumns -> "grid", "Contact details in two columns"
| Sectioned -> "sectioned", "Contact details in sections"
let prefix = if key = "" then "contact-" else "contact-" + key + "-"
let endpoint = "/components/forms/contact" + (if key = "" then "" else "/" + key)
let name =
Input.create "contactName" "Contact name"
|> Input.withId (prefix + "name")
|> Input.withValue details.name
|> Input.required
|> Input.withAttributes [ _autocomplete "name" ]
|> (match Map.tryFind "contactName" errors with Some error -> Input.withValidation error | None -> id)
let email =
Input.create "email" "Email address"
|> Input.withId (prefix + "email")
|> Input.withType InputType.Email
|> Input.withValue details.email
|> Input.required
|> Input.withDescription "Use the address where you receive account correspondence."
|> Input.withAttributes [ _autocomplete "email" ]
|> (match Map.tryFind "email" errors with Some error -> Input.withValidation error | None -> id)
let identityFields =
div {
_class (if layout = Stacked then "grid gap-4" else "grid gap-4 sm:grid-cols-2")
Input.render name
Input.render email
}
let notes =
Textarea.create "notes" "Notes"
|> Textarea.withId (prefix + "notes")
|> Textarea.withValue details.notes
|> Textarea.withDescription "Any additional information we should know."
|> Textarea.render
div {
_id ("components-" + prefix + "region")
_class ("mx-auto grid w-full min-w-0 gap-6 p-6 sm:p-8 " + if layout = Stacked then "max-w-xl" else "max-w-4xl")
if not (Map.isEmpty errors) then
ErrorSummary.create (prefix + "errors") "Check your contact details" [
match Map.tryFind "contactName" errors with
| Some message -> FieldError.create (Input.id name) "Contact name" message
| None -> ()
match Map.tryFind "email" errors with
| Some message -> FieldError.create (Input.id email) "Email address" message
| None -> () ]
|> ErrorSummary.focusOnMount
|> ErrorSummary.render
if accepted then
Notice.create (prefix + "result") "Details are valid" (p { "Validation succeeded. This example does not save your data." })
|> Notice.withTone Tone.Positive
|> Notice.withAnnouncement NoticeAnnouncement.Polite
|> Notice.render
form {
_ariaLabel title
_class "grid gap-6"
_attr ("novalidate", "")
_dataOn ("submit", $"@post('{endpoint}', {{contentType: 'form'}})")
if layout = Sectioned then
Section.create (SectionHeader.create "Contact information" |> SectionHeader.withDescription "Who should we contact?" |> SectionHeader.withDivider) identityFields
|> Section.render (fun (_:unit) -> "")
Section.create (SectionHeader.create "Additional details" |> SectionHeader.withDivider) notes
|> Section.render (fun (_:unit) -> "")
else
identityFields
notes
div {
_class "flex justify-end"
choiceSubmitButton (prefix + "submit") "Validate details"
}
}
}
let private emptyContact = { name = ""; email = ""; notes = "" }
let sectionedFormExample = contactFormRegion Sectioned emptyContact Map.empty false