hlive

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Dec 8, 2023 License: MIT Imports: 27 Imported by: 5

README

HLive

Server-side virtual DOM

HLive is a server-side WebSocket based dynamic template-less view layer for Go.

HLive is a fantastic tool for creating complex and dynamic browser-based user interfaces for developers who want to keep all the logic in Go.

All the power and data available on the backend with the responsive feel of a pure JavaScript solution.

It's a great use case for admin interfaces and internal company tools.

Notice

The first version of the API is under active development. Change is likely. Your feedback is welcome.

Please help the project by building something and giving us your feedback.

Table of contents

Quick Start Tutorial

Step 1: Static Page

Import HLive using the optional alias l:

package main

import l "github.com/SamHennessy/hlive"

Let's create our first page:

func home() *l.Page {
	page := l.NewPage()
	page.DOM.Body.Add("Hello, world.")

	return page
}

Next we use a PageServer to add it to an HTTP router:

func main() {
	http.Handle("/", l.NewPageServer(home))

	log.Println("Listing on :3000")

	if err := http.ListenAndServe(":3000", nil); err != nil {
		log.Println("Error: http listen and serve:", err)
	}
}

Your editor should add the extra imports http and log for you.

You can now run it, for example:

go run ./tutorial/helloworld/helloworld.go

In a browser go to http://localhost:3000 you should see this:

Hello world step 1

Step 2: Interactive Page

HLive is all about interactive content. We're going to add a text input field to let us type our own hello message.

We need to replace our existing home function. We need a string to hold our message:

func home() *l.Page {
	var message string

Now we're going to create a Component. Component's are HTML tags that can react to browser events. We are going to base our Component on the input HTML tag.

	input := l.C("input")

We want to set the input to a text type. We do this adding aAttrs map to our Component.

	input.Add(l.Attrs{"type": "text"})

Here we add an EventBinding to listen to "keyup" JavaScript events. When triggered, the handler function will be called. Our handler will update message. It does this by using the data passed in the Event parameter.

	input.Add(l.On("keyup", func(ctx context.Context, e l.Event) {
		message = e.Value
	}))

We create a new Page like before:

	page := l.NewPage()

Here we add our input to the body but first we wrap it in a div tag.

	page.DOM.Body.Add(l.T("div", input))

Next, we will display our message. Notice that we're passing message by reference. That's key for making this example work. We'll also add an "hr" tag to stop it being squashed todeather.

	page.DOM.Body.Add(l.T("hr"))
	page.DOM.Body.Add("Hello, ", &message)

Finally, we return the Page we created.

	return page
}

Let's see that all together, but this time I'm going to use some shortcuts. Can you spot the differences?

func home() *l.Page {
	var message string

	input := l.C("input",
		l.Attrs{"type": "text"},
		l.OnKeyUp(func(ctx context.Context, e l.Event) {
			message = e.Value
		}),
	)

	page := l.NewPage()
	page.DOM.Body.Add(
		l.T("div", input),
		l.T("hr"),
		"Hello, ", &message,
	)

	return page
}

Run it and type something into the input. The page should update to display what you typed.

Hello world step 2

Examples

The examples can be run from the root of the project using go run <path_to_example>. For example:

go run _example/click/click.go
Simple
Click

_example/click/click.go

Click a button see a counter update.

https://user-images.githubusercontent.com/119867/131120937-64091d27-3232-4820-ab20-e579c86cfb92.mp4

Hover

_example/hover/hover.go

Hover over an element and see another element change

Diff Apply

_example/callback/callback.go

Trigger a Diff Apply event when a DOM change is applied in the browser. Use it to trigger server side logic.

Advanced
Animation

_example/animation/animation.go

Create a continuously changing animation by chaining Diff Apply callbacks.

Clock

_example/clock/clock.go

Push browser DOM changes from the server without the need for a user to interact with the page.

File Upload

_example/fileUpload/fileUpload.go

Use a file input to get information about a file before uploading it. Then trigger a file upload from the server when you're ready.

The file is uploaded via WebSocket as a binary (not base64 encoded) object.

Initial Sync

_example/initialSync/initialSync.go

Some browsers, such as FireFox, will not clear data from form fields when the page is reloaded. To the user there is data in the field and if they submit a form they expect that data to be recognised.

Initial sync is a client side process that will send this data to the server after a page refresh. You can check for this behavior in your event handlers.

This example also shows how to get multiple values from inputs that support that.

Local Render

_example/localRender/localRender.go

By default, all Components are rendered after each Event Binding that a user triggers.

You can disable this by turning Auto Render off for a component. You can then render that manually but this will rerender the whole page.

If you only want to re-render a single component, and it's children you can do that instead. It's easy to introduce subtle bugs when using this feature.

Session

_example/session/session.go

An example of how to implement a user session using middleware and cookies. It also shows our to pass data from middleware to Components.

Using middleware in HLive is just like any Go app.

To Do List

_example/todo/todo.go

A simple To Do list app.

URL Parameters

_example/urlParams/urlParams.go

Passing URL params to Components is not straightforward in HLive. Here is an example of how to do it.

This is due to the HLive having a two-step process of loading a page and Components are primarily designed to get data from Events.

Concepts

Tag

