Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

   /\/\/\/\/\/\/\/\
  <   g r i d s a w   >    the Ripsaw
   \/\/\/\/\/\/\/\/     data cut into clean grids, forms & widgets

gridsaw

A pluggable multi-engine renderer + data-bound, web2py-style components (SmartGrid, CRUD forms) — loosely coupled, reusable in any Python framework.

gridsaw is the rendering/component counterpart to sqladal (the data layer):

  • Render with upytl, YATL (web2py), or Jinja2 — each an optional extra. Use one engine, or mix them.
  • Cross-engine embedding: render an upytl component (the grid/form) inside a Jinja2 or YATL page via a component() bridge (and pure-Jinja / pure-YATL works too).
  • Backend-agnostic data: the SAME SmartGrid/Form work over classic pydal, voodoodal, sqladal, and SQLAlchemy ORMsync or async — via a DataSource adapter (gridsaw.data). ORM validation is auto-derived from the column and can reuse pydal validators via column.info['requires'].
  • HTMX-first interactivity: htmx=True makes grids/forms emit hx-* and the route returns a partial fragment on HX-Request; live updates via SSE/WebSocket (gridsaw.htmx).

Same component, four backends

from gridsaw.components import SmartGrid, Form

SmartGrid(db(db.person))                       # pydal / voodoodal / sqladal (sync)
await SmartGrid(adb(adb.person)).aview_model() # sqladal AsyncDAL (async)
SmartGrid(Person, session=session)             # SQLAlchemy ORM (sync Session)
SmartGrid(Person, session=async_session)       # SQLAlchemy ORM (async AsyncSession)

HTMX in one picture

from gridsaw.htmx import is_htmx
grid = SmartGrid(db(db.person), base_url="/people", htmx=True, target="people-grid")

@app.get("/people")          # sort/search/paginate swap the grid via hx-get
def people():
    frag = grid.render(request_vars=dict(request.query))
    return frag if is_htmx(request.environ) else PAGE.format(body=frag)

Widgets, styles & customization (web2py / py4web style)

  • Pluggable styles (py4web FormStyle / GridClassStyle): style="bulma" (default, responsive), "bootstrap5", "tailwind", "plain", a Style instance, or a dict of per-slot overrides. Every element also keeps a stable gridsaw-* hook so custom CSS always works. (theme= is a back-compat alias.)
  • Bundled field widgets (web2py SQLFORM.widgets): string, text, password, integer/double/decimal, boolean (checkbox), date/time/datetime, select, radio, checkboxes, email/url/number/color/range, file upload, readonly, and a server-backed autocomplete (AutocompleteWidget(url) + autocomplete_options) — chosen by field type. Custom widgets are a one-liner: Form(..., widgets= {"color": lambda field, value, ctx: '<input type=color name=%s>' % ctx.name}).
  • Per-field form options: labels=, comments= (help text), placeholders=, readonly=[...].
  • Grid Columns: Column(name, label=, represent=lambda v,row: ..., html=True, sortable=, css=, virtual=True) for formatters, badges/links, and computed columns; conditional row actions ({"show": lambda row: ...}), truncate=, and editable=/deletable=/details= flags.
  • FK / relationship widgets: a reference (pydal) or ForeignKey (ORM) column renders as a <select> (options from the target table, current value pre-selected) and shows the referenced label in grids.
  • File uploads: an upload field renders a file input and the form switches to multipart/form-data; gridsaw.uploads.save_upload() stores it and merge_files() folds the framework's uploaded files into the values dict.
  • CSRF: gridsaw.security (new_token/verify, or stateless signed_token/verify_signed); Form(csrf=token) embeds the hidden field.

Interactivity: HTMX, Turbo, or Live

