{"id":17179638,"url":"https://github.com/maxmcd/cave","last_synced_at":"2025-09-08T08:34:10.760Z","repository":{"id":54875574,"uuid":"271922526","full_name":"maxmcd/cave","owner":"maxmcd","description":null,"archived":false,"fork":false,"pushed_at":"2023-05-05T02:24:52.000Z","size":90,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-09-01T00:51:26.774Z","etag":null,"topics":["go","live-view","reactive"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maxmcd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-13T01:53:55.000Z","updated_at":"2021-01-22T02:19:29.000Z","dependencies_parsed_at":"2024-06-21T02:04:36.376Z","dependency_job_id":"bee7f5c6-c394-4461-a968-a94bb5e3604e","html_url":"https://github.com/maxmcd/cave","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/maxmcd/cave","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxmcd%2Fcave","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxmcd%2Fcave/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxmcd%2Fcave/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxmcd%2Fcave/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maxmcd","download_url":"https://codeload.github.com/maxmcd/cave/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxmcd%2Fcave/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274158215,"owners_count":25232563,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-08T02:00:09.813Z","response_time":121,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","live-view","reactive"],"created_at":"2024-10-15T00:27:02.403Z","updated_at":"2025-09-08T08:34:10.701Z","avatar_url":"https://github.com/maxmcd.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cave\n\nCave allows you to write fully functional interactive web applications with Go. You describe your templates and components in your Go program and Cave takes care of rendering your UI to the browser and pushing updates from user changes.\n\nCave is similar to [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) although it was originally inspired by [Dash](https://plotly.com/dash/).\n\n*Cave is Alpha software and incomplete. Don't take it seriously!*\n\n## A Minimal Example\n\nLet's walk through a minimal example to get a feel for the structure of Cave.\n\nHere's a simple component:\n\n```go\ntype SimpleComponent struct {\n\tCount    int\n}\n\nvar _ cave.Renderer = new(ToDoApp) // this just ensures that we are implementing this interface\n\nfunc (tda *SimpleComponent) Render() string {\n\treturn `\u003cdiv\u003e{{ .Count }}\u003c/div\u003e`\n}\n```\n\nThis component implements the `cave.Renderer` interface, which is the minimum requirement for a component. When rendering it simply outputs a div with a count value.\n\nYou can't do much with this component in its current form, but this is one of our core building blocks.\n\n## Something More Complicated\n\nHere's the cave version of a ToDo list app. It has a form input and adds items to the list when the form is submitted.\n\n*If you just want to play around with the final result you [can try it out here](https://cave-demo.fly.dev/). The full source is also [here](./examples/to-do).*\n\n```go\ntype ToDoApp struct {\n\tItems    []string\n\tToDoList *ToDoList\n}\n\nvar (\n\t_ cave.OnSubmiter = new(ToDoApp)\n\t_ cave.Renderer   = new(ToDoApp)\n)\n\nfunc (tda *ToDoApp) OnSubmit(name string, form map[string]string) {\n\tif name == \"todo\" {\n\t\ttda.Items = append(tda.Items, form[\"new\"])\n\t}\n}\nfunc (tda *ToDoApp) Render() string {\n\treturn `\n\t\u003cdiv\u003e\n\t\u003ch3\u003eTODO\u003c/h3\u003e\n\t{{ render .ToDoList }}\n\t\u003cform cave-submit=todo\u003e\n\t  \u003clabel for=\"new-todo\"\u003e\n\t\tWhat needs to be done?\n\t  \u003c/label\u003e\n\t  \u003cinput type=\"text\" name=\"new\" /\u003e\n\t  \u003cbutton\u003e\n\t\tAdd {{ len .Items | add 1 }}\n\t  \u003c/button\u003e\n\t\u003c/form\u003e\n  \u003c/div\u003e\n`\n}\n\ntype ToDoList struct {\n\tToDoApp *ToDoApp\n}\n\nvar _ cave.Renderer = new(ToDoList)\n\nfunc (tdl *ToDoList) Render() string {\n\treturn `\n\t\u003cul\u003e\n\t {{range .ToDoApp.Items }}\n\t \t\u003cli\u003e{{.}}\u003c/li\u003e\n\t {{end}}\n\t\u003c/ul\u003e\n`\n}\n```\n\nWe've introduced two things. The `cave.OnSubmiter` interface, and the curious tag `cave-submit`. Both of these things work together! When this component is rendered in the browser, Cave listens for submit events on our form and when they are made it shoots the form details over a websocket. The server then call `OnSubmit` on this component, computes the resulting changes in the HTML and shoots them back over a websocket. Outrageous!\n\n## The Nitty Gritty\n\nNow you're probably thinking \"Rendering basic UI changes by pushing bytes over thousands of miles, count me in! How do I plug this thing into a server and start adding latency to my user experiences?\". Well let's show you how!\n\nNow that we have components we'll need to hook them up to a web server.\n\nLet's create a new Cave, we'll call it cavern because that's cute. Let's put a layout and a component in our cavern.\n```go\ncavern := cave.New()\nif err := cavern.AddTemplateFile(\"main\", \"layout.html\"); err != nil {\n\tlog.Fatal(err)\n}\ncavern.AddComponent(\"main\", NewToDoApp)\n```\n\n`AddComponent` takes a `func() cave.Renderer` so that it can create a new component for every request. So we'll need to set up that function as well.\n\n```go\nfunc NewToDoApp() cave.Renderer {\n\ttda := \u0026ToDoApp{Items: []string{\"breathe\"}}\n\ttld := \u0026ToDoList{}\n\ttda.ToDoList = tld\n\ttld.ToDoApp = tda\n\treturn tda\n}\n```\n\nLayouts are html pages that render the html boilerplate we'll need outside of our components. A minimal example would be this:\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\n    \u003chead\u003e\u003ctitle\u003eHello\u003c/title\u003e\u003c/head\u003e\n    \u003cbody\u003e\n        {{ component \"main\" }}\n        \u003cscript src=\"/bundle.js\" type=\"application/javascript\"\u003e\u003c/script\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\n```\nWe need to load the javascript bundle that contains all the Cave goodies, and we need to mount the component \"main\" right where we want it.\n\nNext we'll need to actually serve the page. I'm going to use [gin](https://github.com/gin-gonic/gin), but you could technically use anything that uses `http.ResponseWriter` and `*http.Response`.\n\n```go\nr.Use(func(c *gin.Context) {\n\tif _, ok := c.Request.URL.Query()[\"cavews\"]; ok {\n\t\tcavern.ServeWS(c.Writer, c.Request)\n\t\tc.Abort()\n\t}\n})\nr.GET(\"/\", func(c *gin.Context) {\n\tc.Writer.Header().Add(\"Content-Type\", \"text/html\")\n\t_ = cavern.Render(\"main\", c.Writer)\n})\nr.GET(\"/bundle.js\", func(c *gin.Context) {\n\tcavern.ServeJS(c.Writer, c.Request)\n})\n```\n\nA few things going on here:\n\n1. We intercept all request with the query param `cavews` and assume they are websocket requests intended for cave.\n2. We render out \"main\" layout at the root path.\n3. We serve our bundle where out layout expects it.\n\nThat's it! Everything else is websocket magic, crazy hacks, and the strange feeling that we're making progress technically while regressing at the same time.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxmcd%2Fcave","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaxmcd%2Fcave","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxmcd%2Fcave/lists"}