⚙️ Concept of Golang HTML render engine with frontend components and dynamic behavior

An HTML render engine concept that brings frontend-like components experience to the server side with native html/template on steroids. Supports any serving basis (nethttp/Gin/etc), that provides io.Writer in response.

Disclaimer 1

Under heavy development, not stable (!!!)

Disclaimer 2

I'm not Golang "rockstar", and code may be not so good quality as you may expect. If you see any problems in the project - feel free to open new Issue.

TOC

Why?

I am trying to minimize the usage of popular SPA/PWA frameworks where it's not needed because it adds a lot of complexity and overhead. I don't want to bring significant runtime, VirtualDOM, and Webpack into the project with minimal dynamic frontend behavior.

This project proves the possibility of keeping most of the logic on the server's side.

What problems does it solve? Why not using plain GoKit?

While developing the website's frontend with Go, I discovered some of the downsides of this approach:

  • With plain html/template you're starting to repeat yourself. It's harder to define reusable parts.
  • You must repeat DTO calls for each page, where you're using reusable parts.
  • With Go's routines approach it's hard to make async-like DTO calls in the handlers.
  • For dynamic things, you still need to use JS and client-side DOM modification.

Complexity is much higher when all of them get combined.

This engine tries to bring components and async experience to the traditional server-side rendering.

Zen

  • Don't replace Go features that exist already.
  • Don't do work that's already done
  • Don't force developers to use a specific solution (Gin/Chi/GORM/sqlx/etc). Let them choose
  • Rely on the server to do the rendering, no JS specifics or client-side only behavior

Features

  • Component approach in mix with html/template
  • Asynchronous operations
  • Component methods that can be called from the client side (Server Side Actions, SSA)
  • Different types of component communication (parent, cross)

Quick start (simple page)

Basic page (based on Gin)

package main

import(
    "html/template"

    "github.com/gin-gonic/gin"
    "github.com/yuriizinets/go-ssc"
)

// PageIndex is an implementation of ssc.Page interface
type PageIndex struct{}

// Template is a required page method. It tells about template configuration
func (*PageIndex) Template() *template.Template {
    // Template body is located in index.html
    // <html>
    //   <body>The most basic example</body>
    // </html>
    tmpl, _ := template.New("index.html").ParseGlob("*.html")
    return tmpl
}

func main() {
    g := gin.Default()

    g.GET("/", func(c *gin.Context) {
        ssc.RenderPage(c.Writer, &PageIndex{})
    })

    g.Run("localhost:25025")
}

Basic concepts

Each page or component is represented by its own structure. For implementing specific functionality, you can use structure's methods with a predefined declaration (f.e. Init(p ssc.Page)). You need to follow declaration rules to match the interfaces required (you can find all interfaces in types.go).
Before implementing any method, you need to understand the rendering lifecycle.

Lifecycle

Each page's lifecycle is hidden under the render function and follows this steps:

  • Defining shared variables (waitgroup, errors channel)
  • Triggering the page's Init() to initialize and register components
  • Running all component's Async() functions in separate goroutines
  • Waiting untill all asynchronous operations are completed
  • Calling AfterAsync() for each component
  • Cleaning up registered components (not needed more for internal usage)
  • Getting page's template and render

Even though methods like Init() or Async() can handle your business logic like forms processing, please, try to avoid that. Keep your app's business logic inside tje handlers, and use this library only for page rendering.

Pages

To implement a page, you need to declare its structure with Template() *template.Template method. This is the only requirements. Also, each page has these optional methods:

  • Init() - used to initialize page, f.e. components registering or providing default values
  • Meta() ssc.Meta - used to provide advanced page meta, like title, description, hreflangs, etc.

Example of page

Reference page is here. Check demo for full review.

package main

import (
    "html/template"

    "github.com/yuriizinets/go-ssc"
)

type PageIndex struct {
    ComponentHttpbinUUID   ssc.Component
}

func (*PageIndex) Template() *template.Template {
    return template.Must(template.New("page.index.html").Funcs(funcmap()).ParseGlob("*.html"))
}

func (p *PageIndex) Init() {
    p.ComponentHttpbinUUID = ssc.RegC(p, &ComponentHttpbinUUID{})
}

