Skip to content

Commit 23f9ef1

Browse files
authored
Subcomponents (#1)
* adding subcomponents * subcomponent submit working * cave-click * node tests
1 parent 8306ebb commit 23f9ef1

File tree

21 files changed

+877
-311
lines changed

21 files changed

+877
-311
lines changed

.github/workflows/tests.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Tests
2+
on: push
3+
jobs:
4+
test-js:
5+
name: test-js
6+
runs-on: ubuntu-latest
7+
steps:
8+
- uses: actions/checkout@v2
9+
- uses: actions/setup-node@v2
10+
with:
11+
node-version: "12"
12+
- uses: nanasess/setup-chromedriver@master
13+
- run: cd cave-js && npm install
14+
- run: cd cave-js && make test
15+
test-go:
16+
name: test-go
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Install Go
20+
uses: actions/setup-go@v2
21+
- name: Checkout code
22+
uses: actions/checkout@v2
23+
- name: Run all tests
24+
run: go test -v ./...

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
*.wasm
12
.vscode

Makefile

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
run_to_do_example: generate
55
cd examples/to-do && go run .
66

7+
.PHONY: run_connect4_example
8+
run_connect4_example: generate
9+
cd examples/connect4 && go run .
710

811
.PHONY: ./cave-js/bundle.js
912
./cave-js/bundle.js:
@@ -22,3 +25,7 @@ demos_push: demos_build
2225

2326
demos_deploy: demos_push
2427
cd cmd/cave-demos && fly deploy -v
28+
29+
.PHONY: test
30+
test:
31+
go test -cover ./...

bundle.go

+2-2
Large diffs are not rendered by default.

cave-gin/cave-gin.go

-44
Original file line numberDiff line numberDiff line change
@@ -1,45 +1 @@
11
package cavegin
2-
3-
import (
4-
"net/http"
5-
6-
"github.com/gin-gonic/gin"
7-
"github.com/gin-gonic/gin/render"
8-
"github.com/maxmcd/cave"
9-
)
10-
11-
func MakeRender(cavern *Cave, renderer cave.Renderer) render.Render {
12-
return &Renderer{
13-
renderer: renderer,
14-
cavern: cavern,
15-
}
16-
}
17-
18-
type Renderer struct {
19-
renderer cave.Renderer
20-
cavern *Cave
21-
}
22-
23-
func (r *Renderer) Render(w http.ResponseWriter) error {
24-
return r.cavern.Render(r.renderer, w)
25-
}
26-
27-
func (r *Renderer) WriteContentType(w http.ResponseWriter) {
28-
w.Header().Add("Content-Type", "text/html")
29-
}
30-
31-
type Cave struct {
32-
cave.Cave
33-
}
34-
35-
func (hc *Cave) Handler(rendererFunc func() cave.Renderer) gin.HandlerFunc {
36-
return func(c *gin.Context) {
37-
c.Render(200, MakeRender(hc, rendererFunc()))
38-
}
39-
}
40-
41-
func New() *Cave {
42-
return &Cave{
43-
Cave: cave.Cave{},
44-
}
45-
}

cave-js/src/messages.ts

+52-26
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,80 @@
1-
export type WebsocketMessage = [string, string, Array<any>];
1+
export type WebsocketMessage = [string, Array<any>, string];
22
export enum MessageType {
33
Patch = "p",
44
Init = "init",
55
Error = "error",
6+
Submit = "submit",
7+
Click = "click",
68
}
79

810
export class ClientMessage {
9-
componentID: string;
10-
event: string;
11-
name: string;
11+
type: MessageType;
1212
data: Array<any>;
13+
componentID?: string;
14+
name?: string;
15+
subcomponentID?: string;
1316
constructor(
14-
componentID: string,
15-
event: string,
16-
name: string,
17-
data: Array<any>
17+
type: MessageType,
18+
data: Array<any>,
19+
componentID?: string,
20+
name?: string,
21+
subcomponentID?: string
1822
) {
23+
this.type = type;
24+
this.data = data;
1925
this.componentID = componentID;
20-
this.event = event;
2126
this.name = name;
22-
this.data = data;
23-
}
24-
serialize(): string {
25-
return JSON.stringify([this.componentID, this.event, this.name, this.data]);
27+
this.subcomponentID = subcomponentID;
2628
}
27-
}
28-
29-
export class ServerMessage {
30-
componentID: string;
31-
event: string;
32-
data: Array<any>;
33-
constructor(input: WebsocketMessage) {
34-
this.componentID = input[0];
35-
this.event = input[1];
36-
this.data = input[2];
29+
prepare(): Array<any> {
30+
let out: Array<any> = [this.type, this.data];
31+
if (!this.componentID) {
32+
return out;
33+
}
34+
out.push(this.componentID);
35+
if (!this.name) {
36+
return out;
37+
}
38+
out.push(this.name);
39+
if (!this.subcomponentID) {
40+
return out;
41+
}
42+
out.push(this.subcomponentID);
43+
return out;
3744
}
3845
serialize(): string {
39-
return JSON.stringify([this.componentID, this.event, this.data]);
46+
return JSON.stringify(this.prepare());
4047
}
4148
}
4249

4350
export class SubmitMessage extends ClientMessage {
4451
data: [string, Record<string, string>];
45-
constructor(componentID: string, name: string, form: HTMLFormElement) {
52+
constructor(
53+
componentID: string,
54+
name: string,
55+
form: HTMLFormElement,
56+
subcomponentID?: string
57+
) {
4658
let formData = new FormData(form);
4759
let formDataMap: Record<string, string> = {};
4860
formData.forEach((v, k) => {
4961
// TODO: file support
5062
formDataMap[k] = v.toString();
5163
});
52-
super(componentID, "submit", name, [formDataMap]);
64+
super(MessageType.Submit, [formDataMap], componentID, name, subcomponentID);
65+
}
66+
}
67+
68+
export class ServerMessage {
69+
componentID: string;
70+
event: string;
71+
data: Array<any>;
72+
constructor(input: WebsocketMessage) {
73+
this.event = input[0];
74+
this.data = input[1];
75+
this.componentID = input[2];
76+
}
77+
serialize(): string {
78+
return JSON.stringify([this.componentID, this.event, this.data]);
5379
}
5480
}

cave-js/src/websocket.ts

+67-27
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
export class WebsocketHandler {
1212
websocket: WebSocket;
1313
constructor() {
14+
this.caveSubmitListener = this.caveSubmitListener.bind(this);
15+
this.caveClickListener = this.caveClickListener.bind(this);
1416
this.addEventListeners();
1517
this.websocket = new WebSocket(
1618
`${wsScheme()}://${window.location.host}${
@@ -30,51 +32,89 @@ export class WebsocketHandler {
3032
componentIDs.push(val);
3133
}
3234
}
33-
this.websocket.send(
34-
new ClientMessage("", MessageType.Init, "", componentIDs).serialize()
35-
);
35+
this.send(new ClientMessage(MessageType.Init, componentIDs).serialize());
36+
}
37+
send(data: any): void {
38+
console.log("sending msg to server", data);
39+
return this.websocket.send(data);
3640
}
3741
onmessage(e: MessageEvent): any {
3842
console.log(e.data);
3943
let msg = new ServerMessage(JSON.parse(e.data));
44+
console.log(msg);
4045
if (msg.event === MessageType.Patch) {
4146
let patches: Array<Patch> = msg.data.map((e) => diff.expandPatch(e));
42-
// is this what we want? is cloning expensive here?
43-
let node = document.querySelector("[cave-component]")?.cloneNode(true);
44-
diff.apply(node, patches);
45-
morphdom(document.querySelector("[cave-component]"), node);
47+
diff.apply(document.querySelector("[cave-component]"), patches);
48+
this.addEventListeners();
4649
}
4750
if (msg.event === MessageType.Error) {
4851
throw new Error("Server error: " + msg.data[0]);
4952
}
5053
}
5154

5255
addEventListeners() {
56+
// TODO: only add listeners to new code?
57+
// how expensive is this?
5358
console.log("adding event listeners");
5459
document.querySelectorAll("[cave-submit]").forEach((elem) => {
55-
let name = elem.getAttribute("cave-submit");
56-
if (!name) {
57-
// attributes must have a value
58-
return;
59-
}
60-
elem.addEventListener("submit", this.caveSubmitListener(<string>name));
60+
elem.addEventListener("submit", this.caveSubmitListener);
6161
});
62+
document.querySelectorAll("[cave-click]").forEach((elem) => {
63+
elem.addEventListener("click", this.caveClickListener);
64+
});
65+
}
66+
findElementContext(
67+
element: HTMLElement
68+
): [string | undefined, string | undefined] {
69+
let componentID =
70+
element.closest("[cave-component]")?.getAttribute("cave-component") ||
71+
undefined;
72+
let subcomponentID =
73+
element
74+
.closest("[cave-subcomponent]")
75+
?.getAttribute("cave-subcomponent") || undefined;
76+
return [componentID, subcomponentID];
6277
}
78+
caveClickListener(e: Event): void {
79+
let element = <HTMLElement>e.target;
80+
const [componentID, subcomponentID] = this.findElementContext(element);
81+
if (!componentID) {
82+
// if we're not in a cave component we shouldn't act
83+
return;
84+
}
85+
e.preventDefault();
86+
let name =
87+
element.getAttribute("cave-click") ||
88+
element.closest("[cave-click]")?.getAttribute("cave-click") ||
89+
undefined;
90+
this.send(
91+
new ClientMessage(
92+
MessageType.Click,
93+
[],
94+
componentID,
95+
name,
96+
subcomponentID
97+
).serialize()
98+
);
99+
}
100+
caveSubmitListener(e: Event): void {
101+
console.log("got submit", e, this);
102+
let formElement = <HTMLFormElement>e.target;
63103

64-
caveSubmitListener(name: string): (e: Event) => void {
65-
return (e) => {
66-
let formElement = <HTMLFormElement>e.currentTarget;
67-
let componentID = formElement
68-
.closest("[cave-component]")
69-
?.getAttribute("cave-component");
70-
if (!componentID) {
71-
// if we're not in a cave component we shouldn't act
72-
return;
73-
}
74-
e.preventDefault();
75-
let formMessage = new SubmitMessage(componentID, name, formElement);
76-
this.websocket.send(formMessage.serialize());
77-
};
104+
let name = formElement.getAttribute("cave-submit");
105+
const [componentID, subcomponentID] = this.findElementContext(formElement);
106+
if (!componentID) {
107+
// if we're not in a cave component we shouldn't act
108+
return;
109+
}
110+
e.preventDefault();
111+
let formMessage = new SubmitMessage(
112+
componentID,
113+
name || "",
114+
formElement,
115+
subcomponentID
116+
);
117+
this.send(formMessage.serialize());
78118
}
79119
}
80120

0 commit comments

Comments
 (0)