golive

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2022 License: MIT Imports: 21 Imported by: 3

README ¶

GoLive

💻 Reactive HTML Server Side Rendered by GoLang over WebSockets 🚀

Use Go and Zero JavaScript to program reactive front-ends!

How?

  1. Render Server Side HTML
  2. Connect to same server using Websocket
  3. Send user events
  4. Change state of component in server
  5. Render Component and get diff
  6. Update instructions are sent to the browser

Getting Started

Any suggestions are absolutely welcome

This project it's strongly inspired by Elixir Phoenix LiveView.

Component Example

package components 

import (
	"github.com/brendonmatos/golive"
	"time"
)

type Clock struct {
	golive.LiveComponentWrapper
	ActualTime string
}

func NewClock() *golive.LiveComponent {
	return golive.NewLiveComponent("Clock", &Clock{})
}

func (t *Clock) Mounted(_ *golive.LiveComponent) {
	go func() {
		for {
			t.ActualTime = time.Now().Format(time.RFC3339Nano)
			time.Sleep((time.Second * 1) / 60)
			t.Commit()
		}
	}()
}

func (t *Clock) TemplateHandler(_ *golive.LiveComponent) string {
	return `
		<div>
			<span>Time: {{ .ActualTime }}</span>
		</div>
	`
}
Server Example
  
package main

import (
	"github.com/brendonmatos/golive"
	"github.com/brendonmatos/golive/examples/components"
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/websocket/v2"
)

func main() {
	app := fiber.New()
	liveServer := golive.NewServer()

	app.Get("/", liveServer.CreateHTMLHandler(components.NewClock, golive.PageContent{
		Lang:  "us",
		Title: "Hello world",
	}))

	app.Get("/ws", websocket.New(liveServer.HandleWSRequest))

	_ = app.Listen(":3000")
}
That's it!

More Examples

Slider

Simple todo

All at once using components!

GoBook

Go to repo

Documentation ¶

Index ¶

Constants ¶

View Source
const (
	LogTrace = iota - 1
	LogDebug
	LogInfo
	LogWarn
	LogError
	LogFatal
	LogPanic
)
View Source
const (
	EventLiveInput          = "li"
	EventLiveMethod         = "lm"
	EventLiveDom            = "ld"
	EventLiveDisconnect     = "lx"
	EventLiveError          = "le"
	EventLiveConnectElement = "lce"
)
View Source
const ComponentIdAttrKey = "go-live-component-id"
View Source
const (
	EventSourceInput = "input"
)
View Source
const PageComponentMounted = 2
View Source
const PageComponentUpdated = 1

Variables ¶