A static HTML tag. A Tag has a name (e.g., an <p></p>'s name is p). A Tag can have zero or more Attributes. A Tag can have child Tags nested inside it. A Tag may be Void, which means it doesn't have a closing tag (e.g., <hr>). Void tags can't have child Tags.

Attribute

An Attribute has a name and a value. (e.g., href="https://example.com" or disabled="").

CSS Classes

The HLive implementation of Tag has an optional special way to work with the class attribute. These types are all designed to make toggling CSS classes on and off easy.

HLive's ClassBool is a map[string]bool type. The key is a CSS class, and the value enables the class for rending if true. This allows you to turn a class on and off. (e.g. l.ClassBool{"foo": true, "bar": true, "fizz": true}). The order of the class names in a single ClassBool is NOT respected. If the order of class names is significant, you can add them as separate ClassBool elements, and the order will be respected. You can add a new ClassBool elements with the same class name, and the original ClassBool element will be updated.

Even better is Class, this is a string type that converts into a CSSBool. (e.g. l.Class("foo bar fizz")). The order of the class names is respected. Each class can still be turned off individually using a ClassBool of the ClassOff string type.

ClassList and ClassListOff are string slices that will enable or disable respectively CSS classes. (e.g. l.ClassList{"foo", "bar", "fizz"})

Style Attribute

The HLive implementation of Tag has an optional special way to work with the style attribute.

HLive's Style is a map[string]interface{} type. The key is the CSS style rule, and the value is the value of the rule. The value can be a string or nil. If nil, the style rule gets removed.

The order of the style rules in a single Style is NOT respected. If the order of rules is significant, you can add them as separate Style elements, and the order will be respected.

Tag Children

Tag has func GetNodes() *l.NodeGroup. This will return can children a Tag has.

This function is called many times and not always when it's time to render. Calls to GetNodes must be deterministic. If you've not made a change to the Tag the output is expected to be the same.

This function should not get or change data. For example, no calls to a remote API or database should happen in this function.

Components

A Compnent wraps a Tag. It adds the ability to bind events that primarily happens in the browser to itself.

EventBinding

An EventBinding is a combination of an EventType (e.g., click, focus, mouseenter), with a Component and an EventHandler.

EventHandler

The EventHandler is a func(ctx context.Context, e Event) type.

These handlers are where you can fetch data from remote APIs or databases.

Depending on the EventType you'll have data in the Event parameter.

Node

A Node is something that can be rendered into an HTML tag. For example, a string, Tag, or Component. An Attribute is not a Node as it can't be rendered to a complete HTML tag.

Element

An Element is anything associated with a Tag or Component. This means that in addition to nodes, Attribute and EventBinding are also Elements.

Page

A Page is the root element in HLive. There will be a single page instance for a single connected user.

Page has HTML5 boilerplate pre-defined. This boilerplate also includes HLive's JavaScript.

HTML vs WebSocket

When a user requests a page, there are two requests. First is the initial request that generates the pages HTML. Then the second request is to establish a WebSocket connection.

HLive considers the initial HTML is can be though of as the Server Side Rendering phase (SSR). This SSR request will not be used when processing WebSocket requests. This render is a good candidate for use in a CDN.

When an HLive SSR page is loaded in a browser, the HLive JavaScript library will kick into action.

The first thing the JavaScript will do is establish a WebSocket connection to the server. This connection is made using the same URL with ?hlive=1 added to the URL. Due to typical load balancing strategies, the server that HLive establishes a Websocket connection to may not be the one that generated the SSR Page.

PageSession

When the JavaScript establishes the WebSocket connection, the backend will create a new session and send down the session id to the browser.

A PageSession represents a single instance of a Page. There will be a single WebSocket connection to a PageSession.

PageServer

The PageServer is what handles incoming HTTP requests. It's an http.Handler, so it can be used in your router of choice. When PageServer receives a request, if the request has the hlive=1 query parameter, it will start the WebSocket flow. It will create a new instance of your Page. It will then make a new PageSession. Finally, it will pass the request to Page ServerWS function.

If not, then it will create a new Page, generate a complete a SSR page render and return that and discard that Page.

Middleware

It's possible to wrap PageServer in middleware. You can add data to the context like normal. The context will be passed to your Component's Mount function if it has one.

PageSessionStore

To manage your all the PageSessions PageServer uses a PageSessionStore. By default, each page gets its own PageSessionStore, but it's recommended that you have a single PageSessionStore that's shared by all your Pages on a server.

PageSessionStore can control the number of active PageSessions you have at one time. This control can prevent your servers from becoming overloaded. Once the PageSession limit is reached, PageSessionStore will make incoming WebSocket requests wait for an existing connection to disconnect.

HTTP vs WebSocket Render

Mount is not called on SSR requests but is called on WebSocket requests.

Tree and Tree Copy

Tree describes a Node and all it's child Nodes.

Tree copy is a critical process that takes your Page's Tree and makes a simplified clone of it. Once done, the only elements in the cloned Tree are Tags and Attributes.

WebSocket Render and Tree Diffing

When it's time to do a WebSocket render, no HTML is rendered (1). What happens is a new Tree Copy is created from the Page. This Tree is compared to the Tree that's in that should be in the browser. The differences are calculated, and instructions are sent to the browser on updating its DOM with our new Tree.

(1) except Attributes, but that's just convenient data format.

First WebSocket Render

When a WebSocket connection is successfully established, we need to do 2 Page renders. The first is to duplicate what should be in the browser. This render will be creating a Tree Copy as if it were going to be an SSR render. This Tree is then set as the "current" Tree. Then a WebSocket Tree Copy is made. This copy will contain several attributes not present in the HTML Tree. Also, each Component in the Tree that implements Mounter will be called with the context, meaning the Tree may also have more detail based on any data fetched. This render will then be diffed against the "current" Tree and the diff instructions sent to the browser like normal.

For an initial, successful Page load there will be 3 renders, 2 HTML renders and a WebSocket render.

AutoRender and Manuel Render

By default, HLive's Component will do a render every time an EventBinding is triggered.

This behaviour can be turned off on Component by setting AutoRender to false.

If you set AutoRender to false you can manually trigger a WebSocket render by calling hlive.Render(ctx context.Context) with the context passed to your handler.

Local Render

If you want only to render a single Component and not the whole page, you can call hlive.RenderComponent(ctx context.Context, comp Componenter) you will also want to set any relevant Components to AutoRender false.

Differ

TODO: What is it and how does it work

Render

TODO: What is it

HTML Type

HLive's HTML type is a special string type that will render what you've set. One rule is that the HTML in HTML have a single root node.

JavaScript

The goal of HLive is not to require the developer to need to write any JavaScript. As such, we have unique solutions for things like giving fields focus.

Nothing is preventing the developer from adding their JavaScript. If JavaScript changes the DOM in the browser, you could cause HLive's diffing to stop working. This is also true in libraries like ReactJS.

Virtual DOM, Browser DOM

HLive is blind to what the actual state of the browser's DOM is. It assumes that it what it has set it to.

Lifecycle

TODO

Known Issues

Invalid HTML

If you use invalid HTML typically by using HTML where you should not, the browser will ignore the HTML and not add it to the browsers DOM. If the element were something like a span tag then it may not be perceivable that it's happened. If this happens then the path finding for these tags, and it's children will not work or will work strangely.

We don't have HTML validation rules in HLive, so there is no way of warning you of this being the problem.

Browser Quirks

Browsers are complex things and sometimes act in unexpected ways. For example, if you have a table without a table body tag (tbody) some browsers will add a tbody to the DOM. This breaks HLives element path finding. Another example is that if you have multiple text nodes next to each other, some browsers will combine them.

We'll try and account for this where we can by mimicking the browser's behavior when doing a Tree Copy. We've done this be the text quirk but not the tbody quirk yet.

Inspiration

Phoenix LiveView

For the concept of server-side rendering for dynamic applications.

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html

gomponents

For it's HTML API.

https://github.com/maragudk/gomponents

ReactJS and JSX

For its component approach and template system.

https://reactjs.org/

Similar Projects

GoLive

https://github.com/brendonmatos/golive

Live views for GoLang with reactive HTML over WebSockets

live

https://github.com/jfyne/live

Live views and components for golang

TODO

v0.2.0

  • Race conditions in examples
  • Update docs based on API changes
  • Add SSR example
API Change
Bugs
  • Need to reflect in the browser virtual DOM that a select option has become selected when a user selects it
    • So that we can reset the selection (e.g. move dropdowns)
  • Can read a POST but can't pass POST data to a render (display errors)
    • Makes Auth logins an issue
    • Workaround it to go a redirect with an url param
  • Preempt disable on click prevents form submit in Chrome
Internal improvements
Groups
  • Add the Grouper interface
    • func GetGroup() []interface{}
    • Add the NoneNodeElementsGroup
Page Pipeline
  • HTTP request w, r
Performance
Other
  • Add log level to client side logging
  • Send config for debug, and log level down to client side
    • Set via an attribute
Can we make a hash of a simplified DOM tree?
  • If that page hash is not found in the cache then we need a fallback
    • Force a browser reload with a new hash?
  • Need a way to know that the version of HLive has changed, if so need a hard page reload and cache bypass
Add support for Wails
  • would be a JS binding for reading incoming messages what just blocks when waiting for a message
  • another binding sending messages
Tests
Performance
  • Need a way to test performance improvement ideas
  • Why are large tables of data slow to page?
    • It's faster to delete all the rows first
    • Can we add a way for a component like List to inform tree copy not to bother doing a diff and just do a full HTML replacement
    • If we check for hid and they are different, then do an HTML replace
Docs
  • Add initial page sync to concepts
    • An input needs to have binding for this to work
  • Add one page one user to concepts
  • How to debug in browser
  • How on mount render order issues
    • Try to update an element that has already been processed the diff will not be noticed
    • Use the dirty tree error?
  • Logging
  • Plugins
  • Preempt pattern
  • Event bubbling
  • Prevent default
  • Stop propagation
  • Explain performance goals
    • Explain why WASM is not a good fit for the goals
  • From the beginning tech intro - https://www.reddit.com/r/golang/comments/w5v4oe/comment/ihcm8i9/?utm_source=share&utm_medium=web2x&context=3
  • Page hooks
Security
New Features/Improvements
  • Look for a CSS class to show on a failed reconnect

    • Set current z-index higher than Bulma menu for default disconnect layer
  • Allow adding mount and unmount function as elements?

  • Add support for "key" to allow better diff logic for lists

    • Use hid
  • Add a func() *l.NodeGroup value

    • Reduce code count
    • Does it solve any real issues
  • ComponentList

    • Operations by ID
      • Get by ID
      • Remove By ID
  • User friendly HTTP error pages

    • Display a request ID if it exits
  • Add can take a func() string this would be kept in the tree and re-run on each render

    • Could be expensive
Multi file upload using WS and HTTP
  • Need a count of files
  • Group them together in an event?
  • Make a channel?
  • File upload progress?
Visibility
  • Is a component visible?
  • Trigger event when visible?
  • Scroll events
    • Page position
    • Viewport

Contributing

Contributions welcome

Run tests
Setup

Install Play wright Go

make install-test

or

go run github.com/playwright-community/playwright-go/cmd/playwright install --with-deps
Run
go test ./...

Documentation

Index

Constants

View Source
const (
	PageHashAttr     = "data-hlive-hash"
	PageHashAttrTmpl = "{data-hlive-hash}"
)
View Source
const (
	PipelineProcessorKeyStripHLiveAttrs      = "hlive_strip_hlive_attr"
	PipelineProcessorKeyRenderer             = "hlive_renderer"
	PipelineProcessorKeyEventBindingCache    = "hlive_eb"
	PipelineProcessorKeyAttributePluginMount = "hlive_attr_mount"
	PipelineProcessorKeyMount                = "hlive_mount"
	PipelineProcessorKeyUnmount              = "hlive_unmount"
	PipelineProcessorKeyConvertToString      = "hlive_conv_str"
)
View Source
const (
	AttrID     = "hid"
	AttrOn     = "hon"
	AttrUpload = "data-hlive-upload"
)

HLive special attributes

View Source
const (
	HTML5DocType                      HTML = "<!doctype html>"
	WebSocketDisconnectTimeoutDefault      = time.Second * 5
	PageSessionLimitDefault                = 1000
	PageSessionGarbageCollectionTick       = time.Second
)

Defaults

View Source
const EventBindingsCacheDefault = 10 // Default for a small page
View Source
const PreventDefaultAttributeName = "data-hlive-pd"
View Source
const StopPropagationAttributeName = "data-hlive-sp"

Variables

View Source
var ErrDOMInvalidated = errors.New("dom invalidated")
View Source
var (
	ErrRenderElement = errors.New("attempted to render an unrecognised element")
)

Public errors

Logger is a global logger used when a logger is not available

View Source
var LoggerDev zerolog.Logger

LoggerDev is a global logger needed for developer warnings to avoid the need for panics

View Source
var PageJavaScript []byte
View Source
var PreventDefaultJavaScript []byte
View Source
var StopPropagationJavaScript []byte

Functions

func CallerStackStr added in v0.2.0

func CallerStackStr() string

func IsElement

func IsElement(el any) bool

IsElement returns true is the pass value is a valid Element.

An Element is anything that cna be rendered at HTML.

func IsNode

func IsNode(node any) bool

IsNode returns true is the pass value is a valid Node.

A Node is a value that could be rendered as HTML by itself. An int for example can be converted to a string which is valid HTML. An attribute would not be valid and doesn't make sense to cast to a string.

func IsNonNodeElement

func IsNonNodeElement(el any) bool

func PageOptionCache added in v0.2.0

func PageOptionCache(cache Cache) func(*Page)

func PageOptionEventBindingCache added in v0.2.0

func PageOptionEventBindingCache(m *hashmap.Map[string, *EventBinding]) func(*Page)

func PreventDefaultRemove

func PreventDefaultRemove(tag Adder)

func Render

func Render(ctx context.Context)

Render will trigger a WebSocket render for the current page

func RenderComponent

func RenderComponent(ctx context.Context, comp Componenter)

RenderComponent will trigger a WebSocket render for the current page from the passed Componenter down only

func StopPropagationRemove

func StopPropagationRemove(tag Adder)

Types

type Adder

type Adder interface {
	// Add elements to a Tagger
	Add(elements ...any)
}

Adder interface for inputting elements to Tagger type values.

type Attribute

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

Attribute represents an HTML attribute e.g. id="submitBtn"

func NewAttribute

func NewAttribute(name string, value string) *Attribute

NewAttribute create a new Attribute

func NewAttributeLockBox added in v0.2.0

func NewAttributeLockBox(name string, value *LockBox[string]) *Attribute

NewAttributeLockBox create a new Attribute using the passed LockBox value

func (*Attribute) Clone

func (a *Attribute) Clone() *Attribute

Clone creates a new Attribute using the data from this Attribute

func (*Attribute) GetName added in v0.2.0

func (a *Attribute) GetName() string

func (*Attribute) GetValue

func (a *Attribute) GetValue() string

func (*Attribute) IsNoEscapeString added in v0.2.0

func (a *Attribute) IsNoEscapeString() bool

func (*Attribute) MarshalMsgpack added in v0.2.0

func (a *Attribute) MarshalMsgpack() ([]byte, error)

func (*Attribute) SetNoEscapeString added in v0.2.0

func (a *Attribute) SetNoEscapeString(noEscapeString bool)

func (*Attribute) SetValue

func (a *Attribute) SetValue(value string)

func (*Attribute) UnmarshalMsgpack added in v0.2.0

func (a *Attribute) UnmarshalMsgpack(b []byte) error

type AttributePluginer

type AttributePluginer interface {
	Attributer

	// Initialize will only be called once per attribute name for diff render
	Initialize(page *Page)
	// InitializeSSR will only be called once per attribute name for server side render
	InitializeSSR(page *Page)
}

type Attributer

type Attributer interface {
	GetName() string
	GetValue() string
	IsNoEscapeString() bool
	Clone() *Attribute
}

func StopPropagation

func StopPropagation() Attributer

type Attrs

type Attrs map[string]string

Attrs is a helper for adding and updating Attributes to nodes

func (Attrs) GetAttributers added in v0.2.0

func (a Attrs) GetAttributers() []Attributer

type AttrsLockBox added in v0.2.0

type AttrsLockBox map[string]*LockBox[string]

func (AttrsLockBox) GetAttributers added in v0.2.0

func (a AttrsLockBox) GetAttributers() []Attributer

type AttrsOff added in v0.2.0

type AttrsOff []string

AttrsOff a helper for removing Attributes

type Cache added in v0.2.0

type Cache interface {
	Get(key any) (value any, hit bool)
	Set(key any, value any)
}

Cache allow cache adapters to be used in HLive

type Class

type Class string

TODO: add tests and docs

type ClassBool

type ClassBool map[string]bool

ClassBool a special Attribute for working with CSS classes on nodes using a bool to toggle them on and off. It supports turning them on and off and allowing overriding. Due to how Go maps work the order of the classes in the map is not preserved. All Classes are de-duped, overriding a Class by adding new ClassBool will result in the old Class getting updated. You don't have to use ClassBool to add a class attribute, but it's the recommended way to do it.

type ClassList

type ClassList []string

TODO: add tests and docs

type ClassListOff

type ClassListOff []string

TODO: add tests and docs

type ClassOff

type ClassOff string

TODO: add tests and docs

type Component

type Component struct {
	*Tag

	AutoRender bool
	// contains filtered or unexported fields
}

Component is the default implementation of Componenter.

func C

func C(name string, elements ...any) *Component

C is a shortcut for NewComponent.

NewComponent is a constructor for Component.

You can add zero or many Attributes and Tags

func NewComponent

func NewComponent(name string, elements ...any) *Component

NewComponent is a constructor for Component.

You can add zero or many Attributes and Tags.

func W

func W(tag *Tag, elements ...any) *Component

W is a shortcut for Wrap.

Wrap takes a Tag and creates a Component with it.

func Wrap

func Wrap(tag *Tag, elements ...any) *Component

Wrap takes a Tag and creates a Component with it.

func (*Component) Add

func (c *Component) Add(elements ...any)

Add an element to this Component.

This is an easy way to add anything.

func (*Component) GetEventBinding

func (c *Component) GetEventBinding(id string) *EventBinding

GetEventBinding will return an EventBinding that exists directly on this element, it doesn't check its children. Returns nil is not found.

func (*Component) GetEventBindings

func (c *Component) GetEventBindings() []*EventBinding

GetEventBindings returns all EventBindings for this component, not it's children.

func (*Component) GetID

func (c *Component) GetID() string

GetID returns this component's unique ID

func (*Component) IsAutoRender

func (c *Component) IsAutoRender() bool

IsAutoRender indicates if this component should trigger "Auto Render"

func (*Component) RemoveEventBinding

func (c *Component) RemoveEventBinding(id string)

RemoveEventBinding removes an EventBinding that matches the passed ID.

No error if the passed id doesn't match an EventBinding. It doesn't check its children.

func (*Component) SetID added in v0.2.0

func (c *Component) SetID(id string)

SetID component's unique ID

type ComponentMountable

type ComponentMountable struct {
	*Component
	// contains filtered or unexported fields
}

func CM

func CM(name string, elements ...any) *ComponentMountable

CM is a shortcut for NewComponentMountable

func NewComponentMountable

func NewComponentMountable(name string, elements ...any) *ComponentMountable

func WM

func WM(tag *Tag, elements ...any) *ComponentMountable

WM is a shortcut for WrapMountable.

func WrapMountable

func WrapMountable(tag *Tag, elements ...any) *ComponentMountable

WrapMountable takes a Tag and creates a Component with it.

func (*ComponentMountable) AddTeardown

func (c *ComponentMountable) AddTeardown(teardown func())

func (*ComponentMountable) Mount

func (c *ComponentMountable) Mount(ctx context.Context)

func (*ComponentMountable) SetMount added in v0.2.0

func (c *ComponentMountable) SetMount(mount func(ctx context.Context))

func (*ComponentMountable) SetUnmount added in v0.2.0

func (c *ComponentMountable) SetUnmount(unmount func(ctx context.Context))

func (*ComponentMountable) Teardown

func (c *ComponentMountable) Teardown()

func (*ComponentMountable) Unmount

func (c *ComponentMountable) Unmount(ctx context.Context)

type Componenter

type Componenter interface {
	UniqueTagger
	// GetEventBinding returns a binding by its id
	GetEventBinding(id string) *EventBinding
	// GetEventBindings returns all event bindings for this tag
	GetEventBindings() []*EventBinding
	// RemoveEventBinding remove an event binding from this component
	RemoveEventBinding(id string)
	// IsAutoRender indicates if the page should rerender after an event binding on this tag is called
	IsAutoRender() bool
}

Componenter builds on UniqueTagger and adds the ability to handle events.

type CtxKey

type CtxKey string
const (
	CtxRender          CtxKey = "render"
	CtxRenderComponent CtxKey = "render_comp"
)

Context keys

type DOM

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

func NewDOM

func NewDOM() DOM

func (DOM) Body

func (dom DOM) Body() Adder

func (DOM) DocType

func (dom DOM) DocType() HTML

func (DOM) HTML

func (dom DOM) HTML() Adder

func (DOM) Head

func (dom DOM) Head() Adder

func (DOM) Meta

func (dom DOM) Meta() Adder

func (DOM) Title

func (dom DOM) Title() Adder

type Diff

type Diff struct {
	// Root element, where to start the path search from, "doc" is a special case that means the browser document
	Root string
	// Position of each child
	Path      string
	Type      DiffType
	Tag       Tagger
	Text      *string
	Attribute *Attribute
	HTML      *HTML
	// Not used for render but for Lifecycle events
	Old any
}

Diff Diffs are from old to new

type DiffType

type DiffType string
const (
	DiffUpdate DiffType = "u"
	DiffCreate DiffType = "c"
	DiffDelete DiffType = "d"
)

Diff types

type Differ

type Differ struct {
	JavaScript []byte
	// contains filtered or unexported fields
}

func NewDiffer

func NewDiffer() *Differ

func (*Differ) SetLogger

func (d *Differ) SetLogger(logger zerolog.Logger)

func (*Differ) Trees

func (d *Differ) Trees(selector, path string, oldNode, newNode any) ([]Diff, error)

Trees diff two node tress

Path: childIndex>childIndex Path: 0>1>3

After tree copy you only have Tagger (with []Attribute), HTML, and strings. Then can be grouped in a NodeGroup

type ElementGroup

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

ElementGroup is a Group of Elements

func E

func E(elements ...any) *ElementGroup

E is shorthand for Elements.

Groups zero or more Element values.

func Elements

func Elements(elements ...any) *ElementGroup

Elements groups zero or more Element values.

func (*ElementGroup) Add

func (g *ElementGroup) Add(elements ...any)

func (*ElementGroup) Get

func (g *ElementGroup) Get() []any

type Event

type Event struct {
	// The binding that was listening for this event
	Binding *EventBinding
	// If an input has a value set by the browsers on page load, different to the inputs value attribute this type of
	// event is sent. This typically happens on page reload after data has been inputted to a field.
	IsInitial bool
	// The value of the field, if relevant
	Value string
	// Used when an event source could have multiple values
	Values []string
	// Selected is true, for the element interacted with, if a radio or checkbox is checked or a select option is selected.
	// Most relevant for checkbox as it always has a value, this lets you know if they are currently checked or not.
	Selected bool
	// TODO: move to nillable value
	// Key related values are only used on keyboard related events
	Key      string
	CharCode int
	KeyCode  int
	ShiftKey bool
	AltKey   bool
	CtrlKey  bool
	// Used for file inputs and uploads
	File *File
	// Extra, for non-browser related data, for use by plugins
	Extra map[string]string
}

type EventBinding

type EventBinding struct {
	// Unique ID for this binding
	ID string
	// Function to call when binding is triggered
	Handler EventHandler
	// Component we are bound to
	Component Componenter
	// Call this binding once then discard it
	Once bool
	// Name of the JavaScript event that will trigger this binding
	Name string
}

func NewEventBinding

func NewEventBinding() *EventBinding

func On

func On(name string, handler EventHandler) *EventBinding

func OnOnce

func OnOnce(name string, handler EventHandler) *EventBinding

type EventHandler

type EventHandler func(ctx context.Context, e Event)

type File

type File struct {
	// File name
	Name string
	// Size of the file in bytes
	Size int
	// Mime type
	Type string
	// The file contents
	Data []byte
	// Which file is this in the total file count, 0 index
	Index int
	// How many files are being uploaded in total
	Total int
}

type GetTagger added in v0.2.1

type GetTagger interface {
	GetTagger() Tagger
}

TODO: add tests

type HTML

type HTML string

HTML must always have a single root element, as we count it as 1 node in the tree but the browser will not if you have multiple root elements

func (*HTML) MarshalMsgpack added in v0.2.0

func (e *HTML) MarshalMsgpack() ([]byte, error)

func (*HTML) UnmarshalMsgpack added in v0.2.0

func (e *HTML) UnmarshalMsgpack(b []byte) error

type LockBox added in v0.2.0

type LockBox[V any] struct {
	// contains filtered or unexported fields
}

func NewLockBox added in v0.2.0

func NewLockBox[V any](val V) *LockBox[V]

func (*LockBox[V]) Get added in v0.2.0

func (b *LockBox[V]) Get() V

func (*LockBox[V]) GetLockedAny added in v0.2.0

func (b *LockBox[V]) GetLockedAny() any

func (*LockBox[V]) Lock added in v0.2.0

func (b *LockBox[V]) Lock(f func(val V) V)

func (*LockBox[V]) Set added in v0.2.0

func (b *LockBox[V]) Set(val V)

type LockBoxer added in v0.2.0

type LockBoxer interface {
	GetLockedAny() any
}

type MessageWS

type MessageWS struct {
	Message  []byte
	IsBinary bool
}

type Mounter

type Mounter interface {
	UniqueTagger
	// Mount is called after a component is mounted
	Mount(ctx context.Context)
}

Mounter wants to be notified after it's mounted.

type NodeBox added in v0.2.0

type NodeBox[V any] struct {
	*LockBox[V]
}

func Box added in v0.2.0

func Box[V any](node V) *NodeBox[V]

func (NodeBox[V]) GetNode added in v0.2.0

func (b NodeBox[V]) GetNode() any

type NodeBoxer added in v0.2.0

type NodeBoxer interface {
	GetNode() any
}

type NodeGroup

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

NodeGroup is a Group of Nodes

func G

func G(nodes ...any) *NodeGroup

G is shorthand for Group.

Group zero or more Nodes together.

func Group

func Group(nodes ...any) *NodeGroup

Group zero or more Nodes together.

func (*NodeGroup) Add

func (g *NodeGroup) Add(nodes ...any)

func (*NodeGroup) Get

func (g *NodeGroup) Get() []any

Get returns all nodes, dereferences any valid pointers

func (*NodeGroup) MarshalMsgpack added in v0.2.0

func (g *NodeGroup) MarshalMsgpack() ([]byte, error)

func (*NodeGroup) UnmarshalMsgpack added in v0.2.0

func (g *NodeGroup) UnmarshalMsgpack(b []byte) error

type Page

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

func NewPage

func NewPage(options ...PageOption) *Page

func (*Page) Close

func (p *Page) Close(ctx context.Context)

func (*Page) DOM

func (p *Page) DOM() DOM

func (*Page) DOMBrowser

func (p *Page) DOMBrowser() any

func (*Page) GetBrowserNodeByID

func (p *Page) GetBrowserNodeByID(id string) *Tag

func (*Page) GetNodes

func (p *Page) GetNodes() *NodeGroup

func (*Page) GetSessionID

func (p *Page) GetSessionID() string

func (*Page) HookAfterRenderAdd added in v0.2.0

func (p *Page) HookAfterRenderAdd(hook func(ctx context.Context, diffs []Diff, send chan<- MessageWS))

func (*Page) HookBeforeEventAdd added in v0.2.0

func (p *Page) HookBeforeEventAdd(hook func(context.Context, Event) (context.Context, Event))

func (*Page) HookBeforeMountAdd added in v0.2.0

func (p *Page) HookBeforeMountAdd(hook func(context.Context, *Page))

func (*Page) HookCloseAdd added in v0.2.0

func (p *Page) HookCloseAdd(hook func(context.Context, *Page))

func (*Page) HookMountAdd added in v0.2.0

func (p *Page) HookMountAdd(hook func(context.Context, *Page))

func (*Page) HookUnmountAdd added in v0.2.0

func (p *Page) HookUnmountAdd(hook func(context.Context, *Page))

func (*Page) PipelineDiff

func (p *Page) PipelineDiff() *Pipeline

func (*Page) PipelineSSR

func (p *Page) PipelineSSR() *Pipeline

func (*Page) RunDiffPipeline added in v0.2.0

func (p *Page) RunDiffPipeline(ctx context.Context, w io.Writer) (*NodeGroup, error)

func (*Page) RunRenderPipeline added in v0.2.0

func (p *Page) RunRenderPipeline(ctx context.Context, w io.Writer) (*NodeGroup, error)

func (*Page) ServeHTTP

func (p *Page) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Page) ServeWS

