diff --git a/README.md b/README.md index 4bb3ecbc..fccae936 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,45 @@ This is a port of [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/Phoeni * **Follow component API design (i.e. `mount`, `render` etc), reimplemented with Typescript (so even more type-safe)** - Components in LiveViewJS follow the `mount`, `render`, `handleEvent`, and `handleInfo` API defined in Phoenix. Again, no need to invent a new API. ### Status - **⍺** -This is still in very early PoC territory. You probably shouldn't put this into production just yet. +This is still in ⍺lpha territory. You probably shouldn't put this into production just yet. But side-projects / internal apps could work. 🧱 + +### Implemented Phoenix Bindings +(See [Phoenix Bindings Docs](https://hexdocs.pm/phoenix_live_view/bindings.html) for more details) + +| Binding | Attribute | Implemented | +|-----------------|----------------------|-------------| +| Params | `phx-value-*` | [x] | +| Click Events | `phx-click` | [x] | +| Click Events | `phx-click-away` | [x] | +| Form Events | `phx-change` | [x] | +| Form Events | `phx-submit` | [x] | +| Form Events | `phx-feedback-for` | [ ] | +| Form Events | `phx-disable-with` | [ ] | +| Form Events | `phx-trigger-action` | [ ] | +| Form Events | `phx-auto-recover` | [ ] | +| Focus Events | `phx-blur` | [ ] | +| Focus Events | `phx-focus` | [ ] | +| Focus Events | `phx-window-blur` | [ ] | +| Focus Events | `phx-window-focus` | [ ] | +| Key Events | `phx-keydown` | [x] | +| Key Events | `phx-keyup` | [x] | +| Key Events | `phx-window-keydown` | [x] | +| Key Events | `phx-window-keyup` | [x] | +| Key Events | `phx-key` | [x] | +| DOM Patching | `phx-update` | [ ] | +| DOM Patching | `phx-remove` | [ ] | +| JS Interop | `phx-hook` | [ ] | +| Rate Limiting | `phx-debounce` | [x] | +| Rate Limiting | `phx-throttle` | [x] | +| Static Tracking | `phx-track-static` | [ ] | + + ### Show me some code! ⌨️ + +**Step 0** Install LiveViewJS +`npm i liveviewjs` + **Step 1** Implement a `LiveViewComponent` ```ts import { SessionData } from "express-session"; diff --git a/src/examples/light_liveview.ts b/src/examples/light_liveview.ts index 479ab708..8189b749 100644 --- a/src/examples/light_liveview.ts +++ b/src/examples/light_liveview.ts @@ -6,11 +6,11 @@ export interface LightContext { brightness: number; } -export type LightEvent = "on" | "off" | "up" | "down"; +export type LightEvent = "on" | "off" | "up" | "down" | "key_update"; export class LightLiveViewComponent extends BaseLiveViewComponent implements LiveViewComponent, - LiveViewExternalEventListener { + LiveViewExternalEventListener { mount(params: LiveViewMountParams, session: Partial, socket: LiveViewSocket) { @@ -18,45 +18,53 @@ export class LightLiveViewComponent extends BaseLiveViewComponent -

Front Porch Light

-
-
${context.brightness}%
+

Front Porch Light

+
+
${brightness}%
+
- - - -
` }; - handleEvent(event: LightEvent, params: never, socket: LiveViewSocket) { + handleEvent(event: LightEvent, params: { key: string }, socket: LiveViewSocket) { const ctx: LightContext = { brightness: socket.context.brightness }; - switch (event) { + // map key_update to arrow keys + const lightEvent = event === "key_update" ? params.key : event; + switch (lightEvent) { case 'off': + case 'ArrowLeft': ctx.brightness = 0; break; case 'on': + case 'ArrowRight': ctx.brightness = 100; break; case 'up': + case 'ArrowUp': ctx.brightness = Math.min(ctx.brightness + 10, 100); break; case 'down': + case 'ArrowDown': ctx.brightness = Math.max(ctx.brightness - 10, 0); break; } diff --git a/src/examples/routeDetails.ts b/src/examples/routeDetails.ts index 873b6bec..b95096ff 100644 --- a/src/examples/routeDetails.ts +++ b/src/examples/routeDetails.ts @@ -10,7 +10,7 @@ export const routeDetails: RouteDetails[] = [ label: "Light", path: "/light", summary: "Control the brightness of a porch light using buttons.", - tags: ["phx-click"] + tags: ["phx-click", "phx-window-keydown", "phx-key"] }, { label: "License", diff --git a/src/server/socket/component_manager.ts b/src/server/socket/component_manager.ts index 397e70e9..f856518c 100644 --- a/src/server/socket/component_manager.ts +++ b/src/server/socket/component_manager.ts @@ -1,6 +1,6 @@ import { WebSocket } from "ws"; import { BaseLiveViewComponent, LiveViewComponent, LiveViewSocket, StringPropertyValues } from ".."; -import { newHeartbeatReply, newPhxReply, PhxClickPayload, PhxDiffReply, PhxFormPayload, PhxHeartbeatIncoming, PhxIncomingMessage, PhxJoinIncoming, PhxJoinPayload, PhxLivePatchIncoming, PhxLivePatchPushPayload, PhxOutgoingLivePatchPush, PhxOutgoingMessage, PhxSocketProtocolNames } from "./types"; +import { newHeartbeatReply, newPhxReply, PhxClickPayload, PhxDiffReply, PhxFormPayload, PhxHeartbeatIncoming, PhxIncomingMessage, PhxJoinIncoming, PhxLivePatchIncoming, PhxOutgoingLivePatchPush, PhxOutgoingMessage, PhxKeyDownPayload, PhxKeyUpPayload } from "./types"; import jwt from 'jsonwebtoken'; import { SessionData } from "express-session"; @@ -68,7 +68,7 @@ export class LiveViewComponentManager { this.sendPhxReply(ws, newHeartbeatReply(message)); } - onEvent(ws: WebSocket, message: PhxIncomingMessage) { + onEvent(ws: WebSocket, message: PhxIncomingMessage) { const [joinRef, messageRef, topic, _, payload] = message; const { type, event } = payload; @@ -80,6 +80,8 @@ export class LiveViewComponentManager { } else if (type === "form") { // @ts-ignore - URLSearchParams has an entries method but not typed value = Object.fromEntries(new URLSearchParams(payload.value)) + } else if (type === "keyup" || type === "keydown") { + value = payload.value; } if (isEventHandler(this.component)) { diff --git a/src/server/socket/message_router.ts b/src/server/socket/message_router.ts index 3768b6aa..afa8f0ef 100644 --- a/src/server/socket/message_router.ts +++ b/src/server/socket/message_router.ts @@ -1,4 +1,4 @@ -import { RenderedNode, PhxOutgoingMessage, PhxJoinIncoming, PhxHeartbeatIncoming, PhxIncomingMessage, PhxClickPayload, PhxFormPayload, PhxDiffReply, PhxLivePatchIncoming } from './types'; +import { PhxJoinIncoming, PhxHeartbeatIncoming, PhxIncomingMessage, PhxClickPayload, PhxFormPayload, PhxLivePatchIncoming, PhxKeyDownPayload, PhxKeyUpPayload } from './types'; import WebSocket from 'ws'; import { LiveViewComponent } from '../types'; import { LiveViewRouter } from '../types'; @@ -32,7 +32,8 @@ export class MessageRouter { // lookup component manager for this topic componentManager = this.topicComponentManager[topic]; if (componentManager) { - componentManager.onEvent(ws, rawPhxMessage as PhxIncomingMessage); + const message = rawPhxMessage as PhxIncomingMessage; + componentManager.onEvent(ws, message); } else { console.log("expected component manager for topic", topic); } diff --git a/src/server/socket/types.ts b/src/server/socket/types.ts index a1c5e056..abaf6d73 100644 --- a/src/server/socket/types.ts +++ b/src/server/socket/types.ts @@ -74,9 +74,14 @@ export type PhxClickPayload = PhxEventPayload<"click", { value: { value: string //{"type":"form","event":"update","value":"seats=3&_target=seats","uploads":{}} export type PhxFormPayload = PhxEventPayload<"form", { value: string }> & PhxEventUploads; - -export type PhxClickEvent = PhxIncomingMessage -export type PhxFormEvent = PhxIncomingMessage +// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values +// for all the string values for the key that kicked off the event +//{type: "keyup", event: "key_update", value: {key: "ArrowUp"}} +// {type: "keyup", event: "key_update", value: {key: "ArrowUp", value: ""}} +// {type: "keyup", event: "key_update", value: {key: "ArrowUp", value: "foo"}} +// NOTE: these payloads are the same for phx-window-key* events and phx-key* events +export type PhxKeyUpPayload = PhxEventPayload<"keyup", { value: { key: string, value?: string } }>; +export type PhxKeyDownPayload = PhxEventPayload<"keydown", { value: { key: string, value?: string } }>; export const newPhxReply = (from: PhxIncomingMessage, payload: any): PhxReply => {