View Source
var (
	ErrComponentNotPrepared = errors.New("Component need to be prepared")
	ErrComponentWithoutLog  = errors.New("Component without log defined")
	ErrComponentNil         = errors.New("Component nil")
)
View Source
var (
	ErrCouldNotProvideValidSelector = fmt.Errorf("could not provide a valid selector")
	ErrElementNotSigned             = fmt.Errorf("element is not signed with go-live-uid")
)
View Source
var BasePage *template.Template
View Source
var BasePageString = `<!DOCTYPE html>
<html lang="{{ .Lang }}">
  <head>
    <meta charset="UTF-8" />
    <title>{{ .Title }}</title>
    {{ .Head }}
  </head>

  <body>
    {{ .Body }}
  </body>

  <script type="application/javascript">
    const GO_LIVE_CONNECTED="go-live-connected",GO_LIVE_COMPONENT_ID="go-live-component-id",EVENT_LIVE_DOM_COMPONENT_ID_KEY="cid",EVENT_LIVE_DOM_INSTRUCTIONS_KEY="i",EVENT_LIVE_DOM_TYPE_KEY="t",EVENT_LIVE_DOM_CONTENT_KEY="c",EVENT_LIVE_DOM_ATTR_KEY="a",EVENT_LIVE_DOM_SELECTOR_KEY="s",EVENT_LIVE_DOM_INDEX_KEY="i",handleChange={"{{ .Enum.DiffSetAttr }}":handleDiffSetAttr,"{{ .Enum.DiffRemoveAttr }}":handleDiffRemoveAttr,"{{ .Enum.DiffReplace }}":handleDiffReplace,"{{ .Enum.DiffRemove }}":handleDiffRemove,"{{ .Enum.DiffSetInnerHTML }}":handleDiffSetInnerHTML,"{{ .Enum.DiffAppend }}":handleDiffAppend,"{{ .Enum.DiffMove }}":handleDiffMove},goLive={server:createConnection(),handlers:[],once:createOnceEmitter(),getLiveComponent(a){return document.querySelector(["*[",GO_LIVE_COMPONENT_ID,"=",a,"]"].join(""))},on(a,b){const c=this.handlers.push({name:a,handler:b});return c-1},findHandler(a){return this.handlers.filter(b=>b.name===a)},emit(a,b){for(const c of this.findHandler(a))c.handler(b)},off(a){this.handlers.splice(a,1)},send(a){goLive.server.send(JSON.stringify(a))},connectChildren(a){const b=a.querySelectorAll("*["+GO_LIVE_COMPONENT_ID+"]");b.forEach(a=>{this.connectElement(a)})},connectElement(a){if(typeof a=="string"){console.warn("is string");return}if(!isElement(a)){console.warn("not element");return}const b=[],c=findLiveClicksFromElement(a);c.forEach(function(a){const c=getComponentIdFromElement(a);a.addEventListener("click",function(b){goLive.send({name:"{{ .Enum.EventLiveMethod }}",component_id:c,method_name:a.getAttribute("go-live-click"),method_data:dataFromElementAttributes(a)})}),b.push(a)});const d=findLiveKeyDownFromElement(a);d.forEach(function(a){const e=getComponentIdFromElement(a),f=a.getAttribute("go-live-keydown"),c=a.attributes;let d=[];for(let a=0;a<c.length;a++)(c[a].name==="go-live-key"||c[a].name.startsWith("go-live-key-"))&&d.push(c[a].value);a.addEventListener("keydown",function(g){const c=String(g.code);let b=!0;if(d.length!==0){b=!1;for(let a=0;a<d.length;a++)if(d[a]===c){b=!0;break}}b&&goLive.send({name:"{{ .Enum.EventLiveMethod }}",component_id:e,method_name:f,method_data:dataFromElementAttributes(a),dom_event:{keyCode:c}})}),b.push(a)});const e=findLiveInputsFromElement(a);e.forEach(function(a){const c=a.getAttribute("type"),d=getComponentIdFromElement(a);a.addEventListener("input",function(e){let b=a.value;c==="checkbox"&&(b=a.checked),goLive.send({name:"{{ .Enum.EventLiveInput }}",component_id:d,key:a.getAttribute("go-live-input"),value:String(b)})}),b.push(a)});for(const a of b)a.setAttribute(GO_LIVE_CONNECTED,!0)},connect(a){const b=goLive.getLiveComponent(a);goLive.connectElement(b),goLive.on("{{ .Enum.EventLiveDom }}",function(b){if(a===b[EVENT_LIVE_DOM_COMPONENT_ID_KEY])for(const c of b[EVENT_LIVE_DOM_INSTRUCTIONS_KEY]){const f=c[EVENT_LIVE_DOM_TYPE_KEY],g=c[EVENT_LIVE_DOM_CONTENT_KEY],h=c[EVENT_LIVE_DOM_ATTR_KEY],d=c[EVENT_LIVE_DOM_SELECTOR_KEY],i=c[EVENT_LIVE_DOM_INDEX_KEY],e=document.querySelector(d);if(!e){console.error("Element not found",d);return}handleChange[f]({content:g,attr:h,index:i},e,a)}})}};goLive.once.on("WS_CONNECTION_OPEN",()=>{goLive.on("{{ .Enum.EventLiveConnectElement }}",a=>{const b=a[EVENT_LIVE_DOM_COMPONENT_ID_KEY];goLive.connect(b)}),goLive.on("{{ .Enum.EventLiveError }}",a=>{console.error("message",a.m),a.m==='{{ index .EnumLiveError ` + "`LiveErrorSessionNotFound`" + `}}'&&window.location.reload(!1)})}),goLive.server.onmessage=a=>{try{const b=JSON.parse(a.data);goLive.emit(b.t,b)}catch(b){console.log("Error",b),console.log("Error message",a.data)}},goLive.server.onopen=()=>{goLive.once.emit("WS_CONNECTION_OPEN")};function createConnection(){const a=[];return window.location.protocol==="https:"?a.push("wss"):a.push("ws"),a.push("://",window.location.host,"/ws"),new WebSocket(a.join(""))}function createOnceEmitter(){const a={},b=(b,c)=>(a[b]={called:c,cbs:[]},a[b]);return{on(d,e){let c=a[d];c||(c=b(d,!1)),c.cbs.push(e)},emit(c,...e){const d=a[c];if(!d){b(c,!0);return}for(const a of d.cbs)a()}}}const findLiveInputsFromElement=a=>a.querySelectorAll(["*[go-live-input]:not([",GO_LIVE_CONNECTED,"])"].join("")),findLiveClicksFromElement=a=>a.querySelectorAll(["*[go-live-click]:not([",GO_LIVE_CONNECTED,"])"].join("")),findLiveKeyDownFromElement=a=>a.querySelectorAll(["*[go-live-keydown]:not([",GO_LIVE_CONNECTED,"])"].join("")),dataFromElementAttributes=c=>{const a=c.attributes;let b={};for(let c=0;c<a.length;c++)a[c].name.startsWith("go-live-data-")&&(b[a[c].name.substring(13)]=a[c].value);return b};function getElementChild(b,c){let a=b.firstElementChild;while(c>0){if(!a){console.error("Element not found in path",b);return}if(a=a.nextSibling,a.nodeType!==Node.ELEMENT_NODE)continue;c--}return a}function isElement(a){return typeof HTMLElement=="object"?a instanceof HTMLElement:a&&typeof a=="object"&&a.nodeType===1&&typeof a.nodeName=="string"}function handleDiffSetAttr(c,b){const{attr:a}=c;a.Name==="value"&&b.value?b.value=a.Value:b.setAttribute(a.Name,a.Value)}function handleDiffRemoveAttr(a,b){const{attr:c}=a;b.removeAttribute(c.Name)}function handleDiffReplace(d,a){const{content:e}=d,b=document.createElement("div");b.innerHTML=e;const c=a.parentElement;c.replaceChild(b.firstChild,a),goLive.connectElement(c)}function handleDiffRemove(c,a){const b=a.parentElement;b.removeChild(a)}function handleDiffSetInnerHTML(c,a){let{content:b}=c;if(b===void 0&&(b=""),a.nodeType===Node.TEXT_NODE){a.textContent=b;return}a.innerHTML=b,goLive.connectElement(a)}function handleDiffAppend(c,a){const{content:d}=c,b=document.createElement("div");b.innerHTML=d;const e=b.firstChild;a.appendChild(e),goLive.connectElement(a)}function handleDiffMove(c,a){const b=a.parentNode;b.removeChild(a);const d=getElementChild(b,c.index);b.replaceChild(a,d)}const getComponentIdFromElement=a=>{const b=a.getAttribute("go-live-component-id");return b?b:a.parentElement?getComponentIdFromElement(a.parentElement):void 0}
  </script>
</html>
`