func (p *Page) ServeWS(ctx context.Context, sessID string, send chan<- MessageWS, receive <-chan MessageWS) error

func (*Page) SetDOMBrowser added in v0.2.0

func (p *Page) SetDOMBrowser(dom any)

func (*Page) SetLogger

func (p *Page) SetLogger(logger zerolog.Logger)

type PageOption added in v0.2.0

type PageOption func(*Page)

type PageServer

type PageServer struct {
	Sessions *PageSessionStore
	Upgrader websocket.Upgrader
	// contains filtered or unexported fields
}

func NewPageServer

func NewPageServer(pf func() *Page) *PageServer

func NewPageServerWithSessionStore

func NewPageServerWithSessionStore(pf func() *Page, sess *PageSessionStore) *PageServer

func (*PageServer) ServeHTTP

func (s *PageServer) ServeHTTP(w http.ResponseWriter, r *http.Request)

type PageSession

type PageSession struct {
	// Buffered channel of outbound messages.
	Send chan MessageWS
	// Buffered channel of inbound messages.
	Receive chan MessageWS
	// contains filtered or unexported fields
}

func (*PageSession) GetContextInitial added in v0.2.0

func (sess *PageSession) GetContextInitial() context.Context

func (*PageSession) GetContextPage added in v0.2.0

func (sess *PageSession) GetContextPage() context.Context

func (*PageSession) GetID added in v0.2.0

func (sess *PageSession) GetID() string

func (*PageSession) GetInitialContextCancel added in v0.2.0

func (sess *PageSession) GetInitialContextCancel() context.CancelFunc

func (*PageSession) GetPage added in v0.2.0

func (sess *PageSession) GetPage() *Page

func (*PageSession) GetPageContextCancel added in v0.2.0

func (sess *PageSession) GetPageContextCancel() context.CancelFunc

func (*PageSession) IsConnected added in v0.2.0

func (sess *PageSession) IsConnected() bool

func (*PageSession) SetConnected added in v0.2.0

func (sess *PageSession) SetConnected(connected bool)

func (*PageSession) SetContextCancel added in v0.2.0

func (sess *PageSession) SetContextCancel(cancel context.CancelFunc)

func (*PageSession) SetContextPage added in v0.2.0

func (sess *PageSession) SetContextPage(ctx context.Context)

func (*PageSession) SetPage added in v0.2.0

func (sess *PageSession) SetPage(page *Page)

type PageSessionStore

type PageSessionStore struct {
	DisconnectTimeout time.Duration
	SessionLimit      uint32

	GarbageCollectionTick time.Duration
	Done                  chan bool
	// contains filtered or unexported fields
}

func NewPageSessionStore

func NewPageSessionStore() *PageSessionStore

func (*PageSessionStore) Delete

func (pss *PageSessionStore) Delete(id string)

func (*PageSessionStore) GarbageCollection

func (pss *PageSessionStore) GarbageCollection()

func (*PageSessionStore) Get

func (pss *PageSessionStore) Get(id string) *PageSession

func (*PageSessionStore) GetSessionCount

func (pss *PageSessionStore) GetSessionCount() int

func (*PageSessionStore) New

func (pss *PageSessionStore) New() *PageSession

New PageSession.

type PipeAttributerHandler

type PipeAttributerHandler func(ctx context.Context, w io.Writer, tag Attributer) (Attributer, error)

type PipeNodeHandler

type PipeNodeHandler func(ctx context.Context, w io.Writer, node any) (any, error)

type PipeNodegroupHandler

type PipeNodegroupHandler func(ctx context.Context, w io.Writer, node *NodeGroup) (*NodeGroup, error)

type PipeTagHandler

type PipeTagHandler func(ctx context.Context, w io.Writer, tag *Tag) (*Tag, error)

type PipeTaggerHandler

type PipeTaggerHandler func(ctx context.Context, w io.Writer, tagger Tagger) (Tagger, error)

type Pipeline

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

