From 3d36d50870c957915690121b6e21591b717e62f6 Mon Sep 17 00:00:00 2001 From: gbanyan Date: Fri, 13 Feb 2026 12:23:32 +0800 Subject: [PATCH] docs(phase-02): research Alpine.js inline forms with Laravel --- .../02-inline-quick-add-ui/02-RESEARCH.md | 360 ++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 .planning/phases/02-inline-quick-add-ui/02-RESEARCH.md diff --git a/.planning/phases/02-inline-quick-add-ui/02-RESEARCH.md b/.planning/phases/02-inline-quick-add-ui/02-RESEARCH.md new file mode 100644 index 0000000..bb8cf31 --- /dev/null +++ b/.planning/phases/02-inline-quick-add-ui/02-RESEARCH.md @@ -0,0 +1,360 @@ +# Phase 2: Inline Quick-Add UI - Research + +**Researched:** 2026-02-13 +**Domain:** Alpine.js inline AJAX forms with Laravel backend +**Confidence:** HIGH + +## Summary + +Phase 2 adds inline note quick-add UI to the existing member list page. The backend API from Phase 1 is complete and ready. Research confirms Alpine.js 3.4 (already in package.json) provides all required functionality for inline state management, AJAX form submission, and reactive UI updates without additional dependencies. + +**Key findings:** +- Alpine.js core handles all requirements (x-data state, @submit.prevent, x-show/x-if directives) +- Alpine AJAX plugin exists but NOT needed - standard axios (already configured) is simpler and sufficient +- Laravel's CSRF token already in meta tag, axios auto-includes it via bootstrap.js +- Tailwind badge patterns already established in codebase (see Member model badge accessor) +- Pagination works with Alpine state - each row has independent x-data scope +- No Alpine.js Persist plugin needed - inline forms are ephemeral (state resets on page load by design) + +**Primary recommendation:** Use vanilla Alpine.js 3.4 + axios for AJAX, no additional plugins. Follow existing codebase patterns for badges and dark mode. + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Alpine.js | 3.4.2 | Reactive state management, DOM manipulation | Already in project, lightweight (15KB), perfect for inline forms | +| Axios | 1.6.4 | AJAX requests with CSRF protection | Already configured in bootstrap.js, auto-includes Laravel CSRF token | +| Tailwind CSS | 3.1.0 | Styling with dark mode support | Project standard, darkMode: 'class' configured | +| Laravel Blade | - | Server-side templating | Project standard for admin UI | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| Laravel Pagination | - | Multi-page member list | Already implemented, works with Alpine state | +| Laravel Validation | - | Server-side validation | Returns 422 JSON errors for AJAX requests | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| Axios | Alpine AJAX plugin | Alpine AJAX is HTML-response-oriented (replaces DOM chunks); our API returns JSON. Axios is simpler for JSON APIs. | +| Alpine.js | Vue.js | Vue is overkill for inline forms; Alpine is already in stack and sufficient | +| Inline forms | Modal dialog | Modal requires navigation away from list context; inline keeps admin in flow | + +**Installation:** +No new packages needed. All dependencies already in package.json. + +## Architecture Patterns + +### Recommended Project Structure +``` +resources/views/admin/members/ +├── index.blade.php # Member list with inline forms +└── _note-form.blade.php # (optional) Blade partial for note form +``` + +### Pattern 1: Per-Row Alpine Component +**What:** Each table row has independent x-data scope for its note form +**When to use:** Inline forms where each row needs independent state (open/closed, loading, errors) +**Example:** +```html +@foreach ($members as $member) + + {{ $member->full_name }} + + + + 備忘錄 + + + + + + +
+ + +
+ + +@endforeach +``` + +### Pattern 2: Alpine Method for AJAX Submission +**What:** x-data method handles form submission, loading state, success/error responses +**When to use:** AJAX form submission with loading state and error display +**Example:** +```javascript +x-data="{ + async submitNote() { + this.isSubmitting = true; + this.errors = {}; + + try { + const response = await axios.post( + '/admin/members/{{ $member->id }}/notes', + { content: this.noteContent } + ); + + // Success: update badge count, clear form, close form + this.noteCount++; + this.noteContent = ''; + this.noteFormOpen = false; + } catch (error) { + if (error.response?.status === 422) { + // Validation errors + this.errors = error.response.data.errors || {}; + } + } finally { + this.isSubmitting = false; + } + } +}" +``` +**Source:** Alpine.js x-data methods documentation (https://github.com/alpinejs/alpine/blob/main/packages/docs/src/en/directives/data.md) + +### Pattern 3: Badge Component with Dark Mode +**What:** Reusable Tailwind classes for count badges with dark mode support +**When to use:** Displaying counts inline with text (e.g., "3 備忘錄") +**Example:** +```html + + + 備忘錄 + +``` +**Source:** Member.php line 269 (existing badge pattern in codebase) + +### Pattern 4: Error Display Below Form Field +**What:** Conditionally show validation errors in Traditional Chinese below textarea +**When to use:** Laravel 422 validation errors from AJAX response +**Example:** +```html + +

