///|
#alias(T)
pub(all) struct Html[Msg](@vdom.Node[Msg])
///|
/// Convert msg type of Html.
///
/// This is a expensive operation and should be used rarely.
pub fn[A, B] Html::map(self : Html[A], f : (A) -> B) -> Html[B] {
self.0.map(f)
}
///|
pub fn[Msg] Html::to_virtual_dom(self : Html[Msg]) -> @vdom.Node[Msg] {
// Wrap the node with a root node. This node represents the root of the real DOM managed by TEA.
node("root", [], [self]).0
}
///|
pub fn[Msg] node(
tag : String,
attributes : Array[Attribute[Msg]],
children : Array[Html[Msg]],
) -> Html[Msg] {
@vdom.node(tag, attributes.map(x => x.0), children.map(x => x.0))
}
///|
fn[Msg] common_node(
style? : Array[String] = [],
id? : String,
class? : String,
tag : String,
attributes : Array[Attribute[Msg]],
children : Array[Html[Msg]],
) -> Html[Msg] {
let attrs = []
if style.length() > 0 {
attrs.push(attribute("style", style.join(";")))
}
if class is Some(class) {
attrs.push(attribute("class", class))
}
if id is Some(id) {
attrs.push(attribute("id", id))
}
node(tag, attributes + attrs, children)
}
///|
/// Represents an empty element
pub fn[M] nothing() -> Html[M] {
@vdom.nothing()
}
///|
pub fn[M] button(
style? : Array[String] = [],
id? : String,
class? : String,
click? : M,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if click is Some(click) {
attrs.push(on_click(_ => click))
}
common_node(style~, class?, id?, "button", attrs, children)
}
///|
pub fn[M] h1(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "h1", [], children)
}
///|
pub fn[M] h2(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "h2", [], children)
}
///|
pub fn[M] h3(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "h3", [], children)
}
///|
pub fn[M] h4(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "h4", [], children)
}
///|
pub fn[M] h5(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "h5", [], children)
}
///|
pub fn[M] h6(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "h6", [], children)
}
// ------ grouping content ------
///|
pub fn[M] div(
style? : Array[String] = [],
id? : String,
class? : String,
click? : M,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if click is Some(click) {
attrs.push(on_click(_ => click))
}
common_node(style~, class?, id?, "div", attrs, children)
}
///|
pub fn[M] p(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "p", [], children)
}
///|
pub fn[M] hr(
style? : Array[String] = [],
id? : String,
class? : String,
children? : Array[Html[M]] = [],
) -> Html[M] {
common_node(style~, class?, id?, "hr", [], children)
}
///|
pub fn[M] pre(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "pre", [], children)
}
///|
pub fn[M] blockquote(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "blockquote", [], children)
}
///|
pub fn[M] section(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "section", [], children)
}
// ---- text ----
///|
pub fn[M] span(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "span", [], children)
}
///|
/// Create `a` element.
///
/// # Parameters
///
/// - `escape`: an optional boolean that determines whether the link
/// should be escaped. By default, it is set to `false`.
///
/// When `escape` is set to `true`, clicking the escaped link will cause the browser
/// to immediately navigate to the `href` target, bypassing any interception logic.
/// As a result, the `UrlRequest` message will not be triggered.
///
/// - `target`: an optional `Target` that specifies where to open the link.
///
/// If the user holds the `ctrl` key (or the command key on macOS) while clicking the link,
/// the click event will be handled by the browser, and the `UrlRequest` will not be triggered either.
pub fn[M] a(
style? : Array[String] = [],
id? : String,
class? : String,
href~ : String,
target? : Target = Self,
children : Array[Html[M]],
escape? : Bool = false,
) -> Html[M] {
let attrs = [
@vdom.attribute("href", href),
@vdom.attribute("target", target.to_string()),
]
if style.length() > 0 {
attrs.push(@vdom.attribute("style", style.join(";")))
}
if class is Some(class) {
attrs.push(@vdom.attribute("class", class))
}
if id is Some(id) {
attrs.push(@vdom.attribute("id", id))
}
@vdom.link(attrs, children.map(x => x.0), escape~)
}
///|
pub fn[M] code(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "code", [], children)
}
///|
pub fn[M] em(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "em", [], children)
}
///|
pub fn[M] strong(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "strong", [], children)
}
///|
pub fn[M] i(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "i", [], children)
}
///|
pub fn[M] b(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "b", [], children)
}
///|
pub fn[M] u(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "u", [], children)
}
///|
pub fn[M] sub(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "sub", [], children)
}
///|
pub fn[M] sup(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "sup", [], children)
}
///|
pub fn[Msg] text(str : String) -> Html[Msg] {
@vdom.text(str)
}
// ---- lists ----
///|
pub fn[M] ul(
style? : Array[String] = [],
id? : String,
class? : String,
click? : M,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if click is Some(click) {
attrs.push(on_click(_ => click))
}
common_node(style~, class?, id?, "ul", attrs, children)
}
///|
/// Notice that the `type` attribute for `ol` is not important for the browser now.
/// If you want to change the type of the list, you should use the `list-style-type` property in CSS.
pub fn[M] ol(
style? : Array[String] = [],
reversed? : Bool,
start? : Int,
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if reversed is Some(reversed) {
// TODO: test this attribute
attrs.push(attribute("reversed", reversed.to_string()))
}
if start is Some(start) {
// TODO: test this attribute
attrs.push(attribute("start", start.to_string()))
}
common_node(style~, class?, id?, "ol", attrs, children)
}
///|
pub fn[M] li(
style? : Array[String] = [],
value? : Int,
id? : String,
class? : String,
click? : M,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if click is Some(click) {
attrs.push(on_click(_ => click))
}
if value is Some(value) {
attrs.push(attribute("value", value.to_string()))
}
common_node(style~, class?, id?, "li", attrs, children)
}
///|
pub fn[M] dl(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "dl", [], children)
}
///|
pub fn[M] dt(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "dt", [], children)
}
///|
pub fn[M] dd(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node(style~, class?, id?, "dd", [], children)
}
// ---- embbded content ----
///|
pub fn[M] img(
style? : Array[String] = [],
id? : String,
class? : String,
src? : String,
alt? : String,
title? : String,
width? : Int,
height? : Int,
border? : Int,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if src is Some(src) {
attrs.push(attribute("src", src))
}
if alt is Some(alt) {
attrs.push(attribute("alt", alt))
}
if title is Some(title) {
attrs.push(attribute("title", title))
}
if width is Some(width) {
attrs.push(attribute("width", width.to_string()))
}
if height is Some(height) {
attrs.push(attribute("height", height.to_string()))
}
if border is Some(border) {
attrs.push(attribute("border", border.to_string()))
}
common_node(style~, class?, id?, "img", attrs, children)
}
///|
pub fn[M] iframe(
style? : Array[String] = [],
id? : String,
class? : String,
src? : String,
title? : String,
width? : Int,
height? : Int,
) -> Html[M] {
let attrs = []
if src is Some(src) {
attrs.push(attribute("src", src))
}
if title is Some(title) {
attrs.push(attribute("title", title))
}
if width is Some(width) {
attrs.push(attribute("width", width.to_string()))
}
if height is Some(height) {
attrs.push(attribute("height", height.to_string()))
}
common_node(style~, class?, id?, "iframe", attrs, [])
}
// ---- inputs ----
///|
pub fn[M] br(
style? : Array[String] = [],
id? : String,
class? : String,
) -> Html[M] {
common_node(style~, class?, id?, "br", [], [])
}
///|
pub(all) enum InputType {
Button
Checkbox
Color
Date
DateTimeLocal
Email
File
Hidden
Image
Month
Number
Password
Radio
Range
Reset
Search
Submit
Tel
Text
Time
Url
Week
}
///|
pub(all) enum AutoComplete {
On
Off
}
///|
pub fn[M] form(
style? : Array[String] = [],
id? : String,
class? : String,
action? : String,
name? : String,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if action is Some(action) {
attrs.push(attribute("action", action))
}
if name is Some(name) {
attrs.push(attribute("name", name))
}
common_node(style~, class?, id?, "form", attrs, children)
}
///|
pub fn[M] label(
style? : Array[String] = [],
id? : String,
class? : String,
for_? : String,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if for_ is Some(for_) {
attrs.push(attribute("for", for_))
}
common_node(style~, class?, id?, "label", attrs, children)
}
///|
pub fn[M] input(
input_type? : InputType = Text,
name? : String,
value? : String,
checked? : Bool,
read_only? : Bool,
multiple? : Bool,
accept? : String,
placeholder? : String,
auto_complete? : AutoComplete,
style? : Array[String] = [],
max? : Int,
min? : Int,
step? : Int,
maxlength? : Int,
minlength? : Int,
pattern? : String,
size? : Int,
width? : Int,
height? : Int,
id? : String,
class? : String,
children? : Array[Html[M]] = [],
change? : (String) -> M,
input? : (String) -> M,
) -> Html[M] {
let input_type = match input_type {
Button => "button"
Checkbox => "checkbox"
Color => "color"
Date => "date"
DateTimeLocal => "datetime-local"
Email => "email"
File => "file"
Hidden => "hidden"
Image => "image"
Month => "month"
Number => "number"
Password => "password"
Radio => "radio"
Range => "range"
Reset => "reset"
Search => "search"
Submit => "submit"
Tel => "tel"
Text => "text"
Time => "time"
Url => "url"
Week => "week"
}
let auto_complete = match auto_complete {
Some(On) => "on"
_ => "off"
}
let attrs = []
attrs.push(attribute("type", input_type))
attrs.push(attribute("autocomplete", auto_complete))
if name is Some(name) {
attrs.push(attribute("name", name))
}
if value is Some(value) {
attrs.push(property("value", String(value)))
}
if checked is Some(checked) {
attrs.push(property("checked", Boolean(checked)))
}
if read_only is Some(read_only) {
attrs.push(property("readonly", Boolean(read_only)))
}
if multiple is Some(multiple) {
attrs.push(property("multiple", Boolean(multiple)))
}
if accept is Some(accept) {
attrs.push(attribute("accept", accept))
}
if max is Some(max) {
attrs.push(attribute("max", max.to_string()))
}
if min is Some(min) {
attrs.push(attribute("min", min.to_string()))
}
if step is Some(step) {
attrs.push(attribute("step", step.to_string()))
}
if maxlength is Some(maxlength) {
attrs.push(attribute("maxlength", maxlength.to_string()))
}
if minlength is Some(minlength) {
attrs.push(attribute("minlength", minlength.to_string()))
}
if pattern is Some(pattern) {
attrs.push(attribute("pattern", pattern))
}
if size is Some(size) {
attrs.push(attribute("size", size.to_string()))
}
if width is Some(width) {
attrs.push(attribute("width", width.to_string()))
}
if height is Some(height) {
attrs.push(attribute("height", height.to_string()))
}
if placeholder is Some(placeholder) {
attrs.push(attribute("placeholder", placeholder))
}
if change is Some(to_msg) {
attrs.push(on_change(to_msg))
}
if input is Some(to_msg) {
attrs.push(on_input(to_msg))
}
common_node("input", attrs, children, style~, id?, class?)
}
///|
pub fn[M] select(
style? : Array[String] = [],
id? : String,
class? : String,
disabled? : Bool,
name? : String,
change? : (String) -> M,
children : Array[Html[M]],
) -> Html[M] {
// TODO: support more attribute for select tag
let attrs = []
if style.length() > 0 {
attrs.push(attribute("style", style.join(";")))
}
if id is Some(id) {
attrs.push(attribute("id", id))
}
if class is Some(class) {
attrs.push(attribute("class", class))
}
if disabled is Some(value) {
attrs.push(property("disabled", Boolean(value)))
}
if name is Some(name) {
attrs.push(attribute("name", name))
}
if change is Some(to_msg) {
attrs.push(on_change(to_msg))
}
node("select", attrs, children)
}
///|
pub fn[M] option(
style? : Array[String] = [],
id? : String,
class? : String,
disabled? : Bool,
value? : String,
selected? : Bool = false,
children : Array[Html[M]],
) -> Html[M] {
// TODO: support more attribute for option tag
let attrs = []
if style.length() > 0 {
attrs.push(attribute("style", style.join(";")))
}
if id is Some(id) {
attrs.push(attribute("id", id))
}
if class is Some(class) {
attrs.push(attribute("class", class))
}
if disabled is Some(value) {
attrs.push(property("disabled", Boolean(value)))
}
if value is Some(value) {
attrs.push(attribute("value", value))
}
attrs.push(property("selected", Boolean(selected)))
node("option", attrs, children)
}
///|
pub fn[Msg] external(
node : @dom.Node,
attrs : Ref[Array[Attribute[Msg]]?],
width~ : Int,
height~ : Int,
) -> Html[Msg] {
let attrs = attrs.map(_.map(_.map(x => x.0)))
@vdom.external(node, attrs, width~, height~)
}
// table
///|
pub fn[M] table(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node("table", [], style~, id?, class?, children)
}
///|
pub fn[M] caption(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node("caption", [], style~, id?, class?, children)
}
///|
pub fn[M] thead(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node("thead", [], style~, id?, class?, children)
}
///|
pub fn[M] tbody(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node("tbody", [], style~, id?, class?, children)
}
///|
pub fn[M] col(
style? : Array[String] = [],
id? : String,
span? : Int,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if span is Some(span) {
attrs.push(attribute("span", span.to_string()))
}
common_node("col", attrs, style~, id?, class?, children)
}
///|
pub fn[M] colgroup(
style? : Array[String] = [],
id? : String,
span? : Int,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if span is Some(span) {
attrs.push(attribute("span", span.to_string()))
}
common_node("colgroup", attrs, style~, id?, class?, children)
}
///|
pub fn[M] tr(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node("tr", [], style~, id?, class?, children)
}
///|
pub fn[M] td(
style? : Array[String] = [],
id? : String,
colspan? : Int,
rowspan? : Int,
headers? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if colspan is Some(colspan) {
attrs.push(attribute("colspan", colspan.to_string()))
}
if rowspan is Some(rowspan) {
attrs.push(attribute("rowspan", rowspan.to_string()))
}
if headers is Some(headers) {
attrs.push(attribute("headers", headers))
}
common_node("td", attrs, style~, id?, class?, children)
}
///|
pub(all) enum Scope {
Row
Col
RowGroup
ColGroup
}
///|
pub fn[M] th(
style? : Array[String] = [],
id? : String,
abbr? : String,
colspan? : Int,
rowspan? : Int,
headers? : String,
scope? : Scope,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if colspan is Some(colspan) {
attrs.push(attribute("colspan", colspan.to_string()))
}
if rowspan is Some(rowspan) {
attrs.push(attribute("rowspan", rowspan.to_string()))
}
if headers is Some(headers) {
attrs.push(attribute("headers", headers))
}
if abbr is Some(abbr) {
attrs.push(attribute("abbr", abbr))
}
if scope is Some(scope) {
attrs.push(
attribute(
"scope",
match scope {
Row => "row"
Col => "col"
RowGroup => "rowgroup"
ColGroup => "colgroup"
},
),
)
}
common_node("th", attrs, style~, id?, class?, children)
}
///|
pub fn[M] tfoot(
style? : Array[String] = [],
id? : String,
class? : String,
children : Array[Html[M]],
) -> Html[M] {
common_node("tfoot", [], style~, id?, class?, children)
}
///|
/// Dialog element.
///
/// Hint: You can use the `show` and `close` commands in the `@dialog` package
/// to manipulate the dialog.
///
/// # Attributes
///
/// - `open`: indicates whether the dialog is open or closed by default.
///
/// # Messages
///
/// - `close`: triggered when the dialog is closed.
///
/// The string payload of the `close` message is the return value of the dialog.
///
/// - `cancel`: triggered when the user instructs the browser that they wish to
/// dismiss the current open dialog.
///
/// It can be triggered when the user presses `esc` or uses the `request_close` command.
///
/// If the `cancel` argument is provided, **the dialog will not close automatically**
/// after this message is triggered. You can use the `@dialog.close` command
/// to close it or `@cmd.none` to keep it open.
///
/// # Example
///
/// ```moonbit skip
/// typealias Model = String
///
/// enum Msg {
/// Open
/// Closed(String)
/// YesNo(Bool)
/// }
///
/// fn update(msg : Msg, model : Model) -> (Cmd[Msg], Model) {
/// match msg {
/// Open => (@dialog.show("confirm"), model)
/// YesNo(answer) => (@dialog.close("confirm", return_value=answer.to_string()), model)
/// Closed(value) => (none(), value)
/// }
/// }
///
/// fn view(model : Model) -> Html[Msg] {
/// div([
/// h1([text(model)]),
/// button(click=Msg::Open, [text("open")]),
/// dialog(id="confirm", close=Msg::Closed, [
/// p([text("Are you sure?")]),
/// button(click=YesNo(true), [text("Yes")]),
/// button(click=YesNo(false), [text("No")]),
/// ]),
/// ])
/// }
/// ```
///
pub fn[M] dialog(
style? : Array[String] = [],
id? : String,
class? : String,
open? : Bool,
close? : (String) -> M,
cancel? : M,
children : Array[Html[M]],
) -> Html[M] {
let attrs = []
if open is Some(open) {
attrs.push(property("open", Boolean(open)))
}
if close is Some(close) {
attrs.push(
@vdom.on(
"close",
HandleEvent(event => {
let html_element = event
.target()
.to_node()
.unwrap()
.to_element()
.unwrap()
.to_html_element()
.unwrap()
.to_html_dialog_element()
.unwrap()
close(html_element.return_value())
}),
),
)
}
if cancel is Some(cancel) {
attrs.push(
@vdom.on(
"cancel",
HandleEvent(event => {
event.prevent_default()
cancel
}),
),
)
}
common_node("dialog", attrs, style~, id?, class?, children)
}