{"id":15527563,"url":"https://github.com/elliotchance/pepper","last_synced_at":"2025-07-10T07:05:02.467Z","repository":{"id":48426829,"uuid":"194843759","full_name":"elliotchance/pepper","owner":"elliotchance","description":"🌶️ Create reactive frontends without ever writing frontend code.","archived":false,"fork":false,"pushed_at":"2021-07-26T23:35:54.000Z","size":26,"stargazers_count":112,"open_issues_count":8,"forks_count":6,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-22T01:44:35.332Z","etag":null,"topics":["experimental","frontend","golang"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elliotchance.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-07-02T10:41:08.000Z","updated_at":"2025-04-08T16:58:29.000Z","dependencies_parsed_at":"2022-09-10T23:11:21.850Z","dependency_job_id":null,"html_url":"https://github.com/elliotchance/pepper","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/elliotchance/pepper","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elliotchance%2Fpepper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elliotchance%2Fpepper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elliotchance%2Fpepper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elliotchance%2Fpepper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elliotchance","download_url":"https://codeload.github.com/elliotchance/pepper/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elliotchance%2Fpepper/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264545017,"owners_count":23625387,"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","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":["experimental","frontend","golang"],"created_at":"2024-10-02T11:07:12.388Z","updated_at":"2025-07-10T07:05:02.447Z","avatar_url":"https://github.com/elliotchance.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🌶️ pepper\n\nCreate reactive frontends without ever writing frontend code.\n\n   * [How Does It Work?](#how-does-it-work)\n   * [What Should/Shouldn't I Use It For?](#what-shouldshouldnt-i-use-it-for)\n   * [Handling Offline and Reconnecting](#handling-offline-and-reconnecting)\n   * [Testing](#testing)\n      * [Unit Testing](#unit-testing)\n   * [Examples](#examples)\n      * [#1: A Simple Counter](#1-a-simple-counter)\n      * [#2: Forms](#2-forms)\n      * [#3: Nested Components](#3-nested-components)\n      * [#4: Ticker](#4-ticker)\n\n# How Does It Work?\n\npepper runs a HTTP server that returns a tiny empty HTML page with just a few\nlines of inline javascript. Immediately after the initial page loads it will\nconnect through a websocket.\n\nAll event triggered on the browser will be sent through the websocket where\nstate changes and rerendering occurs. The result is passed back to the websocket\nto update the UI.\n\nAt the moment it returns the whole rendered component. However, this could be\noptimized in the future to only return the differences.\n\n# What Should/Shouldn't I Use It For?\n\npepper requires a constant connection to the server (for the websocket) so it\nwouldn't work for anything that must function offline, or used in cases where\nthe internet is flaky.\n\nI imagine some good use cases for pepper would be:\n\n1. Showing real time data. Streaming metrics, graphs, logs, dashboards, etc.\n2. Apps that rely on a persistent connection. Such as chat clients, timed\ninteractive exams, etc.\n3. Apps that would benefit from persistent state. The entire state can be saved\nor restored into a serialized format like JSON. Great for forms or surveys with\nmany questions/steps.\n4. Prototyping a frontend app. It's super easy to get up and running and iterate\nchanges without setting up a complex environment, build tools and dependencies.\n\n# Handling Offline and Reconnecting\n\nWhen creating the server you may configure how you want disconnects to be\nhandled. For example:\n\n```go\nserver := pepper.NewServer()\nserver.OfflineAction = pepper.OfflineActionDisableForms\n```\n\nBy default clients will try to reconnect every second. This can be changed with\n`server.ReconnectInterval`.\n\n**Important:** When reconnecting the server treats the new request as a new\nclient, so all state on the page will be lost.\n\n# Testing\n\n## Unit Testing\n\nThe `peppertest` package provides tools to make unit testing easier.\n\n`RenderToDocument` renders then parses the component into a `*Document` from the\n[github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery)\npackage:\n\n```go\nimport (\n\t\"github.com/elliotchance/pepper/peppertest\"\n\t\"github.com/stretchr/testify/require\"\n\t\"testing\"\n)\n\nfunc TestPeople_Add(t *testing.T) {\n\tc := \u0026People{\n\t\tNames: []string{\"Jack\", \"Jill\"},\n\t}\n\n\tc.Name = \"Bob\"\n\tc.Add()\n\n\tdoc, err := peppertest.RenderToDocument(c)\n\trequire.NoError(t, err)\n\n\trows := doc.Find(\"tr\")\n\trequire.Equal(t, 3, rows.Length())\n\trequire.Contains(t, rows.Eq(0).Text(), \"Jack\")\n\trequire.Contains(t, rows.Eq(1).Text(), \"Jill\")\n\trequire.Contains(t, rows.Eq(2).Text(), \"Bob\")\n}\n```\n\nEach of the examples in the\n[examples/](https://github.com/elliotchance/pepper/tree/master/examples)\ndirectory include unit tests.\n\n# Examples\n\n## #1: A Simple Counter\n\n```go\npackage main\n\nimport \"github.com/elliotchance/pepper\"\n\ntype Counter struct {\n\tNumber int\n}\n\nfunc (c *Counter) Render() (string, error) {\n\treturn `\n\t\tCounter: {{ .Number }}\n\t\t\u003cbutton @click=\"AddOne\"\u003e+\u003c/button\u003e\n\t`, nil\n}\n\nfunc (c *Counter) AddOne() {\n\tc.Number++\n}\n\nfunc main() {\n\tpanic(pepper.NewServer().Start(func(_ *pepper.Connection) pepper.Component {\n\t\treturn \u0026Counter{}\n\t}))\n}\n```\n\n- The `Render` method returns a `html/template` syntax, or an error.\n- `@click` will trigger `AddOne` to be called when the button is clicked.\n- Any event triggered from the browser will cause the component to rerender\nautomatically.\n\nTry it now:\n\n```bash\ngo get -u github.com/elliotchance/pepper/examples/ex01_counter\nex01_counter\n```\n\nThen open: [http://localhost:8080/](http://localhost:8080/)\n\n## #2: Forms\n\n```go\npackage main\n\nimport (\n\t\"github.com/elliotchance/pepper\"\n\t\"strconv\"\n)\n\ntype People struct {\n\tNames []string\n\tName  string\n}\n\nfunc (c *People) Render() (string, error) {\n\treturn `\n\t\t\u003ctable\u003e\n\t\t\t{{ range $i, $name := .Names }}\n\t\t\t\t\u003ctr\u003e\u003ctd\u003e\n\t\t\t\t\t{{ $name }}\n\t\t\t\t\t\u003cbutton key=\"{{ $i }}\" @click=\"Delete\"\u003eDelete\u003c/button\u003e\n\t\t\t\t\u003c/td\u003e\u003c/tr\u003e\n\t\t\t{{ end }}\n\t\t\u003c/table\u003e\n\t\tAdd name: \u003cinput type=\"text\" @value=\"Name\"\u003e\n\t\t\u003cbutton @click=\"Add\"\u003eAdd\u003c/button\u003e\n\t`, nil\n}\n\nfunc (c *People) Delete(key string) {\n\tindex, _ := strconv.Atoi(key)\n\tc.Names = append(c.Names[:index], c.Names[index+1:]...)\n}\n\nfunc (c *People) Add() {\n\tc.Names = append(c.Names, c.Name)\n\tc.Name = \"\"\n}\n\nfunc main() {\n\tpanic(pepper.NewServer().Start(func(_ *pepper.Connection) pepper.Component {\n\t\treturn \u0026People{\n\t\t\tNames: []string{\"Jack\", \"Jill\"},\n\t\t}\n\t}))\n}\n```\n\n- Any `html/template` syntax will work, including loops with `{{ range }}`.\n- `@value` will cause the `Name` property to be bound with the text box in both\ndirections.\n- Since there are multiple \"Delete\" buttons (one for each person), you should\nspecify a `key`. The `key` is passed as the first argument to the `Delete`\nfunction.\n\nTry it now:\n\n```bash\ngo get -u github.com/elliotchance/pepper/examples/ex02_form\nex02_form\n```\n\nThen open: [http://localhost:8080/](http://localhost:8080/)\n\n\n## #3: Nested Components\n\n```go\ntype Counters struct {\n\tCounters []*Counter\n}\n\nfunc (c *Counters) Render() (string, error) {\n\treturn `\n\t\t\u003ctable\u003e\n\t\t\t{{ range .Counters }}\n\t\t\t\t\u003ctr\u003e\u003ctd\u003e\n\t\t\t\t\t{{ render . }}\n\t\t\t\t\u003c/td\u003e\u003c/tr\u003e\n\t\t\t{{ end }}\n\t\t\t\u003ctr\u003e\u003ctd\u003e\n\t\t\t\tTotal: {{ call .Total }}\n\t\t\t\u003c/td\u003e\u003c/tr\u003e\n\t\t\u003c/table\u003e\n\t`, nil\n}\n\nfunc (c *Counters) Total() int {\n\ttotal := 0\n\tfor _, counter := range c.Counters {\n\t\ttotal += counter.Number\n\t}\n\n\treturn total\n}\n\nfunc main() {\n\tpanic(pepper.NewServer().Start(func(_ *pepper.Connection) pepper.Component {\n\t\treturn \u0026Counters{\n\t\t\tCounters: []*Counter{\n\t\t\t\t{}, {}, {},\n\t\t\t},\n\t\t}\n\t}))\n}\n```\n\n- This example uses three `Counter` components (from Example #1) and includes a\nlive total.\n- Components can be nested with the `render` function. The nested components do\nnot need to be modified in any way.\n- Invoke methods with the `call` function.\n\nTry it now:\n\n```bash\ngo get -u github.com/elliotchance/pepper/examples/ex03_nested\nex03_nested\n```\n\nThen open: [http://localhost:8080/](http://localhost:8080/)\n\n## #4: Ticker\n\n```go\npackage main\n\nimport (\n\t\"github.com/elliotchance/pepper\"\n\t\"time\"\n)\n\ntype Clock struct{}\n\nfunc (c *Clock) Render() (string, error) {\n\treturn `\n\t\tThe time now is {{ call .Now }}.\n\t`, nil\n}\n\nfunc (c *Clock) Now() string {\n\treturn time.Now().Format(time.RFC1123)\n}\n\nfunc main() {\n\tpanic(pepper.NewServer().Start(func(conn *pepper.Connection) pepper.Component {\n\t\tgo func() {\n\t\t\tfor range time.NewTicker(time.Second).C {\n\t\t\t\tconn.Update()\n\t\t\t}\n\t\t}()\n\n\t\treturn \u0026Clock{}\n\t}))\n}\n```\n\n- The component is updated once per second so the client sees the active time.\n\nTry it now:\n\n```bash\ngo get -u github.com/elliotchance/pepper/examples/ex04_ticker\nex04_ticker\n```\n\nThen open: [http://localhost:8080/](http://localhost:8080/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felliotchance%2Fpepper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felliotchance%2Fpepper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felliotchance%2Fpepper/lists"}