Go bindings for w2ui
Go bindings for the w2ui JavaScript UI Library.
Handles request parsing, response serialization, SQL query building, and database operations for w2grid, w2form, and dropdown components running in JSON mode.


| Todo grid in light theme |
Todo grid in dark theme |
Install
go get github.com/dv1x3r/w2go
Packages
| Package |
Description |
w2 |
Core types, request parsers, and response writers |
w2sql |
Translates w2ui requests into SQL (filters, sorters, limits, updates) using go-sqlbuilder |
w2db |
High-level CRUD helpers that execute queries against a *sql.DB or *sql.Tx |
w2explorer |
Pre-built HTTP handler for SQL Explorer widget |
w2file |
Multipart file upload parsing helpers |
w2sort |
In-memory slice reordering for drag-and-drop support |
w2lib |
Embedded w2ui JS/CSS assets served via embed.FS |
Usage
w2go is framework-agnostic and works with net/http, Echo, Fiber, or any other Go HTTP framework. The snippets below use standard net/http.
Parsing requests and writing responses
The w2 package handles the JSON parsing between w2ui and your server.
w2grid
// GET - load records
req, err := w2.ParseGetGridRequest(r.URL.Query().Get("request"))
res := w2.NewGetGridResponse(records, total)
res.Write(w)
// POST - save inline edits
req, err := w2.ParseSaveGridRequest[Todo](r.Body)
// req.Changes is a typed slice
res := w2.NewSuccessResponse()
res.Write(w, http.StatusOK)
// POST - delete records
req, err := w2.ParseRemoveGridRequest(r.Body)
// req.ID is []int
res := w2.NewSuccessResponse()
res.Write(w, http.StatusOK)
// POST - drag-and-drop reorder (single row)
req, err := w2.ParseReorderGridRequest(r.Body)
// req.RecID int, req.MoveBefore int, req.Bottom bool
res := w2.NewSuccessResponse()
res.Write(w, http.StatusOK)
w2form
// GET - load record
req, err := w2.ParseGetFormRequest(r.URL.Query().Get("request"))
// req.RecID holds the record id
res := w2.NewGetFormResponse(record)
res.Write(w)
// POST - save record
req, err := w2.ParseSaveFormRequest[Todo](r.Body)
// req.Record holds the record
res := w2.NewSaveFormResponse(req.RecID)
res.Write(w)
Dropdown
// GET - load options
req, err := w2.ParseGetDropdownRequest(r.URL.Query().Get("request"))
// req.Search string, req.Max int
res := w2.NewGetDropdownResponse(records)
res.Write(w)
Error response
res := w2.NewErrorResponse("something went wrong")
res.Write(w, http.StatusInternalServerError)
w2.Field[T] - tracking inline edits
Inline grid edits only send changed fields. Wrap nullable or optional fields with w2.Field[T] to distinguish between "not sent", "sent as null", and "sent with a value":
type Todo struct {
ID int `json:"id"`
Name string `json:"name"`
Description w2.Field[string] `json:"description"`
Quantity w2.Field[int] `json:"quantity"`
}
w2.Field[T] implements w2db.Providable, so w2db.Update skips fields that were not sent by the client.
By default, a sent empty field writes SQL NULL; use field.NotNull() when an empty field should write the zero value instead.
w2sql SQL builder integration
w2sql translates w2ui request data into SQL clauses using go-sqlbuilder. Field names are mapped through a whitelist to prevent injection.
req, _ := w2.ParseGetGridRequest(r.URL.Query().Get("request"))
sb := sqlbuilder.Select("t.id", "t.name", "t.description").From("todo as t")
mapping := map[string]string{
"id": "t.id",
"name": "t.name",
}
w2sql.Where(sb, req, mapping) // search filters
w2sql.OrderBy(sb, req, mapping) // column sorting
w2sql.Limit(sb, req) // pagination limit
w2sql.Offset(sb, req) // pagination offset
query, args := sb.BuildWithFlavor(sqlbuilder.SQLite)
Applying inline updates
w2sql.Set sets the column if the field was provided. By default, an empty field writes SQL NULL; use field.NotNull() when an empty field should write the zero value instead.
for _, change := range req.Changes {
ub := sqlbuilder.Update("todo")
ub.Where(ub.EQ("id", change.ID))
w2sql.Set(ub, change.Description.NotNull(), "description")
w2sql.Set(ub, change.Quantity, "quantity")
}
w2db database helpers
w2db eliminates the boilerplate of building queries and scanning rows. Each function accepts a *sql.DB, *sql.Tx, or any value that satisfies the QueryExecer interface. Every function also has a Context variant (e.g. w2db.GetGridContext) that accepts a context.Context as the first argument.
w2grid
// Load records with pagination, sorting, and search
res, err := w2db.GetGrid(db, req, w2db.GetGridOptions[Todo]{
From: "todo as t",
Select: []string{"t.id", "t.name", "t.description"},
Where: map[string]string{"id": "t.id", "name": "t.name"},
OrderBy: map[string]string{"id": "t.id", "name": "t.name"},
Scan: func(rows *sql.Rows, record *Todo) error {
return rows.Scan(&record.ID, &record.Name, &record.Description)
},
})
// Save inline edits
affected, err := w2db.SaveGrid(tx, req, w2db.SaveGridOptions[Todo]{
BuildOptions: func(change Todo) w2db.UpdateOptions {
return w2db.UpdateOptions{
Update: "todo",
Values: map[string]any{
"description": req.Record.Description.NotNull(),
"quantity": req.Record.Quantity,
},
Where: map[string]any{"id": req.RecID},
}
},
})
// Delete records
affected, err := w2db.RemoveGrid(db, req, w2db.RemoveGridOptions{
From: "todo",
IDField: "id",
})
// Reorder rows by updating a position column
affected, err := w2db.ReorderGrid(db, req, w2db.ReorderGridOptions{
Update: "status",
IDField: "id",
SetField: "position",
})
w2form
// Load a single record by ID
res, err := w2db.GetForm(db, req, w2db.GetFormOptions[Todo]{
From: "todo",
IDField: "id",
Select: []string{"id", "name", "description"},
Scan: func(row *sql.Row, record *Todo) error {
return row.Scan(&record.ID, &record.Name, &record.Description)
},
})
// Insert a new record
lastID, err := w2db.Insert(db, w2db.InsertOptions{
Into: "todo",
Values: map[string]any{
"name": req.Record.Name,
"description": req.Record.Description,
},
})
// Update an existing record
affected, err := w2db.Update(db, w2db.UpdateOptions{
Update: "todo",
Values: map[string]any{
"name": req.Record.Name,
"description": req.Record.Description,
},
Where: map[string]any{"id": req.RecID},
})
Dropdown
// Load options filtered by search text
res, err := w2db.GetDropdown(db, req, w2db.GetDropdownOptions{
From: "status",
IDField: "id",
TextField: "name",
OrderByField: "position",
})
Transactions
w2db.WithinTransaction handles begin, commit, and rollback. Pass the *sql.Tx directly into any w2db function since they all accept the QueryExecer interface:
err := w2db.WithinTransaction(db, func(tx *sql.Tx) error {
return nil
})
WithinTransactionContext is also available when you need to pass a context.Context:
err := w2db.WithinTransactionContext(ctx, db, func(ctx context.Context, tx *sql.Tx) error {
return nil
})
w2explorer provides ready-to-use HTTP handlers for SQL Explorer widget.
SQL Explorer
A browser-based SQL query tool with a schema sidebar, query editor, and result grid. Useful for development and debugging.
Register the two backend endpoints:
v1.HandleFunc("GET /sql", w2explorer.SQLiteSchemaHTTPHandler(db))
v1.HandleFunc("POST /sql", w2explorer.SQLExecHTTPHandler(db))
Mount the frontend widget from w2ui.widgets.js:
import { createSqlExplorerLayout } from "/lib/w2ui.widgets.js";
const sqlExplorer = createSqlExplorerLayout({ url: "/api/v1/sql" });
sqlExplorer.render("#container");
Features:
- Schema sidebar with database/table/column tree
- Query editor with
Tab indentation and Alt+Enter to execute
- Execute selection or full query
- Cancel in-flight queries
- Result grid with row count and elapsed time
Note: SQL Explorer executes arbitrary SQL from the client. Do not expose it in production!
If you prefer to handle the HTTP layer yourself, use the lower-level functions directly:
res, err := w2explorer.SQLExecQuery(ctx, db, query)
schema, err := w2explorer.SQLiteSelectSchema(ctx, db)
w2file file uploads
w2file provides helpers for parsing multipart file uploads sent by the w2upload helper in w2ui.helpers.js.
// Parse files[] from a multipart/form-data request (default 32 MB limit)
headers, err := w2file.ParseMultipartFiles(r)
// Custom memory buffer and per-file size limit
headers, err := w2file.ParseMultipartFilesWithOptions(r, w2file.ParseMultipartFilesOptions{
Memory: 64 << 20, // 64 MB in-memory buffer
MaxUploadSize: 10 << 20, // 10 MB per file limit
})
for _, h := range headers {
f, _ := h.Open()
defer f.Close()
// process file...
}
w2sort array reordering
w2sort.ReorderArray applies a drag-and-drop reorder request to a slice of IDs in memory.
req, _ := w2.ParseReorderGridRequest(r.Body)
ids := []int{1, 2, 3, 4, 5} // current order from the database
if err := w2sort.ReorderArray(ids, req); err != nil {
// req.RecID not found in the slice
}
// ids now reflects the new order - persist it to the database
Example
The complete CRUD demo is included using an in-memory SQLite database:
go run ./example/main.go
Open http://localhost:3000 in your browser.
The example also includes a Docker image definition:
docker build -f example/Dockerfile -t w2go-demo .
docker run --rm -p 3000:3000 w2go-demo
# or in readonly mode
docker run --rm -p 3000:3000 w2go-demo --addr 0.0.0.0 -port 3000 -readonly
License
Licensed under the MIT license.