A lightweight, server-rendered UI framework for Go that enables building interactive web applications from your Go code. Define your UI in Go and let the framework handle the rest.
- No JavaScript required
- No build step required
- No need to learn new syntax for writing HTML
- No plugins required for writing HTML
- Server-side rendering with client-side interactivity
- Built-in form handling and validation
- Automatic WebSocket-based live reload for development
- Tailwind CSS integration
- Session management
- File upload and download support
- Customizable HTML components
- Built-in form validation using go-playground/validator
- Support for various input types (text, email, phone, number, date, time, etc.)
go get github.com/michalCapo/go-srui
package main
import (
"fmt"
"github.com/michalCapo/go-srui/ui"
)
func main() {
// Create a new app instance
app := ui.MakeApp("en")
// Enable live reload for development
app.Autoreload()
// Define a page handler
page := func(ctx *ui.Context) string {
return app.Html("My App", "p-8 bg-gray-200",
ui.Div("flex flex-row gap-4")(
ui.Div("flex justify-start gap-4 items-center")(
"Hello, World!",
),
),
)
}
// Register the page
app.Page("/", page)
// Start the server
app.Listen(":8080")
}
Basically as HTMX, but with Go. You define your components or actions in Go. Register these actions so framework can use them. Component is a function that returns HTML. Action is a function that returns HTML or calls another action. When you call action, it will be executed on server and result will be rendered in HTML document.
There are several ways to call actions:
ctx.Call(action, values...)
- call action with valuesctx.Submit(action)
- submit form with actionctx.Click(action)
- click button with actionctx.Send(action)
- send form with action
Depending on your needs you can render result of action in different places:
ctx.Replace(target)
- replace target with resultctx.Render(target)
- render result inside targetctx.None()
- do not render result
Code base is very simple, so please check examples and source code to see how it works.
Div
- Container elementSpan
- Inline text elementForm
- Form containerButton
- Button elementSelect
- Dropdown selectTextarea
- Multi-line text inputA
- Link elementTable
- Table elementLabel
- Label element
IText
- Text inputIPassword
- Password inputINumber
- Numeric inputIDate
- Date inputITime
- Time inputIDateTime
- DateTime inputIArea
- Textarea inputIValue
- Display-only value field
type LoginForm struct {
Email string `validate:"required,email"`
Password string `validate:"required"`
}
func (form *LoginForm) Login(ctx *ui.Context) string {
if err := ctx.Body(form); err != nil {
return form.Render(ctx, &err)
}
// Handle login logic
return form.Success(ctx)
}
Enable live reload during development:
app.Autoreload()
session := ctx.Session(db, "user")
session.Load(&userData)
session.Save(&userData)
// Download file
ctx.DownloadAs(&fileReader, "application/pdf", "document.pdf")
Built-in support for go-playground/validator:
type User struct {
Email string `validate:"required,email"`
Age int `validate:"gte=18"`
}
The framework integrates with Tailwind CSS by default. You can add custom styles through the HtmlHead
method:
app.HtmlHead = append(app.HtmlHead, `<link rel="stylesheet" href="custom.css">`)
A simple counter with increment and decrement buttons:
package main
import (
"fmt"
"github.com/michalCapo/go-srui/ui"
)
// Create counter with initial count value
func Counter(count int) *TCounter {
return &TCounter{Count: count}
}
// Struct definition
type TCounter struct {
Count int
}
func (counter *TCounter) Increment(ctx *ui.Context) string {
// Scan request body and update counter struct
ctx.Body(counter)
// Increase count by 1
counter.Count++
// Render component
return counter.Render(ctx)
}
func (counter *TCounter) Decrement(ctx *ui.Context) string {
// Scan body
ctx.Body(counter)
counter.Count--
if counter.Count < 0 {
counter.Count = 0
}
// Render component as result of this action
return counter.Render(ctx)
}
func (counter *TCounter) Render(ctx *ui.Context) string {
// Temporary id
target := ui.Target()
// Render HTML, see target (placeholder) at the end, this is place where action result will be rendered
return ui.Div("flex gap-2 items-center bg-purple-500 rounded text-white p-px", target)(
ui.Button().
// Click will call decrement action with counter variable as values sent to this action and result will be rendered at target place
Click(ctx.Call(counter.Decrement, counter).Replace(target)).
Class("rounded-l px-5").
Render("-"),
// Display current count
ui.Div("text-2xl")(fmt.Sprintf("%d", counter.Count)),
ui.Button().
// With action result you can replace (overwrite the target component) or render (inline into target component)
Click(ctx.Call(counter.Increment, counter).Replace(target)).
Class("rounded-r px-5").
Render("+"),
)
}
A complete login form with validation and error handling:
package main
import (
"github.com/go-playground/validator/v10"
"github.com/michalCapo/go-srui/ui"
)
func LoginForm(name string) *TLoginForm {
return &TLoginForm{Name: name}
}
// Defining login form with validations for given fields
type TLoginForm struct {
Name string `validate:"required,oneof=user"`
Password string `validate:"required,oneof=password"`
}
// We want to display success message
func (form *TLoginForm) Success(ctx *ui.Context) string {
return ui.Div("text-green-600 max-w-md p-8 text-center font-bold rounded-lg bg-white shadow-xl")("Success")
}
// Login action
func (form *TLoginForm) Login(ctx *ui.Context) string {
// Scan request body, if there is an error render with using render method of this component
if err := ctx.Body(form); err != nil {
return form.Render(ctx, &err)
}
v := validator.New()
// Let's validate our input, and display error if any
if err := v.Struct(form); err != nil {
return form.Render(ctx, &err)
}
// Great a successful login
return form.Success(ctx)
}
// Translations for login form
var translations = map[string]string{
"Name": "User name",
"has invalid value": "is invalid",
}
// Temporary id
var target = ui.Target()
func (form *TLoginForm) Render(ctx *ui.Context, err *error) string {
// Submitting form will call login action and result will be rendered to target id
return ui.Form("flex flex-col gap-4 max-w-md bg-white p-8 rounded-lg shadow-xl", target, ctx.Submit(form.Login).Replace(target))(
// Display all error in one place
ui.ErrorForm(err, &translations),
// Text component
ui.IText("Name", form).
// Is required
Required().
// If there is specific error for this field display it
Error(err).
Render("Name"),
// Password component
ui.IPassword("Password").
Required().
Error(err).
Render("Password"),
// Submit button, see submit part on form several lines above
ui.Button().
Submit().
Color(ui.Blue).
Class("rounded").
Render("Login"),
)
}
A button that toggles between two states:
package main
import (
"github.com/michalCapo/go-srui/ui"
)
func main() {
app := ui.MakeApp("en")
app.Autoreload()
buttonId := ui.Target()
var show **ui.Callable
button := func(ctx *ui.Context) string {
return ui.Button(buttonId).
Click(ctx.Call(show).Replace(buttonId)).
Class("rounded").
Color(ui.Blue).
Render("Click me")
}
hide := app.Callable(button)
page := func(ctx *ui.Context) string {
show = ctx.Callable(func(ctx *ui.Context) string {
return ui.Div("flex gap-2 items-center bg-red-500 rounded text-white p-px pl-4", buttonId)(
"Clicked",
ui.Button().
Click(ctx.Call(hide).Replace(buttonId)).
Class("rounded").
Color(ui.Red).
Render("Hide me"),
)
})
return app.Html("Test", "p-8 bg-gray-200",
ui.Div("flex flex-row gap-4")(
ui.Div("flex justify-start gap-4 items-center")(
button(ctx),
),
),
)
}
app.Page("/", page)
app.Listen(":1422")
}
ctx.Body(output any)
- Parse request body into structctx.Call(method Callable, values ...any)
- Call action with values, returns Actions structctx.Submit(method Callable, values ...any)
- Submit form with action, returns Submits structctx.Click(method Callable, values ...any)
- Click button with action, returns Submits structctx.Send(method Callable, values ...any)
- Send form with action, returns Actions structctx.Success(message string)
- Display success messagectx.Error(message string)
- Display error messagectx.Redirect(href string)
- Redirect to URLctx.Reload()
- Reload current pagectx.DownloadAs(file *io.Reader, content_type string, name string)
- Download filectx.Callable(action Callable)
- Create callable reference for an actionctx.Action(uid string, action Callable)
- Register action with custom UID
.Render(target Attr)
- Render result inside target.Replace(target Attr)
- Replace target with result.None()
- Do not render result
ui.IText(name string, data ...any)
- Text inputui.IPassword(name string, data ...any)
- Password inputui.INumber(name string, data ...any)
- Number inputui.IDate(name string, data ...any)
- Date inputui.ITime(name string, data ...any)
- Time inputui.IDateTime(name string, data ...any)
- DateTime inputui.IArea(name string, data ...any)
- Textarea input
ui.Button(attr ...Attr)
- Create button.Click(action string)
- Set click action.Submit()
- Make submit button.Color(color string)
- Set button color.Class(class ...string)
- Set CSS classes.Disabled(value bool)
- Disable button.Render(text string)
- Render button
ui.Target()
- Generate unique target IDui.ErrorForm(err *error, translations *map[string]string)
- Display form errorsui.Trim(s string)
- Trim whitespaceui.Normalize(s string)
- Normalize string for HTMLui.Classes(classes ...string)
- Join CSS classesui.If(cond bool, value func() string)
- Conditional renderingui.Map(values []T, iter func(*T, int) string)
- Map over sliceui.For(from, to int, iter func(int) string)
- For loop rendering
MIT License - see LICENSE file for details
Contributions are welcome! Please feel free to submit a Pull Request.