feat: add patterns example app — Session 1 (scaffold + forms #1-7)#59
feat: add patterns example app — Session 1 (scaffold + forms #1-7)#59
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new patterns/ working example app that scaffolds a “Patterns” catalog and implements the first 7 Forms & Editing patterns (with shared layout + E2E coverage) as described in the patterns proposal.
Changes:
- Introduces a shared layout + index catalog page listing all pattern categories (7 implemented, others marked coming soon).
- Implements 7 Forms & Editing pattern handlers/controllers, state structs, templates, and sample data.
- Adds chromedp E2E test suite for the index and each implemented pattern; registers
patternsintest-all.sh.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| test-all.sh | Adds patterns to the working examples list so it’s exercised by the full test script. |
| patterns/templates/layout.tmpl | New shared layout with dev/prod LiveTemplate client + CSS loading. |
| patterns/templates/index.tmpl | New index catalog template listing categories + pattern links. |
| patterns/templates/forms/click-to-edit.tmpl | Pattern #1 template (view/edit toggle). |
| patterns/templates/forms/edit-row.tmpl | Pattern #2 template (inline table row editing). |
| patterns/templates/forms/inline-validation.tmpl | Pattern #3 template (server-side validation feedback). |
| patterns/templates/forms/bulk-update.tmpl | Pattern #4 template (batch checkbox updates). |
| patterns/templates/forms/reset-input.tmpl | Pattern #5 template (auto-reset after submit). |
| patterns/templates/forms/file-upload.tmpl | Pattern #6 template (multipart + chunked upload UI). |
| patterns/templates/forms/preserve-inputs.tmpl | Pattern #7 template (preserve inputs across re-renders). |
| patterns/state_forms.go | Adds state structs for the 7 patterns. |
| patterns/patterns_test.go | Adds E2E suite covering index + 7 patterns with UI standards checks. |
| patterns/main.go | App entrypoint wiring routes + layout composition. |
| patterns/handlers_forms.go | Controllers + handlers for patterns #1–#7. |
| patterns/data.go | Sample data helpers + pattern catalog structure. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) | ||
| tmpl := livetemplate.Must(livetemplate.New("layout", opts...)) | ||
| return tmpl.Handle(&IndexController{}, livetemplate.AsState(&IndexState{ | ||
| Title: "LiveTemplate Patterns", |
There was a problem hiding this comment.
The index page sets Title to "LiveTemplate Patterns", but the shared layout appends " — LiveTemplate Patterns" to every <title>, resulting in "LiveTemplate Patterns — LiveTemplate Patterns" on the index. Consider setting the index Title to something shorter (e.g., "Patterns"), or make the layout conditional so the suffix isn't duplicated.
| Title: "LiveTemplate Patterns", | |
| Title: "Patterns", |
| <input name="name" value="{{.Name}}"> | ||
| <input name="email" value="{{.Email}}"> |
There was a problem hiding this comment.
These inputs have no associated (or aria-label/aria-labelledby), which makes the inline row editor difficult to use with screen readers. Add labels (they can be visually hidden) or add explicit aria-label attributes for the name/email fields.
| <input name="name" value="{{.Name}}"> | |
| <input name="email" value="{{.Email}}"> | |
| <input name="name" value="{{.Name}}" aria-label="Name"> | |
| <input name="email" value="{{.Email}}" aria-label="Email"> |
| <p><small>Form clears automatically after successful submission — no extra attributes needed.</small></p> | ||
| <form method="POST"> | ||
| <fieldset role="group"> | ||
| <input name="message" placeholder="Type a message..." required> |
There was a problem hiding this comment.
This text input relies on placeholder text only and has no label/aria-label, which is an accessibility issue for screen readers. Add an explicit (visually-hidden is fine) or an aria-label so the field has an accessible name.
| <input name="message" placeholder="Type a message..." required> | |
| <input name="message" aria-label="Message" placeholder="Type a message..." required> |
| <tr data-key="{{.ID}}"> | ||
| <td>{{.Name}}</td> | ||
| <td>{{.Email}}</td> | ||
| <td><input type="checkbox" name="active-{{.ID}}" {{if .Active}}checked{{end}}></td> |
There was a problem hiding this comment.
The per-row "Active" checkbox inputs have no accessible name. Consider adding an aria-label (e.g., including the user's name) or associating each checkbox with a so screen readers can identify what each checkbox controls.
| <td><input type="checkbox" name="active-{{.ID}}" {{if .Active}}checked{{end}}></td> | |
| <td><input type="checkbox" name="active-{{.ID}}" aria-label="Active for {{.Name}}" {{if .Active}}checked{{end}}></td> |
| <dt>Last Name</dt><dd>{{.LastName}}</dd> | ||
| <dt>Email</dt><dd>{{.Email}}</dd> | ||
| </dl> | ||
| <button name="edit">Edit</button> |
There was a problem hiding this comment.
In view mode the "Edit" button is not inside a
, so Tier 1 (no-JS) fallback can't submit an action to reach the Edit() handler. Per the repo’s Tier 1 guidance (CLAUDE.md:7-21, 49-55), actions should be routed via standard form submissions + button names. Wrap the button in a POST form (or use a button with a form="..." attribute pointing at a POST form).| <button name="edit">Edit</button> | |
| <form method="POST"> | |
| <button name="edit">Edit</button> | |
| </form> |
| // Click Update | ||
| chromedp.Click(`button[name="bulkUpdate"]`, chromedp.ByQuery), | ||
| e2etest.WaitFor(`true`, 2*time.Second), // Wait for re-render |
There was a problem hiding this comment.
This uses e2etest.WaitFor(true, ...) after clicking Update, which doesn’t wait on a meaningful condition and can be flaky (it’s effectively a time-based delay). The repo’s E2E checklist explicitly recommends condition-based waits (CLAUDE.md:78-80). Wait for a concrete DOM condition that reflects the update (e.g., checked state of active-1/active-3) before asserting.
| // Click Update | |
| chromedp.Click(`button[name="bulkUpdate"]`, chromedp.ByQuery), | |
| e2etest.WaitFor(`true`, 2*time.Second), // Wait for re-render | |
| // Click Update and wait for the updated checkbox state to be reflected in the DOM | |
| chromedp.Click(`button[name="bulkUpdate"]`, chromedp.ByQuery), | |
| e2etest.WaitFor(`!document.querySelector('input[name="active-1"]').checked && document.querySelector('input[name="active-3"]').checked`, 2*time.Second), |
| chromedp.SendKeys(`input[name="name"]`, "Test Name", chromedp.ByQuery), | ||
| chromedp.SendKeys(`textarea[name="description"]`, "Test Description", chromedp.ByQuery), | ||
| chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), | ||
| e2etest.WaitFor(`true`, 3*time.Second), |
There was a problem hiding this comment.
This uses e2etest.WaitFor(true, ...) after form submission, which doesn’t wait on any specific post-submit condition and can be flaky. The repo’s E2E checklist recommends condition-based waits (CLAUDE.md:78-80). Prefer waiting for a concrete DOM change (e.g., a success indicator, or the expected input value being present) before reading/asserting field values.
| e2etest.WaitFor(`true`, 3*time.Second), | |
| e2etest.WaitFor(`document.querySelector('input[name="name"]') !== null && document.querySelector('input[name="name"]').value === "Test Name"`, 3*time.Second), |
Implements the patterns example app scaffold and all 7 Forms & Editing patterns from the patterns proposal (#330). Each pattern is a focused, isolated demo with its own handler, controller, template, and E2E tests. Patterns implemented: 1. Click To Edit — toggle view/edit with conditional rendering 2. Edit Row — inline table row editing with data-key identity 3. Inline Validation — Change() with ValidateForm and error display 4. Bulk Update — batch checkbox operations 5. Reset User Input — auto-clear forms after submission 6. File Upload — Tier 1 multipart + Tier 2 chunked upload 7. Preserving File Inputs — lvt-form:preserve across re-renders Also includes: shared layout template, categorized index page with all 31 patterns listed (7 implemented, 24 coming soon), and comprehensive chromedp E2E tests with UI_Standards and Visual_Check subtests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
97091fa to
12b5682
Compare
Replace <dl> with a Pico-styled table for cleaner key-value display. Add outline class to Edit button to avoid full-width block styling. Update tests to match new HTML structure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each pattern is a separate LiveTemplate handler. SPA navigation between handlers shows stale content from the previous handler. Adding lvt-nav:no-intercept forces full page loads when navigating between the index page and individual pattern pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Patterns app improvements after Session 1: - Cross-handler SPA navigation: add dedicated E2E test suite (cross_handler_nav_test.go) covering index↔pattern nav, back/forward buttons, title updates, and WebSocket reconnection. Removed the lvt-nav:no-intercept workaround from index.tmpl/layout.tmpl since client v0.8.22 now handles cross-handler navigation correctly. - File Upload (#6): - Button name="upload" routes to Upload() handler method - WithUpload("document", ...) for Tier 1 multipart uploads - Tier 2 ChunkSize=1024 so upload progress is visible for demo files - Flash messages via FlashTag for success/error - Tier 2 form renders in-progress uploads via {{range .lvt.Uploads}} - Form-lvt:preserve to prevent reset after submit - E2E tests: Submit_Without_File, Tier1_Upload_With_File, Form_Structure - attachFileViaDataTransfer test helper (Docker-Chrome compatible) - Preserving Form Inputs (#7): - ctx.SetFlash("success", "Saved: "+Name) for visual feedback - WithUpload("attachment", ...) so multipart submissions are parsed - Use .lvt.ErrorTag helper instead of manual HasError/Error markup - New E2E tests: Submit_Shows_Flash, Form_Values_Preserved_After_Submit, Values_Survive_Rerender, Submit_With_File_Attached (regression for the fieldset-disabled FormData bug fixed in livetemplate/client#58) - Bulk Update (#4): - Add success flash "Updated N user(s)" via FlashTag - Test verifies flash text via output[data-flash] selector - Inline Validation (#3): - Use .lvt.AriaInvalid and .lvt.ErrorTag helpers instead of manual HasError/Error patterns - LVT_LOCAL_CLIENT env var in main.go lets developers serve a local client build (useful during client development/testing). All 9 E2E test functions pass against livetemplate/client v0.8.22 from CDN (no local override required). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n notes Session 1 shipped as livetemplate/examples#59 (Forms & Editing patterns #1-7 + scaffold + cross-handler nav tests, merged as 471ac9f). This updates the Session 1 tracker and adds an "Implementation Notes" subsection capturing non-obvious learnings so Session 2 runs with fewer iterations. Notes captured: FlashTag render output (output[data-flash], not ins/del), State struct Title+Category shape, data.go :: allPatterns() data-driven index, setupTest/attachFileViaDataTransfer/runUIStandards helpers, WaitFor("true") anti-pattern, WithUpload server registration for Tier 1 multipart, and local dev/visual-check flags. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
) * docs(proposals): mark patterns session 1 complete + add implementation notes Session 1 shipped as livetemplate/examples#59 (Forms & Editing patterns #1-7 + scaffold + cross-handler nav tests, merged as 471ac9f). This updates the Session 1 tracker and adds an "Implementation Notes" subsection capturing non-obvious learnings so Session 2 runs with fewer iterations. Notes captured: FlashTag render output (output[data-flash], not ins/del), State struct Title+Category shape, data.go :: allPatterns() data-driven index, setupTest/attachFileViaDataTransfer/runUIStandards helpers, WaitFor("true") anti-pattern, WithUpload server registration for Tier 1 multipart, and local dev/visual-check flags. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(proposals): address PR #334 bot review feedback - Fix UploadConfig example to use named fields (Copilot): the struct has Accept []string as its first field, so UploadConfig{MaxFileSize, MaxEntries} positional init won't compile. Match the pattern used in the actual Session 1 handlers_forms.go. - Clarify @latest CDN tradeoff (Claude): document that we accept the risk of a client release breaking a demo, rather than pin a version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Patterns implemented
{{if}}conditional renderingdata-keyidentityChange()withValidateForm()and.lvt.HasError/.lvt.Errorctx.GetBool()lvt-uploadchunked uploadlvt-form:preserveretains values across re-rendersArchitecture
WithParseFiles("templates/layout.tmpl", "templates/forms/<pattern>.tmpl")for template compositionlivetemplate.New("layout", ...)with layout as main template entry pointTest plan
LVT_VISUAL_CHECK=true)go buildandgo vetclean"patterns"added to WORKING_EXAMPLES in test-all.sh🤖 Generated with Claude Code