The same grids/forms drive three engines — pick per component:

  • HTMX (htmx=True): sort/search/paginate issue hx-get and swap the grid; forms hx-post and swap themselves. Fragment on HX-Request, page otherwise.
  • Turbo (turbo=True): the grid is a <turbo-frame> (plain links navigate the frame); form submits return a <turbo-stream>. Helpers in gridsaw.turbo (turbo_frame, turbo_stream, is_turbo, turbo_response_headers).
  • Live (gridsaw.live, Meld / Phoenix-LiveView style): a LiveComponent holds server-side state with @action methods; the bundled client opens a WebSocket, sends data-action/data-model events, and morphs the returned HTML in place (Idiomorph) so inputs keep focus. SmartGrid(live=True) / Form(live=True) emit the data-* controls. Built on top:
    • LiveGrid — live search + sort + pagination, plus in-row inline edit (inline_edit=True: click edit → the row's cells become inputs → save).
    • LiveForm — inline create/edit/save with server validation and "Saved ✓" in place; live_validate=True validates as you type (no write).
    • Cross-component pub/sub + multi-user broadcast — a component publishes=("topic",); others subscribes=("topic",) re-render automatically, across all connected clients (a Hub tracks every live connection). LivePresence / online_count() show who's online.
    • The bundled client adds optimistic loading state (aria-busy while an action is in flight) and auto-reconnect out of the box.
from gridsaw.live import LiveComponent, action, register, serve
@register
class Counter(LiveComponent):
    def initial_state(self): return {"n": 0}
    @action
    def inc(self): self.state["n"] += 1
    def render(self): return 'Count: %d <button data-action="inc">+</button>' % self.state["n"]

@app.websocket("/live")
async def live(ws): await serve(ws)   # + serve gridsaw.live.client_js() at /gridsaw-live.js

Runnable demos (under uvicorn)

pixi run python examples/htmx_demo/serve.py      # raw ombott, sqladal + ORM         :8010
pixi run python examples/websaw_htmx/serve.py    # websaw + FK + style/engine switch :8020
pixi run python examples/live_demo/serve.py      # live counter + live-search grid   :8030

Cross-engine in one picture

from sqladal import DAL, Field
from gridsaw import ComponentRegistry
from gridsaw.render import UpytlRenderer, Jinja2Renderer, YatlRenderer
from gridsaw.components import SmartGrid

db = DAL("sqlite://app.db", folder=".")
db.define_table("person", Field("name"), Field("age", "integer"))

reg = ComponentRegistry()
reg.add_engine(UpytlRenderer())
jinja = reg.add_engine(Jinja2Renderer())
yatl  = reg.add_engine(YatlRenderer())

# register an upytl grid, fed by a sqladal query
reg.register("people", SmartGrid(db(db.person), columns=["name", "age"], base_url="/people"))

# ...call it from a Jinja2 page
jinja.render("<h1>Team</h1>{{ component('people', request_vars=rv) }}", {"rv": {"order": "name"}})

# ...or from a YATL page
yatl.render("<h1>Team</h1>{{=component('people', request_vars=rv)}}", {"rv": {"order": "name"}})

The bridge renders the upytl grid and returns it marked safe for the calling engine (markupsafe.Markup for Jinja2, XML() for YATL).

Components

SmartGrid

SmartGrid(db(db.person), columns=["name", "age"], per_page=20,
          searchable=["name"], base_url="/people")

Reads request_vars for search (q), sort (order/dir, sortable header links), and pagination (page); renders an HTML table with a pager. Headers come from Field.label; cells from Field.represent/represent_value. Source may be a Set (db(query)), a Table, or Rows.

CRUD Form

form = Form(db.person, action="/people/new")
form.render()                       # create form (inputs typed from Field metadata)
form.create(request_vars)           # -> validate_and_insert (id / errors / success)

form = Form(db.person, record_id=7, action="/people/7")
form.render()                       # edit form, prefilled
form.update(7, request_vars)        # -> Table.validate_and_update
form.delete(7)                      # -> db(table._id == 7).delete()

Inputs are built from Field type/label/options; validation errors render inline.

Composite & no-primary-key tables

The grid and form follow the data source's real primary key — a single id, a composite key, a natural key, or none at all (legacy / warehouse tables). Composite keys travel through row-action URLs and form fields as one URL-safe token, so /edit/{id} templates keep working:

SmartGrid(db(db.membership), base_url="/m", editable=True)   # composite pk -> tokenised links
Form(db.membership, record_id=token).update(token, vars)     # token decoded to the full key

A table with no primary key renders as a read-only grid (no edit/delete — there's no addressable row), while inserts via a Form still work. The DataSource exposes pk_names(), has_pk(), pk_of(row), and encode_pk/decode_pk for both the pydal-family and SQLAlchemy-ORM backends.

Engines & optional installs

extra enables
gridsaw[upytl] upytl rendering + the component kit (grid/form are upytl-based)
gridsaw[jinja2] Jinja2 rendering
gridsaw[yatl] YATL (web2py) rendering

The core package imports with no engine installed; adapters load lazily.

Framework integration

gridsaw.integration.websaw.RenderFixture wraps any renderer as a websaw Fixture (drop into app.use(...)), injecting the component() bridge — no changes to websaw core required. The same renderers work in Flask, pure-WSGI, CLI, etc.

Status

Early development. pixi run test — 21 tests passing: render adapters, cross-engine embedding, SmartGrid (search/sort/paginate/represent) and CRUD Form on sqladal, and the websaw RenderFixture + a live HTTP request through ombott.

License

BSD-3-Clause.


Part of the websaw-ng platform · forging your dreams · install: pixi add gridsaw from the websaw-ng conda channel.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages