Forms and layout
Arranging a form: boxes, tabs and grids, fields that appear only when they are relevant, and where validation belongs.
Without any layout declaration a form is a plain stack of every editable field. That is fine for five fields and unusable for twenty-five.
Boxes are the layout
A view's layout is a tree of boxes. Each box type maps to a container component on the frontend:
| Box | Renders as |
|---|---|
simple | A plain stack — the fallback |
grid / grid-item | A responsive grid |
tab / tab-item | Tabs, with the open tab kept in the URL |
wizard | A stepped form |
object | The properties of a nested object |
array | A repeatable list of objects |
list | An embedded list of a related resource |
gallery | An image carousel |
A tabbed edit form
- Python
- Ruby
from habarico.schema.boxes import Box, TabBox
from habarico.schema.generators import EditViewSchemaGenerator
class ProductAdmin(ModelAdmin):
model = Product
fields = ["name", "sku", "description", "status", "category", "price", "stock", "created_at"]
view_schema_generators = [
EditViewSchemaGenerator(
box=TabBox(
boxes=[
Box(label="Basics", boxes=["name", "sku", "description"]),
Box(label="Pricing", boxes=["price", "stock"]),
Box(label="Publishing", boxes=["status", "category"]),
Box(label="Audit", read_only=True, boxes=["created_at"]),
]
)
),
]
Fields that appear conditionally
dynamicRender carries two condition expressions evaluated against the form's current values:
showQuery decides visibility, requiredQuery decides whether the field is required. Both are
re-evaluated as the user types, so a form can react without any frontend code.
The grammar is a deliberately small subset of JavaScript — dot paths, literals, the comparison
operators, !, &&, || and parentheses. Nothing is compiled, so the admin runs under a CSP
without unsafe-eval; anything outside the subset (a call, arithmetic, an index) is rejected when
the expression is parsed. The values are addressed under data — the form's current values on top
of the record — with record, formData and parent available beside it.
- Python
- Ruby
from habarico.schema import Field
fields = [
"shipping_method",
Field(
name="pickup_point",
show_query="data.shipping_method === 'pickup'",
required_query="data.shipping_method === 'pickup'",
),
]
A hidden field is hidden, not absent. showQuery is a UI convenience — the backend still has to
validate that a pickup point was supplied when the shipping method demands one.
Nested and repeatable data
An object field with properties becomes an SchemaObjectInput; an
array of objects becomes an SchemaObjectArrayInput with add
and remove controls. Both submit as one payload with the parent record, so the backend serializer
is what creates, updates and deletes the rows — see
Nested serializers.
Where validation belongs
| Rule | Declare it |
|---|---|
| Required, max length, type | On the backend — it reaches the form through the schema |
| Cross-field consistency | On the backend, and optionally as validate on the input for immediate feedback |
| Anything security-relevant | On the backend, always |
The frontend's validate prop exists to save a round trip, never to enforce a rule.
See also
- Form schema generators — the Python side in full
- Create/edit view boxes — the Ruby side
- Box components — the containers doing the rendering, one page each
- Layout components — the page shells they render inside
- SchemaEdit — the view around the form