func NewPipeline

func NewPipeline(pps ...*PipelineProcessor) *Pipeline

func (*Pipeline) Add

func (p *Pipeline) Add(processors ...*PipelineProcessor)

func (*Pipeline) AddAfter

func (p *Pipeline) AddAfter(processorKey string, processors ...*PipelineProcessor)

func (*Pipeline) AddBefore

func (p *Pipeline) AddBefore(processorKey string, processors ...*PipelineProcessor)

func (*Pipeline) RemoveAll

func (p *Pipeline) RemoveAll()

type PipelineProcessor

type PipelineProcessor struct {
	// Will replace an existing processor with the same key. An empty string won't error.
	Key             string
	Disabled        bool
	BeforeWalk      PipeNodegroupHandler
	OnSimpleNode    PipeNodeHandler
	BeforeTagger    PipeTaggerHandler
	BeforeAttribute PipeAttributerHandler
	AfterAttribute  PipeAttributerHandler
	AfterTagger     PipeTagHandler
	AfterWalk       PipeNodegroupHandler
}

func NewPipelineProcessor

func NewPipelineProcessor(key string) *PipelineProcessor

func PipelineProcessorAttributePluginMount

func PipelineProcessorAttributePluginMount(page *Page) *PipelineProcessor

func PipelineProcessorAttributePluginMountSSR

