kyoto

package module
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2022 License: MIT Imports: 15 Imported by: 1

README

kyoto

Go library for creating fast, SSR-first frontend avoiding vanilla templating downsides.

Documentation • Team • Who uses? • Support us

Motivation

Creating asynchronous and dynamic layout parts is a complex problem for larger projects using html/template. Library tries to simplify this process.

What kyoto proposes?

  • Organize code into configurable and standalone components structure
  • Get rid of spaghetti inside of handlers
  • Simple asynchronous lifecycle
  • Built-in dynamics like Hotwire or Laravel Livewire
  • Using a familiar built-in html/template
  • Full control over project setup (minimal dependencies)
  • 0kb JS payload without actions client (~12kb when including a client)
  • Minimalistic utility-first package to simplify work with Go
  • Internationalizing helper
  • Cache control helper package (with a CDN page caching setup guide)

Reasons to opt out

  • API may change drastically between major versions
  • You want to develop SPA/PWA
  • You're just feeling OK with JS frameworks
  • Not situable for a frontend with a lot of client-side logic

Team

Who uses?

Broker One

Website: https://mybrokerone.com

The first version of the site was developed with Vue and suffered from large payload and low performance. After discussion, it was decided to migrate to Go with a built-in html/template due to existing libraries infrastructure inside of the project.
Despite the good performance result, the code was badly structured and it was very uncomfortable to work in existing paradigm.
On the basis of these problems, kyoto was born. Now, this library lies in the core of the platform.

Using the library in your project?

Please tell us about your story! We would love to talk about your usage experience.

Support us

Any project support is appreciated! Providing a feedback, pull requests, new ideas, whatever. Also, donations and sponsoring will help us to keep high updates frequency. Just send us a quick email or a message on contacts provided above.

If you have an option to donate us with a crypto, we have some addresses.

Bitcoin: bc1qgxe4u799f8pdyzk65sqpq28xj0yc6g05ckhvkk
Ethereum: 0xEB2f24e830223bE081264e0c81fb5FD4DDD2B7B0

Also, we have a page on open collective for backers support.

Open Collective: https://opencollective.com/kyoto-framework

Documentation

Overview

Extensible Go library for creating fast, SSR-first frontend avoiding vanilla templating downsides.

Motivation

Creating asynchronous and dynamic layout parts is a complex problem for larger projects using `html/template`. Library tries to simplify this process.

Quick start

Let's go straight into a simple example. Then, we will dig into details, step by step, how it works.

package main

import (
	"net/http"
	"encoding/json"

	"git.sr.ht/~kyoto-framework/kyoto"
)

// This example demonstrates main advantage of kyoto library - asynchronous lifecycle.
// Multiple UUIDs will be fetched from httpbin in asynchronous way, without explicitly touching goroutines
// and synchronization tools like sync.WaitGroup.

type CUUIDState struct {
	UUID string
}

// Let's assume markup of this component is stored in 'component.uuid.html'
//
// {{ define "CUUID" }}
//  <div>UUID: {{ .UUID }}</div>
// {{ end }}
func CUUID(ctx *kyoto.Context) (state CUUIDState) {
	// Fetch uuid data
	resp, _ := http.Get("http://httpbin.org/uuid")
	data := map[string]string{}
	json.NewDecoder(resp.Body).Decode(&data)
	// Set state
	state.UUID = data["uuid"]
	// Return
	return
}

type PIndexState struct {
	UUID1 *kyoto.ComponentF[CUUIDState]
	UUID2 *kyoto.ComponentF[CUUIDState]
}

// Let's assume markup of this page is stored in 'page.index.html'
//
// <!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">
// 	<title>Example</title>
// </head>
// <body>
// 	{{ template "CUUID" await .UUID1 }}
// 	{{ template "CUUID" await .UUID2 }}
// </body>
// </html>
func PIndex(ctx *kyoto.Context) (state PIndexState) {
	// Define rendering
	kyoto.Template(ctx, "page.index.html")
	// Attach components
	state.UUID1 = kyoto.Use(ctx, CUUID)
	state.UUID2 = kyoto.Use(ctx, CUUID)
	// Return
	return
}

func main() {
	// Register page
	kyoto.HandlePage("/", PIndex)
	// Serve
	kyoto.Serve(":8080")
}

Handling requests

Kyoto provides a simple net/http handlers and function wrappers to handle pages rendering and serving.

