fix(generator): improve form element sync and simplify sort options#41
fix(generator): improve form element sync and simplify sort options#41
Conversation
Two related fixes for better UX:
1. Fix morphdom form element sync issues:
- Add data-expected-value attribute to select and input elements
- Add syncFormValues script to sync values after morphdom updates
- Add server-side reversion protection for sort (PrevSortBy, LastSortTime)
- Prevents select dropdowns from reverting to previous values
- Prevents search input from not clearing when X button is clicked
2. Simplify sort dropdown options:
- Only show sort options for the first string field (primary display field)
- Removes confusing Content (A-Z) / Content (Z-A) options when content
isn't visible in the list view
- Users now see: Newest First, Title (A-Z), Title (Z-A), Oldest First
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PR Review: Fix form element sync and simplify sort optionsOverviewThis PR addresses two UX issues in generated LiveTemplate apps:
Code Quality & Best Practices✅ Strengths
|
There was a problem hiding this comment.
Pull request overview
This PR aims to improve UX in generated LiveTemplate applications by fixing morphdom form synchronization issues and simplifying sort dropdown options.
Changes:
- Added
data-expected-valueattribute to sort select elements and a JavaScript sync script to prevent morphdom from reverting form values after DOM updates - Added server-side reversion protection in the Sort handler using
PrevSortByandLastSortTimefields with 500ms threshold - Modified sort dropdown to show options only for the first string field instead of all string fields
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| testdata/golden/resource_handler.go.golden | Added PrevSortBy/LastSortTime fields and server-side reversion protection logic in Sort handler |
| internal/kits/system/single/templates/resource/template.tmpl.tmpl | Added data-expected-value attribute to select, added syncSelectValues script, and changed sort options to first field only |
| internal/kits/system/single/templates/resource/handler.go.tmpl | Template for handler with new state fields and Sort handler logic |
| internal/kits/system/single/components/toolbar.tmpl | Updated sort dropdown to show only first string field options |
| internal/kits/system/single/components/sort.tmpl | Updated sort dropdown to show only first string field options |
| internal/kits/system/multi/templates/resource/template.tmpl.tmpl | Same changes as single kit template |
| internal/kits/system/multi/templates/resource/handler.go.tmpl | Same changes as single kit handler |
| internal/kits/system/multi/components/toolbar.tmpl | Same changes as single kit toolbar |
| internal/kits/system/multi/components/sort.tmpl | Same changes as single kit sort component |
| internal/generator/templates/resource/template.tmpl.tmpl | Same template changes for generator |
| internal/generator/templates/resource/handler.go.tmpl | Same handler changes for generator |
| internal/generator/templates/components/toolbar.tmpl | Same toolbar changes for generator |
| internal/generator/templates/components/sort.tmpl | Same sort component changes for generator |
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | ||
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> |
There was a problem hiding this comment.
When using range $i, $f := .Fields, the field variable is assigned to $f, so field properties should be accessed via $f.Name instead of .Name. The current code [[.Name]] will not work correctly in this context and will likely cause template execution errors.
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | |
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> | |
| <option value="[[$f.Name]]_asc" {{if eq $.SortBy "[[$f.Name]]_asc"}}selected{{end}}>[[$f.Name | title]] (A-Z)</option> | |
| <option value="[[$f.Name]]_desc" {{if eq $.SortBy "[[$f.Name]]_desc"}}selected{{end}}>[[$f.Name | title]] (Z-A)</option> |
| @@ -14,10 +14,10 @@ | |||
| [[- end]] | |||
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort"> | |||
There was a problem hiding this comment.
The select element is missing the data-expected-value attribute that is present in the main template file. For consistency with the morphdom form element sync fix described in the PR, this select should also have data-expected-value="{{.SortBy}}" to ensure proper value synchronization after DOM updates.
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort"> | |
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] data-expected-value="{{.SortBy}}" name="sort_by" lvt-change="sort"> |
| @@ -14,10 +14,10 @@ | |||
| [[- end]] | |||
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort"> | |||
There was a problem hiding this comment.
The select element is missing the data-expected-value attribute that is present in the main template file. For consistency with the morphdom form element sync fix described in the PR, this select should also have data-expected-value="{{.SortBy}}" to ensure proper value synchronization after DOM updates.
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort"> | |
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort" data-expected-value="{{.SortBy}}"> |
| <!-- Fix for morphdom not properly syncing select values --> | ||
| <script> | ||
| (function() { | ||
| function syncSelectValues() { |
There was a problem hiding this comment.
The function name syncSelectValues is misleading given the PR description mentions a syncFormValues script. While the current name accurately reflects what the function does (syncing select elements only), consider renaming to match the PR description or updating the description to match the implementation.
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | ||
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> |
There was a problem hiding this comment.
When using range $i, $f := .Fields, the field variable is assigned to $f, so field properties should be accessed via $f.Name instead of .Name. The current code [[.Name]] will not work correctly in this context and will likely cause template execution errors.
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | |
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> | |
| <option value="[[$f.Name]]_asc" {{if eq $.SortBy "[[$f.Name]]_asc"}}selected{{end}}>[[$f.Name | title]] (A-Z)</option> | |
| <option value="[[$f.Name]]_desc" {{if eq $.SortBy "[[$f.Name]]_desc"}}selected{{end}}>[[$f.Name | title]] (Z-A)</option> |
| @@ -14,10 +14,10 @@ | |||
| [[- end]] | |||
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort"> | |||
There was a problem hiding this comment.
The select element is missing the data-expected-value attribute that is present in the main template file. For consistency with the morphdom form element sync fix described in the PR, this select should also have data-expected-value="{{.SortBy}}" to ensure proper value synchronization after DOM updates.
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort"> | |
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort" data-expected-value="{{.SortBy}}"> |
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | ||
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> |
There was a problem hiding this comment.
When using range $i, $f := .Fields, the field variable is assigned to $f, so field properties should be accessed via $f.Name instead of .Name. The current code [[.Name]] will not work correctly in this context and will likely cause template execution errors.
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | |
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> | |
| <option value="[[$f.Name]]_asc" {{if eq $.SortBy "[[$f.Name]]_asc"}}selected{{end}}>[[$f.Name | title]] (A-Z)</option> | |
| <option value="[[$f.Name]]_desc" {{if eq $.SortBy "[[$f.Name]]_desc"}}selected{{end}}>[[$f.Name | title]] (Z-A)</option> |
| @@ -22,10 +22,10 @@ | |||
| [[- end]] | |||
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort"> | |||
There was a problem hiding this comment.
The select element is missing the data-expected-value attribute that is present in the main template file. For consistency with the morphdom form element sync fix described in the PR, this select should also have data-expected-value="{{.SortBy}}" to ensure proper value synchronization after DOM updates.
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort"> | |
| <select[[if ne (selectClass .CSSFramework) ""]] class="[[selectClass .CSSFramework]]"[[end]] name="sort_by" lvt-change="sort" data-expected-value="{{.SortBy}}"> |
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | ||
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> |
There was a problem hiding this comment.
When using range $i, $f := .Fields, the field variable is assigned to $f, so field properties should be accessed via $f.Name instead of .Name. The current code [[.Name]] will not work correctly in this context and will likely cause template execution errors.
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | |
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> | |
| <option value="[[$f.Name]]_asc" {{if eq $.SortBy "[[$f.Name]]_asc"}}selected{{end}}>[[$f.Name | title]] (A-Z)</option> | |
| <option value="[[$f.Name]]_desc" {{if eq $.SortBy "[[$f.Name]]_desc"}}selected{{end}}>[[$f.Name | title]] (Z-A)</option> |
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | ||
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> |
There was a problem hiding this comment.
When using range $i, $f := .Fields, the field variable is assigned to $f, so field properties should be accessed via $f.Name instead of .Name. The current code [[.Name]] will not work correctly in this context and will likely cause template execution errors.
| <option value="[[.Name]]_asc" {{if eq $.SortBy "[[.Name]]_asc"}}selected{{end}}>[[.Name | title]] (A-Z)</option> | |
| <option value="[[.Name]]_desc" {{if eq $.SortBy "[[.Name]]_desc"}}selected{{end}}>[[.Name | title]] (Z-A)</option> | |
| <option value="[[$f.Name]]_asc" {{if eq $.SortBy "[[$f.Name]]_asc"}}selected{{end}}>[[$f.Name | title]] (A-Z)</option> | |
| <option value="[[$f.Name]]_desc" {{if eq $.SortBy "[[$f.Name]]_desc"}}selected{{end}}>[[$f.Name | title]] (Z-A)</option> |
- Fix range variable access: use $f.Name instead of .Name when iterating with `range $i, $f := .Fields` (the field variable is $f, not .) - Add data-expected-value attribute to sort select in component files for consistency with morphdom sync fix - Rename syncSelectValues to syncFormValues and add input element sync to match PR description Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Pull Request Review: Form Element Sync and Sort Options SimplificationSummaryThis PR addresses two UX issues in generated LiveTemplate applications: morphdom-related form element synchronization problems and overly verbose sort dropdown options. The approach combines client-side value syncing with server-side reversion protection. Code Quality ✅Strengths
Areas for Improvement1. Magic Number - 500ms ThresholdLocation: The 500ms threshold for detecting spurious events is hardcoded: if input.SortBy == state.PrevSortBy && elapsedMs < 500 {Concern: This is a heuristic that may not work reliably across:
Recommendation: Consider making this configurable or add a comment explaining the rationale for 500ms specifically. 2. Server-Side State BloatLocation: Adding PrevSortBy string `json:"prev_sort_by" lvt:"transient"`
LastSortTime int64 `json:"last_sort_time" lvt:"transient"`Concern: This workaround pollutes the state structure for all generated resources. While marked as transient, this is addressing a client-side issue with server-side state. Recommendation: Consider if there's a cleaner client-side-only solution, such as:
Potential Bugs 🐛1. Race Condition in Time-Based CheckLocation: if state.LastSortTime > 0 {
elapsed := now - state.LastSortTime
elapsedMs := elapsed / 1_000_000
if input.SortBy == state.PrevSortBy && elapsedMs < 500 {
return state, nil
}
}Issue: If a user rapidly:
Impact: Medium - could prevent legitimate rapid sort changes Recommendation: Also check that we're reverting to the previous value, not just any duplicate value: if input.SortBy == state.PrevSortBy && input.SortBy != state.SortBy && elapsedMs < 500 {2. Client-Side Observer PerformanceLocation: var observer = new MutationObserver(function(mutations) {
syncFormValues();
});
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['data-expected-value'] });Issue: This observes the entire document body with Impact: Low to Medium - may cause performance issues on complex pages with many form elements Recommendation:
Performance Considerations ⚡1. Redundant Server Calls Prevented ✅The time-based check successfully prevents spurious server round-trips, which is good for performance. 2. Client-Side Overhead
Recommendation: Profile this on a page with many form elements to ensure it doesn't cause jank. 3. Template Range Iteration ChangeLocation: [[- range , := .Fields]]
[[- if and (eq 0) (eq .GoType "string")]]Impact: Positive - reduces template size and rendering time by only showing relevant sort options. Security Concerns 🔒No Issues Found ✅
Test Coverage 📋Current Status
Recommended Tests1. Unit Test for Sort Reversion Detectionfunc TestSortController_IgnoresSpuriousReversions(t *testing.T) {
// Test that rapid duplicate sorts within 500ms are ignored
// Test that legitimate sort changes are not ignored
// Test edge cases (LastSortTime=0, exactly 500ms, etc.)
}2. E2E Browser Test for Form SyncAdd to func TestRendering_SelectPersistence(t *testing.T) {
// 1. Select a sort option
// 2. Verify it persists after morphdom update
// 3. Verify it doesn't revert to previous value
}3. Test Sort Options Filteringfunc TestSortDropdown_OnlyShowsFirstStringField(t *testing.T) {
// Verify sort options only include first string field
}Architecture & Design 🏗️Concerns1. Root Cause Not AddressedThis PR treats symptoms (morphdom reverting values) rather than the root cause. The real issue is likely:
Question: Has the LiveTemplate client library's morphdom configuration been investigated? The library might need a fix rather than generated code. 2. Dual-Layer DefenseThe PR implements both:
Analysis: This is defense-in-depth, but suggests uncertainty about which layer solves the problem. Consider:
3. ScalabilityAdding state fields to every generated resource for this specific issue doesn't scale well if other similar issues arise. Recommendation: Consider a more general solution:
Sort Options Simplification ✅This Change is Good[[- range , := .Fields]]
[[- if and (eq 0) (eq .GoType "string")]]Benefits:
Edge Case: What if the first field isn't the most relevant for sorting? Consider:
Documentation 📚Missing Documentation
Recommendations
Recommendations SummaryCritical (Fix Before Merge)
High Priority (Should Address)
Medium Priority (Consider)
Nice to Have
VerdictApprove with Changes Required
|
Update golden file to reflect: - Added data-expected-value attribute to sort select - Using $.SortBy for accessing parent scope in range Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Code Review - PR #41This PR addresses two UX improvements for generated LiveTemplate apps. Overall, the changes are well-structured and solve real morphdom sync issues. Below are my findings: ✅ Strengths
🔍 Code Quality & Best PracticesTemplate ChangesFile: internal/kits/system/multi/components/sort.tmpl:15-19 The range variable fix is correct - properly uses dollar-f.Name for the field variable and dollar-.SortBy for parent scope Handler LogicFile: internal/kits/system/multi/templates/resource/handler.go.tmpl:294-312 The debounce logic looks solid:
|
The syncFormValues script was only in template.tmpl.tmpl but the actual generated output uses layout.tmpl from kit components. Added the script to all kit layout.tmpl files (multi, single, simple) to properly fix morphdom form element sync issues. Also updates livetemplate to v0.8.0. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PR Review: Fix Form Element Sync and Simplify Sort OptionsSummaryThis PR addresses two UX issues in generated LiveTemplate apps:
Code Quality ✅Strengths:
Areas for Consideration: 1. Magic Number - 500ms TimeoutLocation: if input.SortBy == state.PrevSortBy && elapsedMs < 500 {Issue: The 500ms threshold is hardcoded. Consider:
2. Potential Race ConditionLocation: The server-side deduplication logic assumes sequential processing. If multiple sort requests arrive concurrently (e.g., user rapidly clicking), the timestamp-based detection might not work reliably since:
Recommendation: Document that this assumes single-threaded request processing per session, or add proper synchronization if concurrent requests are possible. 3. JavaScript Observer PerformanceLocation: observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['data-expected-value'] });Issue: Observing the entire Recommendation:
Potential Bugs
|
Summary
Two related fixes for better UX in generated LiveTemplate apps:
1. Fix morphdom form element sync issues
data-expected-valueattribute to select and input elementssyncFormValuesscript to sync values after morphdom DOM updatesPrevSortBy,LastSortTimefields)Problems solved:
2. Simplify sort dropdown options
Before: Newest First, Title (A-Z), Title (Z-A), Content (A-Z), Content (Z-A), Oldest First
After: Newest First, Title (A-Z), Title (Z-A), Oldest First
Test plan
🤖 Generated with Claude Code