func PipelineProcessorAttributePluginMountSSR(page *Page) *PipelineProcessor

func PipelineProcessorConvertToString

func PipelineProcessorConvertToString() *PipelineProcessor

func PipelineProcessorEventBindingCache

func PipelineProcessorEventBindingCache(cache *hashmap.Map[string, *EventBinding]) *PipelineProcessor

func PipelineProcessorMount

func PipelineProcessorMount() *PipelineProcessor

func PipelineProcessorRenderHashAndCache added in v0.2.0

func PipelineProcessorRenderHashAndCache(logger zerolog.Logger, renderer *Renderer, cache Cache) *PipelineProcessor

PipelineProcessorRenderHashAndCache that will cache the returned tree to support SSR

func PipelineProcessorRenderer

func PipelineProcessorRenderer(renderer *Renderer) *PipelineProcessor

func PipelineProcessorUnmount

func PipelineProcessorUnmount(page *Page) *PipelineProcessor

type PreventDefaultAttribute

type PreventDefaultAttribute struct {
	*Attribute
}

func PreventDefault

func PreventDefault() *PreventDefaultAttribute

func (*PreventDefaultAttribute) Initialize

func (a *PreventDefaultAttribute) Initialize(page *Page)

func (*PreventDefaultAttribute) InitializeSSR

