guiapi

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2023 License: Apache-2.0 Imports: 11 Imported by: 1

README

guiapi - Multi Page Web App Framework for Go

Go documentation npm package

Guiapi (an API for GUIs) is a framework for building interactive multi page web applications with minimal JavaScript, using Go on the server side for handling most of the logic and rendering the HTML. Besides rendering the different pages, it can also update the DOM from the server side, after a server Action was triggered by a browser event or JavaScript. It tries to minimize the amount of JavaScript that needs to be written and sent to the browser.

Principles

Guiapi lives between the worlds of an old school web app, that required a full page reload for every user action and a typical modern single page app that requires a REST API and lots of JavaScript to render the different HTML views.

You should give this framework a try, if you agree with the following principles that guiapi is built on:

  • Rendering HTML should not require a REST API
  • Most web apps should be multi page apps out of the box
  • Most of the web apps logic should run on the server side
  • Most HTML should be rendered on the server side
  • JavaScript should only be used for features where it is necessary
Is this an alternative to React and other frontend frameworks?

The short answer is no. The goal of guiapi is to provide a framework for a multi page server rendered web app, which is a different goal than that of a frontend framework. It can make sense to use guiapi and a frontend framework together. The main structure of the different pages and inital page layout can be provided via guiapi pages, and afterwards the frontend framework takes over for user interface components that require a high degree of interactivity. These could be complex form validations, interactive data visualizations and similar, where server roundtrips for every update to the UI would not make sense.

In practice though, many web apps today are built with a frontend framework which renders 100% of the UI in the browser, even if most of the pages don't need this high degree of interactivy. Instead the pages could just as easily be rendered server side. In those cases it might not even be necessary to use a framework, and the same end result can be achieved with just a few lines of vanilla JavaScript for the interactive components.

Concepts

guiapi diagram between server and browser

Pages

Pages represent the different views of your app. They can be accessed directly by navigating to the page URL, in which case the server returns a usuale HTML document. If you are navigating from one guiapi Page to the next, this can even happen via an Action and Update. In this case no full page reload is needed, but the URL and page content is still updated as if the page was visited directly.

Actions

Actions are events that are sent from the browser to the server. They can either be originating from a HTML element with ga-on attribute, or from the guiapi action() JavaScript function. Actions consist of a name and optional arguments. These actions are transferred as JSON via a POST request to the endpoint that is typically called /guiapi. The response to an Action is an Update.

Updates

Updates are sent from the server to the browser. They can be the response to an Action, or they can be sent via a Stream. Updates consist of a list of HTML updates, JS calls and Streams to connect to.

HTML updates

After an update is received, the HTML updates are applied to the DOM. This can for example mean that the the element with the selector #content should be replaced with some different HTML, or that a new element should be inserted before or after a spefic selector.

JS calls

JS calls can be explicitly added to an Update, and the function with the given name will be called with the passed arguments. For this to work the function first needs to get registered. Besides an explicit JS call it is often useful to run some JavaScript in relation to one of the newly added HTML elements. In this case the new HTML needs to have one of the special guiapi attributes like ga-init.

State

Sometimes a web page has a certain state that needs to be known to the server too, for example filter settings for a list of items. This state gets transferred with ever Action to the server, and can be updated by receiving a new state in an Update. The state is similar to a cookie and usually doesn't need to be accessed by client JavaScript functions.

Streams

Streams are similar to Actions that return an Update, but instead of returning just a single Update, the Stream can return many Updates over time, until the Stream is closed. This is not done via a HTTP request, but via a WebSocket connection. Similar to actions, a Stream also consists of a name and arguments.

[!WARNING]
While the other concepts of guiapi (Pages, Actions, Updates) have been proven useful in web applications since 2018, Streams are a new concept for server sent updates and should be considered experimental. They might change significantly in the future.

API Documentation

The guiapi API consists of the Go, JavaScript and HTML attribute APIs.

Go API

See Go package documentation. The main types to look out for are Server which handles HTTP requests, Page for page rendering and updating, and Request and Response that explain the RPC format.

Asset bundling using esbuild

The assets package contains a wrapper around esbuild that can be used to bundle JavaScript and CSS assets.

With this package you don't need an external JS bundler, as the building can happen every time you start the Go binary to embed your assets. The esbuild tool adds about 5 MB to the binary size, so if you don't need this functionality in production and include the built assets in another way, for example with go:embed, then you can use the no_esbuild build tag like this: go build -tags no_esbuild, which replaces the asset building function with a no-op. You can check if the esbuild tool is available with the assets.EsbuildAvailable() function.

HTML attribute API

The following attributes get activated when setupGuiapi() is called after the page load, and they also get initialized whenever they appear in HTML that was updated by an Update from an Action or Stream.

Event handlers: ga-on
<button class="ga" ga-on="click" ga-func="myClickFunction">click me</button>
<button class="ga" ga-on="click" ga-action="Page.Click" ga-args="abc">click me</button>