See functions inside of nethttp.go file for details and advanced usage.

Example:

func main() {
	kyoto.HandlePage("/foo", PageFoo)
	kyoto.HandlePage("/bar", PageBar)

	kyoto.Serve(":8000")
}

Components

Kyoto provides a way to define components. It's a very common approach for modern libraries to manage frontend parts. In kyoto each component is a context receiver, which returns it's state. Each component becomes a part of the page or top-level component, which executes component asynchronously and gets a state future object. In that way your components are executing in a non-blocking way.

Pages are just top-level components, where you can configure rendering and page related stuff.

Example:

// Component is a context receiver, that returns it's state.
// State can be whatever you want (simple type, struct, slice, map, etc).
func CUUID(ctx *kyoto.Context) (state CUUIDState) {
	// Fetch uuid data
	resp, _ := http.Get("http://httpbin.org/uuid")
	data := map[string]string{}
	json.NewDecoder(resp.Body).Decode(&data)
	// Set state
	state.UUID = data["uuid"]
}

// Page is just a top-level component, which attaches components and defines rendering
func PExample(ctx *kyoto.Context) (state PExampleState) {
	// Define rendering
	kyoto.Template(ctx, "page.example.html")
	// Attach components
	state.UUID1 = kyoto.Use(ctx, CUUID)
	state.UUID2 = kyoto.Use(ctx, CUUID)
}

As an option, you can wrap component with another function to accept additional paramenters from top-level page/component.

Example:

func CUUID(hburl string) kyoto.Component[CUUIDState] {
	return func(ctx *kyoto.Context) (state CUUIDState) {
		// Fetch uuid data
		resp, _ := http.Get(hburl)
		data := map[string]string{}
		json.NewDecoder(resp.Body).Decode(&data)
		// Set state
		state.UUID = data["uuid"]
	}
}

Context

Kyoto provides a context, which holds common objects like http.ResponseWriter, *http.Request, etc.

See kyoto.Context for details.

Example:

func Component(ctx *kyoto.Context) (state ComponentState) {
	log.Println(ctx.Request.UserAgent())
	...
}

Template

Kyoto provides a set of parameters and functions to provide a comfortable template building process. You can configure template building parameters with kyoto.TemplateConf configuration.

See template.go for available functions and kyoto.TemplateConfiguration for configuration details.

Example:

func Page(ctx *kyoto.Context) (state PageState) {
	// By default it will:
	// - use kyoto.FuncMap as a FuncMap
	// - parse everything in the current directory with a .ParseGlob("*.html")
	// - render a template with a given name
	kyoto.Template(ctx, "page.index.html")
	...
}

Actions

Kyoto provides a way to simplify building dynamic UIs. For this purpose it has a feature named actions. Logic is pretty simple. Client calls an action (sends a request to the server). Action is executing on server side and server is sending updated component markup to the client which will be morphed into DOM. That's it.

To use actions, you need to go through a few steps. You'll need to include a client into page (JS functions for communication) and register an actions handler for a needed component.

Let's start from including a client.

<html>
	<head>
		...
	</head>
	<body>
		...
		{{ client }}
	</body>
</html>

Then, let's register an actions handler for a needed component.

func main() {
	...
	kyoto.HandleAction(Component)
	...
}

That's all! Now we ready to use actions to provide a dynamic UI.

Example:

...

type CUUIDState struct {
	UUID string
}

// Let's assume markup of this component is stored in 'component.uuid.html'
//
//	{{ define "CUUID" }}
//	<div {{ state . }} name="CUUID">
//		<div>UUID: {{ .UUID }}</div>
//		<button onclick="Action(this, 'Reload')">Reload</button>
//	</div>
//	{{ end }}
func CUUID(ctx *kyoto.Context) (state CUUIDState) {
	// Define uuid loader
	uuid := func() string {
		resp, _ := http.Get("http://httpbin.org/uuid")
		data := map[string]string{}
		json.NewDecoder(resp.Body).Decode(&data)
		return data["uuid"]
	}
	// Handle action
	handled := kyoto.Action(ctx, "Reload", func(args ...any) {
		// We will just set a new uuid and will print a log
		// It's not makes a lot of sense now, but it's just a demonstration example
		state.UUID = uuid()
		log.Println("New uuid was issued:", state.UUID)
	})
	// Prevent further execution if action handled
	if handled {
		return
	}
	// Default loading behavior
	state.UUID = uuid()
	// Return
	return
}

