9.5 KiB
Migration Guide
This guide helps migrate existing templates to use the new standardized UX patterns.
Overview
The ThrillWiki UX standardization introduces:
- Breadcrumb system with Schema.org support
- Skeleton screens for loading states
- Standardized form validation with HTMX
- Toast notifications with action support
- Page headers with consistent layout
- Action bars for button placement
- Enhanced modals with focus trapping
- HTMX response utilities for views
Step-by-Step Migration
Step 1: Update Base Template Usage
Ensure your templates extend the base template correctly:
{% extends 'base/base.html' %}
{% block title %}Your Page Title{% endblock %}
{% block content %}
<!-- Your content -->
{% endblock %}
Step 2: Add Breadcrumbs
In Your View
from apps.core.utils import BreadcrumbBuilder
def your_view(request):
request.breadcrumbs = (
BreadcrumbBuilder()
.add_home()
.add('Section', '/section/')
.add_current('Current Page')
.build()
)
return render(request, 'your_template.html')
In Your Template
The breadcrumbs are automatically rendered if you're using the base template. If you need to place them elsewhere:
{% include 'components/navigation/breadcrumbs.html' %}
Step 3: Replace Page Headers
Before
<div class="page-header">
<h1>Parks</h1>
<a href="/parks/create/" class="btn">Add Park</a>
</div>
After
{% include 'components/layout/page_header.html' with
title='Parks'
subtitle='Browse theme parks worldwide'
primary_action_url='/parks/create/'
primary_action_text='Add Park'
primary_action_icon='fas fa-plus'
%}
Step 4: Update Form Fields
Before
<div class="form-group">
<label for="name">Name</label>
{{ form.name }}
{% if form.name.errors %}
<span class="error">{{ form.name.errors.0 }}</span>
{% endif %}
</div>
After
{% include 'forms/partials/form_field.html' with field=form.name %}
For multiple fields with consistent styling:
{% for field in form %}
{% include 'forms/partials/form_field.html' with field=field %}
{% endfor %}
Step 5: Update Form Actions
Before
<div class="form-buttons">
<a href="/cancel/">Cancel</a>
<button type="submit">Save</button>
</div>
After
{% include 'forms/partials/form_actions.html' with
submit_text='Save Park'
cancel_url='/parks/'
%}
Or use the action bar for more options:
{% include 'components/ui/action_bar.html' with
align='between'
primary_action_text='Save'
tertiary_action_text='Cancel'
tertiary_action_url='/parks/'
%}
Step 6: Add Loading States
For Page Sections
<div id="parks-list"
hx-get="{% url 'parks:list' %}"
hx-trigger="load"
hx-swap="innerHTML">
{% include 'components/skeletons/card_grid_skeleton.html' with cards=6 %}
</div>
For Forms
<form hx-post="{% url 'parks:create' %}"
hx-target="#form-container"
hx-indicator="#form-loading">
{% csrf_token %}
<!-- Form fields -->
<div id="form-loading" class="htmx-indicator">
{% include 'htmx/components/loading_indicator.html' with text='Saving...' %}
</div>
<button type="submit">Save</button>
</form>
Step 7: Update Toast Messages
Before (Django Messages)
from django.contrib import messages
def your_view(request):
messages.success(request, 'Park saved!')
return redirect('parks:list')
After (HTMX Response)
from apps.core.htmx_utils import htmx_success
def your_view(request):
# For HTMX requests
if request.headers.get('HX-Request'):
return htmx_success('Park saved!')
# For regular requests, still use messages
messages.success(request, 'Park saved!')
return redirect('parks:list')
Step 8: Update Modals
Before
<div class="modal" id="edit-modal">
<div class="modal-header">
<h2>Edit Park</h2>
<button onclick="closeModal()">×</button>
</div>
<div class="modal-body">
<!-- Content -->
</div>
</div>
After
<div x-data="{ showEditModal: false }">
<button @click="showEditModal = true">Edit</button>
{% include 'components/modals/modal_base.html' with
modal_id='edit-modal'
show_var='showEditModal'
title='Edit Park'
size='lg'
%}
</div>
For confirmation dialogs:
{% include 'components/modals/modal_confirm.html' with
modal_id='delete-confirm'
show_var='showDeleteModal'
title='Delete Park'
message='Are you sure? This cannot be undone.'
confirm_text='Delete'
confirm_variant='destructive'
confirm_hx_delete='/parks/123/'
%}
Step 9: Update HTMX Views
Before
from django.http import HttpResponse
import json
def create_park(request):
park = Park.objects.create(**form.cleaned_data)
response = HttpResponse('')
response['HX-Trigger'] = json.dumps({
'showMessage': {'message': 'Park created!'}
})
return response
After
from apps.core.htmx_utils import htmx_success, htmx_error, htmx_modal_close
def create_park(request):
form = ParkForm(request.POST)
if not form.is_valid():
return htmx_error('Validation failed', status=422)
park = form.save()
# Simple success
return htmx_success(f'{park.name} created!')
# Or close modal and refresh list
return htmx_modal_close(
message=f'{park.name} created!',
refresh_target='#parks-list'
)
Step 10: Add Page Meta
In Your View
from apps.core.utils import build_meta_context
def park_detail(request, slug):
park = get_object_or_404(Park, slug=slug)
request.page_meta = build_meta_context(
title=park.name,
description=park.description,
instance=park,
request=request,
)
return render(request, 'parks/detail.html', {'park': park})
The meta tags are automatically rendered in the base template.
Component Replacements
| Old Pattern | New Component |
|---|---|
| Custom page header | components/layout/page_header.html |
| Form field div | forms/partials/form_field.html |
| Form buttons | forms/partials/form_actions.html or components/ui/action_bar.html |
| Loading spinner | htmx/components/loading_indicator.html |
| Placeholder content | components/skeletons/*.html |
| Custom modal | components/modals/modal_base.html |
| Confirm dialog | components/modals/modal_confirm.html |
| Custom breadcrumbs | components/navigation/breadcrumbs.html |
| Status labels | components/status_badge.html |
View Helper Replacements
| Old Pattern | New Helper |
|---|---|
HttpResponse + HX-Trigger |
htmx_success() / htmx_error() |
HttpResponse + HX-Redirect |
htmx_redirect() |
| Custom modal close | htmx_modal_close() |
| Inline validation | htmx_validation_response() |
| Check HX-Request header | is_htmx_request() |
Common Issues
Issue: Breadcrumbs Not Showing
- Ensure context processor is added to settings:
TEMPLATES = [{
'OPTIONS': {
'context_processors': [
# ...
'apps.core.context_processors.breadcrumbs',
],
},
}]
- Ensure you set
request.breadcrumbsin your view.
Issue: Toast Not Appearing
- Ensure toast container is in base template:
{% include 'components/ui/toast-container.html' %}
-
Ensure Alpine.js is loaded and toast store is initialized.
-
For HTMX responses, use the helper functions:
return htmx_success('Message here')
Issue: Modal Not Closing
- Ensure you're using the correct
show_var:
<div x-data="{ showModal: false }">
{% include 'components/modals/modal_base.html' with show_var='showModal' %}
</div>
- For HTMX responses, use:
return htmx_modal_close(message='Done!')
Issue: Form Validation Not Working
- Ensure HTMX attributes are correct:
<form hx-post="/create/"
hx-target="#form-container"
hx-swap="outerHTML">
- Return proper status code on errors:
if not form.is_valid():
return render(request, 'form.html', {'form': form}, status=422)
Issue: Skeleton Not Matching Content
Adjust skeleton parameters to match your content:
{# For a 3-column grid with 9 cards #}
{% include 'components/skeletons/card_grid_skeleton.html' with cards=9 cols=3 %}
{# For a table with checkboxes #}
{% include 'components/skeletons/table_skeleton.html' with rows=10 columns=5 show_checkbox=True %}
Testing Migrations
After migrating a template:
- Visual Check: Ensure layout matches design
- Functionality: Test all interactive elements
- Accessibility: Test keyboard navigation and screen readers
- Responsive: Check on mobile, tablet, and desktop
- Loading States: Verify skeletons and indicators work
- Error States: Test form validation and error messages
- Success States: Verify toast notifications appear
Rollback
If issues arise, components are designed to be backwards compatible. You can temporarily revert to old patterns while debugging:
{% comment %}
{% include 'components/layout/page_header.html' with title='Parks' %}
{% endcomment %}
<div class="page-header">
<h1>Parks</h1>
</div>