Skip to content

Commit

Permalink
update gleam version
Browse files Browse the repository at this point in the history
add additional native html event types including form support
change event payload from optional string to dynamic
refactor events api
add tests and test helpers
  • Loading branch information
eliknebel committed Nov 3, 2024
1 parent df8a904 commit 623ea0a
Show file tree
Hide file tree
Showing 15 changed files with 752 additions and 74 deletions.
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
gleam 1.4.1
gleam 1.5.1
nodejs 19.7.0
erlang 26.0.2
48 changes: 45 additions & 3 deletions client/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ export type EventIdentifier = {
id: string;
};

export type EventHandlerProvider = (events: EventIdentifier[]) => On;
export type EventHandlerProvider = (
tag: string,
events: EventIdentifier[]
) => On;

export const initEventHandlerProvider =
(socket: WebSocket): EventHandlerProvider =>
(events: EventIdentifier[]) =>
(el, events: EventIdentifier[]) =>
events.reduce((acc, { kind, id }) => {
const handler = (e) => {
socket.send(
JSON.stringify(["event", { id, kind, value: e.target.value }])
JSON.stringify(["event", { id, kind, value: valueForEvent(e) }])
);
};

Expand All @@ -22,3 +25,42 @@ export const initEventHandlerProvider =
[kind]: handler,
};
}, {});

