reformatted skill
This commit is contained in:
@@ -1,349 +1,134 @@
|
|||||||
---
|
---
|
||||||
name: nicegui-ui-customization
|
name: nicegui-ui-customization
|
||||||
description: Style, customize, and build interactive UIs in a production-ready NiceGUI app.
|
description: 'Design and implement production NiceGUI UIs with reusable components, Tailwind-first styling, event-driven interactions, and troubleshooting for uploads, state, and static assets. Use when building or refactoring NiceGUI pages and interaction flows.'
|
||||||
|
argument-hint: 'What UI outcome should this workflow produce?'
|
||||||
---
|
---
|
||||||
|
|
||||||
# NiceGUI UI Customization & Component Patterns
|
# NiceGUI UI Customization Workflow
|
||||||
|
|
||||||
Customize, style, and build interactive user interfaces in a production-ready NiceGUI application. This skill covers component architecture, styling systems, event-driven interactions, and practical troubleshooting for production deployments.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Create a well-architected, responsive, accessible UI that:
|
|
||||||
- Separates structure (Python layout) from cosmetics (CSS styling)
|
|
||||||
- Handles user interactions via event-driven patterns, not polling
|
|
||||||
- Composes reusable components with clear responsibilities
|
|
||||||
- Integrates custom styling and assets cleanly
|
|
||||||
- Troubleshoots common failures (race conditions, asset caching, upload errors)
|
|
||||||
|
|
||||||
## Project Structure & Conceptual Boundaries
|
|
||||||
|
|
||||||
This skill assumes a project organized with clear, modular boundaries:
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/app/
|
|
||||||
├─ ui/
|
|
||||||
│ ├─ pages/ # Page-level UI modules (route handlers)
|
|
||||||
│ │ ├─ __init__.py
|
|
||||||
│ │ ├─ home.py
|
|
||||||
│ │ ├─ dashboard.py
|
|
||||||
│ │ └─ about.py
|
|
||||||
│ ├─ components/ # Reusable presentation components
|
|
||||||
│ │ ├─ __init__.py
|
|
||||||
│ │ ├─ nav.py
|
|
||||||
│ │ ├─ card_section.py
|
|
||||||
│ │ └─ form_section.py
|
|
||||||
│ └─ static/ # Static assets (CSS, JS, images)
|
|
||||||
│ ├─ css/
|
|
||||||
│ │ ├─ base.css
|
|
||||||
│ │ ├─ theme.css
|
|
||||||
│ │ └─ components.css
|
|
||||||
│ ├─ js/
|
|
||||||
│ └─ images/
|
|
||||||
├─ services/ # Business logic layer
|
|
||||||
│ ├─ __init__.py
|
|
||||||
│ ├─ file_service.py
|
|
||||||
│ ├─ user_service.py
|
|
||||||
│ └─ ...
|
|
||||||
├─ api/ # HTTP endpoints
|
|
||||||
│ ├─ __init__.py
|
|
||||||
│ ├─ health.py
|
|
||||||
│ └─ routes.py
|
|
||||||
├─ bootstrap.py # App composition, CSS/asset setup
|
|
||||||
├─ config.py # Settings and configuration
|
|
||||||
└─ ...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key boundaries**:
|
|
||||||
- **`ui/pages/`**: Route-level adapters that call `services/`
|
|
||||||
- **`ui/components/`**: Reusable presentation components (no business logic)
|
|
||||||
- **`ui/static/`**: Assets loaded at startup in `bootstrap.py`
|
|
||||||
- **`services/`**: Business logic (independent of UI)
|
|
||||||
- **`api/`**: HTTP endpoints
|
|
||||||
|
|
||||||
**Dependency flow**: `pages/` → `components/` + `services/` (no reverse imports)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## UI Component Patterns
|
|
||||||
|
|
||||||
Components are the building blocks of reusable UI. They live in `ui/components/` and are consumed by pages in `ui/pages/`. Each component maps to [NiceGUI elements](https://nicegui.io/documentation/element), which expose a consistent API for structure, styling, and event handling.
|
|
||||||
|
|
||||||
### Reusable Components
|
|
||||||
|
|
||||||
Extract to `ui/components/` when patterns repeat across pages. Keep in-page layouts unique to that page.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# ui/components/card_section.py
|
|
||||||
def card_section(title: str, content: str) -> ui.card:
|
|
||||||
with ui.card().classes("w-full max-w-md") as card:
|
|
||||||
ui.label(title).classes("text-lg font-bold")
|
|
||||||
ui.label(content).classes("text-gray-600")
|
|
||||||
return card
|
|
||||||
```
|
|
||||||
|
|
||||||
### Layout Patterns
|
|
||||||
|
|
||||||
Use Tailwind for all structural styling. Reserve `.style()` for computed values.
|
|
||||||
|
|
||||||
```python
|
Create, style, and ship production NiceGUI UI flows with a repeatable process. The workflow keeps structure in Python, favors Tailwind and Quasar APIs for styling, and uses event-driven interaction patterns over ad-hoc polling.
|
||||||
with ui.column().classes("w-full"):
|
|
||||||
with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"):
|
|
||||||
ui.card().classes("flex-1 min-w-64")
|
|
||||||
ui.card().classes("flex-1 min-w-64")
|
|
||||||
```
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
|
|
||||||
Use [bindable dataclasses](https://nicegui.io/documentation/section_binding_properties) for automatic two-way binding.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nicegui import binding, ui
|
|
||||||
from dataclasses import field
|
|
||||||
|
|
||||||
@binding.bindable_dataclass
|
|
||||||
class PageState:
|
|
||||||
selected_id: int | None = None
|
|
||||||
items: list = field(default_factory=list)
|
|
||||||
|
|
||||||
state = PageState()
|
|
||||||
detail_label = ui.label().bind_text_from(state, 'selected_id')
|
|
||||||
|
|
||||||
def on_select(item_id):
|
|
||||||
state.selected_id = item_id # Auto-updates UI
|
|
||||||
|
|
||||||
state.bind_on_change(lambda: load_detail(), 'selected_id')
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Styling
|
|
||||||
|
|
||||||
Use Tailwind for all styling. Use breakpoint prefixes (`sm:`, `md:`, `lg:`) for responsive design.
|
|
||||||
|
|
||||||
### Custom CSS Injection ⚠️ Avoid This Pattern
|
|
||||||
|
|
||||||
**Avoid injecting custom CSS** via `ui.add_css()` at app startup. Custom CSS injection creates maintenance burden, breaks IDE autocomplete, and makes styling harder to reason about. Instead:
|
|
||||||
|
|
||||||
**✅ Prefer these approaches**:
|
|
||||||
1. **Use [Tailwind utility classes](https://tailwindcss.com/docs/utility-first)** — covers 95% of styling needs with zero CSS overhead.
|
|
||||||
```python
|
|
||||||
with ui.column().classes("w-full bg-gradient-to-br from-gray-100 to-gray-200"):
|
|
||||||
with ui.card().classes("rounded-lg border border-gray-300 shadow-md hover:shadow-lg transition-shadow"):
|
|
||||||
ui.label("Content")
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Use [Quasar props](https://quasar.dev/vue-components)** — high-level component theming built into NiceGUI elements.
|
|
||||||
```python
|
|
||||||
ui.button("Submit").props("color=primary size=lg")
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Extract reusable components** — encapsulate styling patterns in Python functions rather than CSS classes.
|
|
||||||
```python
|
|
||||||
def styled_card(title: str, content: str) -> ui.card:
|
|
||||||
with ui.card().classes("rounded-lg border border-gray-300 shadow-md") as card:
|
|
||||||
ui.label(title).classes("text-lg font-bold")
|
|
||||||
ui.label(content).classes("text-gray-600")
|
|
||||||
return card
|
|
||||||
```
|
|
||||||
|
|
||||||
**If you absolutely must inject CSS** (rare edge cases only):
|
## When To Use
|
||||||
- Store CSS in `src/app/static/css/` files
|
|
||||||
- Load via `ui.add_css(open('src/app/static/css/custom.css').read())` in `bootstrap.py`
|
|
||||||
- Keep injected CSS minimal and well-documented
|
|
||||||
- Avoid hardcoding colors or spacing; use CSS variables instead
|
|
||||||
|
|
||||||
### Static Assets
|
- Building a new NiceGUI page or dashboard
|
||||||
|
- Refactoring a page into reusable components
|
||||||
|
- Adding file upload, form submission, live status, or background-job UX
|
||||||
|
- Troubleshooting race conditions, stale assets, or inconsistent state updates
|
||||||
|
|
||||||
Load CSS files at app startup in `bootstrap.py`:
|
## Target Outcome
|
||||||
```python
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="src/app/static"), name="static")
|
Deliver a responsive, accessible UI flow that:
|
||||||
ui.add_css(open('src/app/static/css/base.css').read())
|
|
||||||
```
|
|
||||||
|
|
||||||
Use CSS variables for design tokens:
|
- keeps clear boundaries between page adapters, reusable components, and services
|
||||||
```css
|
- uses Tailwind-first styling with minimal custom CSS
|
||||||
:root {
|
- updates UI through events and bindings
|
||||||
--color-primary: #3b82f6;
|
- has validation, user feedback, and failure handling
|
||||||
--spacing-unit: 1rem;
|
- passes a production-readiness check at the end
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JS Injection (Rare)
|
## Progressive Loading References
|
||||||
|
|
||||||
Use `ui.run_javascript()` only for unsupported interactions. Prefer [NiceGUI APIs](https://nicegui.io/documentation/section_action_events) first.
|
Load these references only when needed:
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Event-Driven Patterns
|
- Architecture and styling rules: [./references/architecture-and-styling.md](./references/architecture-and-styling.md)
|
||||||
|
- Event and state interaction patterns: [./references/interaction-patterns.md](./references/interaction-patterns.md)
|
||||||
|
- Troubleshooting and release gates: [./references/troubleshooting-and-quality-gates.md](./references/troubleshooting-and-quality-gates.md)
|
||||||
|
|
||||||
NiceGUI provides event-driven APIs for [actions and events](https://nicegui.io/documentation/section_action_events). This section covers common patterns: file uploads, form submissions, WebSocket/SSE for real-time updates, and background tasks.
|
## Procedure
|
||||||
|
|
||||||
### File Upload
|
### 1. Define the UI Slice
|
||||||
|
|
||||||
Validate and notify; delegate storage to service layer.
|
- Capture the user-visible outcome for this task in one sentence.
|
||||||
|
- Identify route-level page modules to touch.
|
||||||
|
- Identify service operations needed by the UI.
|
||||||
|
|
||||||
```python
|
Completion check:
|
||||||
async def handle_upload(e: ui.events.UploadEventArguments):
|
|
||||||
try:
|
|
||||||
if e.size > 10 * 1024 * 1024:
|
|
||||||
raise ValueError("File too large")
|
|
||||||
if not e.name.endswith('.pdf'):
|
|
||||||
raise ValueError("Only PDF allowed")
|
|
||||||
await file_service.store(e.content.read(), e.name)
|
|
||||||
ui.notify(f"Uploaded: {e.name}", type="positive")
|
|
||||||
except ValueError as err:
|
|
||||||
ui.notify(str(err), type="negative")
|
|
||||||
|
|
||||||
ui.upload(on_upload=handle_upload, auto_upload=True)
|
- You can name the target page, component candidates, and service calls before coding.
|
||||||
```
|
|
||||||
|
|
||||||
### Form Submission
|
### 2. Choose Component Extraction Strategy
|
||||||
|
|
||||||
Bind form inputs to dataclass; validate in service.
|
Decision point:
|
||||||
|
|
||||||
```python
|
- If a layout pattern appears in 2 or more pages, extract it to `ui/components/`.
|
||||||
@binding.bindable_dataclass
|
- If a pattern is page-specific, keep it in the page module.
|
||||||
class FormData:
|
|
||||||
name: str = ""
|
|
||||||
email: str = ""
|
|
||||||
|
|
||||||
data = FormData()
|
Completion check:
|
||||||
ui.input("Name").bind_value(data, 'name')
|
|
||||||
ui.input("Email").bind_value(data, 'email')
|
|
||||||
|
|
||||||
async def on_submit():
|
- Reused UI patterns are encapsulated as callable components.
|
||||||
try:
|
|
||||||
await user_service.create_user(name=data.name, email=data.email)
|
|
||||||
ui.notify("User created", type="positive")
|
|
||||||
data.name = data.email = ""
|
|
||||||
except ValueError as err:
|
|
||||||
ui.notify(str(err), type="negative")
|
|
||||||
|
|
||||||
ui.button("Submit").on_click(on_submit)
|
### 3. Build Responsive Layout First
|
||||||
```
|
|
||||||
|
|
||||||
### Real-Time Updates (WebSocket / SSE)
|
- Use Tailwind utility classes for structure and spacing.
|
||||||
|
- Use responsive breakpoints (`sm:`, `md:`, `lg:`).
|
||||||
|
- Reserve `.style()` for dynamic values that cannot be expressed with classes.
|
||||||
|
|
||||||
For streaming updates, use [SSE](https://fastapi.tiangolo.com/advanced/server-sent-events/) (one-way) or [WebSocket](https://fastapi.tiangolo.com/advanced/websockets/) (bidirectional).
|
Completion check:
|
||||||
|
|
||||||
```python
|
- Layout works at mobile and desktop widths without custom CSS overrides.
|
||||||
# api/events.py
|
|
||||||
@app.get("/events/status")
|
|
||||||
async def status_stream():
|
|
||||||
async def gen():
|
|
||||||
while True:
|
|
||||||
yield f"data: {await get_status()}\\n\\n"
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
return StreamingResponse(gen(), media_type="text/event-stream")
|
|
||||||
|
|
||||||
# Page
|
### 4. Add Reactive State And Events
|
||||||
status_label = ui.label()
|
|
||||||
async def listen():
|
|
||||||
async with aiohttp.ClientSession() as s:
|
|
||||||
async with s.get("/api/events/status") as r:
|
|
||||||
async for line in r.content:
|
|
||||||
if line.startswith(b"data:"):
|
|
||||||
status_label.set_text(line.decode().replace("data:", "").strip())
|
|
||||||
|
|
||||||
ui.timer(0.1, lambda: asyncio.create_task(listen()))
|
- Use bindable dataclasses for local page state.
|
||||||
```
|
- Prefer event handlers (`on_click`, `on_upload`, etc.) over periodic polling.
|
||||||
|
- Trigger explicit refreshes with `@ui.refreshable` where needed.
|
||||||
|
|
||||||
### Background Tasks
|
Decision point by interaction type:
|
||||||
|
|
||||||
Delegate long-running work to background tasks; signal UI via polling or WebSocket.
|
- File upload: validate size/type, delegate storage to a service, notify success/failure.
|
||||||
|
- Form submit: bind inputs to dataclass fields, validate in service layer, clear state on success.
|
||||||
|
- Real-time status: use SSE or WebSocket for push updates.
|
||||||
|
- Long jobs: run in background task, update status endpoint or stream.
|
||||||
|
|
||||||
```python
|
Completion check:
|
||||||
# API
|
|
||||||
@app.post("/process")
|
|
||||||
async def process(background_tasks: BackgroundTasks):
|
|
||||||
background_tasks.add_task(long_job)
|
|
||||||
return {"status": "processing"}
|
|
||||||
|
|
||||||
# Page
|
- Every user action has explicit positive and negative feedback via `ui.notify()`.
|
||||||
async def on_start():
|
|
||||||
await client.post("/api/process")
|
|
||||||
ui.notify("Started")
|
|
||||||
|
|
||||||
async def check():
|
### 5. Apply Styling Strategy
|
||||||
status = await client.get("/api/process/status")
|
|
||||||
if status == "done":
|
|
||||||
ui.notify("Complete", type="positive")
|
|
||||||
|
|
||||||
ui.button("Start").on_click(on_start)
|
Preferred order:
|
||||||
ui.timer(1, check)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reactive Refresh
|
1. Tailwind utility classes
|
||||||
|
2. Quasar props
|
||||||
|
3. Reusable styled component functions
|
||||||
|
|
||||||
Use `@ui.refreshable` with explicit refresh triggers; avoid polling everything.
|
Only if absolutely necessary:
|
||||||
|
|
||||||
```python
|
- Load minimal custom CSS once at startup in `bootstrap.py`.
|
||||||
@ui.refreshable
|
- Keep custom CSS tokenized (variables) and documented.
|
||||||
async def item_list():
|
|
||||||
items = await service.list()
|
|
||||||
for item in items:
|
|
||||||
ui.label(item.name)
|
|
||||||
|
|
||||||
ui.button("Refresh").on_click(lambda: item_list.refresh())
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
Completion check:
|
||||||
|
|
||||||
## Advanced Interactions
|
- Styling is mostly class/props-driven and not dependent on scattered ad-hoc CSS.
|
||||||
|
|
||||||
### Dialogs
|
### 6. Harden Against Common Failures
|
||||||
|
|
||||||
```python
|
- Prevent duplicate submissions by disabling controls during in-flight operations.
|
||||||
dialog = ui.dialog()
|
- Avoid overlapping timers for the same state target.
|
||||||
|
- Serialize dependent updates (`await` service call before mutation/render).
|
||||||
|
- Verify static mount paths and cache behavior for changed assets.
|
||||||
|
|
||||||
async def show_confirm():
|
Completion check:
|
||||||
with dialog:
|
|
||||||
ui.label("Are you sure?")
|
|
||||||
ui.button("Yes").on_click(lambda: (confirm_action(), dialog.close()))
|
|
||||||
ui.button("No").on_click(dialog.close)
|
|
||||||
dialog.open()
|
|
||||||
|
|
||||||
ui.button("Delete").on_click(show_confirm)
|
- Race conditions and stale asset symptoms are addressed with explicit safeguards.
|
||||||
```
|
|
||||||
|
|
||||||
### Progress
|
### 7. Final Production Readiness Review
|
||||||
|
|
||||||
```python
|
Pass all checks:
|
||||||
progress = ui.linear_progress(value=0).classes("w-full")
|
|
||||||
|
|
||||||
async def handle_upload(e):
|
- Structure: pages, components, services follow one-way dependency flow.
|
||||||
for chunk in read_chunks(e.content):
|
- Responsiveness: tested at small and large viewport widths.
|
||||||
progress.set_value(uploaded / total)
|
- Accessibility: labels, button text, and action visibility are clear.
|
||||||
ui.notify("Complete", type="positive")
|
- Reliability: validation and exception paths produce user-facing notifications.
|
||||||
```
|
- Maintainability: repeated UI patterns are extracted; business logic stays in services.
|
||||||
|
|
||||||
### Drag & Drop
|
If any check fails, return to the relevant step and iterate.
|
||||||
|
|
||||||
Use [Quasar q-sortable](https://quasar.dev/vue-components/sortable) for native drag-and-drop support via `.props()`.
|
## Completion Contract
|
||||||
|
|
||||||
---
|
This workflow is complete when:
|
||||||
|
|
||||||
## Troubleshooting
|
- the page flow meets the target outcome
|
||||||
|
- architecture boundaries are preserved
|
||||||
### Upload Validation & Errors
|
- chosen interaction pattern is implemented with explicit success and failure feedback
|
||||||
- Validate file size/extension before storage
|
- troubleshooting checks pass
|
||||||
- Catch exceptions and emit `ui.notify()` with type "negative"
|
- production-readiness gate passes
|
||||||
- Log errors for debugging
|
|
||||||
|
|
||||||
### Race Conditions
|
|
||||||
- Serialize updates: `await service.get_data()` before UI update
|
|
||||||
- Avoid multiple timers on same component
|
|
||||||
- Use `button.enabled = False/True` to prevent duplicate submissions
|
|
||||||
|
|
||||||
### Asset Caching
|
|
||||||
- Inject CSS in `bootstrap.py` at startup, not per-page
|
|
||||||
- Clear browser cache (Ctrl+Shift+Del) or use cache-busting query params: `?v=hash`
|
|
||||||
- Verify `/static/` mount path and nginx proxy rewrites
|
|
||||||
|
|
||||||
### Navigation & State
|
|
||||||
- Don't store state in globals; keep in `services/` or request-scoped
|
|
||||||
- Use `ui.navigate.to()` to switch pages
|
|
||||||
- Reload data in destination page's `@ui.page()` decorator
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Architecture and Styling Reference
|
||||||
|
|
||||||
|
## Project Boundaries
|
||||||
|
|
||||||
|
Use this dependency direction:
|
||||||
|
|
||||||
|
- pages import components and services
|
||||||
|
- components contain presentation logic only
|
||||||
|
- services contain business logic and do not import UI
|
||||||
|
- static assets are mounted and loaded once at bootstrap
|
||||||
|
|
||||||
|
Suggested module split:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/app/
|
||||||
|
ui/pages/
|
||||||
|
ui/components/
|
||||||
|
ui/static/
|
||||||
|
services/
|
||||||
|
api/
|
||||||
|
bootstrap.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Extraction Rules
|
||||||
|
|
||||||
|
Extract to ui/components when a pattern appears in two or more pages.
|
||||||
|
|
||||||
|
Keep in-page if the layout is specific to a single route.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def card_section(title: str, content: str) -> ui.card:
|
||||||
|
with ui.card().classes("w-full max-w-md") as card:
|
||||||
|
ui.label(title).classes("text-lg font-bold")
|
||||||
|
ui.label(content).classes("text-gray-600")
|
||||||
|
return card
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tailwind-First Layout Pattern
|
||||||
|
|
||||||
|
Use Tailwind utility classes for structure and spacing.
|
||||||
|
Use breakpoint classes for responsive behavior.
|
||||||
|
Use .style() only for values that must be computed dynamically.
|
||||||
|
|
||||||
|
```python
|
||||||
|
with ui.column().classes("w-full"):
|
||||||
|
with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"):
|
||||||
|
ui.card().classes("flex-1 min-w-64")
|
||||||
|
ui.card().classes("flex-1 min-w-64")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling Decision Order
|
||||||
|
|
||||||
|
1. Tailwind utility classes
|
||||||
|
2. Quasar props
|
||||||
|
3. Reusable styled component functions
|
||||||
|
4. Minimal custom CSS loaded once at bootstrap (only when needed)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
app.mount("/static", StaticFiles(directory="src/app/static"), name="static")
|
||||||
|
ui.add_css(open("src/app/static/css/base.css").read())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static Asset Rules
|
||||||
|
|
||||||
|
- Keep custom CSS small and tokenized with variables.
|
||||||
|
- Avoid per-page CSS injection.
|
||||||
|
- Verify static mount paths and reverse proxy rewrites.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- NiceGUI elements: https://nicegui.io/documentation/element
|
||||||
|
- NiceGUI binding: https://nicegui.io/documentation/section_binding_properties
|
||||||
|
- Tailwind: https://tailwindcss.com/docs/utility-first
|
||||||
|
- Quasar components: https://quasar.dev/vue-components
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Interaction Patterns Reference
|
||||||
|
|
||||||
|
## Reactive State
|
||||||
|
|
||||||
|
Use bindable dataclasses for local page state.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import field
|
||||||
|
from nicegui import binding, ui
|
||||||
|
|
||||||
|
@binding.bindable_dataclass
|
||||||
|
class PageState:
|
||||||
|
selected_id: int | None = None
|
||||||
|
items: list = field(default_factory=list)
|
||||||
|
|
||||||
|
state = PageState()
|
||||||
|
ui.label().bind_text_from(state, "selected_id")
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Upload Pattern
|
||||||
|
|
||||||
|
- Validate extension and size before storing.
|
||||||
|
- Delegate storage to a service method.
|
||||||
|
- Notify success and failure explicitly.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def handle_upload(e: ui.events.UploadEventArguments):
|
||||||
|
try:
|
||||||
|
if e.size > 10 * 1024 * 1024:
|
||||||
|
raise ValueError("File too large")
|
||||||
|
if not e.name.endswith(".pdf"):
|
||||||
|
raise ValueError("Only PDF allowed")
|
||||||
|
await file_service.store(e.content.read(), e.name)
|
||||||
|
ui.notify(f"Uploaded: {e.name}", type="positive")
|
||||||
|
except ValueError as err:
|
||||||
|
ui.notify(str(err), type="negative")
|
||||||
|
|
||||||
|
ui.upload(on_upload=handle_upload, auto_upload=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Submission Pattern
|
||||||
|
|
||||||
|
- Bind UI inputs to dataclass fields.
|
||||||
|
- Perform validation in the service layer.
|
||||||
|
- Clear form state on success.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@binding.bindable_dataclass
|
||||||
|
class FormData:
|
||||||
|
name: str = ""
|
||||||
|
email: str = ""
|
||||||
|
|
||||||
|
data = FormData()
|
||||||
|
ui.input("Name").bind_value(data, "name")
|
||||||
|
ui.input("Email").bind_value(data, "email")
|
||||||
|
|
||||||
|
async def on_submit():
|
||||||
|
try:
|
||||||
|
await user_service.create_user(name=data.name, email=data.email)
|
||||||
|
ui.notify("User created", type="positive")
|
||||||
|
data.name = data.email = ""
|
||||||
|
except ValueError as err:
|
||||||
|
ui.notify(str(err), type="negative")
|
||||||
|
|
||||||
|
ui.button("Submit").on_click(on_submit)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-Time Updates Decision
|
||||||
|
|
||||||
|
Use SSE for one-way status streaming.
|
||||||
|
Use WebSocket for bidirectional messaging.
|
||||||
|
|
||||||
|
SSE endpoint example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.get("/events/status")
|
||||||
|
async def status_stream():
|
||||||
|
async def gen():
|
||||||
|
while True:
|
||||||
|
yield f"data: {await get_status()}\\n\\n"
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return StreamingResponse(gen(), media_type="text/event-stream")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Background Work Pattern
|
||||||
|
|
||||||
|
- Start long jobs in FastAPI background tasks.
|
||||||
|
- Expose status via endpoint or streaming channel.
|
||||||
|
- Guard buttons against duplicate submissions during in-flight tasks.
|
||||||
|
|
||||||
|
## Explicit Refresh Pattern
|
||||||
|
|
||||||
|
Use @ui.refreshable and call refresh intentionally instead of polling unrelated state.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@ui.refreshable
|
||||||
|
async def item_list():
|
||||||
|
items = await service.list()
|
||||||
|
for item in items:
|
||||||
|
ui.label(item.name)
|
||||||
|
|
||||||
|
ui.button("Refresh").on_click(lambda: item_list.refresh())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- NiceGUI action events: https://nicegui.io/documentation/section_action_events
|
||||||
|
- FastAPI SSE: https://fastapi.tiangolo.com/advanced/server-sent-events/
|
||||||
|
- FastAPI WebSockets: https://fastapi.tiangolo.com/advanced/websockets/
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Troubleshooting and Quality Gates
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Upload Errors
|
||||||
|
|
||||||
|
- Validate extension and size before storage.
|
||||||
|
- Catch expected exceptions and return negative notifications.
|
||||||
|
- Log unexpected exceptions with request context.
|
||||||
|
|
||||||
|
### UI Race Conditions
|
||||||
|
|
||||||
|
- Disable triggering controls during async work.
|
||||||
|
- Remove duplicate timers and listeners targeting the same state.
|
||||||
|
- Ensure service call ordering is deterministic before render updates.
|
||||||
|
|
||||||
|
### Asset Caching
|
||||||
|
|
||||||
|
- Confirm static mount and proxy rewrite correctness.
|
||||||
|
- Add cache-busting query strings for changed assets.
|
||||||
|
- Avoid per-page CSS injection.
|
||||||
|
|
||||||
|
### Navigation and State Drift
|
||||||
|
|
||||||
|
- Avoid global mutable UI state.
|
||||||
|
- Keep state request-scoped or service-managed.
|
||||||
|
- Rehydrate page data during route load.
|
||||||
|
|
||||||
|
## Production Readiness Gate
|
||||||
|
|
||||||
|
Pass all checks before shipping:
|
||||||
|
|
||||||
|
- Structure: one-way dependencies between pages, components, and services.
|
||||||
|
- Responsiveness: UI validated at both small and large viewport widths.
|
||||||
|
- Accessibility: labels and actions are clear and readable.
|
||||||
|
- Reliability: validation and exception paths surface user feedback.
|
||||||
|
- Maintainability: repeated UI patterns are extracted; business logic remains in services.
|
||||||
|
|
||||||
|
If any check fails, return to the workflow step that owns that concern.
|
||||||
Reference in New Issue
Block a user