+``` + +### Anti-Patterns to Avoid +- **Global Alpine state across pagination:** Don't use Alpine.store() or x-init to share state across pages. Each page load resets state (by design). +- **Disabling button without :disabled binding:** Always use `:disabled="isSubmitting"` to prevent double-submit. +- **Forgetting dark mode classes:** Every light color needs a `dark:` equivalent (see existing badge patterns). +- **Not clearing form on success:** Always reset `noteContent = ''` and `noteFormOpen = false` after successful submission. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| CSRF protection | Custom token injection | Axios + Laravel bootstrap.js | Already configured, auto-includes X-CSRF-TOKEN header from meta tag | +| JSON error parsing | Custom 422 handler | `error.response.data.errors` | Laravel standardizes error structure in 422 responses | +| Loading spinners | Custom CSS animations | Tailwind + `:disabled` state | Tailwind provides `opacity-50 cursor-not-allowed` via disabled state | +| Badge styling | Custom badge component | Existing Member badge pattern | Already dark-mode compatible, proven in production | + +**Key insight:** Alpine.js + axios + Tailwind provide 90% of inline form functionality. The remaining 10% (business logic like "increment count on success") is trivial custom code. Don't introduce new dependencies. + +## Common Pitfalls + +### Pitfall 1: Forgetting CSRF Token in Axios Requests +**What goes wrong:** POST requests return 419 error (CSRF token mismatch) +**Why it happens:** Axios auto-includes token, but only if bootstrap.js is imported and meta tag exists +**How to avoid:** Verify `resources/js/app.js` imports `./bootstrap.js` and `layouts/app.blade.php` has `` +**Warning signs:** 419 errors in browser network tab for POST requests +**Resolution:** Already configured correctly in codebase (bootstrap.js line 10, app.blade.php line 6) + +### Pitfall 2: N+1 Query for Note Counts +**What goes wrong:** Database query per member to count notes (15 members = 15 extra queries) +**Why it happens:** Accessing `$member->notes()->count()` in Blade instead of using withCount +**How to avoid:** Controller must use `->withCount('notes')` (already implemented in AdminMemberController line 18) +**Warning signs:** Laravel Debugbar shows N+1 queries +**Resolution:** Already prevented in Phase 1 + +### Pitfall 3: Alpine State Lost on Pagination +**What goes wrong:** Admin opens note form on page 1, navigates to page 2, returns to page 1 → form is closed +**Why it happens:** Pagination triggers full page reload; Alpine state is JavaScript, not persisted +**How to avoid:** Accept this as expected behavior. Inline forms are ephemeral. Don't use Alpine.persist() for this. +**Warning signs:** User confusion if they expect form state to persist +**Resolution:** Document as expected behavior; users complete or abandon inline forms on same page + +### Pitfall 4: Dark Mode Colors Missing +**What goes wrong:** UI looks broken in dark mode (white text on white background, invisible badges) +**Why it happens:** Forgetting `dark:` prefix for Tailwind classes +**How to avoid:** Every light color class needs a dark equivalent. Copy pattern from existing badges (Member.php line 269-272) +**Warning signs:** White/invisible elements in dark mode +**Example:** +```html + +Badge + + +Badge +``` + +### Pitfall 5: Not Preventing Form Submit Default +**What goes wrong:** Form submits to server (full page reload) instead of AJAX +**Why it happens:** Missing `.prevent` modifier on `@submit` +**How to avoid:** Always use `@submit.prevent` for AJAX forms +**Warning signs:** Page reloads on form submit instead of AJAX request +**Example:** +```html + +
+ + + +``` +**Source:** Alpine.js .prevent modifier documentation (https://github.com/alpinejs/alpine/blob/main/packages/docs/src/en/directives/on.md) + +## Code Examples + +Verified patterns from official sources and codebase: + +### Alpine.js Form Submission with Loading State +```javascript +// Source: Alpine.js x-data methods + project conventions +x-data="{ + noteFormOpen: false, + noteContent: '', + isSubmitting: false, + errors: {}, + noteCount: {{ $member->notes_count }}, + + async submitNote() { + this.isSubmitting = true; + this.errors = {}; + + try { + const response = await axios.post( + '{{ route('admin.members.notes.store', $member) }}', + { content: this.noteContent } + ); + + // Success: update count, clear form, close + this.noteCount++; + this.noteContent = ''; + this.noteFormOpen = false; + } catch (error) { + if (error.response?.status === 422) { + this.errors = error.response.data.errors || {}; + } + } finally { + this.isSubmitting = false; + } + } +}" +``` + +### Badge with Dynamic Count (Reactive) +```html + + + 備忘錄 + +``` + +### Conditional Form Visibility with Transitions +```html + +
+ +
+``` + +### Error Display Pattern +```html + + +

