Packages
lustre
5.2.0
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/keyed.gleam
//// Lustre uses something called a _virtual DOM_ to work out what has changed
//// between renders and update the DOM accordingly. That means when you render
//// items in a list, Lustre will walk through the list of items and compare them
//// in order to see if they have changed.
////
//// This is often fine but it can be cause problems in cases where we'd like
//// Lustre to reuse existing DOM nodes more efficiently. Consider the example
//// in the [quickstart guide](../../guide/01-quickstart.html): each time the
//// counter is incremented, we insert a new image at the _start_ of the list.
////
//// Let's see how the virtual DOM handles this:
////
//// ```
//// Increment -> Increment ->
//// <img src="a"> -- update -> <img src="b">
//// -- insert -> <img src="a">
//// ```
////
//// Beacuse the virtual DOM compares elements in order, it sees that the first
//// element has its `src` attribute changed from `"a"` to `"b"` and then sees
//// that a new element has been added to the _end_ of the list.
////
//// Intuitively, we know that what _really_ happened is that an element was
//// inserted at the _front_ of the list and ideally the first `<img />` should
//// be left untouched.
////
//// The solution is to assign a unique _key_ to each child element. This gives
//// Lustre enough information to reuse existing DOM nodes and avoid unnecessary
//// updates.
////
//// Keyed elements in Lustre work exactly like regular elements, but their child
//// list is a tuple of a unique key and the child itself:
////
//// ```gleam
//// keyed.div([], list.map(model.cats, fn(cat) {
//// #(cat.id, html.img([attribute.src(cat.url)]))
//// }))
//// ```
////
//// Let's see how the virtual DOM now handles this:
////
//// ```
//// Increment -> Increment ->
//// -- insert -> <img src="b">
//// <img href="a"> -- -> <img src="a">
//// ```
////
//// We can see that Lustre has correctly recognised that the only change is a
//// new image being inserted at the front of the list. The first image is left
//// untouched!
////
// IMPORTS ---------------------------------------------------------------------
import gleam/function
import gleam/list
import lustre/attribute.{type Attribute}
import lustre/element.{type Element}
import lustre/internals/constants
import lustre/internals/mutable_map.{type MutableMap}
import lustre/vdom/vnode
// CONSTRUCTORS ----------------------------------------------------------------
/// Render a _keyed_ element with the given tag. Each child is assigned a unique
/// key, which Lustre uses to identify the element in the DOM. This is useful when
/// a single child can be moved around such as in a to-do list, or when elements
/// are frequently added or removed.
///
/// > **Note**: the key for each child 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 element(
tag: String,
attributes: List(Attribute(msg)),
children: List(#(String, Element(msg))),
) -> Element(msg) {
let #(keyed_children, children, _) = extract_keyed_children(children)
vnode.element(
key: "",
namespace: "",
tag:,
mapper: function.identity,
attributes:,
children:,
keyed_children:,
self_closing: False,
void: False,
)
}
/// Render a _keyed_ element with the given namespace and tag. Each child is
/// assigned a unique key, which Lustre uses to identify the element in the DOM.
/// This is useful when a single child can be moved around such as in a to-do
/// list, or when elements are frequently added or removed.
///
/// > **Note**: the key for each child 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 namespaced(
namespace: String,
tag: String,
attributes: List(Attribute(msg)),
children: List(#(String, Element(msg))),
) -> Element(msg) {
let #(keyed_children, children, _) = extract_keyed_children(children)
vnode.element(
key: "",
mapper: function.identity,
namespace:,
tag:,
attributes:,
children:,
keyed_children:,
self_closing: False,
void: False,
)
}
/// Render a _keyed_ fragment. Each child is assigned a unique key, which Lustre
/// uses to identify the element in the DOM. This is useful when a single child
/// can be moved around such as in a to-do list, or when elements are frequently
/// added or removed.
///
/// > **Note**: the key for each child 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 fragment(children: List(#(String, Element(msg)))) -> Element(msg) {
let #(keyed_children, children, children_count) =
extract_keyed_children(children)
vnode.fragment(
key: "",
mapper: function.identity,
children:,
children_count:,
keyed_children:,
)
}
// ELEMENTS --------------------------------------------------------------------
pub fn ul(
attributes: List(Attribute(msg)),
children: List(#(String, Element(msg))),
) -> Element(msg) {
element("ul", attributes, children)
}
pub fn ol(
attributes: List(Attribute(msg)),
children: List(#(String, Element(msg))),
) -> Element(msg) {
element("ol", attributes, children)
}
pub fn div(
attributes: List(Attribute(msg)),
children: List(#(String, Element(msg))),
) -> Element(msg) {
element("div", attributes, children)
}
pub fn tbody(
attributes: List(Attribute(msg)),
children: List(#(String, Element(msg))),
) -> Element(msg) {
element("tbody", attributes, children)
}
pub fn dl(
attributes: List(Attribute(msg)),
children: List(#(String, Element(msg))),
) -> Element(msg) {
element("dl", attributes, children)
}
// UTILS -----------------------------------------------------------------------
fn extract_keyed_children(
children: List(#(String, Element(msg))),
) -> #(MutableMap(String, Element(msg)), List(Element(msg)), Int) {
do_extract_keyed_children(
children,
mutable_map.new(),
constants.empty_list,
0,
)
}
fn do_extract_keyed_children(
key_children_pairs: List(#(String, Element(msg))),
keyed_children: MutableMap(String, Element(msg)),
children: List(Element(msg)),
children_count: Int,
) -> #(MutableMap(String, Element(msg)), List(Element(msg)), Int) {
case key_children_pairs {
[] -> #(keyed_children, list.reverse(children), children_count)
[#(key, element), ..rest] -> {
let keyed_element = vnode.to_keyed(key, element)
// Children with empty keys are not inserted into the lookup, but they are
// still returned in the children list.
let keyed_children = case key {
"" -> keyed_children
_ -> mutable_map.insert(keyed_children, key, keyed_element)
}
let children = [keyed_element, ..children]
let children_count = children_count + vnode.advance(keyed_element)
do_extract_keyed_children(rest, keyed_children, children, children_count)
}
}
}