Code automatically generated. DO NOT EDIT. > go run ci/create_html_page.go

View Source
var (
	LiveErrorSessionNotFound = "session_not_found"
)

Functions ¶

func AttrMapFromNode ¶

func AttrMapFromNode(node *html.Node) map[string]string

AttrMapFromNode todo

func GenerateRandomString ¶

func GenerateRandomString(n int) (string, error)

func LiveErrorMap ¶

func LiveErrorMap() map[string]string

Types ¶

type BrowserEvent ¶

type BrowserEvent struct {
	Name        string            `json:"name"`
	ComponentID string            `json:"component_id"`
	MethodName  string            `json:"method_name"`
	MethodData  map[string]string `json:"method_data"`
	StateKey    string            `json:"key"`
	StateValue  string            `json:"value"`
	DOMEvent    *DOMEvent         `json:"dom_event"`
}

type ChildLiveComponent ¶

type ChildLiveComponent interface{}

type ComponentContext ¶ added in v0.0.2

type ComponentContext struct {
	Pairs map[string]interface{}
}

func NewComponentContext ¶ added in v0.0.2

func NewComponentContext() ComponentContext

type ComponentLifeCycle ¶

type ComponentLifeCycle chan ComponentLifeTimeMessage

type ComponentLifeTime ¶

