open Htmlit
type input_type =
| Text
| Email
| Password
| Number
| Tel
| Url
| Search
| Date
| Time
| Datetime_local
type validation_state =
| Valid
| Invalid
| Warning
type field_type =
| Input of input_type
| Textarea
| Select of (string * string) list (* value, label pairs *)
| Checkbox
| Radio of string (* value *)
| Switch
type t = {
field_type: field_type;
label: string option;
placeholder: string option;
value: string option;
name: string option;
id: string option;
rows: int option; (* for textarea *)
required: bool;
disabled: bool;
readonly: bool;
checked: bool; (* for checkbox/radio/switch *)
validation: validation_state option;
helper_text: string option;
error_text: string option;
classes: Tailwind.t option;
attributes: (string * string) list;
}
let classes_attr tailwind_classes =
At.class' (Tailwind.to_string tailwind_classes)
let base_input_classes = Tailwind.Css.tw [
Tailwind.Display.flex;
Tailwind.Layout.(to_class (height (Tailwind.Size.rem 2.5)));
Tailwind.Layout.w_full;
Tailwind.Effects.rounded_md;
Tailwind.Effects.border;
Tailwind.Color.border (Tailwind.Color.make `Gray ~variant:`V300 ());
Tailwind.Color.bg Tailwind.Color.white;
Tailwind.Spacing.(to_class (px (Tailwind.Size.rem 0.75)));
Tailwind.Spacing.(to_class (py (Tailwind.Size.rem 0.5)));
Tailwind.Typography.(to_class (font_size `Sm));
]
let validation_classes = function
| Some Valid -> Tailwind.Css.tw [
Tailwind.Color.border (Tailwind.Color.make `Green ~variant:`V500 ());
Tailwind.Variants.focus (Tailwind.Color.border (Tailwind.Color.make `Green ~variant:`V600 ()));
]
| Some Invalid -> Tailwind.Css.tw [
Tailwind.Color.border (Tailwind.Color.make `Red ~variant:`V500 ());
Tailwind.Variants.focus (Tailwind.Color.border (Tailwind.Color.make `Red ~variant:`V600 ()));
]
| Some Warning -> Tailwind.Css.tw [
Tailwind.Color.border (Tailwind.Color.make `Yellow ~variant:`V500 ());
Tailwind.Variants.focus (Tailwind.Color.border (Tailwind.Color.make `Yellow ~variant:`V600 ()));
]
| None -> Tailwind.Css.empty
let input ?input_type ?label ?placeholder ?value ?name ?id ?required ?disabled ?readonly ?validation ?helper_text ?error_text ?classes ?attributes () = {
field_type = Input (match input_type with Some t -> t | None -> Text);
label;
placeholder;
value;
name;
id;
rows = None;
required = (match required with Some r -> r | None -> false);
disabled = (match disabled with Some d -> d | None -> false);
readonly = (match readonly with Some r -> r | None -> false);
checked = false;
validation;
helper_text;
error_text;
classes;
attributes = (match attributes with Some a -> a | None -> []);
}
let textarea ?label ?placeholder ?value ?name ?id ?rows ?required ?disabled ?readonly ?validation ?helper_text ?error_text ?classes ?attributes () = {
field_type = Textarea;
label;
placeholder;
value;
name;
id;
rows;
required = (match required with Some r -> r | None -> false);
disabled = (match disabled with Some d -> d | None -> false);
readonly = (match readonly with Some r -> r | None -> false);
checked = false;
validation;
helper_text;
error_text;
classes;
attributes = (match attributes with Some a -> a | None -> []);
}
let select ?label ?name ?id ?required ?disabled ?validation ?helper_text ?error_text ?classes ?attributes ~options () = {
field_type = Select options;
label;
placeholder = None;
value = None;
name;
id;
rows = None;
required = (match required with Some r -> r | None -> false);
disabled = (match disabled with Some d -> d | None -> false);
readonly = false;
checked = false;
validation;
helper_text;
error_text;
classes;
attributes = (match attributes with Some a -> a | None -> []);
}
let checkbox ?label ?name ?id ?checked ?disabled ?classes ?attributes () = {
field_type = Checkbox;
label;
placeholder = None;
value = None;
name;
id;
rows = None;
required = false;
disabled = (match disabled with Some d -> d | None -> false);
readonly = false;
checked = (match checked with Some c -> c | None -> false);
validation = None;
helper_text = None;
error_text = None;
classes;
attributes = (match attributes with Some a -> a | None -> []);
}
let radio ?label ?name ?id ?value ?checked ?disabled ?classes ?attributes () = {
field_type = Radio (match value with Some v -> v | None -> "");
label;
placeholder = None;
value;
name;
id;
rows = None;
required = false;
disabled = (match disabled with Some d -> d | None -> false);
readonly = false;
checked = (match checked with Some c -> c | None -> false);
validation = None;
helper_text = None;
error_text = None;
classes;
attributes = (match attributes with Some a -> a | None -> []);
}
let switch ?label ?name ?id ?checked ?disabled ?classes ?attributes () = {
field_type = Switch;
label;
placeholder = None;
value = None;
name;
id;
rows = None;
required = false;
disabled = (match disabled with Some d -> d | None -> false);
readonly = false;
checked = (match checked with Some c -> c | None -> false);
validation = None;
helper_text = None;
error_text = None;
classes;
attributes = (match attributes with Some a -> a | None -> []);
}
let input_type_to_string = function
| Text -> "text"
| Email -> "email"
| Password -> "password"
| Number -> "number"
| Tel -> "tel"
| Url -> "url"
| Search -> "search"
| Date -> "date"
| Time -> "time"
| Datetime_local -> "datetime-local"
let to_html field =
let field_classes = Tailwind.Css.tw [
base_input_classes;
validation_classes field.validation;
(match field.classes with Some c -> c | None -> Tailwind.Css.empty);
] in
let base_attrs = [classes_attr field_classes] in
let optional_attrs = List.filter_map (fun x -> x) [
Option.map At.placeholder field.placeholder;
Option.map At.value field.value;
Option.map At.name field.name;
Option.map At.id field.id;
(if field.required then Some At.required else None);
(if field.disabled then Some At.disabled else None);
(if field.readonly then Some (At.v "readonly" "readonly") else None);
(if field.checked then Some At.checked else None);
] in
let custom_attrs = List.map (fun (k, v) -> At.v k v) field.attributes in
let all_attrs = base_attrs @ optional_attrs @ custom_attrs in
let input_element = match field.field_type with
| Input input_type ->
El.input ~at:(At.type' (input_type_to_string input_type) :: all_attrs) ()
| Textarea ->
let textarea_attrs = match field.rows with
| Some r -> At.rows r :: all_attrs
| None -> all_attrs
in
El.textarea ~at:textarea_attrs []
| Select options ->
let option_elements = List.map (fun (value, label) ->
El.option ~at:[At.value value] [El.txt label]
) options in
El.select ~at:all_attrs option_elements
| Checkbox ->
El.input ~at:(At.type' "checkbox" :: all_attrs) ()
| Radio value ->
El.input ~at:(At.type' "radio" :: At.value value :: all_attrs) ()
| Switch ->
(* Switch is implemented as a styled checkbox *)
El.input ~at:(At.type' "checkbox" :: all_attrs) ()
in
let label_element = match field.label with
| Some label_text ->
El.label [
El.txt label_text;
input_element;
]
| None -> input_element
in
(* Add helper and error text if provided *)
let help_elements = List.filter_map (fun x -> x) [
Option.map (fun text ->
El.p ~at:[classes_attr (Tailwind.Css.tw [
Tailwind.Typography.(to_class (font_size `Sm));
Tailwind.Color.text (Tailwind.Color.make `Gray ~variant:`V600 ());
Tailwind.Spacing.(to_class (mt (Tailwind.Size.rem 0.25)));
])] [El.txt text]
) field.helper_text;
Option.map (fun text ->
El.p ~at:[classes_attr (Tailwind.Css.tw [
Tailwind.Typography.(to_class (font_size `Sm));
Tailwind.Color.text (Tailwind.Color.make `Red ~variant:`V600 ());
Tailwind.Spacing.(to_class (mt (Tailwind.Size.rem 0.25)));
])] [El.txt text]
) field.error_text;
] in
(* Wrap everything in a container *)
match help_elements with
| [] -> label_element
| _ -> El.div ([label_element] @ help_elements)
let group ?classes ~fields () =
let group_classes = Tailwind.Css.tw [
Tailwind.Display.grid;
Tailwind.Layout.w_full;
Tailwind.Spacing.(to_class (gap `All (Tailwind.Size.rem 1.0)));
(match classes with Some c -> c | None -> Tailwind.Css.empty);
] in
let field_elements = List.map to_html fields in
El.div ~at:[classes_attr group_classes] field_elements
let form ?action ?method_ ?classes ?attributes ~fields ?submit () =
let form_classes = Tailwind.Css.tw [
Tailwind.Display.grid;
Tailwind.Layout.w_full;
Tailwind.Spacing.(to_class (gap `All (Tailwind.Size.rem 1.5)));
(match classes with Some c -> c | None -> Tailwind.Css.empty);
] in
let base_attrs = [classes_attr form_classes] in
let optional_attrs = List.filter_map (fun x -> x) [
Option.map At.action action;
(match method_ with
| Some `Get -> Some (At.method' "get")
| Some `Post -> Some (At.method' "post")
| None -> None);
] in
let custom_attrs = match attributes with
| Some attrs -> List.map (fun (k, v) -> At.v k v) attrs
| None -> []
in
let all_attrs = base_attrs @ optional_attrs @ custom_attrs in
let field_elements = List.map to_html fields in
let submit_element = match submit with
| Some btn -> [Button.to_html btn]
| None -> []
in
El.form ~at:all_attrs (field_elements @ submit_element)