The ga-on attribute is used to trigger a server action or JavaScript functions every time the event name specified in the attribute happens event listeners on HTML elements. In the first example above, the myClickFunction function is called every time the button is clicked. In the second example, the Page.Click server action is called with "abc" as the argument.

<input class="my-form" type="text" name="name" />
<input class="my-form" type="number" name="amount" />
<button class="ga" ga-on="click" ga-action="Page.Click" ga-values=".my-form">submit</button>

If you want to submit multiple inputs to a server action, you can use the ga-values attribute. The value of the attribute gets passed to document.querySelectorAll() and all .... value, name

Initializer functions: ga-init
<div class="ga" ga-init="myInitFunction" ga-args='{"val":123}'></div>

If the ga-args can't be parsed as JSON, they are passed as a string to the function.

<a href="/other/page" class="ga" ga-link>other page</a>

If you add ga-link attribute to an a with a href, clicking on the link will navigate to the other page via a guiapi call and partial page update without reloading the whole page. Behind the scenes the history API is used, so that navigating back and forth still works as expected. This is useful if you have some JavaScript logic that should keep running between pages and should also speed up page navigation.

JavaScript API

To make a guiapi app work, the setupGuiapi() function needs to be called. Before that, any functions that are referenced from HTML or update JSCalls need to be registered with registerFunctions().

Calling a server action from JavaScript
action(name: string, args: any, callback: (error: any) => void)

This can be used to run a server action from any JavaScript. The callback is called with any potential error after the update from the server was applied.

Registering your JS functions for guiapi
registerFunctions(obj: { string: Function })

Registers functions that can be called from HTML via ga-func or ga-init and also makes them available for JSCalls coming via a server update.

Initializing the guiapi app
setupGuiapi(config: {
  state: any,
  stream: {
    name: string,
    args: any,
  },
  debug: boolean,
  errorHandler: (error: any) => void,
})

This initializes all HTML elements with the ga class and sets up the event listeners. Functions that are referenced from the HTML with ga-func or ga-init need to be registered with registerFunctions() before calling setupGuiapi().

Debug logging
debugPrinting(enable: boolean)

With this function you can turn on or off loging whenever guiapi calls an action and receives an update from the server. This can be useful during development.

Examples

Go to ./examples and start the web server with go run . to access the 3 examples with your web browser at localhost:8000.

The 3 examples are:

  • / contains a guiapi implentation of TodoMVC
  • /counter is a simple counter that can be increased and decreased
  • /reports is a demonstrates streams by updating the page from the server

The examples are a good starting point to build your own app with guiapi.

Contributing

If you are using guiapi and have any feedback, please let me know. Issues and discussions are always welcome.


Built by @mbertschler

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ActionCtx

type ActionCtx struct {
	Writer  http.ResponseWriter
	Request *http.Request
	State   json.RawMessage
	Args    json.RawMessage
}

ActionCtx is the context that is passed to an ActionFunc. It extends the Request and Writer from a typical HTTP request handler with State and Args fields from the Action call that were sent from the browser.

type ActionFunc

type ActionFunc func(c *ActionCtx) (*Update, error)

ActionFunc is the action handler function that should return an Update in response to the call from the client. ActionFuncs are registered with the Server using the AddAction() function.

type Page

type Page interface {
	WriteHTML(io.Writer) error
}

Page gets returned from a PageFunc. The page needs to be able to write the HTML representation of the page to an io.Writer. It can be extended into an UpdateablePage by implementing Update().

type PageCtx

type PageCtx struct {
	Writer  http.ResponseWriter
	Request *http.Request
	Params  httprouter.Params // params from placeholders in the URL
}

PageCtx is the context that is passed to a PageFunc. It extends the Request and Writer from a typical HTTP request handler with Params from the HTTP router.

For more info on httrouter.Params see: https://github.com/julienschmidt/httprouter

type PageFunc

type PageFunc func(*PageCtx) (Page, error)

PageFunc is the page handler function that should return a Page value in response to a HTTP request or guiapi page request. PageFuncs are registered with the using the AddPage() function.

type Server

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

Server contains all the registered Pages, Actions, Files and Streams. It implements the http.Handler interface, so it can be directly passed to a function like http.ListenAndServe(). When a request comes in, the server will handle GET requests for pages, POST requests for actions, and WebSocket requests for streams.

func New

func New() *Server

New returns a new guiapi Server. After registering all the Pages, Actions, Files and Streams, the server can be directly used as a http.Handler.

func (*Server) AddAction

func (s *Server) AddAction(name string, fn ActionFunc)

AddAction registers an ActionFunc with the passed name and handler function on the server.

func (*Server) AddFiles

func (s *Server) AddFiles(baseURL string, fs http.FileSystem)

AddFiles registers a http.FileSystem with the passed baseURL on the server. The files can also be in a subdirectory of the baseURL. This function is the main way of serving static files from the guiapi server.

func (*Server) AddPage

func (s *Server) AddPage(path string, fn PageFunc)

AddPage registers a PageFunc with the passed path and page handler function on the server. The URL path can contain placeholders in the form of :name, which then get passed as Params in a PageCtx.

See https://github.com/julienschmidt/httprouter for more info.

func (*Server) AddStream

func (s *Server) AddStream(name string, fn StreamFunc)

AddStream registers a StreamFunc with the passed name and handler function on the server.

func (*Server) Router

func (s *Server) Router() *httprouter.Router

Router returns the underlying HTTP router. This way any additional endpoints can be added to the HTTP server, bypassing guiapi.

See https://github.com/julienschmidt/httprouter for more info.

func (*Server) ServeHTTP

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

ServeHTTP implements the http.Handler interface. This means that the Server can directly passed to a function like http.ListenAndServe().

type StreamFunc

type StreamFunc func(ctx context.Context, args json.RawMessage, res chan<- *Update) error

StreamFunc is the type of a stream handler function. The initial arguments from the client side are passed as JSON in args. Any time an update is ready to be sent, it needs to be sent to the res channel. The stream can be closed by returning from the function. If the client side closes the connection, the context will be canceled. Because of this it is important to check ctx.Done() regularly.

type Update

type Update struct {
	Name   string           `json:",omitempty"` // Name of the action that was called
	URL    string           `json:",omitempty"` // URL that was loaded
	Error  *api.Error       `json:",omitempty"` // Error that occurred while handling the action
	HTML   []api.HTMLUpdate `json:",omitempty"` // DOM updates to apply
	JS     []api.JSCall     `json:",omitempty"` // JS calls to execute
	State  any              `json:",omitempty"` // State to pass back to the browser
	Stream []api.Stream     `json:",omitempty"` // Stream to subscribe to via websocket
}

Update is the returned body of a GUI API call.

func InsertAfter

func InsertAfter(selector, content string) *Update

InsertAfter returns a new Update that inserts HTML content on the same level after the passed selector. The selector gets passed to document.querySelector, so it can be any valid CSS selector.

func InsertBefore

func InsertBefore(selector, content string) *Update

InsertBefore returns a new Update that inserts HTML content on the same level before the passed selector. The selector gets passed to document.querySelector, so it can be any valid CSS selector.

func JSCall

func JSCall(name string, args any) *Update

JSCall returns a new Update that will call the registered JavaScript function that is identified by the name, and will pass the arguments.

func ReplaceContent

func ReplaceContent(selector, content string) *Update

ReplaceContent returns a new Update that replaces the content of the element that gets selected by the passed selector with the HTML content. The selector gets passed to document.querySelector, so it can be any valid CSS selector.

func ReplaceElement

func ReplaceElement(selector, content string) *Update

ReplaceElement returns a new Update that replaces the whole element that gets selected by the passed selector with the HTML content. The selector gets passed to document.querySelector, so it can be any valid CSS selector.

func Stream

func Stream(name string, args any) *Update

Stream returns a new Update that will connect to a stream with the passed name and arguments.

func (*Update) AddInsertAfter

func (u *Update) AddInsertAfter(selector, content string)

AddInsertAfter adds a HTML update that inserts HTML content on the same level after the passed selector. The selector gets passed to document.querySelector, so it can be any valid CSS selector.

func (*Update) AddInsertBefore

func (u *Update) AddInsertBefore(selector, content string)

AddInsertBefore adds a HTML update that inserts HTML content on the same level before the passed selector. The selector gets passed to document.querySelector, so it can be any valid CSS selector.

func (*Update) AddJSCall

func (u *Update) AddJSCall(name string, args any)

AddJSCall adds a new JSCall Update that will call the registered JavaScript function that is identified by the name, and will pass the arguments.

func (*Update) AddReplaceContent

func (u *Update) AddReplaceContent(selector, content string)

AddReplaceContent adds a new HTML Update that replaces the content of the element that gets selected by the passed selector with the HTML content. The selector gets passed to document.querySelector, so it can be any valid CSS selector.

func (*Update) AddReplaceElement

func (u *Update) AddReplaceElement(selector, content string)

AddReplaceElement adds a HTML Update that replaces the whole element that gets selected by the passed selector with the HTML content. The selector gets passed to document.querySelector, so it can be any valid CSS selector.

func (*Update) AddStream

func (u *Update) AddStream(name string, args any)

AddStream adds new stream Update that will connect to a stream with the passed name and arguments.

type UpdateablePage

type UpdateablePage interface {
	Page
	Update() (*Update, error)
}

UpdateablePage is a Page that can also produce a relative Update. This means that the browser can just update the relevant parts of the page and doesn't have to reload the whole page.

Directories

Path Synopsis
examples module

Jump to

Keyboard shortcuts

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