GithubHelp home page GithubHelp logo

pepper's Introduction

🌶️ pepper

Create reactive frontends without ever writing frontend code.

How Does It Work?

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.

What Should/Shouldn't I Use It For?

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:

  1. Showing real time data. Streaming metrics, graphs, logs, dashboards, etc.
  2. Apps that rely on a persistent connection. Such as chat clients, timed interactive exams, etc.
  3. Apps that would benefit from persistent state. The entire state can be saved or restored into a serialized format like JSON. Great for forms or surveys with many questions/steps.
  4. Prototyping a frontend app. It's super easy to get up and running and iterate changes without setting up a complex environment, build tools and dependencies.

Handling Offline and Reconnecting

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.

Testing

Unit Testing

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.

Examples

#1: A Simple Counter

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{}
	}))
}
  • The Render method returns a html/template syntax, or an error.
  • @click will trigger AddOne to be called when the button is clicked.
  • Any event triggered from the browser will cause the component to rerender automatically.

Try it now:

go get -u github.com/elliotchance/pepper/examples/ex01_counter
ex01_counter

Then open: http://localhost:8080/

#2: Forms

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"},
		}
	}))
}
  • Any 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.
  • Since there are multiple "Delete" buttons (one for each person), you should specify a 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/

#3: Nested Components

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{
				{}, {}, {},
			},
		}
	}))
}
  • This example uses three Counter components (from Example #1) and includes a live total.
  • Components can be nested with the render function. The nested components do not need to be modified in any way.
  • Invoke methods with the call function.

Try it now:

go get -u github.com/elliotchance/pepper/examples/ex03_nested
ex03_nested

Then open: http://localhost:8080/

#4: Ticker

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{}
	}))
}
  • The component is updated once per second so the client sees the active time.

Try it now:

go get -u github.com/elliotchance/pepper/examples/ex04_ticker
ex04_ticker

Then open: http://localhost:8080/

pepper's People

Contributors

carsonoid avatar elliotchance avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pepper's Issues

About the tag v1.0.0

By default, go module will treat v1.0.0 as the latest, but actually, it's v0.0.1
The latest is v0.0.6, they don't even share the same APIs,so, maybe we can delete tag v1.0.0?

DOM Diff/Patch changes in real-time?

I would love to see the server-side rendered DOM changes applied to the client-side only apply the changes ala React.

Using morphdom could be a good choice here as its pretty lightweight and designed specifically just for the low-level bits you need; diff/patch (although you probably want to do the diff on the server-side anyway and keep state there).

Loving this library/framework/approach so far :)

Improve logging

Improve logging on the client and server-side to help with debugging. At the moment it's a bit raw and not always reliable to follow.

Restore connection on reconnect

Websockets can be flakey and the idea of the websocket dropping out for even a moment would be disastrous. I believe this can easily be resolved by:

  1. When a client connects it should send an identifier back.
  2. The connections should be held in a global map. Only expiring/removing connections sometime after they have disconnected (like 10 minutes, or more).
  3. When a client connects with a previous identifier it can use that connection instead of calling the newConnectionFn.

This way all state is maintained through dropouts.

Forms example does not work

The forms example compiles and runs. But there is a panic anytime you try and type into the field. All of the other examples work as expected.

My go version is 1.12.7

2019/10/13 15:03:17 http: panic serving 127.0.0.1:59112: reflect: call of reflect.Value.Elem on zero Value
goroutine 68 [running]:
net/http.(*conn).serve.func1(0xc00027c000)
	/usr/local/go/src/net/http/server.go:1769 +0x139
panic(0x7be440, 0xc00010a2a0)
	/usr/local/go/src/runtime/panic.go:522 +0x1b5
reflect.Value.Elem(0x0, 0x0, 0x0, 0x0, 0x0, 0x1)
	/usr/local/go/src/reflect/value.go:806 +0x1c8
github.com/elliotchance/pepper.(*Connection).start(0xc00010a140, 0x8b2b80, 0xc000108270)
	/home/carsona/go/pkg/mod/github.com/elliotchance/[email protected]/connection.go:73 +0xa25
github.com/elliotchance/pepper.websocketHandler.func1(0x8b96a0, 0xc00026e0e0, 0xc000280000)
	/home/carsona/go/pkg/mod/github.com/elliotchance/[email protected]/connection.go:31 +0x183
net/http.HandlerFunc.ServeHTTP(0xc000012020, 0x8b96a0, 0xc00026e0e0, 0xc000280000)
	/usr/local/go/src/net/http/server.go:1995 +0x44
net/http.(*ServeMux).ServeHTTP(0xb698e0, 0x8b96a0, 0xc00026e0e0, 0xc000280000)
	/usr/local/go/src/net/http/server.go:2375 +0x1d6
net/http.serverHandler.ServeHTTP(0xc00011c1a0, 0x8b96a0, 0xc00026e0e0, 0xc000280000)
	/usr/local/go/src/net/http/server.go:2774 +0xa8
net/http.(*conn).serve(0xc00027c000, 0x8b9da0, 0xc00026c0c0)
	/usr/local/go/src/net/http/server.go:1878 +0x851
created by net/http.(*Server).Serve
	/usr/local/go/src/net/http/server.go:2884 +0x2f4

Add interfaces for saving/restoring

An interface to provide a standard way to save and restore state (perhaps a type of marshaller). This can be used by an adapter for things like autosave, or restoring based on session, etc.

More elaborate examples?

Hi,

I went through your examples; and I like the overall idea and concept. Its an old idea made new again; but that's okay :)

Do you happen to have any more elaborates examples or projects you've made using this framework?

Kind Regards

James

@actions should not be a regex

The renderer uses a regexp to find events like @click, but that's problematic if it matches the wrong thing. It should provide the known events/bindings to the template as a function like:

<button {{onClick "AddOne"}}>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.