live

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 5, 2021 License: MIT Imports: 20 Imported by: 28

README

live

Real-time user experiences with server-rendered HTML in Go. Inspired by and borrowing from Phoenix LiveViews.

Live is intended as a replacement for React, Vue, Angular etc. You can write an interactive web app just using Go and its templates.

Compatible with net/http, so will play nicely with middleware and other frameworks.

Roadmap

  • Release standalone NPM package for JS integration.
  • Implement any missing phx events that make sense.
  • Enable persisting state across visits?
  • File uploads.
  • Think about how live components could work.

Getting Started

Install
go get github.com/jfyne/live

See the examples for usage.

First handler

As of writing, each handler defaults to rendering a root.html template. This can be overriden using WithRootTemplate and defining another template, the chat example does this.

<!doctype html>
<html>
    <head>
        <title>{{ template "title" . }}</title>
    </head>
    <body>
        {{ template "view" . }}
        <!-- This is embedded in the library and enables live to work -->
        <script type="text/javascript" src="/live.js"></script>
    </body>
</html>

Notice the script tag. Live's javascript is embedded within the library for ease of use, and is required to be included for it to work.

We would then define a view like this (from the clock example):

{{ define "title" }} {{.FormattedTime}} {{ end }}
{{ define "view" }}
<time>{{.FormattedTime}}</time>
{{ end }}

And in go

t, _ := template.ParseFiles("examples/root.html", "examples/clock/view.html")
h, _ := live.NewHandler(t, sessionStore)

And then just serve like you normallly would

// Here we are using `http.Handle` but you could use
// `gorilla/mux` or whatever you want. 

// Serve the handler itself.
http.Handle("/clock", h)

// This serves the javscript for live to work and is required. This is what
// we referenced in the `root.html`.
http.Handle("/live.js", live.Javascript{})

http.ListenAndServe(":8080", nil)

Features

Click Events
  • live-capture-click
  • live-click
  • live-value-*

The live-click binding is used to send click events to the server.

<div live-click="inc" live-value-myvar1="val1" live-value-myvar2="val2"></div>

See the buttons example for usage.

Focus / Blur Events
  • live-window-focus
  • live-window-blur
  • live-focus
  • live-blur

Focus and blur events may be bound to DOM elements that emit such events, using the live-blur, and live-focus bindings, for example:

<input name="email" live-focus="myfocus" live-blur="myblur"/>
Key Events
  • live-window-keyup
  • live-window-keydown
  • live-keyup
  • live-keydown
  • live-key

The onkeydown, and onkeyup events are supported via the live-keydown, and live-keyup bindings. Each binding supports a live-key attribute, which triggers the event for the specific key press. If no live-key is provided, the event is triggered for any key press. When pushed, the value sent to the server will contain the "key" that was pressed.

See the buttons example for usage.

Form Events
  • live-auto-recover
  • live-trigger-action
  • live-disable-with
  • live-feedback-for
  • live-submit
  • live-change

To handle form changes and submissions, use the live-change and live-submit events. In general, it is preferred to handle input changes at the form level, where all form fields are passed to the handler's event handler given any single input change. For example, to handle real-time form validation and saving, your template would use both live-change and live-submit bindings.

See the form example for usage.

Rate Limiting
  • live-throttle
  • live-debounce
Dom Patching
  • live-update

A container can be marked with live-update, allowing the DOM patch operations to avoid updating or removing portions of the view, or to append or prepend the updates rather than replacing the existing contents. This is useful for client-side interop with existing libraries that do their own DOM operations. The following live-update values are supported:

  • replace - replaces the element with the contents
  • ignore - ignores updates to the DOM regardless of new content changes
  • append - append the new DOM contents instead of replacing
  • prepend - prepend the new DOM contents instead of replacing

When using live-update If using "append" or "prepend", a DOM ID must be set for each child.

See the chat example for usage.

JS Interop
  • live-hook

Hooks take the following form.

/**
 * Hooks supplied for interop.
 */
export interface Hooks {
    [id: string]: Hook;
}