+

+``` + +### Button with Loading State +```html + + +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| jQuery AJAX | Alpine.js + axios | Alpine 3.x release (2021) | Smaller bundle, reactive state management | +| Manual DOM updates | Alpine reactive bindings | Alpine 3.x | Eliminates manual `$('#count').text(count)` calls | +| Separate JS files per page | Inline x-data in Blade | Alpine convention | Co-located logic with markup | +| Global CSS for badges | Tailwind utility classes | Tailwind 3.x | Dark mode via `dark:` prefix, no custom CSS | + +**Deprecated/outdated:** +- **Alpine AJAX plugin for JSON APIs**: Designed for HTML-response pattern (like htmx). For JSON APIs, vanilla axios is simpler. +- **Alpine.store() for ephemeral forms**: Overkill for inline forms that reset on page load. + +## Open Questions + +1. **Should note forms auto-focus textarea on open?** + - What we know: Alpine has `x-init="$refs.textarea.focus()"` pattern + - What's unclear: Is auto-focus a11y-friendly for inline forms? (risk: unexpected focus jump) + - Recommendation: Skip auto-focus initially. Add only if user feedback requests it. + +2. **Should we limit textarea rows/max-length?** + - What we know: StoreNoteRequest validates `min:1`, no max-length validation + - What's unclear: Is there a business constraint on note length? + - Recommendation: Use `rows="3"` for UI, add `max:1000` validation if long notes cause issues. + +3. **Should we show success flash message or silent update?** + - What we know: Badge updates immediately, form closes + - What's unclear: Do users need explicit "備忘錄已新增" confirmation? + - Recommendation: Start silent (badge update is confirmation). Add toast notification only if users report uncertainty. + +## Sources + +### Primary (HIGH confidence) +- Alpine.js official docs (GitHub) - x-data, @submit.prevent, x-show, methods +- Axios documentation - Already configured in bootstrap.js (line 7-10) +- Laravel validation JSON responses - 422 error structure (Feature tests confirm structure) +- Tailwind CSS 3.1 - dark mode classes (existing Member badge pattern line 269) +- AdminMemberController.php - withCount('notes') implementation (line 18) + +### Secondary (MEDIUM confidence) +- [Flowbite Badge Components](https://flowbite.com/docs/components/badge/) - Tailwind badge class patterns +- [Alpine AJAX Inline Validation](https://alpine-ajax.js.org/examples/inline-validation/) - Validation error display pattern +- [Scroll to validation errors in Laravel using Alpine.js](https://www.amitmerchant.com/scroll-to-validation-error-in-laravel-using-alpinejs/) - Alpine validation patterns + +### Tertiary (LOW confidence) +- None - All critical patterns verified in codebase or official docs + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - All libraries already in package.json, versions confirmed +- Architecture: HIGH - Patterns verified in existing codebase (Member badges, Alpine modal component) +- Pitfalls: HIGH - Common Alpine/Laravel gotchas well-documented, CSRF already configured +- Dark mode: HIGH - Existing pattern in Member.php line 269-272 confirmed working + +**Research date:** 2026-02-13 +**Valid until:** 2026-03-13 (30 days - stable stack, unlikely to change)