const valueForEvent = (e) => {
if (e instanceof InputEvent || e instanceof PointerEvent) {
return {
target: {
value: (e.target as any).value,
},
};
}

if (e instanceof MouseEvent) {
return {
clientX: e.clientX,
clientY: e.clientY,
};
}

if (e instanceof KeyboardEvent) {
return {
key: e.key,
};
}

if (e instanceof SubmitEvent) {
// prevent the default form submission and page reload
e.preventDefault();

const formData = {};
new FormData(e.target as HTMLFormElement).forEach(
(value, key) => (formData[key] = value)
);

return {
formData: formData,
};
}

return null;
};
2 changes: 1 addition & 1 deletion client/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function renderElement(element: Element, providers: Providers): VNode {

// wire up event handlers
if (element.events.length > 0) {
data.on = eventHandlerProvider(element.events);
data.on = eventHandlerProvider(element.tag, element.events);
}

return h(
Expand Down
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal_modules = [
"sprocket/render",
"sprocket/runtime",
]
gleam = ">= 1.0.0"
gleam = ">= 1.1.0"

[dependencies]
gleam_stdlib = "~> 0.39"
Expand Down
4 changes: 2 additions & 2 deletions src/sprocket.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub fn new(

type Payload {
JoinPayload(csrf_token: String, initial_props: Option(Dict(String, String)))
EventPayload(kind: String, id: String, value: Option(String))
EventPayload(kind: String, id: String, value: Dynamic)
HookEventPayload(id: String, event: String, payload: Option(Dynamic))
EmptyPayload(nothing: Option(String))
}
Expand Down Expand Up @@ -215,7 +215,7 @@ fn decode_event(data: Dynamic) {
EventPayload,
field("kind", dynamic.string),
field("id", dynamic.string),
optional_field("value", dynamic.string),
field("value", dynamic.dynamic),
),
)
}
Expand Down
12 changes: 2 additions & 10 deletions src/sprocket/context.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,10 @@ import sprocket/internal/utils/ordered_map.{type OrderedMap}
import sprocket/internal/utils/unique.{type Unique}

pub type HandlerFn =
fn(Option(CallbackParam)) -> Nil

pub type CallbackParam {
CallbackString(value: String)
}

pub fn callback_param_from_string(value: String) -> CallbackParam {
CallbackString(value)
}
fn(Dynamic) -> Nil

pub type IdentifiableHandler {
IdentifiableHandler(id: Unique, handler_fn: HandlerFn)
IdentifiableHandler(id: Unique, handler: HandlerFn)
}

pub type Attribute {
Expand Down
7 changes: 6 additions & 1 deletion src/sprocket/hooks.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import sprocket/context.{
import sprocket/internal/constants.{call_timeout}
import sprocket/internal/exceptions.{throw_on_unexpected_hook_result}
import sprocket/internal/logger
import sprocket/internal/utils/unique
import sprocket/internal/utils/unique.{type Unique}
import sprocket/internal/utils/unsafe_coerce.{unsafe_coerce}

/// Callback Hook
Expand Down Expand Up @@ -75,6 +75,11 @@ fn maybe_trigger_update(
}
}

/// Client hook attribute that can be used to reference a client hook by its id.
pub fn client_hook(id: Unique, name: String) -> Attribute {
ClientHook(id, name)
}

/// Client Hook
/// -----------
/// Creates a client hook that can be used to facilitate communication with a client
Expand Down
29 changes: 1 addition & 28 deletions src/sprocket/html/attributes.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,12 @@ import gleam/dynamic
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string_builder
import sprocket/context.{
type Attribute, type IdentifiableHandler, Attribute, ClientHook, Event,
}
import sprocket/internal/utils/unique.{type Unique}
import sprocket/context.{type Attribute, Attribute}

pub fn attribute(name: String, value: any) -> Attribute {
Attribute(name, dynamic.from(value))
}

pub fn event(name: String, handler: IdentifiableHandler) -> Attribute {
Event(name, handler)
}

pub fn client_hook(id: Unique, name: String) -> Attribute {
ClientHook(id, name)
}

pub fn on_click(handler: IdentifiableHandler) -> Attribute {
event("click", handler)
}

pub fn on_doubleclick(handler: IdentifiableHandler) -> Attribute {
event("doubleclick", handler)
}

pub fn on_change(handler: IdentifiableHandler) -> Attribute {
event("change", handler)
}

pub fn on_input(handler: IdentifiableHandler) -> Attribute {
event("input", handler)
}

pub fn media(value: String) -> Attribute {
attribute("media", value)
}
Expand Down
121 changes: 121 additions & 0 deletions src/sprocket/html/events.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import gleam/dict.{type Dict}
import gleam/dynamic.{type DecodeError, type Dynamic}
import gleam/result
import sprocket/context.{
type Attribute, type IdentifiableHandler, Attribute, Event,
}

// Events

pub fn event(name: String, handler: IdentifiableHandler) -> Attribute {
Event(name, handler)
}

pub fn on_blur(handler: IdentifiableHandler) -> Attribute {
event("blur", handler)
}

pub fn on_change(handler: IdentifiableHandler) -> Attribute {
event("change", handler)
}

pub fn on_check(handler: IdentifiableHandler) -> Attribute {
event("check", handler)
}

pub fn on_click(handler: IdentifiableHandler) -> Attribute {
event("click", handler)
}

pub fn on_doubleclick(handler: IdentifiableHandler) -> Attribute {
event("doubleclick", handler)
}

pub fn on_focus(handler: IdentifiableHandler) -> Attribute {
event("focus", handler)
}

pub fn on_input(handler: IdentifiableHandler) -> Attribute {
event("input", handler)
}

pub fn on_keydown(handler: IdentifiableHandler) -> Attribute {
event("keydown", handler)
}

pub fn on_keypress(handler: IdentifiableHandler) -> Attribute {
event("keypress", handler)
}

pub fn on_keyup(handler: IdentifiableHandler) -> Attribute {
event("keyup", handler)
}

pub fn on_mousedown(handler: IdentifiableHandler) -> Attribute {
event("mousedown", handler)
}

pub fn on_mouseenter(handler: IdentifiableHandler) -> Attribute {
event("mouseenter", handler)
}

pub fn on_mouseleave(handler: IdentifiableHandler) -> Attribute {
event("mouseleave", handler)
}

pub fn on_mousemove(handler: IdentifiableHandler) -> Attribute {
event("mousemove", handler)
}

pub fn on_mouseout(handler: IdentifiableHandler) -> Attribute {
event("mouseout", handler)
}

pub fn on_mouseover(handler: IdentifiableHandler) -> Attribute {
event("mouseover", handler)
}

pub fn on_mouseup(handler: IdentifiableHandler) -> Attribute {
event("mouseup", handler)
}

pub fn on_submit(handler: IdentifiableHandler) -> Attribute {
event("submit", handler)
}

// Decoders used to extract values from events

///
/// Decode the value from an event `event.target.value`.
pub fn decode_value(event: Dynamic) -> Result(String, List(DecodeError)) {
event
|> dynamic.field("target", dynamic.field("value", dynamic.string))
}

// Decode the checked state from an event `event.target.checked`.
pub fn decode_checked(event: Dynamic) -> Result(Bool, List(DecodeError)) {
event
|> dynamic.field("target", dynamic.field("checked", dynamic.bool))
}

/// Decode the mouse position from any event that has a `clientX` and `clientY`.
pub fn decode_mouse_position(
event: Dynamic,
) -> Result(#(Int, Int), List(DecodeError)) {
use x <- result.then(dynamic.field("clientX", dynamic.int)(event))
use y <- result.then(dynamic.field("clientY", dynamic.int)(event))

Ok(#(x, y))
}

/// Decode the form data from a form submit event.
pub fn decode_form_data(
event: Dynamic,
) -> Result(Dict(String, String), List(DecodeError)) {
dynamic.field("formData", dynamic.dict(dynamic.string, dynamic.string))(event)
}

/// Decode the key from a key press event.
pub fn decode_keypress(event: Dynamic) -> Result(String, List(DecodeError)) {
event |> dynamic.field("key", dynamic.string)
}
18 changes: 7 additions & 11 deletions src/sprocket/runtime.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import sprocket/context.{
type EffectResult, type Element, type Hook, type HookDependencies,
type IdentifiableHandler, type Updater, Callback, Changed, Client, Context,
Dispatcher, Effect, EffectResult, Handler, IdentifiableHandler, Memo, Reducer,
Unchanged, Updater, callback_param_from_string, compare_deps,
Unchanged, Updater, compare_deps,
}
import sprocket/internal/constants.{call_timeout}
import sprocket/internal/exceptions.{throw_on_unexpected_hook_result}
Expand Down Expand Up @@ -52,11 +52,11 @@ pub opaque type State {
pub opaque type Message {
Shutdown
GetReconciled(reply_with: Subject(Option(ReconciledElement)))
ProcessEvent(id: String, payload: Option(String))
ProcessEvent(id: String, payload: Dynamic)
ProcessEventImmediate(
reply_with: Subject(Result(Nil, Nil)),
id: String,
payload: Option(String),
payload: Dynamic,
)
ProcessClientHook(
id: String,
Expand Down Expand Up @@ -99,9 +99,7 @@ fn handle_message(message: Message, state: State) -> actor.Next(Message, State)
case handler {
Ok(context.IdentifiableHandler(_, handler_fn)) -> {
// call the event handler function
payload
|> option.map(callback_param_from_string)
|> handler_fn()
handler_fn(payload)

actor.continue(state)
}
Expand All @@ -123,9 +121,7 @@ fn handle_message(message: Message, state: State) -> actor.Next(Message, State)
case handler {
Ok(context.IdentifiableHandler(_, handler_fn)) -> {
// call the event handler function
payload
|> option.map(callback_param_from_string)
|> handler_fn()
handler_fn(payload)

actor.send(reply_with, Ok(Nil))

Expand Down Expand Up @@ -323,7 +319,7 @@ pub fn get_reconciled(actor) {
}

/// Get the event handler for a given id
pub fn process_event(actor, id: String, payload: Option(String)) {
pub fn process_event(actor, id: String, payload: Dynamic) {
logger.debug("process.try_call ProcessEvent")

actor.send(actor, ProcessEvent(id, payload))
Expand All @@ -332,7 +328,7 @@ pub fn process_event(actor, id: String, payload: Option(String)) {
pub fn process_event_immediate(
actor,
id: String,
payload: Option(String),
payload: Dynamic,
) -> Result(Nil, Nil) {
logger.debug("process.try_call ProcessEventImmediate")

Expand Down
Loading

0 comments on commit 623ea0a

Please sign in to comment.