/**
 * A hook for running external JS.
 */
export interface Hook {
    /**
     * The element has been added to the DOM and its server
     * LiveHandler has finished mounting
     */
    mounted?: () => void;

    /**
     * The element is about to be updated in the DOM.
     * Note: any call here must be synchronous as the operation
     * cannot be deferred or cancelled.
     */
    beforeUpdate?: () => void;

    /**
     * The element has been updated in the DOM by the server
     */
    updated?: () => void;

    /**
     * The element is about to be removed from the DOM.
     * Note: any call here must be synchronous as the operation
     * cannot be deferred or cancelled.
     */
    beforeDestroy?: () => void;

    /**
     * The element has been removed from the page, either by
     * a parent update, or by the parent being removed entirely
     */
    destroyed?: () => void;

    /**
     * The element's parent LiveHandler has disconnected from
     * the server
     */
    disconnected?: () => void;

    /**
     * The element's parent LiveHandler has reconnected to the
     * server
     */
    reconnected?: () => void;
}

In scope when these functions are called.

  • el - attribute referencing the bound DOM node,
  • pushEvent(event: { t: string, d: any }) - method to push an event from the client to the Live server
  • handleEvent(event: string, cb: ((payload: any) => void)) - method to handle an event pushed from the server.

See the chat example for usage.

Errors and exceptions

There are two types of errors in a live handler, and how these are handled are separate.

Unexpected errors

Errors that occur during the initial mount, initial render and web socket upgrade process are handled by the handler ErrorHandler func.

Errors that occur while handling incoming web socket messages will trigger a response back with the error.

Expected errors

In general errors which you expect to happen such as form validations etc. should be handled by just updating the data on the socket and re-rendering.

If you return an error in the event handler live will send an "err" event to the socket. You can handle this with a hook. An example of this can be seen in the error example.

Loading state and errors

By default, the following classes are applied to the LiveView's parent container:

  • live-connected - applied when the view has connected to the server
  • live-disconnected - applied when the view is not connected to the server
  • live-error - applied when an error occurs on the server. Note, this class will be applied in conjunction with live-disconnected if connection to the server is lost.

All live- event bindings apply their own css classes when pushed. For example the following markup:

<button live-click="clicked" live-window-keydown="key">...</button>

On click, would receive the live-click-loading class, and on keydown would receive the live-keydown-loading class. The css loading classes are maintained until an acknowledgement is received on the client for the pushed event.

The following events receive css loading classes:

  • live-click - live-click-loading
  • live-change - live-change-loading
  • live-submit - live-submit-loading
  • live-focus - live-focus-loading
  • live-blur - live-blur-loading
  • live-window-keydown - live-keydown-loading
  • live-window-keyup - live-keyup-loading

Documentation

Overview

Example (Temperature)

Example_temperature shows a simple temperature control using the "live-click" event.

// Model of our thermostat.
type ThermoModel struct {
	C float32
}

// Helper function to get the model from the socket data.
NewThermoModel := func(s *Socket) *ThermoModel {
	m, ok := s.Assigns().(*ThermoModel)
	if !ok {
		m = &ThermoModel{
			C: 19.5,
		}
	}
	return m
}

// Parsing nil as a template to NewHandler will error if we do not set
// a render function ourselves.
h, err := NewHandler(nil, NewCookieStore("session-name", []byte("weak-secret")))
if err != nil {
	log.Fatal("could not create handler")
}

// By default the handler will automatically render any template parsed into the
// NewHandler function. However you can override and render an HTML string like
// this.
h.Render = func(ctc context.Context, t *template.Template, data interface{}) (io.Reader, error) {
	tmpl, err := template.New("thermo").Parse(`
            <div>{{.C}}</div>
            <button live-click="temp-up">+</button>
            <button live-click="temp-down">-</button>
            <!-- Include to make live work -->
            <script src="/live.js"></script>
        `)
	if err != nil {
		return nil, err
	}
	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, data); err != nil {
		return nil, err
	}
	return &buf, nil
}

