Skip to content

Commit

Permalink
Merge pull request #13 from floodfx/keybindings
Browse files Browse the repository at this point in the history
Keybindings
  • Loading branch information
floodfx authored Feb 8, 2022

Verified

This commit was signed with the committer’s verified signature.
booc0mtaco Holloway
2 parents f407287 + 5c2f636 commit fa00b67
Showing 6 changed files with 76 additions and 24 deletions.
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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";
38 changes: 23 additions & 15 deletions src/examples/light_liveview.ts
Original file line number Diff line number Diff line change
@@ -6,57 +6,65 @@ 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<LightContext, never> implements
LiveViewComponent<LightContext, never>,
LiveViewExternalEventListener<LightContext, LightEvent, never> {
LiveViewExternalEventListener<LightContext, LightEvent, { key: string }> {


mount(params: LiveViewMountParams, session: Partial<SessionData>, socket: LiveViewSocket<LightContext>) {
return { brightness: 10 };
};

render(context: LightContext) {
const { brightness } = context;
return html`
<div id="light">
<h1>Front Porch Light </h1>
<div class="meter">
<div>${context.brightness}%</div><progress id="light_level" value="${context.brightness}" max="100"></progress>
<h1>Front Porch Light</h1>
<div>
<div>${brightness}%</div>
<progress id="light_meter" style="width: 300px; height: 2em; opacity: ${brightness / 100}" value="${brightness}" max="100"></progress>
</div>
<button phx-click="off">
Off
<button phx-click="off" phx-window-keydown="key_update" phx-key="ArrowLeft">
⬅️ Off
</button>
<button phx-click="down">
Down
<button phx-click="down" phx-window-keydown="key_update" phx-key="ArrowDown">
⬇️ Down
</button>
<button phx-click="up">
Up
<button phx-click="up" phx-window-keydown="key_update" phx-key="ArrowUp">
⬆️ Up
</button>
<button phx-click="on">
On
<button phx-click="on" phx-window-keydown="key_update" phx-key="ArrowRight">
➡️ On
</button>
</div>
`
};

handleEvent(event: LightEvent, params: never, socket: LiveViewSocket<LightContext>) {
handleEvent(event: LightEvent, params: { key: string }, socket: LiveViewSocket<LightContext>) {
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;
}
2 changes: 1 addition & 1 deletion src/examples/routeDetails.ts
Original file line number Diff line number Diff line change
@@ -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",
6 changes: 4 additions & 2 deletions src/server/socket/component_manager.ts
Original file line number Diff line number Diff line change
@@ -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<PhxClickPayload | PhxFormPayload>) {
onEvent(ws: WebSocket, message: PhxIncomingMessage<PhxClickPayload | PhxFormPayload | PhxKeyUpPayload | PhxKeyDownPayload>) {
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)) {
5 changes: 3 additions & 2 deletions src/server/socket/message_router.ts
Original file line number Diff line number Diff line change
@@ -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<PhxClickPayload | PhxFormPayload>);
const message = rawPhxMessage as PhxIncomingMessage<PhxClickPayload | PhxFormPayload | PhxKeyDownPayload | PhxKeyUpPayload>;
componentManager.onEvent(ws, message);
} else {
console.log("expected component manager for topic", topic);
}
11 changes: 8 additions & 3 deletions src/server/socket/types.ts
Original file line number Diff line number Diff line change
@@ -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<PhxClickPayload>
export type PhxFormEvent = PhxIncomingMessage<PhxFormPayload>
// 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<unknown>, payload: any): PhxReply => {

0 comments on commit fa00b67

Please sign in to comment.