hg

package module
v0.0.0-...-4faf635 Latest Latest
Warning

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

Go to latest
Published: Jul 12, 2023 License: BSD-2-Clause Imports: 20 Imported by: 2

README

hg

hg is a small html library for go, which provides some comforting utilities and encourages an event based html SSR (server-side rendering) flow style. It is compatible with the standard library and can be integrated into any router and http server setup.

Influenced by

This library has been influenced by the following patterns, theories and technologies:

Conceptual data flow

flow

Introduced tags

Like htmx, hg provides a bunch of tokens to annotate the html to make it interactive.

hg-hotreload

hg supports hot-reloading through a long-poll mechanism. To enable, set the hg-hotreload attribute to the body tag.

Example, which polls the default endpoint /version/poll:

<body hg-hotreload>
    {{template "page" .}}
</body>

Example, which polls a custom endpoint:

<body hg-hotreload="/my/custom/poll/handler">
    {{template "page" .}}
</body>
hg-event, hg-data and hg-trigger

To re-render a page, you have to tell hg what and when to send a message. So first, take any Element and put an hg-event attribute on it, which must contain a string which matches the registered case in your page handler. Internally, a normal javascript event listener is registered using the type declared in the attribute hg-trigger.

Note: This behavior will change slightly to support more complex cases like delays and message cancellations.

In your page handler configure your message and model transformation:

hg.Update(hg.Case("add", func(model PageState, msg AddEvent) PageState {
    model.Count += int(msg)
    return model
}))

Trigger the event as follows in your html template:

<button hg-event="add" hg-data="2" hg-trigger="click">Your clicked sum is {{.Count}}</button>

Note, that the hg-event and hg-trigger attributes are obligatory. The hg-data attribute is optional and depends on your interpretation of the message.

Triggers

Currently, the following triggers are supported.

Default trigger events

By default, any trigger is treated as a javascript event type and registered as such.

Polling

You can trigger an event automatically, by using the every Xs syntax. Note the magic !refresh event, which just issues an re-render request for the current page state. This issues no additional page model update.

<p hg-event="!refresh" hg-trigger="every 5s">Current Time: {{.Time}}</p>
hg-href

To trigger the DOM merging for regular navigations, you can add the hg-href attribute to any element.

<button hg-href="/go/ahead?some=thing">Go ahead</button>

Example

Take a look at the standalone hg example project.

Why?

At first, encouraging SSR in 2023 may seem to be out of time, however there is the interesting trend of all large SPA frameworks, to provide SSR concepts anyway, where the page is pre-rendered and delivered and hydrated at the client side, to get the best of both worlds. On the other hand, most SPA applications are just pure technical overkill, because not a single unique advantage is used. Take a look of this excerpt of possible features:

  • Offline and Caching Support
  • Complex interactive and responsive UI like Editors
  • faster Response-Time, due to smaller requests (usually REST)
  • Offloading Rendering to the Client
  • enforces some kind of frontend-backend architecture (typically REST or graphql as a communication protocol)
  • Optimizations only work effectively with a lot of handcrafted work, otherwise the result will usually contain everything at once
  • complex and awesome UX

And, there are also a lot of disadvantages:

  • SPA frameworks are generally incompatible across regular major version updates, which becomes really expensive over time
  • SPA frameworks require an often used supply chain attack vector to be buildable at all, e.g. npm. There are regularly massive npm security flaws and compromised third party libraries, which are used without any actually benefit (left pad or is even/is odd anyone?)
  • Today, search-engines support the execution of Javascript, but it is generally not compatible with the asynchronous workflow of SPA frameworks.
  • Without transpilers and the usage of other languages like Typescript, maintaining larger projects is infeasible.
  • Even though there have been millions of dollars and person hours already spend, Javascript has an unprofessional background without the possibility to detach from its past. This still results in a lot of intransparent optimizations, compatibility shims and re-inventions to apply any sort of workarounds. For example tree shaking (respective dead code elimination) and other compiler or link time optimizations are solved problems, which are well known and have been implemented more than 50 years ago for any reasonable language.
  • You can optimize it with a lot of effort, but the page must first load (more or less) everything anyway.
  • You have to create a REST API (or similar) without any actual need. This means more work to implement and test, so its more expensive. Also, creating meaningful APIs which are not just some repository CRUDs are not that easy.

Here we are. Even though proposing to go back to SSR looks like a step backwards and may even cause the absence of a defined (RPC-)API, it looks like we can be optimistic regarding maintainability, when considering improvements in our understanding of software architecture patterns. Our assumptions are (still to be proofed):

  • faster time to market
  • less expensive to develop
  • less expensive to maintain
  • more backend developers can be fullstack developers
  • more than good enough for low-frequently used tools, especially for domain experts
  • easier to debug and profile
  • reliable and state-of-the-art compilers and toolings
  • better architecture than the average mainstream SPA framework, especially when comparing with the typical CRUD API styles.
  • no additional harm for the software architecture in general, due to layered architectures and the application of domain-driven design patterns.

Documentation

Overview

Package hg provides utilities functions around opinionated patterns around "html for go".

