🌶️ Create reactive frontends without ever writing frontend code.
Create reactive frontends without ever writing frontend code.
pepper runs a HTTP server that returns a tiny empty HTML page with just a few lines of inline javascript. Immediately after the initial page loads it will connect through a websocket.
All event triggered on the browser will be sent through the websocket where state changes and rerendering occurs. The result is passed back to the websocket to update the UI.
At the moment it returns the whole rendered component. However, this could be optimized in the future to only return the differences.
pepper requires a constant connection to the server (for the websocket) so it wouldn't work for anything that must function offline, or used in cases where the internet is flaky.
I imagine some good use cases for pepper would be:
When creating the server you may configure how you want disconnects to be handled. For example:
server := pepper.NewServer()
server.OfflineAction = pepper.OfflineActionDisableForms
By default clients will try to reconnect every second. This can be changed with
server.ReconnectInterval
.
Important: When reconnecting the server treats the new request as a new client, so all state on the page will be lost.
The peppertest
package provides tools to make unit testing easier.
RenderToDocument
renders then parses the component into a *Document
from the
github.com/PuerkitoBio/goquery
package:
import (
"github.com/elliotchance/pepper/peppertest"
"github.com/stretchr/testify/require"
"testing"
)
func TestPeople_Add(t *testing.T) {
c := &People{
Names: []string{"Jack", "Jill"},
}
c.Name = "Bob"
c.Add()
doc, err := peppertest.RenderToDocument(c)
require.NoError(t, err)
rows := doc.Find("tr")
require.Equal(t, 3, rows.Length())
require.Contains(t, rows.Eq(0).Text(), "Jack")
require.Contains(t, rows.Eq(1).Text(), "Jill")
require.Contains(t, rows.Eq(2).Text(), "Bob")
}
Each of the examples in the examples/ directory include unit tests.
package main
import "github.com/elliotchance/pepper"
type Counter struct {
Number int
}
func (c *Counter) Render() (string, error) {
return `
Counter: {{ .Number }}
<button @click="AddOne">+</button>
`, nil
}
func (c *Counter) AddOne() {
c.Number++
}
func main() {
panic(pepper.NewServer().Start(func(_ *pepper.Connection) pepper.Component {
return &Counter{}
}))
}
Render
method returns a html/template
syntax, or an error.@click
will trigger AddOne
to be called when the button is clicked.Try it now:
go get -u github.com/elliotchance/pepper/examples/ex01_counter
ex01_counter
Then open: http://localhost:8080/
package main
import (
"github.com/elliotchance/pepper"
"strconv"
)
type People struct {
Names []string
Name string
}
func (c *People) Render() (string, error) {
return `
<table>
{{ range $i, $name := .Names }}
<tr><td>
{{ $name }}
<button key="{{ $i }}" @click="Delete">Delete</button>
</td></tr>
{{ end }}
</table>
Add name: <input type="text" @value="Name">
<button @click="Add">Add</button>
`, nil
}
func (c *People) Delete(key string) {
index, _ := strconv.Atoi(key)
c.Names = append(c.Names[:index], c.Names[index+1:]...)
}
func (c *People) Add() {
c.Names = append(c.Names, c.Name)
c.Name = ""
}
func main() {
panic(pepper.NewServer().Start(func(_ *pepper.Connection) pepper.Component {
return &People{
Names: []string{"Jack", "Jill"},
}
}))
}
html/template
syntax will work, including loops with {{ range }}
.@value
will cause the Name
property to be bound with the text box in both
directions.key
. The key
is passed as the first argument to the Delete
function.Try it now:
go get -u github.com/elliotchance/pepper/examples/ex02_form
ex02_form
Then open: http://localhost:8080/
type Counters struct {
Counters []*Counter
}
func (c *Counters) Render() (string, error) {
return `
<table>
{{ range .Counters }}
<tr><td>
{{ render . }}
</td></tr>
{{ end }}
<tr><td>
Total: {{ call .Total }}
</td></tr>
</table>
`, nil
}
func (c *Counters) Total() int {
total := 0
for _, counter := range c.Counters {
total += counter.Number
}
return total
}
func main() {
panic(pepper.NewServer().Start(func(_ *pepper.Connection) pepper.Component {
return &Counters{
Counters: []*Counter{
{}, {}, {},
},
}
}))
}
Counter
components (from Example #1) and includes a
live total.render
function. The nested components do
not need to be modified in any way.call
function.Try it now:
go get -u github.com/elliotchance/pepper/examples/ex03_nested
ex03_nested
Then open: http://localhost:8080/
package main
import (
"github.com/elliotchance/pepper"
"time"
)
type Clock struct{}
func (c *Clock) Render() (string, error) {
return `
The time now is {{ call .Now }}.
`, nil
}
func (c *Clock) Now() string {
return time.Now().Format(time.RFC1123)
}
func main() {
panic(pepper.NewServer().Start(func(conn *pepper.Connection) pepper.Component {
go func() {
for range time.NewTicker(time.Second).C {
conn.Update()
}
}()
return &Clock{}
}))
}
Try it now:
go get -u github.com/elliotchance/pepper/examples/ex04_ticker
ex04_ticker
Then open: http://localhost:8080/