|
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 | +[](https://pkg.go.dev/github.com/mbertschler/guiapi) |
| 4 | +[](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 | + |
| 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) |
0 commit comments