Packages
lustre
5.5.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/component.gleam
//// Lustre's component system is built on top of the Custom Elements API and
//// the Shadow DOM API. This module helps you configure new components and
//// interact with existing ones.
////
//// While it's not required, understanding the spec and how it works will help
//// you get the most out of Lustre's component system. The following resources
//// are a great place to start:
////
//// - https://developer.mozilla.org/en-US/docs/Web/Web_Components
////
//// - https://css-tricks.com/web-components-demystified/
////
//// - https://github.com/web-padawan/awesome-web-components
////
//// ## Examples
////
//// We have a small number of examples showing how to set up and use components
//// that are a great place to see some code:
////
//// - [`Basic setup`](https://github.com/lustre-labs/lustre/tree/main/examples/05-components/01-basic-setup)
////
//// - [`Custom attributes and events`](https://github.com/lustre-labs/lustre/tree/main/examples/05-components/02-attributes-and-events)
////
//// - [`Slots`](https://github.com/lustre-labs/lustre/tree/main/examples/05-components/03-slots)
////
//// This list of examples is likely to grow over time, so be sure to check back
//// every now and then to see what's new!
////
//// ## Getting help
////
//// If you're having trouble with Lustre or not sure what the right way to do
//// something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
//// You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
////
// IMPORTS ---------------------------------------------------------------------
import gleam/dict
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode.{type Decoder}
import gleam/list
import gleam/option.{Some}
import gleam/string
import lustre/attribute.{type Attribute, attribute}
import lustre/effect.{type Effect}
import lustre/element.{type Element}
import lustre/element/html
import lustre/internals/constants
import lustre/runtime/server/runtime
// TYPES -----------------------------------------------------------------------
/// The configuration for a Lustre [`component`](../lustre.html#component). In
/// Lustre, components are real custom elements. You can use this configuration
/// to define what features the component supports and what platform functionality
/// it should have access to.
///
pub opaque type Config(msg) {
Config(
//
open_shadow_root: Bool,
adopt_styles: Bool,
delegates_focus: Bool,
//
attributes: List(#(String, fn(String) -> Result(msg, Nil))),
properties: List(#(String, Decoder(msg))),
contexts: List(#(String, Decoder(msg))),
//
is_form_associated: Bool,
on_form_autofill: option.Option(fn(String) -> msg),
on_form_reset: option.Option(msg),
on_form_restore: option.Option(fn(String) -> msg),
)
}
/// Options are used to configure a component's behaviour.
///
/// - [`on_attribute_change`](#on_attribute_change) lets you register a callback
/// that runs whenever an [`attribute`](./attribute.html#attribute) is set on
/// your component in the DOM.
///
/// - [`on_property_change`](#on_property_change) lets you register a decoder to
/// run whenever a [`property`](./attribute.html#property) is set on the
/// component.
///
/// See [this note](https://github.com/lustre-labs/lustre/blob/main/pages/hints/attributes-vs-properties.md)
/// on the difference between attributes and properties.
///
/// - [`form_associated`](#form_associated) marks the component as "form-associated",
/// allowing your component to participate in form submission and get accesss
/// to form-specific events.
///
/// - [`on_form_autofill`](#on_form_autofill) lets you register a callback that
/// runs when the browser autofills your component's `"value"` attribute.
///
/// - [`on_form_reset`](#on_form_reset) lets you register a message that runs
/// when a form containing your component is reset.
///
/// - [`on_form_restore`](#on_form_restore) lets you register a callback that
/// runs when the browser restores your component's `"value"` attribute, often
/// after a page reload or the user navigating back or forward in their history.
///
/// - [`open_shadow_root`](#open_shadow_root) lets you control whether the component
/// uses an open or closed [shadow root](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/mode).
///
/// - [`adopt_styles`](#adopt_styles) lets you control whether the component should
/// attempt to adopt stylesheets from its parent document. All Lustre components
/// use shadow DOM to get access to certain features like form-associated elements
/// or HTML slots. Unfortunately, this means typically styles in the shadow DOM
/// are isolated from the parent document.
///
/// Setting `adopt_styles` to `True` tells Lustre to attempt to adopt or clone
/// stylesheets from the parent document _into_ the shadow DOM. This can give
/// you an experience similar to components in other frameworks like React or
/// Vue.
///
/// > **Note**: Not all options are available for server components. For example
/// > server components cannot be form-associated and participate in form submission.
///
pub opaque type Option(msg) {
Option(apply: fn(Config(msg)) -> Config(msg))
}
/// 🚨 This is an **internal** function and should not be consumed by user code.
/// Internal functions may depend on unstable APIs or require certain usage
/// patterns: no guarantees are made about the stability _or_ reliability of
/// internal functions.
///
/// Construct a new `Config` record and apply a list of `Option`s to it. Options
/// are applied in order and later options may override earlier ones.
///
@internal
pub fn new(options: List(Option(msg))) -> Config(msg) {
let init =
Config(
//
open_shadow_root: True,
adopt_styles: True,
delegates_focus: False,
//
attributes: constants.empty_list,
properties: constants.empty_list,
contexts: constants.empty_list,
//
is_form_associated: False,
on_form_autofill: option.None,
on_form_reset: option.None,
on_form_restore: option.None,
)
use config, option <- list.fold(options, init)
option.apply(config)
}
// BUILDERS --------------------------------------------------------------------
/// Register a decoder to run whenever the named attribute changes. Attributes
/// can be set in Lustre using the [`attribute`](./attribute.html#attribute)
/// function, set directly on the component's HTML tag, or in JavaScript using
/// the [`setAttribute`](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute)
/// method.
///
/// Attributes are always strings, but your decoder is responsible for decoding
/// the string into a message that your component can understand.
///
pub fn on_attribute_change(
name: String,
decoder: fn(String) -> Result(msg, Nil),
) -> Option(msg) {
use config <- Option
let attributes = [#(name, decoder), ..config.attributes]
Config(..config, attributes:)
}
/// Register decoder to run whenever the given property is set on the component.
/// Properties can be set in Lustre using the [`property`](./attribute.html#property)
/// function or in JavaScript by setting a property directly on the component
/// object.
///
/// Properties can be any JavaScript object. For server components, properties
/// will be any _JSON-serialisable_ value.
///
pub fn on_property_change(name: String, decoder: Decoder(msg)) -> Option(msg) {
use config <- Option
let properties = [#(name, decoder), ..config.properties]
Config(..config, properties:)
}
/// Register a decoder to run whenever a parent component or application
/// [provides](./effect.html#provide) a new context value for the given `key`.
/// Contexts are a powerful feature that allow parents to inject data into
/// child components without knowledge of the DOM structurre, making them great
/// for advanced use-cases like design systems and flexible component hierarchies.
///
/// Contexts can be any JavaScript object. For server components, contexts will
/// be any _JSON-serialisable_ value.
///
pub fn on_context_change(key: String, decoder: Decoder(msg)) -> Option(msg) {
use config <- Option
let contexts = [#(key, decoder), ..config.contexts]
Config(..config, contexts:)
}
/// Mark a component as "form-associated". This lets your component participate
/// in form submission and respond to additional form-specific events such as
/// the form being reset or the browser autofilling this component's value.
///
/// > **Note**: form-associated components are not supported in server components
/// > for both technical and ideological reasons. If you'd like a component that
/// > participates in form submission, you should use a client component!
///
pub fn form_associated() -> Option(msg) {
use config <- Option
Config(..config, is_form_associated: True)
}
/// Register a callback that runs when the browser autofills this
/// [form-associated](#form_associated) component's `"value"` attribute. The
/// callback should convert the autofilled value into a message that you handle
/// in your `update` function.
///
/// > **Note**: server components cannot participate in form submission and configuring
/// > this option will do nothing.
///
pub fn on_form_autofill(handler: fn(String) -> msg) -> Option(msg) {
use config <- Option
Config(..config, is_form_associated: True, on_form_autofill: Some(handler))
}
/// Set a message to be dispatched whenever a form containing this
/// [form-associated](#form_associated) component is reset.
///
/// > **Note**: server components cannot participate in form submission and configuring
/// > this option will do nothing.
///
pub fn on_form_reset(msg: msg) -> Option(msg) {
use config <- Option
Config(..config, is_form_associated: True, on_form_reset: Some(msg))
}
/// Set a callback that runs when the browser restores this
/// [form-associated](#form_associated) component's `"value"` attribute. This is
/// often triggered when the user navigates back or forward in their history.
///
/// > **Note**: server components cannot participate in form submission and configuring
/// > this option will do nothing.
///
pub fn on_form_restore(handler: fn(String) -> msg) -> Option(msg) {
use config <- Option
Config(..config, is_form_associated: True, on_form_restore: Some(handler))
}
/// Configure whether a component's [Shadow Root](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot)
/// is open or closed. A closed shadow root means the elements rendered inside
/// the component are not accessible from JavaScript outside the component.
///
/// By default a component's shadow root is **open**. You may want to configure
/// this option manually if you intend to build a component for use outside of
/// Lustre.
///
pub fn open_shadow_root(open: Bool) -> Option(msg) {
use config <- Option
Config(..config, open_shadow_root: open)
}
/// Configure whether a component should attempt to adopt stylesheets from
/// its parent document. Components in Lustre use the shadow DOM to unlock native
/// web component features like slots, but this means elements rendered inside a
/// component are isolated from the document's styles.
///
/// To get around this, Lustre can attempt to adopt all stylesheets from the
/// parent document when the component is first created; meaning in many cases
/// you can use the same CSS to style your components as you do the rest of your
/// application.
///
/// By default, this option is **enabled**. You may want to disable this option
/// if you are building a component for use outside of Lustre and do not want
/// document styles to interfere with your component's styling
///
pub fn adopt_styles(adopt: Bool) -> Option(msg) {
use config <- Option
Config(..config, adopt_styles: adopt)
}
/// Indicates whether or not this component should delegate focus to its children.
/// When set to `True`, a number of focus-related features are enabled:
///
/// - Clicking on any non-interactive part of the component will automatically
/// focus the first focusable child element.
///
/// - The component can receive focus through the `.focus()` method or the
/// `autofocus` attribute, and it will automatically focus the first
/// focusable child element.
///
/// - The component receives the `:focus` CSS pseudo-class when any of its
/// focusable children have focus.
///
/// By default this option is **disabled**. You may want to enable this option
/// when creating complex interactive widgets.
///
pub fn delegates_focus(delegates: Bool) -> Option(msg) {
use config <- Option
Config(..config, delegates_focus: delegates)
}
// CONVERSIONS -----------------------------------------------------------------
/// 🚨 This is an **internal** function and should not be consumed by user code.
/// Internal functions may depend on unstable APIs or require certain usage
/// patterns: no guarantees are made about the stability _or_ reliability of
/// internal functions.
///
/// The server component runtime has to define its own `Config` type to avoid
/// circular dependencies. This function converts the public-facing component
/// config into an internal server component one: which is handy because not all
/// options are available in server components anyway.
///
@internal
pub fn to_server_component_config(config: Config(msg)) -> runtime.Config(msg) {
runtime.Config(
open_shadow_root: config.open_shadow_root,
adopt_styles: config.adopt_styles,
// we reverse both lists here such that the last added value takes precedence
attributes: dict.from_list(list.reverse(config.attributes)),
properties: dict.from_list(list.reverse(config.properties)),
contexts: dict.from_list(list.reverse(config.contexts)),
)
}
// ELEMENTS --------------------------------------------------------------------
/// Create a default slot for a component. Any elements rendered as children of
/// the component will be placed inside the default slot unless explicitly
/// redirected using the [`slot`](#slot) attribute.
///
/// If no children are placed into the slot, the `fallback` elements will be
/// rendered instead.
///
/// To learn more about Shadow DOM and slots, see this excellent guide:
///
/// https://javascript.info/slots-composition
///
pub fn default_slot(
attributes: List(Attribute(msg)),
fallback: List(Element(msg)),
) -> Element(msg) {
html.slot(attributes, fallback)
}
/// Create a named slot for a component. Any elements rendered as children of
/// the component with a [`slot`](#slot) attribute matching the `name` will be
/// rendered inside this slot.
///
/// If no children are placed into the slot, the `fallback` elements will be
/// rendered instead.
///
/// To learn more about Shadow DOM and slots, see this excellent guide:
///
/// https://javascript.info/slots-composition
///
pub fn named_slot(
name: String,
attributes: List(Attribute(msg)),
fallback: List(Element(msg)),
) -> Element(msg) {
html.slot([attribute("name", name), ..attributes], fallback)
}
// ATTRIBUTES ------------------------------------------------------------------
/// Lustre's component system is built on top the Custom Elements API and the
/// Shadow DOM API. A component's `view` function is rendered inside a shadow
/// root, which means the component's HTML is isolated from the rest of the
/// document.
///
/// This can make it difficult to style components from CSS outside the component.
/// To help with this, the `part` attribute lets you expose parts of your component
/// by name to be styled by external CSS.
///
/// For example, if the `view` function for a component called `"my-component`"
/// looks like this:
///
/// ```gleam
/// import gleam/int
/// import lustre/component
/// import lustre/element/html
///
/// fn view(model) {
/// html.div([], [
/// html.button([], [html.text("-")]),
/// html.p([component.part("count")], [html.text(int.to_string(model.count))]),
/// html.button([], [html.text("+")]),
/// ])
/// }
/// ```
///
/// Then the following CSS in the **parent** document can be used to style the
/// `<p>` element:
///
/// ```css
/// my-component::part(count) {
/// color: red;
/// }
/// ```
///
/// To learn more about the CSS Shadow Parts specification, see:
///
/// - https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part
///
/// - https://developer.mozilla.org/en-US/docs/Web/CSS/::part
///
pub fn part(name: String) -> Attribute(msg) {
attribute("part", name)
}
/// A convenience function that makes it possible to toggle different parts on or
/// off in a single call. This is useful for example when you have a menu item
/// that may be active and you want to conditionally assign the `"active"` part:
///
/// ```gleam
/// import lustre/component
/// import lustre/element/html
///
/// fn view(item) {
/// html.li(
/// [
/// component.parts([
/// #("item", True)
/// #("active", item.is_active)
/// ]),
/// ],
/// [html.text(item.label)],
/// ])
/// }
/// ```
///
pub fn parts(names: List(#(String, Bool))) -> Attribute(msg) {
part(do_parts(names, ""))
}
fn do_parts(names: List(#(String, Bool)), part: String) -> String {
case names {
[] -> part
[#(name, True), ..rest] -> part <> name <> " " <> do_parts(rest, part)
[#(_, False), ..rest] -> do_parts(rest, part)
}
}
/// While the [`part`](#part) attribute can be used to expose parts of a component
/// to its parent, these parts will not automatically become available to the
/// _document_ when components are nested inside each other.
///
/// The `exportparts` attribute lets you forward the parts of a nested component
/// to the parent component so they can be styled from the parent document.
///
/// Consider we have two components, `"my-component"` and `"my-nested-component"`
/// with the following `view` functions:
///
/// ```gleam
/// import gleam/int
/// import lustre/attribute.{property}
/// import lustre/component
/// import lustre/element.{element}
/// import lustre/element/html
///
/// fn my_component_view(model) {
/// html.div([], [
/// html.button([], [html.text("-")]),
/// element(
/// "my-nested-component",
/// [
/// property("count", model.count),
/// component.exportparts(["count"]),
/// ],
/// []
/// )
/// html.button([], [html.text("+")]),
/// ])
/// }
///
/// fn my_nested_component_view(model) {
/// html.p([component.part("count")], [html.text(int.to_string(model.count))])
/// }
/// ```
///
/// The `<my-nested-component />` component has a part called `"count"` which the
/// `<my-component />` then forwards to the parent document using the `"exportparts"`
/// attribute. Now the following CSS can be used to style the `<p>` element nested
/// deep inside the `<my-component />`:
///
/// ```css
/// my-component::part(count) {
/// color: red;
/// }
/// ```
///
/// Notice how the styles are applied to the `<my-component />` element, not the
/// `<my-nested-component />` element!
///
/// To learn more about the CSS Shadow Parts specification, see:
///
/// - https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/exportparts
///
/// - https://developer.mozilla.org/en-US/docs/Web/CSS/::part
///
pub fn exportparts(names: List(String)) -> Attribute(msg) {
attribute("exportparts", string.join(names, ", "))
}
/// Associate an element with a [named slot](#named_slot) in a component. Multiple
/// elements can be associated with the same slot name.
///
/// To learn more about Shadow DOM and slots, see:
///
/// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot
///
/// https://javascript.info/slots-composition
///
pub fn slot(name: String) -> Attribute(msg) {
attribute("slot", name)
}
// EFFECTS ---------------------------------------------------------------------
/// Set the value of a [form-associated component](#form_associated). If the
/// component is rendered inside a `<form>` element, the value will be
/// automatically included in the form submission and available in the form's
/// `FormData` object.
///
pub fn set_form_value(value: String) -> Effect(msg) {
use _, root <- effect.before_paint
do_set_form_value(root, value)
}
@external(javascript, "./runtime/client/component.ffi.mjs", "set_form_value")
fn do_set_form_value(_root: Dynamic, _value: String) -> Nil {
Nil
}
/// Clear a form value previously set with [`set_form_value`](#set_form_value).
/// When the form is submitted, this component's value will not be included in
/// the form data.
///
pub fn clear_form_value() -> Effect(msg) {
use _, root <- effect.before_paint
do_clear_form_value(root)
}
@external(javascript, "./runtime/client/component.ffi.mjs", "clear_form_value")
fn do_clear_form_value(_root: Dynamic) -> Nil {
Nil
}
/// Set a custom state on the component. This state is not reflected in the DOM
/// but can be selected in CSS using the `:state` pseudo-class. For example,
/// calling `set_pseudo_state("checked")` on a component called `"my-checkbox"`
/// means the following CSS will apply:
///
/// ```css
/// my-checkbox:state(checked) {
/// border: solid;
/// }
/// ```
///
/// If you are styling a component by rendering a `<style>` element _inside_ the
/// component, the previous CSS would be rewritten as:
///
/// ```css
/// :host(:state(checked)) {
/// border: solid;
/// }
/// ```
///
pub fn set_pseudo_state(value: String) -> Effect(msg) {
use _, root <- effect.before_paint
do_set_pseudo_state(root, value)
}
@external(javascript, "./runtime/client/component.ffi.mjs", "set_pseudo_state")
fn do_set_pseudo_state(_root: Dynamic, _value: String) -> Nil {
Nil
}
/// Remove a custom state set by [`set_pseudo_state`](#set_pseudo_state).
///
pub fn remove_pseudo_state(value: String) -> Effect(msg) {
use _, root <- effect.before_paint
do_remove_pseudo_state(root, value)
}
@external(javascript, "./runtime/client/component.ffi.mjs", "remove_pseudo_state")
fn do_remove_pseudo_state(_root: Dynamic, _value: String) -> Nil {
Nil
}