Layout API Examples¶
This document provides real-world examples of using the Layout API.
Table of Contents¶
- Simple Forms
- Complex Layouts
- Collections (Inlines)
- Conditional Fields
- Computed Fields
- Real-World Scenarios
Simple Forms¶
Example 1: Basic Contact Form¶
Works without plugin
from djadmin import ModelAdmin, register, Layout, Field, Fieldset
@register(Contact)
class ContactAdmin(ModelAdmin):
layout = Layout(
Field('name', required=True),
Field('email', widget='email', required=True),
Field('phone'),
Field('message', widget='textarea', attrs={'rows': 6}),
)
Renders as: - Simple vertical stack - Email field with proper input type - Textarea for message with 6 rows
Example 2: Two-Column Name Fields¶
Works without plugin
@register(Person)
class PersonAdmin(ModelAdmin):
layout = Layout(
Row(
Field('first_name', css_classes=['flex-1', 'pr-2']),
Field('last_name', css_classes=['flex-1', 'pl-2']),
),
Field('email', widget='email'),
Field('birth_date', widget='date'),
)
Renders as: - First/last name side-by-side (50/50 split) - Email and birth date below (full width)
Example 3: Address Form¶
Works without plugin
@register(Address)
class AddressAdmin(ModelAdmin):
layout = Layout(
Field('street_address'),
Field('street_address_2', required=False, label='Apt/Suite'),
Row(
Field('city', css_classes=['flex-1', 'pr-2']),
Field('state', css_classes=['flex-1', 'px-2']),
Field('zip_code', css_classes=['w-32', 'pl-2']),
),
Field('country', widget='select'),
)
Renders as: - Street addresses full width - City/State/Zip in row (city and state flex, zip fixed width) - Country dropdown below
Complex Layouts¶
Example 4: Product Form with Sections¶
Works without plugin
@register(Product)
class ProductAdmin(ModelAdmin):
layout = Layout(
Fieldset('Basic Information',
Field('name', required=True),
Field('sku', label='SKU', required=True),
Row(
Field('price', widget='number', css_classes=['flex-1', 'pr-2']),
Field('cost', widget='number', css_classes=['flex-1', 'pl-2']),
),
),
Fieldset('Description',
Field('short_description', widget='textarea', attrs={'rows': 3}),
Field('full_description', widget='textarea', attrs={'rows': 10}),
),
Fieldset('Inventory',
Row(
Field('stock', widget='number', css_classes=['flex-1', 'pr-2']),
Field('reorder_level', widget='number', css_classes=['flex-1', 'pl-2']),
),
Field('supplier', widget='select'),
),
)
Features: - Three distinct sections with legends - Price/cost and stock/reorder in rows - Text areas with different sizes - Mix of field types
Example 5: Article Form¶
Works without plugin
@register(Article)
class ArticleAdmin(ModelAdmin):
layout = Layout(
Fieldset('Article Details',
Field('title', required=True),
Field('slug', help_text='Auto-generated if left blank'),
Row(
Field('author', css_classes=['flex-1', 'pr-2']),
Field('category', css_classes=['flex-1', 'pl-2']),
),
),
Fieldset('Content',
Field('excerpt', widget='textarea', attrs={'rows': 4}),
Field('body', widget='textarea', attrs={'rows': 20}),
),
Fieldset('Publishing',
Row(
Field('status', widget='select', css_classes=['flex-1', 'pr-2']),
Field('published_at', widget='datetime', css_classes=['flex-1', 'pl-2']),
),
Field('featured', widget='checkbox'),
),
)
Features: - Logical grouping by function - Help text for slug field - Different textarea sizes - Status and publish date in row - Checkbox for featured flag
Collections (Inlines)¶
⚠️ All examples in this section require the djadmin-formset plugin
Example 6: Simple Collection¶
@register(Author)
class AuthorAdmin(ModelAdmin):
layout = Layout(
Fieldset('Author Information',
Row(
Field('first_name', css_classes=['flex-1', 'pr-2']),
Field('last_name', css_classes=['flex-1', 'pl-2']),
),
Field('bio', widget='textarea', attrs={'rows': 6}),
),
Fieldset('Publications',
Collection('books',
model=Book,
fields=['title', 'isbn', 'published_date'],
extra_siblings=1,
),
),
)
Features: - Inline editing of books - Simple field list - One empty form shown by default
Example 7: Collection with Custom Layout¶
@register(Author)
class AuthorAdmin(ModelAdmin):
layout = Layout(
Field('name'),
Collection('books',
model=Book,
layout=Layout(
Row(
Field('title', css_classes=['flex-2', 'pr-2']),
Field('isbn', css_classes=['flex-1', 'pl-2']),
),
Row(
Field('published_date', css_classes=['flex-1', 'pr-2']),
Field('pages', widget='number', css_classes=['flex-1', 'pl-2']),
),
),
is_sortable=True,
),
)
Features: - Custom layout for each book - Title takes 2/3 width, ISBN 1/3 - Drag-and-drop reordering (is_sortable)
Example 8: Nested Collections¶
@register(Company)
class CompanyAdmin(ModelAdmin):
layout = Layout(
Fieldset('Company Details',
Field('name'),
Field('website', widget='email'),
),
Fieldset('Departments',
Collection('departments',
model=Department,
layout=Layout(
Field('name'),
Field('budget', widget='number'),
Collection('employees', # Nested!
model=Employee,
fields=['name', 'title', 'email', 'phone'],
max_siblings=50,
),
),
),
),
)
Features: - Two-level nesting (Company → Department → Employee) - Each department can have up to 50 employees - Mix of simple and complex layouts
Example 9: Collection with Constraints¶
@register(Album)
class AlbumAdmin(ModelAdmin):
layout = Layout(
Field('title'),
Field('artist'),
Field('release_date'),
Collection('tracks',
model=Track,
fields=['number', 'title', 'duration'],
min_siblings=1, # Must have at least 1 track
max_siblings=30, # Can't exceed 30 tracks
extra_siblings=3, # Show 3 empty forms
is_sortable=True, # Drag-and-drop track order
),
)
Features: - Minimum/maximum constraints - Multiple empty forms - Sortable tracks
Conditional Fields¶
⚠️ Requires djadmin-formset plugin
Example 10: Product Type Conditionals¶
@register(Product)
class ProductAdmin(ModelAdmin):
layout = Layout(
Field('name'),
Field('product_type', widget='select'),
# Physical product fields
Field('weight',
show_if=".product_type === 'physical'",
widget='number'
),
Field('dimensions',
show_if=".product_type === 'physical'"
),
Field('shipping_class',
show_if=".product_type === 'physical'",
widget='select'
),
# Digital product fields
Field('file_size',
show_if=".product_type === 'digital'",
widget='number'
),
Field('download_limit',
show_if=".product_type === 'digital'",
widget='number'
),
)
Behavior: - Shows weight/dimensions/shipping for physical products - Shows file_size/download_limit for digital products - Fields appear/disappear instantly (no page refresh)
Example 11: Subscription Form¶
@register(Subscription)
class SubscriptionAdmin(ModelAdmin):
layout = Layout(
Field('email', widget='email'),
Field('plan', widget='select'),
# Show billing info only for paid plans
Fieldset('Billing Information',
Field('billing_name',
show_if=".plan !== 'free'"
),
Field('billing_email',
show_if=".plan !== 'free'",
widget='email'
),
Row(
Field('card_number',
show_if=".plan !== 'free'",
css_classes=['flex-2']
),
Field('cvv',
show_if=".plan !== 'free'",
css_classes=['flex-1']
),
),
),
)
Behavior: - Entire billing section hidden for free plan - Shows immediately when selecting paid plan
Example 12: Shipping Options¶
@register(Order)
class OrderAdmin(ModelAdmin):
layout = Layout(
Field('customer'),
Field('shipping_method', widget='select'),
# Pickup location
Field('pickup_location',
show_if=".shipping_method === 'pickup'",
widget='select'
),
# Delivery address
Fieldset('Delivery Address',
Field('street',
show_if=".shipping_method === 'delivery'"
),
Row(
Field('city',
show_if=".shipping_method === 'delivery'",
css_classes=['flex-1']
),
Field('zip',
show_if=".shipping_method === 'delivery'",
css_classes=['flex-1']
),
),
),
# Express shipping
Field('delivery_date',
show_if=".shipping_method === 'express'",
widget='date'
),
)
Behavior: - Different fields for pickup vs delivery vs express - Address fields only show for delivery option
Computed Fields¶
⚠️ Requires djadmin-formset plugin
Example 13: Price Calculator¶
@register(OrderItem)
class OrderItemAdmin(ModelAdmin):
layout = Layout(
Field('product', widget='select'),
Row(
Field('quantity', widget='number', css_classes=['flex-1']),
Field('unit_price', widget='number', css_classes=['flex-1']),
Field('total',
calculate='.quantity * .unit_price',
widget='number',
attrs={'readonly': True},
css_classes=['flex-1']
),
),
)
Behavior: - Total auto-updates when quantity or price changes - Readonly field (display only) - Updates instantly without page refresh
Example 14: Discount Calculation¶
@register(Product)
class ProductAdmin(ModelAdmin):
layout = Layout(
Field('name'),
Row(
Field('price', widget='number', css_classes=['flex-1']),
Field('discount_percent', widget='number', css_classes=['flex-1']),
Field('final_price',
calculate='.price * (1 - .discount_percent / 100)',
widget='number',
attrs={'readonly': True},
css_classes=['flex-1']
),
),
)
Behavior: - Final price updates as you type - Percentage automatically applied
Example 15: Complex Calculation¶
@register(Invoice)
class InvoiceAdmin(ModelAdmin):
layout = Layout(
Field('subtotal', widget='number'),
Field('tax_rate', widget='number', label='Tax Rate (%)'),
Field('shipping', widget='number'),
# Calculated fields
Field('tax_amount',
calculate='.subtotal * (.tax_rate / 100)',
widget='number',
attrs={'readonly': True}
),
Field('total',
calculate='.subtotal + (.subtotal * (.tax_rate / 100)) + .shipping',
widget='number',
attrs={'readonly': True},
label='Grand Total'
),
)
Behavior: - Tax calculated from subtotal and rate - Total includes subtotal, tax, and shipping - All calculations update live
Real-World Scenarios¶
Example 16: E-commerce Product¶
Complete product form with all features
@register(Product)
class ProductAdmin(ModelAdmin):
layout = Layout(
Fieldset('Product Information',
Field('name', required=True),
Field('slug'),
Row(
Field('sku', label='SKU', css_classes=['flex-1', 'pr-2']),
Field('category', widget='select', css_classes=['flex-1', 'pl-2']),
),
),
Fieldset('Pricing',
Row(
Field('price', widget='number', css_classes=['flex-1', 'pr-2']),
Field('cost', widget='number', css_classes=['flex-1', 'px-2']),
Field('margin',
calculate='(.price - .cost) / .price * 100',
widget='number',
attrs={'readonly': True},
label='Margin %',
css_classes=['flex-1', 'pl-2']
),
),
),
Fieldset('Description',
Field('short_description', widget='textarea', attrs={'rows': 3}),
Field('full_description', widget='textarea', attrs={'rows': 15}),
),
Fieldset('Inventory',
Row(
Field('stock', widget='number', css_classes=['flex-1', 'pr-2']),
Field('reorder_level', widget='number', css_classes=['flex-1', 'pl-2']),
),
Field('low_stock_alert', widget='checkbox'),
),
Fieldset('Images',
Collection('images',
model=ProductImage,
fields=['image', 'alt_text', 'is_primary'],
min_siblings=1,
max_siblings=10,
is_sortable=True,
),
),
Fieldset('Variants',
Collection('variants',
model=ProductVariant,
layout=Layout(
Row(
Field('name', css_classes=['flex-2']),
Field('sku', css_classes=['flex-1']),
),
Row(
Field('price', widget='number', css_classes=['flex-1']),
Field('stock', widget='number', css_classes=['flex-1']),
),
),
),
),
)
Features: - Automatic margin calculation - Image collection with sorting - Variant collection with custom layout - Logical grouping by function
Example 17: Event Registration¶
Conditional fields based on event type
@register(Event)
class EventAdmin(ModelAdmin):
layout = Layout(
Fieldset('Event Details',
Field('title', required=True),
Field('event_type', widget='select'),
Field('date', widget='date'),
Row(
Field('start_time', widget='time', css_classes=['flex-1']),
Field('end_time', widget='time', css_classes=['flex-1']),
),
),
Fieldset('Location',
# In-person events
Field('venue_name',
show_if=".event_type === 'in-person'"
),
Field('address',
show_if=".event_type === 'in-person'"
),
Field('capacity',
show_if=".event_type === 'in-person'",
widget='number'
),
# Virtual events
Field('meeting_url',
show_if=".event_type === 'virtual'",
widget='email'
),
Field('meeting_password',
show_if=".event_type === 'virtual'",
widget='password'
),
# Hybrid events
Field('stream_url',
show_if=".event_type === 'hybrid'",
widget='email'
),
),
Fieldset('Registration',
Field('max_attendees', widget='number'),
Field('registration_deadline', widget='date'),
Field('price', widget='number'),
),
Fieldset('Speakers',
Collection('speakers',
model=Speaker,
fields=['name', 'title', 'bio', 'photo'],
max_siblings=10,
),
),
)
Features: - Different fields for in-person/virtual/hybrid events - Speaker collection - Time range validation - Price calculation ready
Example 18: Customer Profile¶
Complete customer management
@register(Customer)
class CustomerAdmin(ModelAdmin):
layout = Layout(
Fieldset('Personal Information',
Row(
Field('first_name', css_classes=['flex-1', 'pr-2']),
Field('last_name', css_classes=['flex-1', 'pl-2']),
),
Field('email', widget='email', required=True),
Field('phone'),
Field('birth_date', widget='date'),
),
Fieldset('Account',
Field('username', required=True),
Row(
Field('account_type', widget='select', css_classes=['flex-1', 'pr-2']),
Field('status', widget='select', css_classes=['flex-1', 'pl-2']),
),
Field('notes', widget='textarea', attrs={'rows': 4}),
),
Fieldset('Addresses',
Collection('addresses',
model=Address,
layout=Layout(
Field('label', label='Address Label (Home, Work, etc.)'),
Field('street'),
Field('street_2', required=False),
Row(
Field('city', css_classes=['flex-1']),
Field('state', css_classes=['flex-1']),
Field('zip', css_classes=['w-32']),
),
Field('is_default', widget='checkbox'),
),
max_siblings=5,
),
),
Fieldset('Payment Methods',
Collection('payment_methods',
model=PaymentMethod,
fields=['type', 'last_four', 'expiry', 'is_default'],
max_siblings=3,
),
),
)
Features: - Complete customer profile - Multiple addresses with custom layout - Payment methods collection - Default flags for addresses and payment methods
Next Steps¶
- Component Reference - Detailed API for each component
- Django Admin Migration - Convert existing code
- Layout API Overview - Complete feature overview
Summary¶
The Layout API supports a wide range of use cases:
- ✅ Simple forms - Basic layouts without plugin
- ✅ Complex layouts - Fieldsets, rows, nested structures
- ✅ Collections - Inline editing (requires plugin)
- ✅ Conditional logic - Show/hide fields (requires plugin)
- ✅ Computed fields - Auto-calculations (requires plugin)
- ✅ Real-world scenarios - E-commerce, events, CRM, etc.
Start simple and progressively enhance with plugins as needed!