func (*PageIndex) Meta() ssc.Meta {
    return ssc.Meta{
        Title: "SSC Example",
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    {{ meta . }}
    {{ dynamics }}
    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
    <h1 class="mt-4 text-5xl text-center">Go SSC Demo Page</h1>
    <div class="pt-16"></div>
    <h2 class="text-3xl text-center">Httpbin UUID</h2>
    <p class="text-center">UUID, fetched on the server side, asynchronously, from httpbin.org</p>
    <div class="mt-2 text-center">
        {{ template "ComponentHttpbinUUID" .ComponentHttpbinUUID }}
    </div>
</body>
</html>

Components

To implement a component, you just need to declare its structure. There are no requirements for declaring a component. Also, each component has these optional methods:

  • Init(p ssc.Page) - used to initialize component, f.e. nested components registering or providing default values
  • Async() error - method is called asynchronously with goroutines and processed concurrently during lifecycle. You can use it for fetching information from DB or API
  • AfterAsync() - method is called after all finishing all async operations
  • Actions() ActionsMap - used for providing SSA. Check Server Side Actions for details

Component example

Reference component is here. Check demo for full review.

Example of a component that fetches and displays UUID response from httpbin.org

package main

import (
    "io/ioutil"
    "net/http"

    "github.com/yuriizinets/go-ssc"
)

type ComponentHttpbinUUID struct {
    UUID string
}

// Async method is handled by library under the hood
// Each async method is called asynchronously with goroutines and processed concurrently
func (c *ComponentHttpbinUUID) Async() error {
    resp, err := http.Get("http://httpbin.org/uuid")
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }
    c.UUID = string(data)
    return nil
}

For component usage you can check example of page.

Server Side Actions

Server Side Actions (SSA) - a way to execute logic on the server side and update component's DOM. You don't need to define any custom JS to update and redraw your component, your template will be reused for this. This feature works with thin JS layer, so, you'll need to include {{ dynamics }} row in your page. Also, you'll need to register endpoint handler (ssc.SSAHandler) with prefix /SSA/ for Actions to work. As an example for built-in net/http, you need to attach handler in that way mux.HandleFunc("/SSA/", ssc.SSAHandler)

SSA Example

Reference component is here. Check demo for full review.

Example of Action

package main

import "github.com/yuriizinets/go-ssc"

type ComponentCounter struct {
    Count int
}

func (c *ComponentCounter) Actions() ssc.ActionsMap {
    return ssc.ActionsMap{
        "Increment": func(args ...interface{}) {
            c.Count++
        },
    }
}
{{ define "ComponentCounter" }}
<div {{ componentattrs . }} class="border shadow rounded p-4">
    <div class="text-2xl font-semibold">Counter Demo</div>
    <div class="py-2 w-full flex flex-col items-center">
        <div class="text-2xl">{{ .Count }}</div>
        <button onclick="{{ action `Increment` }}" class="mt-2 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded w-32">Increment</button>
    </div>
</div>
{{ end }}
Comments
  • Prototype: Multi-stage component UI update on Action

    Prototype: Multi-stage component UI update on Action

    Explore possibility to use server-sent events to deliver multiple UI updates.
    Logic must to be similar to "flush" functionality of ORM frameworks, something like kyoto.SSAFlush(p)

    Required knowledge:

    • SSE API (https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
    • SSA lifecycle theory and code (https://kyoto.codes/extended-features.html#ssa-lifecycle and https://github.com/yuriizinets/kyoto/blob/master/ext.ssa.go)
    • SSA communication layer code (https://github.com/yuriizinets/kyoto/blob/master/payload/src/dynamics.ts)

    Issue might be pretty hard to implement for people unfamiliar with project, but very interesting for those who want to explore project codebase.

  • Prototype of functional way to define pages/components

    Prototype of functional way to define pages/components

    Current working prototype looks like this:

    ...
    
    func PageIndex(b *kyoto.Builder) {
    	b.Template(func() *template.Template {
    		return template.Must(template.New("page.index.html").ParseGlob("*.html"))
    	})
    	b.Init(func() {
    		b.State.Set("Title", "Kyoto in a functional way")
    		b.Component("UUID1", ComponentUUID)
    		b.Component("UUID2", ComponentUUID)
    	})
    }
    
    func ComponentUUID(b *kyoto.Builder) {
    	b.Async(func() error {
    		// Execute request
    		resp, err := http.Get("http://httpbin.org/uuid")
    		if err != nil {
    			return err
    		}
    		// Defer closing of response body
    		defer resp.Body.Close()
    		// Decode response
    		data := map[string]string{}
    		json.NewDecoder(resp.Body).Decode(&data)
    		// Set state
    		b.State.Set("UUID", data["uuid"])
    		// Return
    		return nil
    	})
    }
    
  • Component actions not being executed

    Component actions not being executed

    I have an action which I've defined on a component. The component code looks like this:

    package components
    
    import (
    	"log"
    	"net/http"
    
    	"github.com/gin-gonic/gin"
    	"github.com/yuriizinets/go-ssc"
    
    	"github.com/kodah/blog/controller"
    	"github.com/kodah/blog/service"
    )
    
    type SignIn struct {
    	Context  *gin.Context
    	Username string
    	Password string
    	Errors   []string
    }
    
    func (c *SignIn) Actions() ssc.ActionsMap {
    	return ssc.ActionsMap{
    		"DoSignIn": func(args ...interface{}) {
    			var configService service.ConfigService = service.ConfigurationService("")
    			if configService.Error() != nil {
    				log.Printf("Error while connecting to DB service. error=%s", configService.Error())
    				c.Errors = append(c.Errors, "Internal server error")
    
    				return
    			}
    
    			log.Printf("Triggered sign in")
    
    			var dbService service.DBService = service.SQLiteDBService("")
    			if dbService.Error() != nil {
    				log.Printf("Error while connecting to DB service. error=%s", dbService.Error())
    				c.Errors = append(c.Errors, "Internal server error")
    
    				return
    			}
    
    			var loginService service.LoginService = service.DynamicLoginService(dbService)
    			var jwtService service.JWTService = service.JWTAuthService()
    			var loginController controller.LoginController = controller.LoginHandler(loginService, jwtService)
    
    			c.Context.Set("Username", c.Username)
    			c.Context.Set("Password", c.Password)
    
    			session := service.NewSessionService(c.Context, false)
    			token := loginController.Login(c.Context)
    
    			session.Set("token", token)
    
    			err := session.Save()
    			if err != nil {
    				log.Printf("Error while saving session. error=%s", err)
    			}
    
    			log.Printf("Login successful. user=%s", c.Username)
    
    			c.Context.Redirect(http.StatusFound, "/")
    		},
    	}
    }
    

    The template:

    {{ define "SignIn" }}
    <div {{ componentattrs . }}>
        <h1 class="title is-4">Sign in</h1>
        <p id="errorFeedback" class="help has-text-danger is-hidden">
            {{ .Username }} {{ .Password }}
        </p>
        <div class="field">
            <div class="control">
                <input class="input is-medium" value="{{ .Username }}" oninput="{{ bind `Username` }}" type="text" placeholder="username">
            </div>
        </div>
    
        <div class="field">
            <div class="control">
                <input class="input is-medium" value="{{ .Password }}" oninput="{{ bind `Password` }}" type="password" placeholder="password">
            </div>
        </div>
        <button onclick="{{ action `DoSignIn` `{}` }}" class="button is-block is-primary is-fullwidth is-medium">Submit</button>
        <br />
        <small><em>Be nice to the auth system.</em></small>
    </div>
    {{ end }}
    

    and is included like this:

                    <div class="column sign-in has-text-centered">
                        {{ template "SignIn" .SignInComponent }}
                    </div>
    

    Where the component inclusion looks like this:

    package frontend
    
    import (
    	"html/template"
    
    	"github.com/gin-gonic/gin"
    	"github.com/yuriizinets/go-ssc"
    
    	"github.com/kodah/blog/frontend/components"
    )
    
    type PageSignIn struct {
    	ctx             *gin.Context
    	SignInComponent ssc.Component
    }
    
    func (p *PageSignIn) Template() *template.Template {
    	return template.Must(template.New("page.signin.html").Funcs(ssc.Funcs()).ParseGlob("frontend/new_templates/*/*.html"))
    }
    
    func (p *PageSignIn) Init() {
    	p.SignInComponent = ssc.RegC(p, &components.SignIn{
    		Context: p.ctx,
    	})
    
    	return
    }
    
    func (*PageSignIn) Meta() ssc.Meta {
    	return ssc.Meta{
    		Title:       "Test kodah's blog",
    		Description: "Test description",
    		Canonical:   "",
    		Hreflangs:   nil,
    		Additional:  nil,
    	}
    }
    

    When I run the DoSignIn action it is not executed though;

        <button onclick="{{ action `DoSignIn` `{}` }}" class="button is-block is-primary is-fullwidth is-medium">Submit</button>
    

    I realize there's not a lot of documentation but I went off of the examples and this seems right.

  • Using ParseFS and embed.FS

    Using ParseFS and embed.FS

    I've been trying to figure out a pattern to use embed.FS and template.ParseFS for delivering the HTML files

    Do you have any examples for this? I've been struggling with a couple of different errors and including the Funcs with the parsed templates

    template: \"templates/pages.home.html\" is an incomplete or empty template

    	//go:embed templates
    	Templates embed.FS
    
    	return template.Must(
    		template.New("templates/pages.home.html").Funcs(kyoto.TFuncMap()).ParseFS(assets.Templates, "templates/pages.home.html"),
    	)
    
  • Up-to-date on Using the UIKit

    Up-to-date on Using the UIKit

    Hi guys,

    First of all, just wanted to take a moment and express my gratitude to your guys for making Kyoto a reality. It's awesome and really makes a lot sense for gophers to build out frontends on the fly. That said, I was wondering if you guys had any updated docs on using the UIKit. So far all the docs and demo projects I've come across were on an older version of the codebase and the apis have changed quite a bit since then. Would love some guidance here. Thanks in advance.

    Best, Jay

  • It is not possible to use `render.Custom` per component

    It is not possible to use `render.Custom` per component

    Each render.Custom call overrides global context. With this approach it wouldn't be possible to define custom rendering on a component level.
    As an option, we can store this in the state as internal, helpers.ComponentSerialize will remove it on state serialization.

  • Better Actions protocol implementation

    Better Actions protocol implementation

    Current approach is far from ideal. State and arguments are passed in SSE query, response must to be base64 encoded to avoid newlines issue (which increases the size of the response).

    Websockets cannot be considered:

    • They don't scale well (in case of keeping connection alive all the time)
    • Sometimes it requires special configuration of networking, especially on big projects
    • In some countries/hotels/public places you can see ports/protocols whitelisting, which immediately kills this approach
  • Support `tinygo` compiler

    Support `tinygo` compiler

    tinygo may provide a lot of benefits to the project, like compiling to smaller wasm payload and using in places where payload size is critical. I also consider as an option using kyoto for creating frontend, rendered on the Edge Network (f.e. Cloudflare Workers). Workers have a lot of limitations and tinygo may satisfy them.

  • Passing Go packages to Pages/Components

    Passing Go packages to Pages/Components

    Been playing around with Kyoto and really liking the pattern once I managed to get my head around it

    But now I'm starting to wonder what are the best practices for passing Go packages to Pages/Components seen as the handlers create a new instance of the page on each page load it's not possible to pass in a dependency inside the Page/Component structs

    One way I've managed to do this is passing it into the Context but I don't really want to fill up my context with lots of dependencies but I feel like there must be a nicer way of doing this that is more scalable?

  • Question - set cookie on response to SSA call

    Question - set cookie on response to SSA call

    How is it possible to access the request context from a call to a Server-Side Action. Say, for instance you want to set a cookie in the response to an SSA call.

    In the example demo app for the form submission example (email validator) you have the following:

    type ComponentDemoEmailValidator struct {
    	Email   string
    	Message string
    	Color   string
    }
    
    func (c *ComponentDemoEmailValidator) Actions() ssc.ActionMap {
    	return ssc.ActionMap{
    		"Submit": func(args ...interface{}) {
    			if emailregex.MatchString(c.Email) {
    				c.Message = "Provided email is valid"
    				c.Color = "green"
    			} else {
    				c.Message = "Provided email is not valid"
    				c.Color = "red"
    			}
    		},
    	}
    }
    

    I am aware you can create an "Init" method function to access the request context i.e.

    func (c *ComponentDemoEmailValidator) Init(p ssc.Page) {
        c.Page = p
        r := ssc.GetContext(p, "internal:r").(*http.Request)
        rw := ssc.GetContext(p, "internal:rw").(http.ResponseWriter)
    }
    

    But how do you access the request/response context from within the "Actions()" method so that you can, for example, set a cookie in the response. Is this possible?

  • Interaction with complex state across different adapters is uncomfortable

    Interaction with complex state across different adapters is uncomfortable

    In the struct mode it's much easier to initialize nested components and interact with them in next lifecycle steps. In case of func mode (which is default now) we need to interact with kyoto.Store instance, which is OK for simple state, but interaction with nested components is awful. You need to understand how Core.Component works and use explicit type casting.

    Needs to figure out, how to simplify work with components.

Material Design Components for use with Vecty in the most minimalistic fashion.

mdc Material Design Components for use with Vecty in the most minimalistic, dead simple fashion. Currently on MDC version 13.0.0. Based on the good wo

Mar 6, 2022
Go Lang Web Assembly bindings for DOM, HTML etc

WebAPI Go Language Web Assembly bindings for DOM, HTML etc WARNING: The current API is in very early state and should be consider to be expremental. T

Dec 28, 2022
This library provides WebAssembly capability for goja Javascript engine

This module provides WebAssembly functions into goja javascript engine.

Jan 10, 2022
Golang-WASM provides a simple idiomatic, and comprehensive API and bindings for working with WebAssembly for Go and JavaScript developers
Golang-WASM provides a simple idiomatic, and comprehensive API and bindings for working with WebAssembly for Go and JavaScript developers

A bridge and bindings for JS DOM API with Go WebAssembly. Written by Team Ortix - Hamza Ali and Chan Wen Xu. GOOS=js GOARCH=wasm go get -u github.com/

Dec 22, 2022
Aes for go and java; build go fo wasm and use wasm parse java response.

aes_go_wasm_java aes for go and java; build go fo wasm and use wasm parse java response. vscode setting config settings.json { "go.toolsEnvVars":

Dec 14, 2021
WebAssembly for Proxies (Golang host implementation)

WebAssembly for Proxies (GoLang host implementation) The GoLang implementation for proxy-wasm, enabling developer to run proxy-wasm extensions in Go.

Dec 29, 2022
A WASM Filter for Envoy Proxy written in Golang

envoy-proxy-wasm-filter-golang A WASM Filter for Envoy Proxy written in Golang Build tinygo build -o optimized.wasm -scheduler=none -target=wasi ./mai

Nov 6, 2022
Istio wasm api demo with golang

istio-wasm-api-demo 1. Setup the latest Istio Setup k8s cluster: e.g. kind create cluster --name test Download the latest Istioctl from the GitHub rel

Nov 1, 2022
DOM library for Go and WASM

Go DOM binding (and more) for WebAssembly This library provides a Go API for different Web APIs for WebAssembly target. It's in an active development,

Dec 23, 2022
Go compiler for small places. Microcontrollers, WebAssembly, and command-line tools. Based on LLVM.

TinyGo - Go compiler for small places TinyGo is a Go compiler intended for use in small places such as microcontrollers, WebAssembly (Wasm), and comma

Dec 30, 2022
WebAssembly interop between Go and JS values.

vert Package vert provides WebAssembly interop between Go and JS values. Install GOOS=js GOARCH=wasm go get github.com/norunners/vert Examples Hello W

Dec 28, 2022
Fast face detection, pupil/eyes localization and facial landmark points detection library in pure Go.
Fast face detection, pupil/eyes localization and facial landmark points detection library in pure Go.

Pigo is a pure Go face detection, pupil/eyes localization and facial landmark points detection library based on Pixel Intensity Comparison-based Objec

Dec 24, 2022
A package to build progressive web apps with Go programming language and WebAssembly.
A package to build progressive web apps with Go programming language and WebAssembly.

Go-app is a package for building progressive web apps (PWA) with the Go programming language (Golang) and WebAssembly (Wasm). Shaping a UI is done by

Dec 30, 2022
A package to build progressive web apps with Go programming language and WebAssembly.
A package to build progressive web apps with Go programming language and WebAssembly.

Go-app is a package for building progressive web apps (PWA) with the Go programming language (Golang) and WebAssembly (Wasm). Shaping a UI is done by

Jan 2, 2023
Bed and Breakfast web app written in Go

BOOKINGS AND RESERVATIONS This repository contains the files for my RareBnB application RareBnB is an "AirBnB" style clone providing a user the abilit

Jan 11, 2022
⚙️ Concept of Golang HTML render engine with frontend components and dynamic behavior

SSC Engine An HTML render engine concept that brings frontend-like components experience to the server side with native html/template on steroids. Sup

Nov 25, 2022
Composable HTML components in Golang
Composable HTML components in Golang

daz Composable HTML components in Golang Daz is a "functional" alternative to using templates, and allows for nested components/lists Also enables tem

Oct 3, 2022
Frongo is a Golang package to create HTML/CSS components using only the Go language.

Frongo Frongo is a Go tool to make HTML/CSS document out of Golang code. It was designed with readability and usability in mind, so HTML objects are c

Jul 29, 2021
Seatsserver - Combined frontend and backend to serve HTML versions of seats
Seatsserver - Combined frontend and backend to serve HTML versions of seats

seatsserver Combined frontend and backend to serve HTML versions of github.com/s

Jan 28, 2022
The Dual-Stack Dynamic DNS client, the world's first dynamic DNS client built for IPv6.

dsddns DsDDNS is the Dual-Stack Dynamic DNS client. A dynamic DNS client keeps your DNS records in sync with the IP addresses associated with your hom

Sep 27, 2022