type PIndexState struct {
	UUID1 *kyoto.ComponentF[CUUIDState]
	UUID2 *kyoto.ComponentF[CUUIDState]
}

// Let's assume markup of this page is stored in 'page.index.html'
//
//	<!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">
//		<title>Example</title>
//	</head>
//	<body>
//		{{ template "CUUID" await .UUID1 }}
//		{{ template "CUUID" await .UUID2 }}
//		{{ client }}
//	</body>
//	</html>
func PIndex(ctx *kyoto.Context) (state PIndexState) {
	// Define rendering
	kyoto.Template(ctx, "page.index.html")
	// Attach components
	state.UUID1 = kyoto.Use(ctx, CUUID)
	state.UUID2 = kyoto.Use(ctx, CUUID)
	// Return
	return
}

func main() {
	kyoto.HandlePage("/", PIndex)
	kyoto.HandleAction(CUUID)
	kyoto.Serve(":8000")
}

In this example you can see provided modifications to the quick start example.

First, we've added a state and name into our components' markup. In this way we are saving our components' state between actions and find a component root. Unfortunately, we have to manually provide a component name for now, we haven't found a way to provide it dynamically.

Second, we've added a reload button with onclick function call. We're using a function Action provided by a client. Action triggering will be described in details later.

Third, we've added an action handler inside of our component. This handler will be executed when a client calls an action with a corresponding name.

It's highly recommended to keep components' state as small as possible. It will be transmitted on each action call.

Action triggering

Kyoto have multiple ways to trigger actions. Now we will check them one by one.

Action(this, "<action>", <args...>)

This is the simplest way to trigger an action. It's just a function call with a referer (usually 'this', f.e. button) as a first argument (used to determine root), action name as a second argument and arguments as a rest. Arguments must to be JSON serializable.

It's possible to trigger an action of another component. If you want to call an action of parent component, use $ prefix in action name. If you want to call an action of component by id, use <id:action> as an action name.

FormSubmit(this, event)

This is a specific action which is triggered when a form is submitted. Usually called in onsubmit="..." attribute of a form. You'll need to implement 'Submit' action to handle this trigger.

ssa:onload="<action>"

This is a special HTML attribute which will trigger an action on page load. This may be useful for components' lazy loading.

ssa:poll="<action>"
ssa:poll.interval="<interval>"

With this special HTML attributes you can trigger an action with interval. Useful for components that must to be updated over time (f.e. charts, stats, etc). You can use this trigger with ssa:poll and ssa:poll.interval HTML attributes.

ssa:onintersect="<action>"

This one attribute allows you to trigger an action when an element is visible on the screen. May be useful for lazy loading.

Action flow control

Kyoto provides a way to control action flow. For now, it's possible to control display style on component call and push multiple UI updates to the client during a single action.

ssa:oncall.display="<display>"

Because kyoto makes a roundtrip to the server every time an action is triggered on the page, there are cases where the page may not react immediately to a user event (like a click). That's why the library provides a way to easily control display attributes on action call. You can use this HTML attribute to control display during action call. At the end of an action the layout will be restored.

A small note. Don't forget to set a default display for loading elements like spinners and loaders.

kyoto.ActionFlush(ctx, state)

You can push multiple component UI updates during a single action call. Just call kyoto.ActionFlush(ctx, state) to initiate an update.

Action rendering options

Kyoto provides a way to control action rendering.

ssa:render.mode="replace"

Now there is at least 2 rendering options after an action call: morph (default) and replace. Morph will try to morph received markup to the current one with morphdom library. In case of an error, or explicit "replace" mode, markup will be replaced with x.outerHTML = '...'.

Index

Constants

This section is empty.

Variables

View Source
var ActionClient = "" /* 8999-byte string literal not displayed */
View Source
var ActionConf = ActionConfiguration{
	Path:       "/internal/actions/",
	Terminator: "=!EOC!=",
}

ActionConf is a global configuration that will be used during actions handling. See ActionConfiguration for more details.

View Source
var FuncMap = template.FuncMap{
	"await":  Await,
	"state":  actionFuncState,
	"client": actionFuncClient,
}

FuncMap holds a library predefined template functions. You have to include it into TemplateConf.FuncMap (or your raw templates) to use kyoto properly.