// Mount function is called on initial HTTP load and then initial web
// socket connection. This should be used to create the initial state,
// the connected variable will be true if the mount call is on a web
// socket connection.
h.Mount = func(ctx context.Context, h *Handler, r *http.Request, s *Socket, connected bool) (interface{}, error) {
	return NewThermoModel(s), nil
}

// This handles the `live-click="temp-up"` button. First we load the model from
// the socket, increment the temperature, and then return the new state of the
// model. Live will now calculate the diff between the last time it rendered and now,
// produce a set of diffs and push them to the browser to update.
h.HandleEvent("temp-up", func(s *Socket, _ map[string]interface{}) (interface{}, error) {
	model := NewThermoModel(s)
	model.C += 0.1
	return model, nil
})

// This handles the `live-click="temp-down"` button.
h.HandleEvent("temp-down", func(s *Socket, _ map[string]interface{}) (interface{}, error) {
	model := NewThermoModel(s)
	model.C -= 0.1
	return model, nil
})

http.Handle("/thermostat", h)

// This serves the JS needed to make live work.
http.Handle("/live.js", Javascript{})
http.ListenAndServe(":8080", nil)
Output:

Index

Examples

Constants

View Source
const (
	// EventError indicates an error has occured.
	EventError = "err"
	// EventPatch a patch event containing a diff.
	EventPatch = "patch"
	// EventAck sent when an event is ackknowledged.
	EventAck = "ack"
	// EventHello sent as soon as the server accepts the
	// WS connection.
	EventHello = "hello"
)

Variables

View Source
var ErrMessageMalformed = errors.New("message malformed")

ErrMessageMalformed returned when a message could not be parsed correctly.

View Source
var ErrNoEventHandler = errors.New("view missing event handler")

ErrNoEventHandler returned when a handler has no event handler for that event.

View Source
var ErrNoSocket = errors.New("no socket")

ErrNoSocket returned when a socket doesn't exist.

Functions

func NewID

func NewID() string

NewID returns a new ID.

func ParamCheckbox added in v0.2.3

func ParamCheckbox(params map[string]interface{}, name string) bool

ParamCheckbox helper to return a boolean from params referring to a checkbox input.

func ParamFloat32

func ParamFloat32(params map[string]interface{}, key string) float32

ParamFloat32 helper to return a float32 from the params.

func ParamInt

func ParamInt(params map[string]interface{}, key string) int

ParamInt helper to return an int from the params.

func ParamString

func ParamString(params map[string]interface{}, key string) string

ParamString helper to return a string from the params.

func StartHandler

func StartHandler(h *Handler)

StartHandler run a handler so that it's events can be dealt with. This is called by `NewHandler` so is only required if you are manually creating a handler.

Types

type CookieStore added in v0.2.0

type CookieStore struct {
	Store *sessions.CookieStore
	// contains filtered or unexported fields
}

CookieStore a `gorilla/sessions` based cookie store.

func NewCookieStore added in v0.2.0

func NewCookieStore(sessionName string, keyPairs ...[]byte) *CookieStore

NewCookieStore create a new `gorilla/sessions` based cookie store.

func (CookieStore) Get added in v0.2.0

func (c CookieStore) Get(r *http.Request) (Session, error)

Get get a session.

func (CookieStore) Save added in v0.2.0

func (c CookieStore) Save(w http.ResponseWriter, r *http.Request, session Session) error

Save a session.

type ErrorHandler added in v0.3.0

type ErrorHandler func(ctx context.Context, w http.ResponseWriter, r *http.Request, err error)

ErrorHandler if an error occurs during the mount and render cycle a handler of this type will be called.

type Event

type Event struct {
	T    string      `json:"t"`
	ID   int         `json:"i,omitempty"`
	Data interface{} `json:"d,omitempty"`
}

Event messages that are sent and received by the socket.

func (Event) Params

func (e Event) Params() (map[string]interface{}, error)

Params extract params from inbound message.

type EventHandler

type EventHandler func(*Socket, map[string]interface{}) (interface{}, error)

EventHandler a function to handle events, returns the data that should be set to the socket after handling.