type ComponentLifeTime interface {
	Create(component *LiveComponent)
	TemplateHandler(component *LiveComponent) string
	Mounted(component *LiveComponent)
	BeforeMount(component *LiveComponent)
	BeforeUnmount(component *LiveComponent)
}

type ComponentLifeTimeMessage ¶

type ComponentLifeTimeMessage struct {
	Stage     LifeTimeStage
	Component *LiveComponent
	Source    *EventSource
}

type DOMEvent ¶

type DOMEvent struct {
	KeyCode string `json:"keyCode"`
}

type DiffType ¶

type DiffType int
const (
	Append DiffType = iota
	Remove
	SetInnerHTML
	SetAttr
	RemoveAttr
	Replace
	Move
)

type EventSource ¶

type EventSource struct {
	Type  EventSourceType
	Value string
}

type EventSourceType ¶

type EventSourceType string

type HTTPHandlerCtx ¶

type HTTPHandlerCtx func(ctx *fiber.Ctx, pageCtx context.Context)

HTTPHandlerCtx HTTP Handler with a page level context.

type HTTPMiddleware ¶

type HTTPMiddleware func(next HTTPHandlerCtx) HTTPHandlerCtx

HTTPMiddleware Middleware to run on HTTP requests.

type LifeTimeStage ¶

type LifeTimeStage int
const (
	WillCreate LifeTimeStage = iota
	Created

	WillMount

	WillMountChildren
	ChildrenMounted

	Mounted

	Rendered
	Updated

	WillUnmount
	Unmounted
)

type LiveComponent ¶

type LiveComponent struct {
	Name string

	IsMounted bool
	IsCreated bool
	Exited    bool

	Context ComponentContext
	// contains filtered or unexported fields
}

func NewLiveComponent ¶

func NewLiveComponent(name string, component ComponentLifeTime) *LiveComponent

NewLiveComponent ...

func (*LiveComponent) Create ¶

func (l *LiveComponent) Create(life *ComponentLifeCycle) error

func (*LiveComponent) GetFieldFromPath ¶

func (l *LiveComponent) GetFieldFromPath(path string) *reflect.Value

GetFieldFromPath ...

func (*LiveComponent) InvokeMethodInPath ¶

func (l *LiveComponent) InvokeMethodInPath(path string, data map[string]string, domEvent *DOMEvent) error

InvokeMethodInPath ...

func (*LiveComponent) Kill ¶

func (l *LiveComponent) Kill() error

Kill ...

func (*LiveComponent) KillChildren ¶

func (l *LiveComponent) KillChildren()

func (*LiveComponent) LiveRender ¶

func (l *LiveComponent) LiveRender() (*diff, error)

LiveRender render a new version of the Component, and detect differences from the last render and sets the "new old" version of render

func (*LiveComponent) Mount ¶

func (l *LiveComponent) Mount() error

Mount 2. the Component loading html

func (*LiveComponent) MountChildren ¶

func (l *LiveComponent) MountChildren() error

func (*LiveComponent) Render ¶

func (l *LiveComponent) Render() (string, error)

Render ...

func (*LiveComponent) RenderChild ¶

func (l *LiveComponent) RenderChild(fn reflect.Value, _ ...reflect.Value) template.HTML

func (*LiveComponent) SetValueInPath ¶

func (l *LiveComponent) SetValueInPath(value string, path string) error

SetValueInPath ...

func (*LiveComponent) Update ¶

func (l *LiveComponent) Update()