View Source
var TemplateConf = TemplateConfiguration{
	ParseGlob: "*.html",
	FuncMap:   FuncMap,
}

TemplateConf is a global configuration that will be used during template building. Feel free to modify it as needed.

Functions

func Action

func Action(c *Context, name string, action func(args ...any)) bool

Action is a function that handles an action request. Returns an execution flag (true if action was executed). You can use a flag to prevent farther execution of a component.

Example:

func Foo(ctx *kyoto.Context) (state FooState) {
	// Handle action
	bar := kyoto.Action(ctx, "Bar", func(args ...any) {
		// Do something
	})
	// Prevent further execution
	if bar {
		return
	}
	// Default non-action behavior
	// ...
}

func ActionFlush

func ActionFlush(ctx *Context, state any)

ActionFlush allows to push multiple component UI updates during single action call. Call it when you need to push an updated component markup to the client.

Example:

	func CompFoo(ctx *kyoto.Context) (state CompFooState) {
		...
		// Handle example action
		kyoto.Action(ctx, "Bar", func(args ...any) {
			// Do something with a state
			state.Content = "Bar"
			// Push updated UI to the client
			kyoto.ActionFlush(ctx, state)
			// Do something else with a state
			state.Content = "Baz"
			// Push updated UI to the client
			kyoto.ActionFlush(ctx, state)
		})
		...
}

func ActionPreload

func ActionPreload[T any](c *Context, state T)

ActionPreload unpacks a component state from an action request. Executing only in case of an action request.

Example:

func CompFoo(ctx *kyoto.Context) (state CompFooState) {
	// Preload state
	kyoto.ActionPreload(ctx, &state)
	// Handle actions
	...
}

func ActionRedirect added in v1.0.13

func ActionRedirect(ctx *Context, location string)

ActionRedirect is a function to trigger redirect during action handling.

Example:

func CompFoo(ctx *kyoto.Context) (state CompFooState) {
	...
	// Handle example action
	kyoto.Action(ctx, "Bar", func(args ...any) {
		// Redirect to the home page
		kyoto.ActionRedirect(ctx, "/")
	})
	...
}

func Await

func Await(component any) any

Await accepts any awaitable component and returns it's state. It's a function supposed to be used as a template function.

Template example:

{{ template "CompBar" await .Bar }}

Go example:

barf = kyoto.Use(ctx, CompBar) // Awaitable *kyoto.ComponentF[CompBarState]
...
bar = kyoto.Await(barf) // CompBarState

func ComponentName

func ComponentName(component any) string

ComponentName takes a component function and tries to extract it's name. Be careful while using this function, may lead to undefined behavior in case of wrong value.

Example:

func CompBar(ctx *kyoto.Context) (state CompBarState) {
	...
}

func main() {
	fmt.Println(kyoto.ComponentName(CompBar)) // "CompBar"
}

func ComposeFuncMap

func ComposeFuncMap(fmaps ...template.FuncMap) template.FuncMap

ComposeFuncMap is a function for composing multiple FuncMap instances into one.

Example:

func MyFuncMap() template.FuncMap {
	return kyoto.ComposeFuncMap(
		funcmap1,
		funcmap2,
		...
	)
}

func HandleAction

func HandleAction[T any](component Component[T], ctx ...*Context)

HandleAction registers a component action handler with a predefined pattern in the DefaultServeMux. It's a wrapper around http.HandleFunc, but accepts a component instead of usual http.HandlerFunc.

Example:

kyoto.HandleAction(CompFoo) // Register a usual component
kyoto.HandleAction(CompBar("")) // Register a component which accepts arguments and returns wrapped function

func HandlePage

func HandlePage[T any](pattern string, page Component[T])

HandlePage registers the page for the given pattern in the DefaultServeMux. It's a wrapper around http.HandlePage, but accepts a page instead of usual http.HandlerFunc.

Example:

func PageFoo(ctx *kyoto.Context) (state PageFooState) { ... }

func main() {
	...
	kyoto.HandlePage("/foo", PageFoo)
	...
}

func HandlerAction

func HandlerAction[T any](component Component[T], _ctx ...*Context) func(w http.ResponseWriter, r *http.Request)

HandlerAction returns a http.HandlerFunc that handles an action request for a specified component. Pattern still must to correspond to the provided component. It's recommended to use HandleAction instead.

Example:

http.HandleFunc("/internal/actions/Foo/", kyoto.HandlerAction(Foo))

func HandlerPage

func HandlerPage[T any](page Component[T]) http.HandlerFunc

HandlerPage returns a http.HandlerPage that renders the page.

Example:

func PageFoo(ctx *kyoto.Context) (state PageFooState) { ... }

func main() {
	...
	http.HandleFunc("/foo", kyoto.HandlerPage(PageFoo))
	...
}

func MarshalState

func MarshalState(state any) string

MarshalState encodes components' state for a client. Supposed to be used as a template function.

Template example:

{{ state . }}

Go example:

compStateEnc := kyoto.MarshalState(compState)

func Serve

func Serve(addr string)

Serve is a simple wrapper around http.ListenAndServe, which will log server starting and will panic on error.

Example:

func main() {
	...
	kyoto.Serve(":8000")
}

func Template

func Template(c *Context, name string)

Template creates a new template with a given name, using global parameters stored in kyoto.TemplateConf. Stores template in the context.

Example:

func PageFoo(ctx *kyoto.Context) (state PageFooState) {
	// By default uses kyoto.FuncMap
	// and parses everything in the current directory with a .ParseGlob("*.html")
	kyoto.Template(ctx, "page.foo.html")
	...
}

func TemplateInline

func TemplateInline(c *Context, tmplsrc string)

TemplateInline creates a new template with a given template source, using global parameters stored in kyoto.TemplateConf. Stores template in the context.

Example:

func PageFoo(ctx *kyoto.Context) (state PageFooState) {
	// By default uses kyoto.FuncMap
	// and parses everything in the current directory with a .ParseGlob("*.html")
	kyoto.TemplateInline(ctx, `<html>...</html>`)
	...
}

func TemplateRaw

func TemplateRaw(c *Context, tmpl *template.Template)

TemplateRaw handles a raw template. Stores template in the context.

Example:

func PageFoo(ctx *kyoto.Context) (state PageFooState) {
	tmpl := MyTemplateBuilder("page.foo.html") // *template.Template
	kyoto.TemplateRaw(ctx, tmpl)
	...
}

func UnmarshalState

func UnmarshalState(state string, target any)

UnmarshalState decodes components' state from a client. Supposed to be used internaly only, exposed just in case.

Types

type ActionConfiguration

type ActionConfiguration struct {
	Path       string // Configure a path prefix for action calls
	Terminator string // Configure a terminator sequence which responsible for chunk separation
}

ActionConfiguration holds a global actions configuration.

type ActionParameters

type ActionParameters struct {
	Component string
	Action    string
	State     string
	Args      []any
	// contains filtered or unexported fields
}

ActionParameters is a Go representation of an action request.

func (*ActionParameters) Parse

func (p *ActionParameters) Parse(r *http.Request) error

Parse extracts action data from a provided request.

type Component

type Component[T any] func(*Context) T

Component represents a kyoto component, defined as a function.

type ComponentF

type ComponentF[T any] zen.Future[T]

ComponentF represents a future for a component work result. Under the hood it wraps zen.Future[T].

func Use

func Use[T any](c *Context, component Component[T]) *ComponentF[T]

Use is a function to use your components in your code. Triggers component execution and returns a future for a component work result (ComponentF).

Example:

func CompBar(ctx *kyoto.Context) (state CompBarState) {
	...
}

func PageFoo(ctx *kyoto.Context) (state PageFooState) {
	...
	state.Bar = kyoto.Use(ctx, CompBar)  // Awaitable *kyoto.ComponentF[CompBarState]
	...
}

func (*ComponentF[T]) MarshalJSON

func (f *ComponentF[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements future marshalling.

func (*ComponentF[T]) UnmarshalJSON

func (f *ComponentF[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON implements future unmarshalling.

type Context

type Context struct {
	// Handler
	ResponseWriter http.ResponseWriter
	Request        *http.Request

	// Rendering
	Template     *template.Template
	TemplateConf *TemplateConfiguration

	// Action
	Action ActionParameters
}

Context is the context of the current request. It is passed to the pages and components.

type TemplateConfiguration

type TemplateConfiguration struct {
	ParseGlob string
	ParseFS   *embed.FS
	FuncMap   template.FuncMap
}

TemplateConfiguration holds template building configuration.

Jump to

Keyboard shortcuts

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