type Handler

type Handler struct {
	// Mount a user should provide the mount function. This is what
	// is called on initial GET request and later when the websocket connects.
	// Data to render the view should be fetched here and returned.
	Mount MountHandler

	// Render is called to generate the HTML of a Socket. It is defined
	// by default and will render any template provided.
	Render RenderHandler

	// Error is called when an error occurs during the mount and render
	// stages of the handler lifecycle.
	Error ErrorHandler
	// contains filtered or unexported fields
}

Handler to be served by an HTTP server.

func NewHandler

func NewHandler(t *template.Template, store SessionStore, configs ...HandlerConfig) (*Handler, error)

NewHandler creates a new live handler.

func (*Handler) Broadcast

func (h *Handler) Broadcast(msg Event)

Broadcast send a message to all sockets connected to this view.

func (*Handler) HandleEvent

func (h *Handler) HandleEvent(t string, handler EventHandler)

HandleEvent handles an event that comes from the client. For example a click from `live-click="myevent"`.

func (*Handler) HandleSelf

func (h *Handler) HandleSelf(t string, handler EventHandler)

HandleSelf handles an event that comes from the view. For example calling h.Self(socket, msg) will be handled here.

func (*Handler) Self

func (h *Handler) Self(sock *Socket, msg Event)

Self sends a message to the socket on this view.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves this handler.

type HandlerConfig

type HandlerConfig func(h *Handler) error

HandlerConfig applies config to a handler.

func WithRootTemplate

func WithRootTemplate(rootTemplate string) HandlerConfig

WithRootTemplate set the renderer to use a different root template. This changes the handlers Render function.

type HandlerEvent

type HandlerEvent struct {
	S   *Socket
	Msg Event
}

HandlerEvent an event sent by the handler.

type Javascript

type Javascript struct {
}

Javascript handles serving the client side portion of live.

func (Javascript) ServeHTTP

func (j Javascript) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP.

type JavascriptMap

type JavascriptMap struct {
}

JavascriptMap handles serving source map.

func (JavascriptMap) ServeHTTP

func (j JavascriptMap) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP.

type MountHandler

type MountHandler func(ctx context.Context, h *Handler, r *http.Request, c *Socket, connected bool) (interface{}, error)

MountHandler the func that is called by a handler to gather data to be rendered in a template. This is called on first GET and then later when the web socket first connects.

type Patch

type Patch struct {
	Path   []int
	Action PatchAction
	HTML   string
}

Patch a location in the frontend dom.

func Diff

func Diff(current, proposed *html.Node) ([]Patch, error)

Diff compare two node states and return patches.

type PatchAction

type PatchAction uint32

PatchAction available actions to take by a patch.

const (
	Noop PatchAction = iota
	Insert
	Replace
	Append
	Prepend
)

Actions available.

type RenderHandler

type RenderHandler func(ctx context.Context, t *template.Template, data interface{}) (io.Reader, error)

RenderHandler the func that is called to render the current state of the data for the socket.

type Session

type Session struct {
	ID string
}

Session what we will actually store across page loads.

func NewSession

func NewSession() Session

NewSession create a new session.

type SessionStore added in v0.2.0

type SessionStore interface {
	Get(*http.Request) (Session, error)
	Save(http.ResponseWriter, *http.Request, Session) error
}

SessionStore handles storing and retrieving sessions.

type Socket

type Socket struct {
	Session Session
	// contains filtered or unexported fields
}

Socket describes a socket from the outside.

func NewSocket

func NewSocket(s Session) *Socket

NewSocket creates a new socket.

func (*Socket) Assign

func (s *Socket) Assign(data interface{})

Assign set data to this socket. This will happen automatically if you return data from and `EventHander`.

func (*Socket) Assigns

func (s *Socket) Assigns() interface{}

Assigns returns the data currently assigned to this socket.

func (*Socket) Send

func (s *Socket) Send(msg Event)

Send an event to this socket.

type ValueKey

type ValueKey string

ValueKey type for session keys.

Directories

Path Synopsis
examples
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL