write Go to change HTML on the go
- go definesno logic in HTML
- server drivesno exposed endpoints
- no compromiseresponsive like SPA & native like MPA
as explicit as it gets
directly update HTML as UI events occurs
// composable ui fragment
type Counter struct {
// dynamic element
node doors.Node
// some state
count int
}
// click handler function
func (c *Counter) handler(ctx context.Context, _ d.REvent[d.PointerEvent]) bool {
c.count += 1
// updading node with new new content
c.node.Update(ctx, c.display())
// not done, keep hook active
return false
}
// display click count
templ (c *Counter) display() {
Clicked { fmt.Sprint(c.count) } time(s)!
}
// fragment render function
templ (c *Counter) Render() {
// render dynamic element
@c.node {
// initial content
← Click
}
// button with attribute constructor and attached handler on click action
<button { d.A(ctx, d.AClick { On: c.handler })...}>
Click Me!
</button>
}
superior routing
reactive, typed and declarative
// *this* page path is deserialized into struct (model)
type Path struct {
// path option 1: root
Main bool `path:""`
// path option 2: /show + parameter value, save to Id
Show bool `path:"show/:Id"`
Id int
// query parameter
Filter string `query:"filter"`
}
// prepare simple components to show on different path variants
templ home() {
Just home page
}
templ show(id int, filter string) {
id: { fmt.Sprint(id) }, filter: { filter }
}
// functional component
// Beam - source of path model updates (and also state piece, that you can create yourself)
templ Demo(beam doors.Beam[Path]) {
// subscribe to path changes and render based on active path
@doors.Sub(beam, func(p Path) templ.Component {
// if option 1 is active - render home
if p.Main {
return home()
// if option 2 is active - render show
} else if p.Show {
return show(p.Id, p.Filter)
}
// render nothing otherwise
return nil
})
// prepare model to form href attr
{{ model := common.MainPath { Main: true } }}
// construct link with model and active link setup
<a { d.A(ctx, d.AHref { Model: model })... }>
Main
</a>
// prepare model for another path variant
{{ model = common.MainPath { Show: true, Id: 9, Filter: "color" } }}
<a { d.A(ctx, d.AHref { Model: model } )... }>
Show #9
</a>
}
it's time to switch branches
In programming, a bad decision is like node_modules directory — it never stops growing.
Sometimes you have to step back to realize you’ve been solving problems that shouldn’t have existed in the first place.
I couldn’t shake that feeling — whether I was using frontend frameworks or dealing with sophisticated API access control schemas on the server side.
Now, after years of building and learning, I finally have the experience — and resources — to offer an alternative.
native and friendly form handeling
use deserialization or deal with HTTP request yourself
// form data for parsing
type FormData struct {
Name string `form:"name"`
Email string `form:"email"`
}
// composable ui fragment
type FormFragment struct {
// dunamic mode
n doors.Node
}
// form handler
func (f *FormFragment) handler(ctx context.Context, r doors.RForm[FormData]) bool {
// wait some time for demo purposes
<-time.NewTimer(time.Second).C
// show submitted data
f.n.Update(ctx, f.display(r.Data()))
return false
}
// display form values
templ (f *FormFragment) display(d FormData) {
name: { d.Name }, email: { d.Email }
}
// let's setup some form pending indication, that we will use later
var indicators = []doors.Indicate{
// set attribute to #form-submit
doors.IndicateAttrQuery("#form-submit", "aria-busy", "true"),
// change content of #form-submit
doors.IndicateContentQuery("#form-submit", "Simulating delay..."),
}
templ (f *FormFragment) Render() {
@f.n {
// initial dynamic node content
Submit Form
}
<form
// construct form attributes to hook on form submittion
// you can also use doors.ARawSubmit if you want to do form parsing/reading from http request yourself
{ doors.A(ctx, doors.ASubmit[FormData]{
// use our indication
Indicate: indicators,
// block form submission until previous one is processed
Mode: doors.ModeBlock(),
// attach handler
On: f.handler },
) ... }
>
<input
name="name"
placeholder="Name"
required="true"
/>
<input
type="email"
name="email"
placeholder="Email"
required="true"
/>
<button id="form-submit" role="submit">Submit</button>
</form>
}
server-driven for the real world
technology you shouldn’t avoid anymore
seamless frontend ↔ backend integration
trigger Go hooks from JS, call JS functions from Go, and pass data effortlessly
/*
in this *dirty* demo we will pass data to frontend,
then use that data in JS to trigger Go hook
in which js code will be called to show alert...
*/
templ Demo() {
// generate id
{{ id := uuid.New().String() }}
// use generated id
<button id={ id }></button>
// first we save attributes to a variable for readablity
{{ attrs := doors.A(ctx,
// pass data to frontend
doors.AData{
Value: id,
Name: "id",
},
// attach hook with int input and int output
doors.AHook[int, int]{
// block until previous one is processed
Mode: doors.ModeBlock(),
// hook name
Name: "callback",
// hook handler
On: func(ctx context.Context, r doors.RHook[int]) (int, bool) {
// make "alert" call to the frontend (check call handler in script below)
doors.Call(ctx, doors.CallConf{
Name: "alert",
Arg: fmt.Sprint("Go will multiply: ", r.Data()),
/* you can also process js call return value and respond to it */
})
// hook response
return r.Data() * 2, false
},
},
) }}
//
// convert inline script to script with src
// access atributes with $d, don't worry about global scope, and use async
@doors.Script {
// use our saved attributes on a js webpage script
<script { attrs... } />
// read data "id" from using special $d object (scoped to current script for attrubute reading)
const id = $d.data("id")
// use id to get button element
const button = document.getElementById(id);
let total = 1;
button.innerHTML = `multiply ${total} by 2 in Go`
// attach call with name "alert" (to closest dynamic parent element or root),
$d.on("alert", (message) => {
// just alert whatever go gives
alert(message)
/* you can also provide return value here */
}) /* <- you can also handle Go response to call result (🤯) in third argument*/
// add plain event listener to our button
button.addEventListener('click', async () => {
try {
// trigger hook with the name "callback" and total value as argument
total = await $d.hook("callback", total)
// update button content with the response
button.innerHTML = `multiply ${total} by 2 in Go`
}catch(e) {
// check if hook blocked (we set block mode, so it's ok)
if(e.isBlocked()) {
console.log("Call blocked")
return
}
throw e
}
})
</script>
}
}
complete web dev toolkit
and different in a good way
- create state sources and derive new ones from it
- update or mutate state, all changes will be distributed in a parallel and safe way
- subscribe to introduce controlled DOM updates
- parallel rendering & event handeling
- controlled resource consumption with a goroutine pool and instance limit per session
- extra-thin client side
- indication for pending operations and active links
- debouncing and other event handeling modes
- integration of js apps and modules for UI-heavy stuff
- no exposed endpoints you don't have to deal endpoinds at all
- always SSR hydration hell is in the past
- unique page instance users can only interact with what they see
- minimal js zero NPM dependencies
- build your js/ts modules
- cook import map & CSP headers
- host files in protected or public modes
- transform inline scripts and styles to external
- it's just router - use it with standart Go http server
- build with awesome templating solution - templ
- batteries included bundle and build js/ts without extra tools
a lifetime affordable license
to ensure independence and sustainability
- not backed by big company or VC funding
- not relying on spare-time maintanance
- here to give you the best tool
don't miss
tell me that you are interested, get email on the beta launch - no subscription