Skip to main content

CRUD with Database

This example builds a complete CRUD (Create, Read, Update, Delete) interface using Forms and the built-in Database. The user can list, search, add, edit, and delete records — all in a single reactive page.

info

This pattern uses a reactive page with custom buttons. Side effects (insert, update, delete) run only when the corresponding button is clicked — never on every keystroke.

Table setup

Create a table called employees in the Database with these columns:

ColumnType
iduuid (default)
nametext
emailtext
departmenttext

Full example

from abstra.forms import (
run,
TextInput,
DropdownInput,
PandasRowSelectionInput,
MarkdownOutput,
Button,
Row,
)
from abstra.tables import select_df, insert, update, delete, is_ilike

DEPARTMENTS = [
{"value": "engineering", "label": "Engineering"},
{"value": "sales", "label": "Sales"},
{"value": "marketing", "label": "Marketing"},
{"value": "hr", "label": "Human Resources"},
]


def employees_page(state):
# --- Handle button actions ---

if state.get("delete"):
selected = state.get("selected")
if selected:
delete("employees", {"id": selected["id"]})
state["selected"] = None
state["delete"] = False

if state.get("save_new"):
insert("employees", {
"name": state["new_name"],
"email": state["new_email"],
"department": state["new_department"],
})
state["adding"] = False
state["save_new"] = False

if state.get("save_edit"):
selected = state.get("selected")
if selected:
update("employees",
where={"id": selected["id"]},
set={
"name": state["edit_name"],
"email": state["edit_email"],
"department": state["edit_department"],
},
)
state["editing"] = False
state["selected"] = None
state["save_edit"] = False

if state.get("cancel"):
state["adding"] = False
state["editing"] = False
state["cancel"] = False

# --- Add form ---

if state.get("add"):
state["adding"] = True
state["add"] = False

if state.get("adding"):
return [
MarkdownOutput("## New employee"),
Row([
TextInput("Name", key="new_name"),
TextInput("Email", key="new_email"),
]),
DropdownInput("Department", options=DEPARTMENTS, key="new_department"),
], [
Button("Save", key="save_new"),
Button("Cancel", key="cancel"),
]

# --- Edit form ---

if state.get("edit"):
selected = state.get("selected")
if selected:
state["editing"] = True
state["edit_name"] = selected["name"]
state["edit_email"] = selected["email"]
state["edit_department"] = selected["department"]
state["edit"] = False

if state.get("editing"):
return [
MarkdownOutput("## Edit employee"),
Row([
TextInput("Name", key="edit_name"),
TextInput("Email", key="edit_email"),
]),
DropdownInput("Department", options=DEPARTMENTS, key="edit_department"),
], [
Button("Save", key="save_edit"),
Button("Cancel", key="cancel"),
]

# --- List view ---

search = state.get("search", "")
if search:
df = select_df("employees", where={
"name": is_ilike(f"%{search}%"),
})
else:
df = select_df("employees")

df = df[["id", "name", "email", "department"]]

has_selection = state.get("selected") is not None

return [
MarkdownOutput("## Employees"),
TextInput("Search by name", key="search", required=False),
PandasRowSelectionInput(df, key="selected", required=False, label=""),
], [
Button("Add", key="add"),
Button("Edit", key="edit", disabled=not has_selection),
Button("Delete", key="delete", disabled=not has_selection),
]


run([employees_page])

How it works

The entire CRUD lives in a single reactive page function. The flow is controlled by button keys stored in state:

  1. List — The default view queries the database with select_df and displays the results in a PandasRowSelectionInput. A search field filters by name using is_ilike.

  2. Add — Clicking "Add" sets state["adding"] = True, which swaps the UI to a creation form. "Save" calls insert and returns to the list.

  3. Edit — Clicking "Edit" pre-fills the form with the selected row's data. "Save" calls update with the row's id.

  4. Delete — Clicking "Delete" calls delete on the selected row and clears the selection.

warning

Side effects (insert, update, delete) must only run inside button-handling branches — never in the main rendering path. See Step Types for details.