func (a *PreventDefaultAttribute) InitializeSSR(page *Page)

type Renderer

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

func NewRenderer

func NewRenderer() *Renderer

func (*Renderer) Attribute

func (r *Renderer) Attribute(attrs []Attributer, w io.Writer) error

Attribute renders an Attribute to it's HTML string representation While it's possible to have HTML attributes without values it simplifies things if we always have a value

func (*Renderer) HTML

func (r *Renderer) HTML(w io.Writer, el any) error

HTML renders items that can be render to valid HTML nodes

func (*Renderer) SetLogger

func (r *Renderer) SetLogger(logger zerolog.Logger)

type StopPropagationAttribute

type StopPropagationAttribute struct {
	*Attribute
}

func (*StopPropagationAttribute) Initialize

func (a *StopPropagationAttribute) Initialize(page *Page)

func (*StopPropagationAttribute) InitializeSSR

func (a *StopPropagationAttribute) InitializeSSR(page *Page)

type Style

type Style map[string]string

Style is a special Element that allows you to work the properties of style attribute. A property and value will be added or updated. You don't have to use Style to add a style attribute, but it's the recommended way to do it.

type StyleLockBox added in v0.2.0

type StyleLockBox map[string]*LockBox[string]

StyleLockBox like Style but, you can update the property values indirectly TODO: add test

type StyleOff added in v0.2.0

type StyleOff []string

StyleOff remove an existing style property, ignored if the property doesn't exist TODO: add test

type Tag

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

Tag is the default HTML tag implementation.

Use T or NewTag to create a value.

func NewTag

func NewTag(name string, elements ...any) *Tag

NewTag creates a new Tag value.

func T

func T(name string, elements ...any) *Tag

T is a shortcut for NewTag.

NewTag creates a new Tag value.

func (*Tag) Add

func (t *Tag) Add(element ...any)

Add zero or more elements to this Tag.

func (*Tag) AddAttributes

func (t *Tag) AddAttributes(attrs ...any)

AddAttributes will add zero or more attributes types (Attributer, Attribute, Attrs, Style, ClassBool).

Adding an attribute with the same name will override an existing attribute.

func (*Tag) GetAttribute

func (t *Tag) GetAttribute(name string) Attributer

GetAttribute returns an Attributer value by its name.

This includes attribute values related to Class, and Style. If an Attributer of the passed name has not been set `nil` it's returned.

func (*Tag) GetAttributeValue

func (t *Tag) GetAttributeValue(name string) string

GetAttributeValue returns a value for a given Attributer name.

If an attribute has not yet been set, then an empty string is returned.

func (*Tag) GetAttributes

func (t *Tag) GetAttributes() []Attributer

GetAttributes returns a list of Attributer values that this tag has.

Any Class, Style values are returned here as Attribute values.

func (*Tag) GetName

func (t *Tag) GetName() string

GetName get the tag name.

func (*Tag) GetNodes

func (t *Tag) GetNodes() *NodeGroup

GetNodes returns a NodeGroup with any child Nodes that have been added to this Node.

func (*Tag) IsNil

func (t *Tag) IsNil() bool

IsNil returns true if pointer is nil

func (*Tag) IsVoid

func (t *Tag) IsVoid() bool

IsVoid indicates if this is a void type tag, e.g. `<hr>`.

func (*Tag) MarshalMsgpack added in v0.2.0

func (t *Tag) MarshalMsgpack() ([]byte, error)

func (*Tag) RemoveAttributes

func (t *Tag) RemoveAttributes(names ...string)

func (*Tag) SetName

func (t *Tag) SetName(name string)

SetName sets the tag name, e.g. for a `<div>` it's the `div` part.

func (*Tag) SetVoid

func (t *Tag) SetVoid(void bool)

SetVoid sets the tag to be a void type tag e.g. `<hr>`.

func (*Tag) UnmarshalMsgpack added in v0.2.0

func (t *Tag) UnmarshalMsgpack(b []byte) error

type Tagger

type Tagger interface {
	// GetName returns a tag's name. For example <hr>'s name is hr.
	GetName() string
	// GetAttributes returns all attributes for this tag.
	GetAttributes() []Attributer
	// GetNodes returns this tags children nodes, to be rendered inside this tag.
	GetNodes() *NodeGroup
	// IsVoid indicates if this has a closing tag or not. Void tags don't have a closing tag.
	IsVoid() bool
	// IsNil returns true if pointer is nil.
	//
	// It's easy to create something like `var t *Tag` but forget to give it a value.
	// This allows us to not have panics in that case.
	IsNil() bool
}

Tagger represents a static HTML tag.

type Teardowner

type Teardowner interface {
	UniqueTagger
	// AddTeardown adds a teardown function
	AddTeardown(teardown func())
	// Teardown call the set teardown function passed in SetTeardown
	Teardown()
}

Teardowner wants to have manual control when it needs to be removed from a Page. If you have a Mounter or Unmounter that will be permanently removed from a Page they must call the passed function to clean up their references.

type UniqueAdder

type UniqueAdder interface {
	Adder
	// GetID will return a unique id
	GetID() string
}

UniqueAdder is an Adder that can be uniquely identified in a DOM Tree.

type UniqueTagger

type UniqueTagger interface {
	Tagger
	// GetID will return a unique id
	GetID() string
	// SetID Components will be assigned a unique id
	SetID(id string)
}

UniqueTagger is a Tagger that can be uniquely identified in a DOM Tree.

type Unmounter

type Unmounter interface {
	UniqueTagger
	// Unmount is called before a component is unmounted
	Unmount(ctx context.Context)
}

Unmounter wants to be notified before it's unmounted.

Jump to

Keyboard shortcuts

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