Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

105 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 example in light theme Todo grid example in dark theme
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
w2widget Pre-built HTTP handlers for common UI widgets (SQL explorer)
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"},
    WhereMapping:   map[string]string{"id": "t.id", "name": "t.name"},
    OrderByMapping: 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
})

w2widget built-in widgets

w2widget provides ready-to-use HTTP handlers for common UI widgets.

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",  w2widget.SQLiteSchemaHTTPHandler(db))
v1.HandleFunc("POST /sql", w2widget.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

Built-in SQL explorer widget

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 := w2widget.SQLExecQuery(ctx, db, query)
schema, err := w2widget.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.

About

Data-driven Go bindings for w2ui

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages