Skip to content

Commit b547ff3

Browse files
committed
document most of guiapi in the README, including the principles and concepts
1 parent 84f053c commit b547ff3

File tree

4 files changed

+260
-30
lines changed

4 files changed

+260
-30
lines changed

README.md

Lines changed: 252 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,252 @@
1-
# guiapi
2-
3-
Guiapi
4-
5-
## TODOs
6-
7-
- [x] is the Layout returning as beautiful as it can be?
8-
- [x] nope, we should return an interface
9-
- [x] something for long running requests, loading indicator?
10-
- [x] debounce as a feature _ga-block_?
11-
- [x] websocket for server side updates
12-
- [x] server side instant redirects
13-
- [x] global error handler
14-
- [x] check bundling API again
15-
- [x] page only init stuff needs nicer API (what is put in <script> globals)
16-
- [x] consider removing html coupling from the API
17-
- [ ] clean up library and examples
18-
- [ ] documentation
19-
- [ ] tests
20-
- [ ] maybe use https://github.com/chromedp/chromedp
21-
22-
### Refactoring ideas
23-
24-
- [x] try reflection for component config. Nope - reflection is never clear
25-
- [x] split Context into PageCtx and ActionCtx
26-
- [ ] turn `StreamRouter` into `map[string]StreamFunc{}`, follow name/args convention
27-
- [ ] move as much as possible into subpackages, asset building, the JSON api objects
1+
# `guiapi` - Multi Page Web App Framework for Go
2+
3+
[![Go documentation](https://img.shields.io/badge/Go_documentation-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/mbertschler/guiapi)
4+
[![npm package](https://img.shields.io/badge/npm_package-cc0000?logo=npm&logoColor=white)](https://www.npmjs.com/package/guiapi)
5+
6+
Guiapi (an API for GUIs) is a framework for building interactive multi page web
7+
applications with minimal JavaScript, using Go on the server side for handling
8+
most of the logic and rendering the HTML. Besides rendering the different pages,
9+
it can also update the DOM from the server side, after a server Action was triggered
10+
by a browser event or JavaScript. It tries to minimize the amount of JavaScript
11+
that needs to be written and sent to the browser.
12+
13+
# Principles
14+
15+
Guiapi lives between the worlds of an old school web app, that required a full
16+
page reload for every user action and a typical modern single page app that
17+
requires a REST API and lots of JavaScript to render the different HTML views.
18+
19+
You should give this framework a try, if you agree with the following principles
20+
that `guiapi` is built on:
21+
22+
- Rendering HTML should not require a REST API
23+
- Most web apps should be multi page apps out of the box
24+
- Most of the web apps logic should run on the server side
25+
- Most HTML should be rendered on the server side
26+
- JavaScript should only be used for features where it is necessary
27+
28+
### Is this an alternative to React and other frontend frameworks?
29+
30+
The short answer is no. The goal of guiapi is to provide a framework for a multi
31+
page server rendered web app, which is a different goal than that of a frontend
32+
framework. It can make sense to use guiapi and a frontend framework together.
33+
The main structure of the different pages and inital page layout can be provided
34+
via guiapi pages, and afterwards the frontend framework takes over for user
35+
interface components that require a high degree of interactivity. These could be
36+
complex form validations, interactive data visualizations and similar, where server
37+
roundtrips for every update to the UI would not make sense.
38+
39+
In practice though, many web apps today are built with a frontend framework which
40+
renders 100% of the UI in the browser, even if most of the pages don't need this high
41+
degree of interactivy. Instead the pages could just as easily be rendered server side.
42+
In those cases it might not even be necessary to use a framework, and the same
43+
end result can be achieved with just a few lines of vanilla JavaScript for the
44+
interactive components.
45+
46+
# Concepts
47+
48+
![guiapi diagram between server and browser](./doc/guiapi_browser_server_diagram.svg)
49+
50+
### Pages
51+
52+
Pages represent the different views of your app. They can be accessed directly by
53+
navigating to the page URL, in which case the server returns a usuale HTML document.
54+
If you are navigating from one guiapi Page to the next, this can even happen via an
55+
Action and Update. In this case no full page reload is needed, but the URL and page
56+
content is still updated as if the page was visited directly.
57+
58+
### Actions
59+
60+
Actions are events that are sent from the browser to the server. They can either be
61+
originating from a HTML element with `ga-on` attribute, or from the guiapi `action`
62+
JavaScript function. Actions consist of a name and optional arguments. These
63+
actions are transferred as JSON via a POST request to the endpoint that is typically
64+
called `/guiapi`. The response to an Action is an Update.
65+
66+
### Updates
67+
68+
Updates are sent from the server to the browser. They can be the response to an
69+
Action, or they can be sent via a Stream. Updates consist of a list of HTML updates,
70+
JS calls and Streams to connect to.
71+
72+
#### HTML updates
73+
After an update is received, the HTML updates are applied to the DOM. This can for
74+
example mean that the the element with the selector `#content` should be replaced
75+
with some different HTML, or that a new element should be inserted before or after
76+
a spefic selector.
77+
78+
#### JS calls
79+
JS calls can be explicitly added to an Update, and the function with the given name
80+
will be called with the passed arguments. For this to work the function first needs
81+
to get registered. Besides an explicit JS call it is often useful to run some
82+
JavaScript in relation to one of the newly added HTML elements. In this case the
83+
new HTML needs to have one of the special guiapi attributes like `ga-init`.
84+
85+
### State
86+
87+
Sometimes a web page has a certain state that needs to be known to the server too,
88+
for example filter settings for a list of items. This state gets transferred with
89+
ever Action to the server, and can be updated by receiving a new state in an Update.
90+
The state is similar to a cookie and usually doesn't need to be accessed by client
91+
JavaScript functions.
92+
93+
### Streams
94+
95+
Streams are similar to Actions that return an Update, but instead of returning just
96+
a single Update, the Stream can return many Updates over time, until the Stream is
97+
closed. This is not done via a HTTP request, but via a WebSocket connection. Similar
98+
to actions, a Stream also consists of a name and arguments.
99+
100+
> [!WARNING]
101+
> While the other concepts of guiapi (Pages, Actions, Updates) have proven useful
102+
> web applications since 2018, Streams are a new concept for server side updates and
103+
> should be considered experimental. They might change significantly in the future.
104+
105+
# API Documentation
106+
107+
The guiapi API consists of the Go, JavaScript and HTML attribute APIs.
108+
109+
## Go API
110+
111+
See [Go package documentation](https://pkg.go.dev/github.com/mbertschler/guiapi).
112+
The main types to look out for are `Server` which handles HTTP requests, `Page`
113+
for page rendering and updating, and `Request` and `Response` that explain the
114+
RPC format.
115+
116+
### Asset bundling using `esbuild`
117+
118+
The [assets package](https://pkg.go.dev/github.com/mbertschler/guiapi/assets) contains
119+
a wrapper around [esbuild](https://esbuild.github.io/) that can be used to bundle
120+
JavaScript and CSS assets.
121+
122+
With this package you don't need an external JS bundler, as the building can happen
123+
every time you start the Go binary to embed your assets. The `esbuild` tool adds about
124+
5 MB to the binary size, so if you don't need this functionality in production and
125+
include the built assets in another way, for example with `go:embed`, then you can use
126+
the `no_esbuild` build tag like this: `go build -tags no_esbuild`, which replaces the
127+
asset building function with a no-op. You can check if the `esbuild` tool is available
128+
with the `assets.EsbuildAvailable()` function.
129+
130+
## HTML attribute API
131+
132+
The following attributes get activated when `setupGuiapi()` is called after the page
133+
load, and they also get initialized whenever they appear in HTML that was updated by
134+
an Update from an Action or Stream.
135+
136+
#### Event handlers: `ga-on`
137+
138+
```html
139+
<button class="ga" ga-on="click" ga-func="myClickFunction">click me</button>
140+
<button class="ga" ga-on="click" ga-action="Page.Click" ga-args="abc">click me</button>
141+
```
142+
143+
The `ga-on` attribute is used to trigger a server action or JavaScript functions every time
144+
the event name specified in the attribute happens event listeners on HTML elements. In
145+
the first example above, the `myClickFunction` function is called every time the button is
146+
clicked. In the second example, the `Page.Click` server action is called with "abc" as the
147+
argument.
148+
149+
```html
150+
<input class="my-form" type="text" name="name" />
151+
<input class="my-form" type="number" name="amount" />
152+
<button class="ga" ga-on="click" ga-action="Page.Click" ga-values=".my-form">submit</button>
153+
```
154+
155+
If you want to submit multiple inputs to a server action, you can use the `ga-values`
156+
attribute. The value of the attribute gets passed to `document.querySelectorAll()` and
157+
all .... value, name
158+
159+
#### Initializer functions: `ga-init`
160+
161+
```html
162+
<div class="ga" ga-init="myInitFunction" ga-args='{"val":123}'></div>
163+
```
164+
165+
If the `ga-args` can't be parsed as JSON, they are passed as a string to the
166+
function.
167+
168+
#### Lightweight page load: `ga-link`
169+
170+
```html
171+
<a href="/other/page" class="ga" ga-link>other page</a>
172+
```
173+
174+
If you add `ga-link` attribute to an `a` with a `href`, clicking on the link
175+
will navigate to the other page via a guiapi call and partial page update
176+
without reloading the whole page. Behind the scenes the history API is used,
177+
so that navigating back and forth still works as expected. This is useful if
178+
you have some JavaScript logic that should keep running between pages and
179+
should also speed up page navigation.
180+
181+
## JavaScript API
182+
183+
To make a guiapi app work, the `setupGuiapi()` function needs to be called.
184+
Before that, any functions that are referenced from HTML or update JSCalls
185+
need to be registered with `registerFunctions()`.
186+
187+
#### Calling a server action from JavaScript
188+
189+
```ts
190+
action(name: string, args: any, callback: (error: any) => void)
191+
```
192+
193+
This can be used to run a server action from any JavaScript. The callback is called
194+
with any potential error after the update from the server was applied.
195+
196+
#### Registering your JS functions for guiapi
197+
198+
```ts
199+
registerFunctions(obj: { string: Function })
200+
```
201+
202+
Registers functions that can be called from HTML via `ga-func` or `ga-init` and
203+
also makes them available for JSCalls coming via a server update.
204+
205+
#### Initializing the guiapi app
206+
207+
```ts
208+
setupGuiapi(config: {
209+
state: any,
210+
stream: {
211+
name: string,
212+
args: any,
213+
},
214+
debug: boolean,
215+
errorHandler: (error: any) => void,
216+
})
217+
```
218+
219+
This initializes all HTML elements with the `ga` class and sets up the event listeners.
220+
Functions that are referenced from the HTML with `ga-func` or `ga-init` need to be
221+
registered with `registerFunctions()` before calling `setupGuiapi()`.
222+
223+
#### Debug logging
224+
225+
```ts
226+
debugPrinting(enable: boolean)
227+
```
228+
229+
With this function you can turn on or off loging whenever guiapi calls an action and
230+
receives an update from the server. This can be useful during development.
231+
232+
## Examples
233+
234+
Go to `./examples` and running them with `go run .` will start a webserver at
235+
localhost:8000.
236+
237+
It contains 3 examples:
238+
239+
- `/` is a guiapi implentation of [TodoMVC](https://todomvc.com/)
240+
- `/counter` is a simple counter that can be increased and decreased
241+
- `/reports` is a demonstrates streams by updating the page from the server
242+
243+
The examples are a good starting point to build your own app with guiapi.
244+
245+
# Contributing
246+
247+
If you are using guiapi and have any feedback, please let me know.
248+
Issues and discussions are always welcome.
249+
250+
---
251+
252+
Built by [@mbertschler](https://x.com/mbertschler)

doc/guiapi_browser_server_diagram.svg

Lines changed: 1 addition & 0 deletions
Loading

request_response.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type Request struct {
1515
// Args as object, gets parsed by the called function
1616
Args json.RawMessage `json:",omitempty"`
1717
// State is can be passed back and forth between the server and browser.
18-
// It is held in a Javascript variable, so there is one per browser tab.
18+
// It is held in a JavaScript variable, so there is one per browser tab.
1919
State json.RawMessage `json:",omitempty"`
2020
}
2121

server.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type Page interface {
5858
WriteHTML(io.Writer) error
5959
}
6060

61-
type PageUpdater interface {
61+
type UpdateablePage interface {
6262
Page
6363
Update() (*Response, error)
6464
}
@@ -96,7 +96,7 @@ func (s *Server) pageUpdate(path string, page PageFunc) {
9696
http.Error(c.Writer, "Internal Server Error", http.StatusInternalServerError)
9797
return
9898
}
99-
updater, ok := res.(PageUpdater)
99+
updater, ok := res.(UpdateablePage)
100100
if !ok {
101101
err := fmt.Sprintf("page %q is not updateable", path)
102102
log.Println(err)
@@ -122,6 +122,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
122122
s.httpRouter.ServeHTTP(w, r)
123123
}
124124

125+
func (s *Server) Router() *httprouter.Router {
126+
return s.httpRouter
127+
}
128+
125129
func (s *Server) AddStream(name string, fn StreamFunc) {
126130
s.streams[name] = fn
127131
}

0 commit comments

Comments
 (0)