Packages
lustre
4.4.3
5.7.1
5.7.0
5.6.0
5.5.2
5.5.1
5.5.0
5.4.0
5.3.5
5.3.4
5.3.3
5.3.2
5.3.1
5.3.0
5.2.1
5.2.0
5.1.1
5.1.0
5.0.3
5.0.2
5.0.1
5.0.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.0
4.0.0-rc1
4.0.0-rc.2
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.2
3.0.0-rc.1
2.0.1
2.0.0
1.3.0
1.2.0
1.1.0
1.0.0
Create HTML templates, single page applications, Web Components, and real-time server components in Gleam!
Current section
Files
Jump to
Current section
Files
src/lustre/element.gleam
//// Lustre wouldn't be much use as a frontend framework if it didn't provide a
//// way to create HTML elements. This module contains the basic functions
//// necessary to construct and manipulate different HTML elements.
////
//// It is also possible to use Lustre as a HTML templating library, without
//// using its runtime or framework features.
////
// IMPORTS ---------------------------------------------------------------------
import gleam/int
import gleam/list
import gleam/string
import gleam/string_builder.{type StringBuilder}
import lustre/attribute.{type Attribute, attribute}
import lustre/internals/vdom.{Element, Fragment, Map, Text}
// TYPES -----------------------------------------------------------------------
/// The `Element` type is how Lustre represents chunks of HTML. The `msg` type
/// variable is used to represent the types of messages that can be produced from
/// events on the element or its children.
///
/// **Note**: Just because an element _can_ produces messages of a given type,
/// doesn't mean that it _will_! The `msg` type variable is used to represent the
/// potential for messages to be produced, not a guarantee.
///
/// The most basic ways to create elements are:
///
/// - The [`element`](#element) function to construct arbitrary HTML elements.
/// You can also use this render Custom Elements (like those registered as
/// Lustre components).
///
/// - The [`text`](#text) function to turn a Gleam string into a text node.
///
/// - The [`none`](#none) function to render nothing - useful for conditional
/// rendering.
///
/// If you have more complex needs, there are two more-advanced functions:
///
/// - The [`namespaced`](#namespaced) function to create elements in a specific
/// XML namespace. This is useful for SVG or MathML elements, for example.
///
/// - The [`advanced`](#advanced) function to create elements with more control
/// over how the element is rendered when converted to a string. This is
/// necessary because some HTML, SVG, and MathML elements are self-closing or
/// void elements, and Lustre needs to know how to render them correctly!
///
/// For most applications, you'll only need to use the simpler functions; usually
/// the [`text`](#text) and [`none`](#none) functions are enough. This is because
/// Lustre already provides a module with all the standard HTML and SVG elements
/// ready to use in [`lustre/element/html`](./element/html.html) and
/// [`lustre/element/svg`](./element/svg.html).
///
pub type Element(msg) =
vdom.Element(msg)
// CONSTRUCTORS ----------------------------------------------------------------
/// A general function for constructing any kind of element. In most cases you
/// will want to use the [`lustre/element/html`](./element/html.html) instead but this
/// function is particularly handing when constructing custom elements, either
/// from your own Lustre components or from external JavaScript libraries.
///
/// **Note**: Because Lustre is primarily used to create HTML, this function
/// special-cases the following tags which render as
/// [void elements](https://developer.mozilla.org/en-US/docs/Glossary/Void_element):
///
/// - area
/// - base
/// - br
/// - col
/// - embed
/// - hr
/// - img
/// - input
/// - link
/// - meta
/// - param
/// - source
/// - track
/// - wbr
///
/// This will only affect the output of `to_string` and `to_string_builder`!
/// If you need to render any of these tags with children, *or* you want to
/// render some other tag as self-closing or void, use [`advanced`](#advanced)
/// to construct the element instead.
///
pub fn element(
tag: String,
attrs: List(Attribute(msg)),
children: List(Element(msg)),
) -> Element(msg) {
case tag {
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr" ->
Element(
key: "",
namespace: "",
tag: tag,
attrs: attrs,
children: [],
self_closing: False,
void: True,
)
_ ->
Element(
key: "",
namespace: "",
tag: tag,
attrs: attrs,
children: children,
self_closing: False,
void: False,
)
}
}
/// Keying elements is an optimisation that helps the runtime reuse existing DOM
/// nodes in cases where children are reordered or removed from a list. Maybe you
/// have a list of elements that can be filtered or sorted in some way, or additions
/// to the front are common. In these cases, keying elements can help Lustre avoid
/// unecessary DOM manipulations by pairing the DOM nodes with the elements in the
/// list that share the same key.
///
/// You can easily take an element from `lustre/element/html` and key its children
/// by making use of Gleam's [function capturing syntax](https://tour.gleam.run/functions/function-captures/):
///
/// ```gleam
/// import gleam/list
/// import lustre/element
/// import lustre/element/html
///
/// fn example() {
/// element.keyed(html.ul([], _), {
/// use item <- list.map(todo_list)
/// let child = html.li([], [view_item(item)])
///
/// #(item.id, child)
/// })
/// }
/// ```
///
/// **Note**: The key must be unique within the list of children, but it doesn't
/// have to be unique across the whole application. It's fine to use the same key
/// in different lists.
///
///
pub fn keyed(
el: fn(List(Element(msg))) -> Element(msg),
children: List(#(String, Element(msg))),
) -> Element(msg) {
el({
use #(key, child) <- list.map(children)
do_keyed(child, key)
})
}
fn do_keyed(el: Element(msg), key: String) -> Element(msg) {
case el {
Element(_, namespace, tag, attrs, children, self_closing, void) ->
Element(
key: key,
namespace: namespace,
tag: tag,
attrs: attrs,
children: children,
self_closing: self_closing,
void: void,
)
Map(subtree) -> Map(fn() { do_keyed(subtree(), key) })
Fragment(elements, _) ->
elements
|> list.index_map(fn(element, idx) {
case element {
Element(el_key, _, _, _, _, _, _) -> {
let new_key = case el_key {
"" -> key <> "-" <> int.to_string(idx)
_ -> key <> "-" <> el_key
}
do_keyed(element, new_key)
}
_ -> do_keyed(element, key)
}
})
|> Fragment(key)
_ -> el
}
}
/// A function for constructing elements in a specific XML namespace. This can
/// be used to construct SVG or MathML elements, for example.
///
pub fn namespaced(
namespace: String,
tag: String,
attrs: List(Attribute(msg)),
children: List(Element(msg)),
) -> Element(msg) {
Element(
key: "",
namespace: namespace,
tag: tag,
attrs: attrs,
children: children,
self_closing: False,
void: False,
)
}
/// A function for constructing elements with more control over how the element
/// is rendered when converted to a string. This is necessary because some HTML,
/// SVG, and MathML elements are self-closing or void elements, and Lustre needs
/// to know how to render them correctly!
///
pub fn advanced(
namespace: String,
tag: String,
attrs: List(Attribute(msg)),
children: List(Element(msg)),
self_closing: Bool,
void: Bool,
) -> Element(msg) {
Element(
key: "",
namespace: namespace,
tag: tag,
attrs: attrs,
children: children,
self_closing: self_closing,
void: void,
)
}
/// A function for turning a Gleam string into a text node. Gleam doesn't have
/// union types like some other languages you may be familiar with, like TypeScript.
/// Instead, we need a way to take a `String` and turn it into an `Element` somehow:
/// this function is exactly that!
///
pub fn text(content: String) -> Element(msg) {
Text(content)
}
/// A function for rendering nothing. This is mostly useful for conditional
/// rendering, where you might want to render something only if a certain
/// condition is met.
///
pub fn none() -> Element(msg) {
Text("")
}
/// A function for wrapping elements to be rendered within a parent container without
/// specififying the container on definition. Allows the treatment of List(Element(msg))
/// as if it were Element(msg). Useful when generating a list of elements from data but
/// used downstream.
///
pub fn fragment(elements: List(Element(msg))) -> Element(msg) {
// remove redundant fragments to simplify rendering
flatten_fragment_elements(elements)
|> Fragment("")
}
fn flatten_fragment_elements(elements: List(Element(msg))) {
list.fold_right(elements, [], fn(new_elements, element) {
case element {
// Only flatten one level, the runtime handles next level children
// alternatively, this could flatten deeply, but it doesn't save
// iteration later given a fragment is iterated the same as an equivalent
// list of children
Fragment(fr_elements, _) -> list.append(fr_elements, new_elements)
el -> [el, ..new_elements]
}
})
}
// MANIPULATIONS ---------------------------------------------------------------
/// The `Element` type is parameterised by the type of messages it can produce
/// from events. Sometimes you might end up with a fragment of HTML from another
/// library or module that produces a different type of message: this function lets
/// you map the messages produced from one type to another.
///
/// Think of it like `list.map` or `result.map` but for HTML events!
///
pub fn map(element: Element(a), f: fn(a) -> b) -> Element(b) {
case element {
Text(content) -> Text(content)
Map(subtree) -> Map(fn() { map(subtree(), f) })
Element(key, namespace, tag, attrs, children, self_closing, void) ->
Map(fn() {
Element(
key: key,
namespace: namespace,
tag: tag,
attrs: list.map(attrs, attribute.map(_, f)),
children: list.map(children, map(_, f)),
self_closing: self_closing,
void: void,
)
})
Fragment(elements, key) -> {
Map(fn() { Fragment(list.map(elements, map(_, f)), key) })
}
}
}
// CONVERSIONS -----------------------------------------------------------------
/// Convert a Lustre `Element` to a string. This is _not_ pretty-printed, so
/// there are no newlines or indentation. If you need to pretty-print an element,
/// reach out on the [Gleam Discord](https://discord.gg/Fm8Pwmy) or
/// [open an issue](https://github.com/lustre-labs/lustre/issues/new) with your
/// use case and we'll see what we can do!
///
pub fn to_string(element: Element(msg)) -> String {
vdom.element_to_string(element)
}
/// Converts an element to a string like [`to_string`](#to_string), but prepends
/// a `<!doctype html>` declaration to the string. This is useful for rendering
/// complete HTML documents.
///
/// If the provided element is not an `html` element, it will be wrapped in both
/// a `html` and `body` element.
///
pub fn to_document_string(el: Element(msg)) -> String {
vdom.element_to_string(case el {
Element(tag: "html", ..) -> el
Element(tag: "head", ..) | Element(tag: "body", ..) ->
element("html", [], [el])
Map(subtree) -> subtree()
_ -> element("html", [], [element("body", [], [el])])
})
|> string.append("<!doctype html>\n", _)
}
/// Convert a Lustre `Element` to a `StringBuilder`. This is _not_ pretty-printed,
/// so there are no newlines or indentation. If you need to pretty-print an element,
/// reach out on the [Gleam Discord](https://discord.gg/Fm8Pwmy) or
/// [open an issue](https://github.com/lustre-labs/lustre/issues/new) with your
/// use case and we'll see what we can do!
///
pub fn to_string_builder(element: Element(msg)) -> StringBuilder {
vdom.element_to_string_builder(element)
}
/// Converts an element to a `StringBuilder` like [`to_string_builder`](#to_string_builder),
/// but prepends a `<!doctype html>` declaration. This is useful for rendering
/// complete HTML documents.
///
/// If the provided element is not an `html` element, it will be wrapped in both
/// a `html` and `body` element.
///
pub fn to_document_string_builder(el: Element(msg)) -> StringBuilder {
vdom.element_to_string_builder(case el {
Element(tag: "html", ..) -> el
Element(tag: "head", ..) | Element(tag: "body", ..) ->
element("html", [], [el])
Map(subtree) -> subtree()
_ -> element("html", [], [element("body", [], [el])])
})
|> string_builder.prepend("<!doctype html>\n")
}