Skip to main content

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:

BoxRenders as
simpleA plain stack — the fallback
grid / grid-itemA responsive grid
tab / tab-itemTabs, with the open tab kept in the URL
wizardA stepped form
objectThe properties of a nested object
arrayA repeatable list of objects
listAn embedded list of a related resource
galleryAn image carousel

A tabbed edit form​

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.

from habarico.schema import Field

fields = [
"shipping_method",
Field(
name="pickup_point",
show_query="data.shipping_method === 'pickup'",
required_query="data.shipping_method === 'pickup'",
),
]
caution

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​

RuleDeclare it
Required, max length, typeOn the backend — it reaches the form through the schema
Cross-field consistencyOn the backend, and optionally as validate on the input for immediate feedback
Anything security-relevantOn the backend, always

The frontend's validate prop exists to save a round trip, never to enforce a rule.

See also​