Index

Constants

This section is empty.

Variables

View Source
var Assets embed.FS

Assets contains a small js shim containing the request-response utilities and the idiomorph library.

View Source
var Tailwind embed.FS

Tailwind contains a bundled CDN version of tailwind, which uses a JIT compiler to support all tailwind features on the fly. Its usage is not really recommended for production sites, however it works good enough for small things. Also, you can always decide to not include it and instead use the tailwind cli to compile a tailored version.

Functions

func Handler

func Handler[Model any](renderer ViewFunc[Model], options ...RenderOption[Model]) http.HandlerFunc

Handler builds a http handlerfunc based on a model-view transformation

func LongPollHandler

func LongPollHandler(timeout time.Duration) http.HandlerFunc

Types

type MsgHandler

type MsgHandler[Model any] interface {
	Alias() string

	Transform(in Model, r *http.Request) (Model, error)
}

A MsgHandler transforms an incoming request into a new Model state.

type Redirect

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

Redirect offers the possibility to send non-http redirection requests (JSON) to the javascript shim, which evaluates the redirection response and applies the according navigation stack operation. This kind of navigation is not possible by using standard http redirects.

func Forward

func Forward(url string) Redirect

Forward creates a new forward Redirect to the given url. Use query parameters to provide some (bookmarkable) contextual information.

func (Redirect) Redirection

func (r Redirect) Redirection() Redirect

type RenderOption

type RenderOption[Model any] func(hnd *rHnd[Model])

RenderOptions provides a package private Options pattern.

func Case

func Case[Model, Evt any](alias string, update UpdateFunc[Model, Evt]) RenderOption[Model]

Case is invoked if the message alias is matched and tries to unmarshal the form value _eventData message into a new value of type Msg. It then calls the UpdateFunc to transform the given Model into a new state. To apply navigation, see also Redirect.

func CaseWithQualifier

func CaseWithQualifier[Model, Msg any](update UpdateFunc[Model, Msg]) RenderOption[Model]

CaseWithQualifier is like Case but uses as alias the package path and the type Name of the Message.

func OnRequest

func OnRequest[Model any](f UpdReqFunc[Model]) RenderOption[Model]

OnRequest sets the first untyped transformer which is called for each request to the handler. This always happens, independently if a message has been sent. Thus, this transformer is especially helpful to inject any transitional state, which cannot be serialized, even things like contexts or use case services.

There can be only one OnRequest update function. The last call replaces the prior registered func.

func Update

func Update[Model any](messages ...MsgHandler[Model]) RenderOption[Model]

Update is the entry point to register a bunch of message handlers. See also Case and CaseWithQualifier for shortcuts. Any already registered handlers (see Alias) are replaced.

type RequestUpdateFunc

type RequestUpdateFunc[Model any] func(model Model, r *http.Request) (Model, error)

RequestUpdateFunc mutates the model by decode and applying the msg and returning an altered Model.

type TplFile

type TplFile struct {
	Name string
	Data string
	// contains filtered or unexported fields
}

func (*TplFile) RenameTplStmts

func (f *TplFile) RenameTplStmts(oldname, newname string)

RenameTplStmts replaces all {{ template "<oldname>" .}} occurrences with {{ template "<newname>" .}}

type TplFilePreprocessor

type TplFilePreprocessor func(file *TplFile)

type TplOption

type TplOption func(p *htmlParser)

func Execute

func Execute(tplName string) TplOption

Execute will invoke the given template Name instead of evaluating the entire template set anonymously.

func FS

func FS(fsys ...fs.FS) TplOption

func NamedFunc

func NamedFunc(name string, fun any) TplOption

func TplPreProc

func TplPreProc(f TplPreprocessor) TplOption

func TplPreProcEach

func TplPreProcEach(f TplFilePreprocessor) TplOption

type TplPreprocessor

type TplPreprocessor func(files []*TplFile) []*TplFile

type UpdReqFunc

type UpdReqFunc[Model any] func(r *http.Request, model Model) Model

type UpdateFunc

type UpdateFunc[Model, Evt any] func(model Model, evt Evt) Model

UpdateFunc mutates the model by applying the Msg and returning an altered Model.

type ViewFunc

type ViewFunc[Model any] func(w http.ResponseWriter, r *http.Request, model Model)

func MustParse

func MustParse[Model any](options ...TplOption) ViewFunc[Model]

func Parse

func Parse[Model any](options ...TplOption) (ViewFunc[Model], error)

func (ViewFunc[Model]) Handle

func (f ViewFunc[Model]) Handle(model Model) http.HandlerFunc

func (ViewFunc[Model]) ToString

func (f ViewFunc[Model]) ToString(model Model) string

ToString applies an empty request to invoke the ViewFunc and converts the result without validation into a string. This is mainly for debugging purposes.

type ViewHtmlFunc

type ViewHtmlFunc[Model any] func(model Model) template.HTML

ViewHtmlFunc transforms a Model into a raw HTML string.

func (ViewHtmlFunc[Model]) Handle

func (f ViewHtmlFunc[Model]) Handle(model Model) http.HandlerFunc

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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