func (*LiveComponent) UpdateWithSource ¶

func (l *LiveComponent) UpdateWithSource(source *EventSource)

type LiveComponentWrapper ¶

type LiveComponentWrapper struct {
	Name      string
	Component *LiveComponent
}

LiveComponentWrapper is a struct

func (*LiveComponentWrapper) BeforeMount ¶

func (l *LiveComponentWrapper) BeforeMount(_ *LiveComponent)

BeforeMount the Component loading html

func (*LiveComponentWrapper) BeforeUnmount ¶

func (l *LiveComponentWrapper) BeforeUnmount(_ *LiveComponent)

BeforeUnmount before we kill the Component

func (*LiveComponentWrapper) Commit ¶

func (l *LiveComponentWrapper) Commit()

Commit puts an boolean to the commit channel and notifies who is listening

func (*LiveComponentWrapper) Create ¶

func (l *LiveComponentWrapper) Create(lc *LiveComponent)

func (*LiveComponentWrapper) Mounted ¶

func (l *LiveComponentWrapper) Mounted(_ *LiveComponent)

BeforeMount the Component loading html

func (*LiveComponentWrapper) TemplateHandler ¶

func (l *LiveComponentWrapper) TemplateHandler(_ *LiveComponent) string

TemplateHandler ...

type LiveEventsChannel ¶

type LiveEventsChannel chan LivePageEvent

type LivePageEvent ¶

type LivePageEvent struct {
	Type      int
	Component *LiveComponent
	Source    *EventSource
}

type LiveRenderer ¶

type LiveRenderer struct {
	// contains filtered or unexported fields
}

func (*LiveRenderer) LiveRender ¶

func (lr *LiveRenderer) LiveRender(data interface{}) (*diff, error)

func (*LiveRenderer) Render ¶

func (lr *LiveRenderer) Render(data interface{}) (string, *html.Node, error)

type LiveResponse ¶

type LiveResponse struct {
	Rendered string
	Session  string
}

type LiveServer ¶

type LiveServer struct {
	// Wire ...
	Wire *LiveWire

	// CookieName ...
	CookieName string
	Log        Log
}

func NewServer ¶

func NewServer() *LiveServer

func (*LiveServer) CreateHTMLHandler ¶

func (s *LiveServer) CreateHTMLHandler(f func() *LiveComponent, c PageContent) func(ctx *fiber.Ctx) error

func (*LiveServer) CreateHTMLHandlerWithMiddleware ¶

func (s *LiveServer) CreateHTMLHandlerWithMiddleware(f func(ctx context.Context) *LiveComponent, content PageContent,
	middlewares ...HTTPMiddleware) func(c *fiber.Ctx) error

func (*LiveServer) HandleFirstRequest ¶

func (s *LiveServer) HandleFirstRequest(lc *LiveComponent, c PageContent) (*LiveResponse, error)

func (*LiveServer) HandleHTMLRequest ¶

func (s *LiveServer) HandleHTMLRequest(ctx *fiber.Ctx, lc *LiveComponent, c PageContent)

func (*LiveServer) HandleWSRequest ¶

func (s *LiveServer) HandleWSRequest(c *websocket.Conn)

type LiveState ¶

type LiveState struct {
	// contains filtered or unexported fields
}

type LiveWire ¶

type LiveWire struct {
	Sessions WireSessions
}

func NewWire ¶

func NewWire() *LiveWire

func (*LiveWire) CreateSession ¶

func (w *LiveWire) CreateSession() (string, *Session, error)

func (*LiveWire) DeleteSession ¶

func (w *LiveWire) DeleteSession(s string)

func (*LiveWire) GetSession ¶

func (w *LiveWire) GetSession(s string) *Session

type Log ¶

type Log func(level int, message string, extra map[string]interface{})

type LoggerBasic ¶

type LoggerBasic struct {
	Level      int
	Prefix     string
	TimeFormat string
}

func NewLoggerBasic ¶

func NewLoggerBasic() *LoggerBasic

func (*LoggerBasic) Log ¶

func (l *LoggerBasic) Log(level int, message string, extra map[string]interface{})

