Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee

fix: fix alpine issue on profile page

pdewey.com 9b633dd6 c19f4a85

verified
+85 -6
+82
.skills/htmx-alpine-integration.md
··· 1 + # HTMX + Alpine.js Integration Pattern 2 + 3 + ## Problem: "Alpine Expression Error: [variable] is not defined" 4 + 5 + When HTMX swaps in content containing Alpine.js directives (like `x-show`, `x-if`, `@click`), Alpine may not automatically process the new DOM elements, resulting in console errors like: 6 + 7 + ``` 8 + Alpine Expression Error: activeTab is not defined 9 + Expression: "activeTab === 'brews'" 10 + ``` 11 + 12 + ## Root Cause 13 + 14 + HTMX loads and swaps content into the DOM after Alpine has already initialized. The new elements contain Alpine directives that reference variables in a parent Alpine component's scope, but Alpine doesn't automatically bind these new elements to the existing component. 15 + 16 + ## Solution 17 + 18 + Use HTMX's `hx-on::after-swap` event to manually tell Alpine to initialize the new DOM tree: 19 + 20 + ```html 21 + <div id="content" 22 + hx-get="/api/data" 23 + hx-trigger="load" 24 + hx-swap="innerHTML" 25 + hx-on::after-swap="Alpine.initTree($el)"> 26 + </div> 27 + ``` 28 + 29 + ### Key Points 30 + 31 + - `hx-on::after-swap` - HTMX event that fires after content swap completes 32 + - `Alpine.initTree($el)` - Tells Alpine to process all directives in the swapped element 33 + - `$el` - HTMX provides this as the target element that received the swap 34 + 35 + ## Common Scenario 36 + 37 + **Parent template** (defines Alpine scope): 38 + ```html 39 + <div x-data="{ activeTab: 'brews' }"> 40 + <!-- Static content with tab buttons --> 41 + <button @click="activeTab = 'brews'">Brews</button> 42 + 43 + <!-- HTMX loads dynamic content here --> 44 + <div id="content" 45 + hx-get="/api/tabs" 46 + hx-trigger="load" 47 + hx-swap="innerHTML" 48 + hx-on::after-swap="Alpine.initTree($el)"> 49 + </div> 50 + </div> 51 + ``` 52 + 53 + **Loaded partial** (uses parent scope): 54 + ```html 55 + <div x-show="activeTab === 'brews'"> 56 + <!-- Brew content --> 57 + </div> 58 + <div x-show="activeTab === 'beans'"> 59 + <!-- Bean content --> 60 + </div> 61 + ``` 62 + 63 + Without `Alpine.initTree($el)`, the `x-show` directives won't be bound to the parent's `activeTab` variable. 64 + 65 + ## Alternative: Alpine Morph Plugin 66 + 67 + For more complex scenarios with nested Alpine components, use the Alpine Morph plugin: 68 + 69 + ```html 70 + <script src="https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/cdn.min.js"></script> 71 + <div hx-swap="morph"></div> 72 + ``` 73 + 74 + This preserves Alpine state during swaps but requires the plugin. 75 + 76 + ## When to Use 77 + 78 + Apply this pattern whenever: 79 + 1. HTMX loads content containing Alpine directives 80 + 2. The loaded content references variables from a parent Alpine component 81 + 3. You see "Expression Error: [variable] is not defined" in console 82 + 4. Alpine directives in HTMX-loaded content don't work (no reactivity, clicks ignored, etc.)
+2 -5
justfile
··· 1 1 run: 2 - @LOG_LEVEL=debug LOG_FORMAT=console go run cmd/server/main.go 2 + @LOG_LEVEL=debug LOG_FORMAT=console go run cmd/server/main.go -firehose 3 3 4 4 run-production: 5 - @LOG_FORMAT=json SECURE_COOKIES=true go run cmd/server/main.go 6 - 7 - run-firehose: 8 - @LOG_LEVEL=debug LOG_FORMAT=console go run cmd/server/main.go -firehose 5 + @LOG_FORMAT=json SECURE_COOKIES=true go run cmd/server/main.go -firehose 9 6 10 7 test: 11 8 @go test ./... -cover -coverprofile=cover.out
+1 -1
templates/profile.tmpl
··· 87 87 </div> 88 88 89 89 <!-- Tab content loaded via HTMX --> 90 - <div id="profile-content" hx-get="/api/profile/{{.Profile.Handle}}" hx-trigger="load" hx-swap="innerHTML"> 90 + <div id="profile-content" hx-get="/api/profile/{{.Profile.Handle}}" hx-trigger="load" hx-swap="innerHTML" hx-on::after-swap="Alpine.initTree($el)"> 91 91 <!-- Loading skeleton --> 92 92 <div class="animate-pulse"> 93 93 <!-- Brews Tab Skeleton -->