gowired

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2021 License: MIT Imports: 21 Imported by: 0

README ¶

GoWired

💻 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/patrickcurl/gowired"
 "time"
)

type Clock struct {
 gowired.WiredComponentWrapper
 ActualTime string
}

func NewClock() *gowired.WiredComponent {
 return gowired.NewWiredComponent("Clock", &Clock{})
}

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

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

package main

import (
 "github.com/patrickcurl/gowired"
 "github.com/patrickcurl/gowired/examples/components"
 "github.com/gofiber/fiber/v2"
 "github.com/gofiber/websocket/v2"
)

func main() {
 app := fiber.New()
 wiredServer := gowired.NewServer()

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

 app.Get("/ws", websocket.New(wiredServer.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 (
	EventWiredInput          = "li"
	EventWiredMethod         = "lm"
	EventWiredDom            = "ld"
	EventWiredDisconnect     = "lx"
	EventWiredError          = "le"
	EventWiredConnectElement = "lce"
)
View Source
const ComponentIdAttrKey = "go-wired-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-wired-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_WIRED_CONNECTED="go-wired-connected",GO_WIRED_COMPONENT_ID="go-wired-component-id",EVENT_WIRED_DOM_COMPONENT_ID_KEY="cid",EVENT_WIRED_DOM_INSTRUCTIONS_KEY="i",EVENT_WIRED_DOM_TYPE_KEY="t",EVENT_WIRED_DOM_CONTENT_KEY="c",EVENT_WIRED_DOM_ATTR_KEY="a",EVENT_WIRED_DOM_SELECTOR_KEY="s",EVENT_WIRED_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},goWired={server:createConnection(),handlers:[],once:createOnceEmitter(),getWiredComponent(a){return document.querySelector(["*[",GO_WIRED_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){goWired.server.send(JSON.stringify(a))},connectChildren(a){const b=a.querySelectorAll("*["+GO_WIRED_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=findWiredClicksFromElement(a);c.forEach(function(a){const c=getComponentIdFromElement(a);a.addEventListener("click",function(b){goWired.send({name:"{{ .Enum.EventWiredMethod }}",component_id:c,method_name:a.getAttribute("go-wired-click"),method_data:dataFromElementAttributes(a)})}),b.push(a)});const d=findWiredKeyDownFromElement(a);d.forEach(function(a){const e=getComponentIdFromElement(a),f=a.getAttribute("go-wired-keydown"),c=a.attributes;let d=[];for(let a=0;a<c.length;a++)(c[a].name==="go-wired-key"||c[a].name.startsWith("go-wired-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&&goWired.send({name:"{{ .Enum.EventWiredMethod }}",component_id:e,method_name:f,method_data:dataFromElementAttributes(a),dom_event:{keyCode:c}})}),b.push(a)});const e=findWiredInputsFromElement(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),goWired.send({name:"{{ .Enum.EventWiredInput }}",component_id:d,key:a.getAttribute("go-wired-input"),value:String(b)})}),b.push(a)});for(const a of b)a.setAttribute(GO_WIRED_CONNECTED,!0)},connect(a){const b=goWired.getWiredComponent(a);goWired.connectElement(b),goWired.on("{{ .Enum.EventWiredDom }}",function(b){if(a===b[EVENT_WIRED_DOM_COMPONENT_ID_KEY])for(const c of b[EVENT_WIRED_DOM_INSTRUCTIONS_KEY]){const f=c[EVENT_WIRED_DOM_TYPE_KEY],g=c[EVENT_WIRED_DOM_CONTENT_KEY],h=c[EVENT_WIRED_DOM_ATTR_KEY],d=c[EVENT_WIRED_DOM_SELECTOR_KEY],i=c[EVENT_WIRED_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)}})}};goWired.once.on("WS_CONNECTION_OPEN",()=>{goWired.on("{{ .Enum.EventWiredConnectElement }}",a=>{const b=a[EVENT_WIRED_DOM_COMPONENT_ID_KEY];goWired.connect(b)}),goWired.on("{{ .Enum.EventWiredError }}",a=>{console.error("message",a.m),a.m==='{{ index .EnumWiredError ` + "`WiredErrorSessionNotFound`" + `}}'&&window.location.reload(!1)})}),goWired.server.onmessage=a=>{try{const b=JSON.parse(a.data);goWired.emit(b.t,b)}catch(b){console.log("Error",b),console.log("Error message",a.data)}},goWired.server.onopen=()=>{goWired.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 findWiredInputsFromElement=a=>a.querySelectorAll(["*[go-wired-input]:not([",GO_WIRED_CONNECTED,"])"].join("")),findWiredClicksFromElement=a=>a.querySelectorAll(["*[go-wired-click]:not([",GO_WIRED_CONNECTED,"])"].join("")),findWiredKeyDownFromElement=a=>a.querySelectorAll(["*[go-wired-keydown]:not([",GO_WIRED_CONNECTED,"])"].join("")),dataFromElementAttributes=c=>{const a=c.attributes;let b={};for(let c=0;c<a.length;c++)a[c].name.startsWith("go-wired-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),goWired.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,goWired.connectElement(a)}function handleDiffAppend(c,a){const{content:d}=c,b=document.createElement("div");b.innerHTML=d;const e=b.firstChild;a.appendChild(e),goWired.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-wired-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 (
	WiredErrorSessionNotFound = "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 WiredErrorMap ¶

func WiredErrorMap() 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 Change ¶ added in v0.0.2

type Change 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 ChangeType ¶ added in v0.0.2

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

type ChildWiredComponent ¶

type ChildWiredComponent interface{}

type ComponentContext ¶

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

func NewComponentContext ¶

func NewComponentContext() ComponentContext

type ComponentLifeCycle ¶

type ComponentLifeCycle chan ComponentLifeTimeMessage

type ComponentLifeTime ¶

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

type ComponentLifeTimeMessage ¶

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

type DOMEvent ¶

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

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 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              WiredEventsChannel
	ComponentsLifeCycle *ComponentLifeCycle

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

func NewWiredPage ¶

func NewWiredPage(c *WiredComponent) *Page

func (*Page) Emit ¶

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

func (*Page) EmitWithSource ¶

func (lp *Page) EmitWithSource(lts int, c *WiredComponent, 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
	EnumWiredError map[string]string
}

type PageEnum ¶

type PageEnum struct {
	EventWiredInput          string
	EventWiredMethod         string
	EventWiredDom            string
	EventWiredConnectElement string
	EventWiredError          string
	NodeSetAttr              ChangeType
	NodeRemoveAttr           ChangeType
	NodeReplace              ChangeType
	NodeRemove               ChangeType
	NodeSetInnerHTML         ChangeType
	NodeAppend               ChangeType
	NodeMove                 ChangeType
}

type PatchBrowser ¶

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

func NewPatchBrowser ¶

func NewPatchBrowser(componentID string) *PatchBrowser

type PatchNodeChildren ¶

type PatchNodeChildren map[int]*PatchTreeNode

type PatchTreeNode ¶

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

type Random ¶

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

func NewWiredID ¶

func NewWiredID() *Random

func (Random) GenerateSmall ¶

func (g Random) GenerateSmall() string

type Session ¶

type Session struct {
	WiredPage  *Page
	OutChannel chan PatchBrowser

	Status SessionStatus
	// contains filtered or unexported fields
}

func NewSession ¶

func NewSession() *Session

func (*Session) ActivatePage ¶

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

func (*Session) IngestMessage ¶

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

func (*Session) QueueMessage ¶

func (session *Session) QueueMessage(message PatchBrowser)

func (*Session) WiredRenderComponent ¶

func (s *Session) WiredRenderComponent(c *WiredComponent, source *EventSource) error

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

type SessionStatus ¶

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

type WireSessions ¶

type WireSessions map[string]*Session

type WiredComponent ¶

type WiredComponent struct {
	Name string

	IsMounted bool
	IsCreated bool
	Exited    bool

	Context ComponentContext
	// contains filtered or unexported fields
}

func NewWiredComponent ¶

func NewWiredComponent(name string, component ComponentLifeTime) *WiredComponent

NewWiredComponent ...

func (*WiredComponent) Create ¶

func (w *WiredComponent) Create(life *ComponentLifeCycle) error

func (*WiredComponent) GetFieldFromPath ¶

func (w *WiredComponent) GetFieldFromPath(path string) *reflect.Value

GetFieldFromPath ...

func (*WiredComponent) InvokeMethodInPath ¶

func (w *WiredComponent) InvokeMethodInPath(path string, data map[string]string, domEvent *DOMEvent) error

InvokeMethodInPath ...

func (*WiredComponent) Kill ¶

func (w *WiredComponent) Kill() error

Kill ...

func (*WiredComponent) KillChildren ¶

func (w *WiredComponent) KillChildren()

func (*WiredComponent) Mount ¶

func (w *WiredComponent) Mount() error

Mount 2. the Component loading html

func (*WiredComponent) MountChildren ¶

func (w *WiredComponent) MountChildren() error

func (*WiredComponent) Render ¶

func (w *WiredComponent) Render() (string, error)

Render ...

func (*WiredComponent) RenderChild ¶

func (w *WiredComponent) RenderChild(fn reflect.Value, _ ...reflect.Value) template.HTML

func (*WiredComponent) SetValueInPath ¶

func (w *WiredComponent) SetValueInPath(value string, path string) error

SetValueInPath ...

func (*WiredComponent) Update ¶

func (w *WiredComponent) Update()

func (*WiredComponent) UpdateWithSource ¶

func (w *WiredComponent) UpdateWithSource(source *EventSource)

func (*WiredComponent) WiredRender ¶

func (w *WiredComponent) WiredRender() (*patches, error)

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

type WiredComponentWrapper ¶

type WiredComponentWrapper struct {
	Name      string
	Component *WiredComponent
}

WiredComponentWrapper is a struct

func (*WiredComponentWrapper) BeforeMount ¶

func (l *WiredComponentWrapper) BeforeMount(_ *WiredComponent)

BeforeMount the Component loading html

func (*WiredComponentWrapper) BeforeUnmount ¶

func (l *WiredComponentWrapper) BeforeUnmount(_ *WiredComponent)

BeforeUnmount before we kill the Component

func (*WiredComponentWrapper) Commit ¶

func (l *WiredComponentWrapper) Commit()

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

func (*WiredComponentWrapper) Create ¶

func (l *WiredComponentWrapper) Create(lc *WiredComponent)

func (*WiredComponentWrapper) Mounted ¶

func (l *WiredComponentWrapper) Mounted(_ *WiredComponent)

BeforeMount the Component loading html

func (*WiredComponentWrapper) TemplateHandler ¶

func (l *WiredComponentWrapper) TemplateHandler(_ *WiredComponent) string

TemplateHandler ...

type WiredEventsChannel ¶

type WiredEventsChannel chan WiredPageEvent

type WiredPageEvent ¶

type WiredPageEvent struct {
	Type      int
	Component *WiredComponent
	Source    *EventSource
}

type WiredRenderer ¶

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

func (*WiredRenderer) Render ¶

func (renderer *WiredRenderer) Render(data interface{}) (string, *html.Node, error)

func (*WiredRenderer) WiredRender ¶

func (renderer *WiredRenderer) WiredRender(data interface{}) (*patches, error)

type WiredResponse ¶

type WiredResponse struct {
	Rendered string
	Session  string
}

type WiredServer ¶

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

	// CookieName ...
	CookieName string
	Log        Log
}

func NewServer ¶

func NewServer() *WiredServer

func (*WiredServer) CreateHTMLHandler ¶

func (s *WiredServer) CreateHTMLHandler(f func() *WiredComponent, c PageContent) func(ctx *fiber.Ctx) error

func (*WiredServer) CreateHTMLHandlerWithMiddleware ¶

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

func (*WiredServer) HandleFirstRequest ¶

func (s *WiredServer) HandleFirstRequest(lc *WiredComponent, c PageContent) (*WiredResponse, error)

func (*WiredServer) HandleHTMLRequest ¶

func (s *WiredServer) HandleHTMLRequest(ctx *fiber.Ctx, lc *WiredComponent, c PageContent)

func (*WiredServer) HandlePollRequest ¶

func (s *WiredServer) HandlePollRequest()

func (*WiredServer) HandleWSRequest ¶

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

type WiredState ¶

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

Directories ¶

Path Synopsis
examples

Jump to

Keyboard shortcuts

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