type Page ¶

type Page struct {
	Events              LiveEventsChannel
	ComponentsLifeCycle *ComponentLifeCycle

	// Components is a list that handle all the components from the page
	Components map[string]*LiveComponent
	// contains filtered or unexported fields
}

func NewLivePage ¶

func NewLivePage(c *LiveComponent) *Page

func (*Page) Emit ¶

func (lp *Page) Emit(lts int, c *LiveComponent)

func (*Page) EmitWithSource ¶

func (lp *Page) EmitWithSource(lts int, c *LiveComponent, source *EventSource)

func (*Page) HandleBrowserEvent ¶

func (lp *Page) HandleBrowserEvent(m BrowserEvent) error

func (*Page) Mount ¶

func (lp *Page) Mount()

Call the Component in sequence of life cycle

func (*Page) Render ¶

func (lp *Page) Render() (string, error)

func (*Page) SetContent ¶

func (lp *Page) SetContent(c PageContent)

type PageContent ¶

type PageContent struct {
	Lang          string
	Body          template.HTML
	Head          template.HTML
	Script        string
	Title         string
	Enum          PageEnum
	EnumLiveError map[string]string
}

type PageEnum ¶

type PageEnum struct {
	EventLiveInput          string
	EventLiveMethod         string
	EventLiveDom            string
	EventLiveConnectElement string
	EventLiveError          string
	DiffSetAttr             DiffType
	DiffRemoveAttr          DiffType
	DiffReplace             DiffType
	DiffRemove              DiffType
	DiffSetInnerHTML        DiffType
	DiffAppend              DiffType
	DiffMove                DiffType
}

type PatchBrowser ¶

type PatchBrowser struct {
	ComponentID  string             `json:"cid,omitempty"`
	Type         string             `json:"t"`
	Message      string             `json:"m"`
	Instructions []PatchInstruction `json:"i,omitempty"`
}

func NewPatchBrowser ¶

func NewPatchBrowser(componentID string) *PatchBrowser

func (*PatchBrowser) AddInstruction ¶

func (pb *PatchBrowser) AddInstruction(pi PatchInstruction)

type PatchInstruction ¶

type PatchInstruction struct {
	Name     string      `json:"n"`
	Type     string      `json:"t"`
	Attr     interface{} `json:"a,omitempty"`
	Content  string      `json:"c,omitempty"`
	Selector string      `json:"s"`
	Index    int         `json:"i,omitempty"`
}

type PatchNodeChildren ¶

type PatchNodeChildren map[int]*PatchTreeNode

type PatchTreeNode ¶

type PatchTreeNode struct {
	Children    PatchNodeChildren  `json:"c,omitempty"`
	Instruction []PatchInstruction `json:"i"`
}

type Random ¶

type Random struct {
	// contains filtered or unexported fields
}

func NewLiveID ¶

func NewLiveID() *Random

func (Random) GenerateSmall ¶

func (g Random) GenerateSmall() string

type Session ¶

type Session struct {
	LivePage   *Page
	OutChannel chan PatchBrowser

	Status SessionStatus
	// contains filtered or unexported fields
}

func NewSession ¶

func NewSession() *Session

func (*Session) ActivatePage ¶

func (s *Session) ActivatePage(lp *Page)

func (*Session) IngestMessage ¶

func (s *Session) IngestMessage(message BrowserEvent) error

func (*Session) LiveRenderComponent ¶

func (s *Session) LiveRenderComponent(c *LiveComponent, source *EventSource) error

LiveRenderComponent render the updated Component and compare with last state. It may apply with *all child components*

func (*Session) QueueMessage ¶

func (s *Session) QueueMessage(message PatchBrowser)

type SessionStatus ¶ added in v0.0.2

type SessionStatus string
const (
	SessionNew    SessionStatus = "n"
	SessionOpen   SessionStatus = "o"
	SessionClosed SessionStatus = "c"
)

type WireSessions ¶

type WireSessions map[string]*Session

Directories ¶

Path Synopsis
examples

Jump to

Keyboard shortcuts

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