implement webc context protocol for lustre
This commit is contained in:
parent
389bd27b33
commit
1d95b6f917
14 changed files with 318 additions and 60 deletions
|
|
@ -1,6 +0,0 @@
|
|||
import field_adder
|
||||
|
||||
pub fn main() -> Nil {
|
||||
let assert Ok(_) = field_adder.register()
|
||||
Nil
|
||||
}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/cells.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/lens-controls.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/viewer-components.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/viewer-controller.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/glm_dist/field_adder.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/field_adder_component.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/viewer_controller_component.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/cell_text_component.mjs"></script>
|
||||
<link rel="stylesheet" href="{{ settings.root_path }}/viewer.css">
|
||||
<table class="viewer">
|
||||
<viewer-controller root-path="{{ settings.root_path }}">
|
||||
<table class="viewer">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for field in fields %}
|
||||
|
|
@ -45,5 +44,6 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</viewer-controller>
|
||||
{% endblock %}
|
||||
|
|
|
|||
15
mise.toml
15
mise.toml
|
|
@ -16,17 +16,12 @@ run = "cargo run serve"
|
|||
description = "Run the server. For development: `mise watch --restart serve`."
|
||||
sources = ["**/*.rs", "**/*.html"]
|
||||
|
||||
[tasks.vite]
|
||||
run = "deno task build"
|
||||
dir = "./components"
|
||||
sources = ["**/*.ts"]
|
||||
[tasks.build-js]
|
||||
run = "sh build.sh"
|
||||
dir = "./webc"
|
||||
sources = ["webc/src/**/*.gleam", "webc/src/**/*.mjs"]
|
||||
|
||||
[tasks.lustre]
|
||||
run = "gleam run -m lustre/dev build component field_adder --outdir=../glm_dist"
|
||||
dir = "./glm"
|
||||
sources = ["**/*.gleam"]
|
||||
|
||||
[tasks.sass]
|
||||
[tasks.build-css]
|
||||
run = "sass sass/:css_dist/"
|
||||
sources = ["sass/**/*.scss"]
|
||||
|
||||
|
|
|
|||
0
glm/.gitignore → webc/.gitignore
vendored
0
glm/.gitignore → webc/.gitignore
vendored
5
webc/build.sh
Normal file
5
webc/build.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Compile each .gleam file in src/ as a separate component module.
|
||||
ls src | \
|
||||
grep '_component.gleam' | \
|
||||
xargs -I {} basename {} .gleam | \
|
||||
xargs -I {} gleam run -m lustre/dev build component {} --outdir=../js_dist
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
name = "glm"
|
||||
name = "webc"
|
||||
version = "1.0.0"
|
||||
target = "javascript"
|
||||
|
||||
88
webc/src/cell_text_component.gleam
Normal file
88
webc/src/cell_text_component.gleam
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import gleam/dynamic
|
||||
import gleam/dynamic/decode
|
||||
import lustre.{type App}
|
||||
import lustre/attribute as attr
|
||||
import lustre/component
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
|
||||
import context
|
||||
|
||||
pub const name: String = "cell-text"
|
||||
|
||||
pub fn component() -> App(Nil, Model, Msg) {
|
||||
lustre.component(init, update, view, [
|
||||
component.on_attribute_change("value", fn(value) {
|
||||
ParentChangedValue(value) |> Ok
|
||||
}),
|
||||
component.on_attribute_change("column", fn(value) {
|
||||
ParentChangedColumn(value) |> Ok
|
||||
}),
|
||||
component.on_attribute_change("selected", fn(value) {
|
||||
ParentChangedSelected(value != "") |> Ok
|
||||
}),
|
||||
component.on_attribute_change("editing", fn(value) {
|
||||
ParentChangedEditing(value != "") |> Ok
|
||||
}),
|
||||
component.on_property_change("pkeys", {
|
||||
decode.list(of: decode.string) |> decode.map(ParentChangedPkeys)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
pub type Model {
|
||||
Model(
|
||||
root_path: String,
|
||||
column: String,
|
||||
pkeys: List(String),
|
||||
selected: Bool,
|
||||
editing: Bool,
|
||||
)
|
||||
}
|
||||
|
||||
fn init(_) -> #(Model, Effect(Msg)) {
|
||||
#(
|
||||
Model(
|
||||
root_path: "",
|
||||
column: "",
|
||||
pkeys: [""],
|
||||
selected: False,
|
||||
editing: False,
|
||||
),
|
||||
context.request_context(
|
||||
context: dynamic.string("root_path"),
|
||||
subscribe: False,
|
||||
decoder: decode.string |> decode.map(AncestorChangedRootPath),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
AncestorChangedRootPath(String)
|
||||
ParentChangedColumn(String)
|
||||
ParentChangedEditing(Bool)
|
||||
ParentChangedValue(String)
|
||||
ParentChangedSelected(Bool)
|
||||
ParentChangedPkeys(List(String))
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
case msg {
|
||||
AncestorChangedRootPath(root_path) -> #(
|
||||
Model(..model, root_path:),
|
||||
effect.none(),
|
||||
)
|
||||
_ -> #(model, effect.none())
|
||||
}
|
||||
}
|
||||
|
||||
fn view(model: Model) -> Element(Msg) {
|
||||
element.fragment([
|
||||
html.link([
|
||||
attr.rel("stylesheet"),
|
||||
attr.href(model.root_path <> "/css_dist/cell_text/index.css"),
|
||||
]),
|
||||
html.div([], [html.text(model.root_path)]),
|
||||
])
|
||||
}
|
||||
17
webc/src/context.ffi.mjs
Normal file
17
webc/src/context.ffi.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class ContextRequestEvent extends Event {
|
||||
constructor(context, subscribe, callback) {
|
||||
super("context-request", { bubbles: true, composed: true });
|
||||
this.context = context;
|
||||
this.subscribe = subscribe;
|
||||
this.callback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
export function emitContextRequest(root, context, subscribe, callback) {
|
||||
root.dispatchEvent(new ContextRequestEvent(context, subscribe, callback));
|
||||
}
|
||||
|
||||
// FFI shim so that Gleam can pass around Javascript functions as dynamic values
|
||||
export function callCallback(callback, value) {
|
||||
callback(value);
|
||||
}
|
||||
93
webc/src/context.gleam
Normal file
93
webc/src/context.gleam
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import gleam/dynamic.{type Dynamic}
|
||||
import gleam/dynamic/decode.{type Decoder}
|
||||
import gleam/option.{type Option}
|
||||
import lustre/attribute.{type Attribute}
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/event
|
||||
|
||||
/// WARNING: Lifecycle hooks in Lustre are currently limited to non-existent,
|
||||
/// so it's not possible to unsubscribe from context updates on component
|
||||
/// teardown. Be cautious using this effect with components that are not
|
||||
/// expected to be permanent fixtures on a page. (Refer to
|
||||
/// https://github.com/lustre-labs/lustre/issues/320.)
|
||||
pub fn request_context(
|
||||
context context: Dynamic,
|
||||
subscribe subscribe: Bool,
|
||||
decoder decoder: Decoder(msg),
|
||||
) -> Effect(msg) {
|
||||
use dispatch, root <- effect.before_paint
|
||||
use value, _unsubscribe <- emit_context_request(root, context, subscribe)
|
||||
case decode.run(value, decoder) {
|
||||
Ok(msg) -> {
|
||||
dispatch(msg)
|
||||
}
|
||||
Error(_) -> Nil
|
||||
}
|
||||
Nil
|
||||
}
|
||||
|
||||
@external(javascript, "./context.ffi.mjs", "emitContextRequest")
|
||||
fn emit_context_request(
|
||||
root _root: Dynamic,
|
||||
context _context: Dynamic,
|
||||
subscribe _subscribe: Bool,
|
||||
callback _callback: fn(Dynamic, Option(fn() -> Nil)) -> Nil,
|
||||
) -> Nil {
|
||||
Nil
|
||||
}
|
||||
|
||||
/// Capture "context-request" events querying a particular context type.
|
||||
/// Intended to be used in a context provider, on a child element near the
|
||||
/// component root.
|
||||
pub fn on_context_request(
|
||||
context match_context: Dynamic,
|
||||
handler handler: fn(Dynamic, Bool) -> msg,
|
||||
) -> Attribute(msg) {
|
||||
event.advanced("context-request", {
|
||||
use ev_context <- decode.field("context", decode.dynamic)
|
||||
use subscribe <- decode.field("subscribe", decode.bool)
|
||||
use callback <- decode.field("callback", decode.dynamic)
|
||||
case ev_context == match_context {
|
||||
True -> {
|
||||
decode.success(event.handler(
|
||||
handler(callback, subscribe),
|
||||
False,
|
||||
ev_context == match_context,
|
||||
))
|
||||
}
|
||||
False ->
|
||||
// returning a DecodeError seems like the only way to not trigger a message dispatch
|
||||
decode.failure(
|
||||
event.handler(handler(callback, subscribe), False, False),
|
||||
"Context",
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Effect for passing context value back to consumers who have requested it.
|
||||
pub fn update_consumers(
|
||||
callbacks callbacks: List(Dynamic),
|
||||
value value: Dynamic,
|
||||
) -> Effect(msg) {
|
||||
use _dispatch <- effect.from
|
||||
do_update_consumers(callbacks:, value:)
|
||||
}
|
||||
|
||||
fn do_update_consumers(
|
||||
callbacks callbacks: List(Dynamic),
|
||||
value value: Dynamic,
|
||||
) -> Nil {
|
||||
case callbacks {
|
||||
[] -> Nil
|
||||
[cb, ..rest] -> {
|
||||
call_callback(callback: cb, value:)
|
||||
do_update_consumers(rest, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@external(javascript, "./context.ffi.mjs", "callCallback")
|
||||
fn call_callback(callback _callback: Dynamic, value _value: Dynamic) -> Nil {
|
||||
Nil
|
||||
}
|
||||
66
webc/src/viewer_controller_component.gleam
Normal file
66
webc/src/viewer_controller_component.gleam
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import gleam/dynamic.{type Dynamic}
|
||||
import lustre.{type App}
|
||||
import lustre/component
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
|
||||
import context
|
||||
|
||||
pub const name: String = "viewer-controller"
|
||||
|
||||
pub fn component() -> App(Nil, Model, Msg) {
|
||||
lustre.component(init, update, view, [
|
||||
component.on_attribute_change("root-path", fn(value) {
|
||||
ParentChangedRootPath(value) |> Ok
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
pub type Model {
|
||||
Model(root_path: String, root_path_consumers: List(Dynamic))
|
||||
}
|
||||
|
||||
fn init(_) -> #(Model, Effect(Msg)) {
|
||||
#(Model(root_path: "", root_path_consumers: []), effect.none())
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
ParentChangedRootPath(String)
|
||||
ChildRequestedRootPath(Dynamic, Bool)
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
case msg {
|
||||
ParentChangedRootPath(root_path) -> #(
|
||||
Model(..model, root_path:),
|
||||
context.update_consumers(
|
||||
model.root_path_consumers,
|
||||
dynamic.string(root_path),
|
||||
),
|
||||
)
|
||||
ChildRequestedRootPath(callback, subscribe) -> #(
|
||||
case subscribe {
|
||||
True ->
|
||||
Model(..model, root_path_consumers: [
|
||||
callback,
|
||||
..model.root_path_consumers
|
||||
])
|
||||
False -> model
|
||||
},
|
||||
context.update_consumers([callback], dynamic.string(model.root_path)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn view(_: Model) -> Element(Msg) {
|
||||
html.div(
|
||||
[
|
||||
context.on_context_request(
|
||||
dynamic.string("root_path"),
|
||||
ChildRequestedRootPath,
|
||||
),
|
||||
],
|
||||
[component.default_slot([], [])],
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue