mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 12:11:09 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
437
docs/ux/migration-guide.md
Normal file
437
docs/ux/migration-guide.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# Migration Guide
|
||||
|
||||
This guide helps migrate existing templates to use the new standardized UX patterns.
|
||||
|
||||
## Overview
|
||||
|
||||
The ThrillWiki UX standardization introduces:
|
||||
|
||||
1. **Breadcrumb system** with Schema.org support
|
||||
2. **Skeleton screens** for loading states
|
||||
3. **Standardized form validation** with HTMX
|
||||
4. **Toast notifications** with action support
|
||||
5. **Page headers** with consistent layout
|
||||
6. **Action bars** for button placement
|
||||
7. **Enhanced modals** with focus trapping
|
||||
8. **HTMX response utilities** for views
|
||||
|
||||
## Step-by-Step Migration
|
||||
|
||||
### Step 1: Update Base Template Usage
|
||||
|
||||
Ensure your templates extend the base template correctly:
|
||||
|
||||
```django
|
||||
{% extends 'base/base.html' %}
|
||||
|
||||
{% block title %}Your Page Title{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Your content -->
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Step 2: Add Breadcrumbs
|
||||
|
||||
#### In Your View
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```django
|
||||
{% include 'components/navigation/breadcrumbs.html' %}
|
||||
```
|
||||
|
||||
### Step 3: Replace Page Headers
|
||||
|
||||
#### Before
|
||||
|
||||
```html
|
||||
<div class="page-header">
|
||||
<h1>Parks</h1>
|
||||
<a href="/parks/create/" class="btn">Add Park</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```django
|
||||
{% 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
|
||||
|
||||
```html
|
||||
<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
|
||||
|
||||
```django
|
||||
{% include 'forms/partials/form_field.html' with field=form.name %}
|
||||
```
|
||||
|
||||
For multiple fields with consistent styling:
|
||||
|
||||
```django
|
||||
{% for field in form %}
|
||||
{% include 'forms/partials/form_field.html' with field=field %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Step 5: Update Form Actions
|
||||
|
||||
#### Before
|
||||
|
||||
```html
|
||||
<div class="form-buttons">
|
||||
<a href="/cancel/">Cancel</a>
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```django
|
||||
{% include 'forms/partials/form_actions.html' with
|
||||
submit_text='Save Park'
|
||||
cancel_url='/parks/'
|
||||
%}
|
||||
```
|
||||
|
||||
Or use the action bar for more options:
|
||||
|
||||
```django
|
||||
{% 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
|
||||
|
||||
```django
|
||||
<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
|
||||
|
||||
```django
|
||||
<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)
|
||||
|
||||
```python
|
||||
from django.contrib import messages
|
||||
|
||||
def your_view(request):
|
||||
messages.success(request, 'Park saved!')
|
||||
return redirect('parks:list')
|
||||
```
|
||||
|
||||
#### After (HTMX Response)
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```html
|
||||
<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
|
||||
|
||||
```django
|
||||
<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:
|
||||
|
||||
```django
|
||||
{% 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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
1. Ensure context processor is added to settings:
|
||||
|
||||
```python
|
||||
TEMPLATES = [{
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
# ...
|
||||
'apps.core.context_processors.breadcrumbs',
|
||||
],
|
||||
},
|
||||
}]
|
||||
```
|
||||
|
||||
2. Ensure you set `request.breadcrumbs` in your view.
|
||||
|
||||
### Issue: Toast Not Appearing
|
||||
|
||||
1. Ensure toast container is in base template:
|
||||
|
||||
```django
|
||||
{% include 'components/ui/toast-container.html' %}
|
||||
```
|
||||
|
||||
2. Ensure Alpine.js is loaded and toast store is initialized.
|
||||
|
||||
3. For HTMX responses, use the helper functions:
|
||||
|
||||
```python
|
||||
return htmx_success('Message here')
|
||||
```
|
||||
|
||||
### Issue: Modal Not Closing
|
||||
|
||||
1. Ensure you're using the correct `show_var`:
|
||||
|
||||
```django
|
||||
<div x-data="{ showModal: false }">
|
||||
{% include 'components/modals/modal_base.html' with show_var='showModal' %}
|
||||
</div>
|
||||
```
|
||||
|
||||
2. For HTMX responses, use:
|
||||
|
||||
```python
|
||||
return htmx_modal_close(message='Done!')
|
||||
```
|
||||
|
||||
### Issue: Form Validation Not Working
|
||||
|
||||
1. Ensure HTMX attributes are correct:
|
||||
|
||||
```html
|
||||
<form hx-post="/create/"
|
||||
hx-target="#form-container"
|
||||
hx-swap="outerHTML">
|
||||
```
|
||||
|
||||
2. Return proper status code on errors:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```django
|
||||
{# 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:
|
||||
|
||||
1. **Visual Check**: Ensure layout matches design
|
||||
2. **Functionality**: Test all interactive elements
|
||||
3. **Accessibility**: Test keyboard navigation and screen readers
|
||||
4. **Responsive**: Check on mobile, tablet, and desktop
|
||||
5. **Loading States**: Verify skeletons and indicators work
|
||||
6. **Error States**: Test form validation and error messages
|
||||
7. **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:
|
||||
|
||||
```django
|
||||
{% comment %}
|
||||
{% include 'components/layout/page_header.html' with title='Parks' %}
|
||||
{% endcomment %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Parks</h1>
|
||||
</div>
|
||||
```
|
||||
Reference in New Issue
Block a user