diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2023-07-18 00:18:38 +0100 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2023-07-18 00:18:38 +0100 |
commit | f8dfa31c7abe3654f3a6dc9949424afea6eba4ce (patch) | |
tree | 8e262e579a1b6013946edd7dd83dfb6f01616ecf | |
parent | 6c6ba6661aeca67238bc1cd738f400f2374cd9e4 (diff) | |
download | lustre-f8dfa31c7abe3654f3a6dc9949424afea6eba4ce.tar.gz lustre-f8dfa31c7abe3654f3a6dc9949424afea6eba4ce.zip |
:recycle: Represent attributes as a normal Gleam custom type.
-rw-r--r-- | src/lustre/attribute.gleam | 184 | ||||
-rw-r--r-- | src/lustre/event.gleam | 78 |
2 files changed, 179 insertions, 83 deletions
diff --git a/src/lustre/attribute.gleam b/src/lustre/attribute.gleam index 2553321..7716440 100644 --- a/src/lustre/attribute.gleam +++ b/src/lustre/attribute.gleam @@ -3,14 +3,19 @@ import gleam/dynamic.{Dynamic} import gleam/int import gleam/list +import gleam/option.{Option} import gleam/string +import gleam/string_builder.{StringBuilder} // TYPES ----------------------------------------------------------------------- /// Attributes are attached to specific elements. They're either key/value pairs /// or event handlers. /// -pub type Attribute(msg) +pub opaque type Attribute(msg) { + Attribute(String, Dynamic) + Event(String, fn(Dynamic) -> Option(msg)) +} // CONSTRUCTORS ---------------------------------------------------------------- @@ -22,19 +27,184 @@ pub type Attribute(msg) /// - `Some(a)` -> `a` /// - `None` -> `undefined` /// -@external(javascript, "../lustre.ffi.mjs", "attr") -pub fn attribute(name name: String, value value: any) -> Attribute(msg) +pub fn attribute(name: String, value: any) -> Attribute(msg) { + let dyn = dynamic.from(value) + + case dynamic.classify(dyn) { + "String" -> + dynamic.unsafe_coerce(dyn) + |> escape("", _) + |> dynamic.from + |> Attribute(name, _) + _ -> Attribute(name, dyn) + } +} + +fn escape(escaped: String, content: String) -> String { + case string.pop_grapheme(content) { + Ok(#("<", xs)) -> escape(escaped <> "<", xs) + Ok(#(">", xs)) -> escape(escaped <> ">", xs) + Ok(#("&", xs)) -> escape(escaped <> "&", xs) + Ok(#("\"", xs)) -> escape(escaped <> """, xs) + Ok(#("'", xs)) -> escape(escaped <> "'", xs) + Ok(#(x, xs)) -> escape(escaped <> x, xs) + Error(_) -> escaped <> content + } +} + +/// Attach custom event handlers to an element. A number of helper functions exist +/// in this module to cover the most common events and use-cases, so you should +/// check those out first. +/// +/// If you need to handle an event that isn't covered by the helper functions, +/// then you can use `on` to attach a custom event handler. The callback is given +/// the event object as a `Dynamic`. +/// +/// As a simple example, you can implement `on_click` like so: +/// +/// ```gleam +/// import gleam/option.{Some} +/// import lustre/attribute.{Attribute} +/// import lustre/event +/// +/// pub fn on_click(msg: msg) -> Attribute(msg) { +/// use _ <- event.on("click") +/// Some(msg) +/// } +/// ``` +/// +/// By using `gleam/dynamic` you can decode the event object and pull out all sorts +/// of useful data. This is how `on_input` is implemented: +/// +/// ```gleam +/// import gleam/dynamic +/// import gleam/option.{None, Some} +/// import gleam/result +/// import lustre/attribute.{Attribute} +/// import lustre/event +/// +/// pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg) { +/// use event, dispatch <- on("input") +/// let decode = dynamic.field("target", dynamic.field("value", dynamic.string)) +/// +/// case decode(event) { +/// Ok(value) -> Some(msg(value)) +/// Error(_) -> None +/// } +/// } +/// ``` +/// +/// You can take a look at the MDN reference for events +/// [here](https://developer.mozilla.org/en-US/docs/Web/API/Event) to see what +/// you can decode. +/// +/// Unlike the helpers in the rest of this module, it is possible to simply ignore +/// the dispatch function and not dispatch a message at all. In fact, we saw this +/// with the `on_input` example above: if we can't decode the event object, we +/// simply return `None` and emit nothing. +/// +/// Beyond ignoring errors, this can be used to perform side effects we don't need +/// to observe in our main application loop, such as logging... +/// +/// ```gleam +/// import gleam/io +/// import gleam/option.{None} +/// import lustre/attribute.{Attribute} +/// import lustre/event +/// +/// pub fn log_on_click(msg: String) -> Attribute(msg) { +/// use _ <- event.on("click") +/// io.println(msg) +/// None +/// } +/// ``` +/// +pub fn on(name: String, handler: fn(Dynamic) -> Option(msg)) -> Attribute(msg) { + Event("on" <> name, handler) +} + +// MANIPULATIONS --------------------------------------------------------------- + +/// +/// +pub fn map(attr: Attribute(a), f: fn(a) -> b) -> Attribute(b) { + case attr { + Attribute(name, value) -> Attribute(name, value) + Event(on, handler) -> Event(on, fn(e) { option.map(handler(e), f) }) + } +} + +// CONVERSIONS ----------------------------------------------------------------- + +/// +/// +pub fn to_string(attr: Attribute(msg)) -> String { + case attr { + Attribute(name, value) -> { + case dynamic.classify(value) { + "String" -> name <> "=\"" <> dynamic.unsafe_coerce(value) <> "\"" + + // Boolean attributes are determined based on their presence, eg we don't + // want to render `disabled="false"` if the value is `false` we simply + // want to omit the attribute altogether. + "Boolean" -> + case dynamic.unsafe_coerce(value) { + True -> name + False -> "" + } + + // For everything else we'll just make a best-effort serialisation. + _ -> name <> "=\"" <> string.inspect(value) <> "\"" + } + } + Event(on, _) -> "data-lustre-on:" <> on + } +} + +/// +/// +pub fn to_string_builder(attr: Attribute(msg)) -> StringBuilder { + case attr { + Attribute(name, value) -> { + case dynamic.classify(value) { + "String" -> + [name, "=\"", dynamic.unsafe_coerce(value), "\""] + |> string_builder.from_strings + + // Boolean attributes are determined based on their presence, eg we don't + // want to render `disabled="false"` if the value is `false` we simply + // want to omit the attribute altogether. + "Boolean" -> + case dynamic.unsafe_coerce(value) { + True -> string_builder.from_string(name) + False -> string_builder.new() + } + + // For everything else we'll just make a best-effort serialisation. + _ -> + [name, "=\"", string.inspect(value), "\""] + |> string_builder.from_strings + } + } + Event(on, _) -> + ["data-lustre-on:", on] + |> string_builder.from_strings + } +} // COMMON ATTRIBUTES ----------------------------------------------------------- /// pub fn style(properties: List(#(String, String))) -> Attribute(msg) { - attribute("style", styles(properties)) + attribute( + "style", + { + use styles, #(name, value) <- list.fold(properties, "") + styles <> name <> ":" <> value <> ";" + }, + ) } -@external(javascript, "../lustre.ffi.mjs", "styles") -fn styles(properties properties: List(#(String, String))) -> Dynamic - /// pub fn class(name: String) -> Attribute(msg) { attribute("class", name) diff --git a/src/lustre/event.gleam b/src/lustre/event.gleam index eb3ccd1..95de6c9 100644 --- a/src/lustre/event.gleam +++ b/src/lustre/event.gleam @@ -3,89 +3,15 @@ // IMPORTS --------------------------------------------------------------------- import gleam/dynamic.{DecodeError, Dynamic} -import gleam/option.{None, Option, Some} +import gleam/option.{None, Some} import gleam/result -import lustre/attribute.{Attribute} +import lustre/attribute.{Attribute, on} // TYPES ----------------------------------------------------------------------- type Decoded(a) = Result(a, List(DecodeError)) -// CONSTRUCTORS ---------------------------------------------------------------- - -/// Attach custom event handlers to an element. A number of helper functions exist -/// in this module to cover the most common events and use-cases, so you should -/// check those out first. -/// -/// If you need to handle an event that isn't covered by the helper functions, -/// then you can use `on` to attach a custom event handler. The callback is given -/// the event object as a `Dynamic`. -/// -/// As a simple example, you can implement `on_click` like so: -/// -/// ```gleam -/// import gleam/option.{Some} -/// import lustre/attribute.{Attribute} -/// import lustre/event -/// -/// pub fn on_click(msg: msg) -> Attribute(msg) { -/// use _ <- event.on("click") -/// Some(msg) -/// } -/// ``` -/// -/// By using `gleam/dynamic` you can decode the event object and pull out all sorts -/// of useful data. This is how `on_input` is implemented: -/// -/// ```gleam -/// import gleam/dynamic -/// import gleam/option.{None, Some} -/// import gleam/result -/// import lustre/attribute.{Attribute} -/// import lustre/event -/// -/// pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg) { -/// use event, dispatch <- on("input") -/// let decode = dynamic.field("target", dynamic.field("value", dynamic.string)) -/// -/// case decode(event) { -/// Ok(value) -> Some(msg(value)) -/// Error(_) -> None -/// } -/// } -/// ``` -/// -/// You can take a look at the MDN reference for events -/// [here](https://developer.mozilla.org/en-US/docs/Web/API/Event) to see what -/// you can decode. -/// -/// Unlike the helpers in the rest of this module, it is possible to simply ignore -/// the dispatch function and not dispatch a message at all. In fact, we saw this -/// with the `on_input` example above: if we can't decode the event object, we -/// simply return `None` and emit nothing. -/// -/// Beyond ignoring errors, this can be used to perform side effects we don't need -/// to observe in our main application loop, such as logging... -/// -/// ```gleam -/// import gleam/io -/// import gleam/option.{None} -/// import lustre/attribute.{Attribute} -/// import lustre/event -/// -/// pub fn log_on_click(msg: String) -> Attribute(msg) { -/// use _ <- event.on("click") -/// io.println(msg) -/// None -/// } -/// ``` -/// -@external(javascript, "../lustre.ffi.mjs", "on") -pub fn on(name name: String, handler handler: fn(Dynamic) -> Option(msg)) -> Attribute( - msg, -) - // MOUSE EVENTS ---------------------------------------------------------------- /// |