Skip to content

Commit

Permalink
✨ Proper submission form data
Browse files Browse the repository at this point in the history
  • Loading branch information
wesen committed Feb 26, 2025
1 parent bc500bc commit 9256bac
Show file tree
Hide file tree
Showing 6 changed files with 537 additions and 233 deletions.
64 changes: 51 additions & 13 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# Enhanced UI Action Handling

Improved the UI action system to focus on data-relevant events and provide better form submission data:
- Enhanced form submission to include complete form data in the action payload
- Implemented smart logging that prioritizes data-relevant events (clicked, changed, submitted)
- Added detailed form data logging for form submissions
- Used INFO level for important events and DEBUG level for less important ones
- Improved checkbox handling in form data collection
- Maintained backward compatibility with existing event system

# UI Component Action Endpoint

Added a generic event handler system for UI components that sends events to a REST endpoint:
- Created new `/api/ui-action` endpoint to receive component actions
- Added JavaScript function to send component actions to the server
- Updated all UI components to use the new action system
- Actions include component ID, action type, and optional data
- Server logs all actions for monitoring and debugging
- Maintained backward compatibility with existing console logging

# Added GitHub Pull Request Listing Command

Added a new command to list pull requests from GitHub repositories:
Expand Down Expand Up @@ -1310,20 +1330,38 @@ Fixed SSE extension integration to use the correct attributes and script:
- Fixed SSE connection syntax to use proper format
- Added SRI integrity hashes for security

# Changelog
## Fixed Form Submission Data Collection

## HTMX SSE Extension Update
Fixed SSE extension integration to use the correct attributes and script:
Fixed an issue where form input values weren't being properly collected during form submission:
- Added explicit collection of all input fields by ID during form submission
- Ensured input elements have name attributes matching their IDs
- Simplified form submission handling by consolidating data collection logic
- Added additional logging for form submission data
- Fixed email input value collection in subscription forms

- Updated SSE extension script to use htmx-ext-sse@2.2.2 package
- Changed SSE attributes to use sse-* prefix (from hx-sse-*)
- Fixed SSE connection syntax to use proper format
- Added SRI integrity hashes for security
# Enhanced UI Action Handling

## Server-Sent Events (SSE) Support for UI Updates
Added SSE support to enable real-time UI updates through server-sent events. This allows components to be updated individually without full page reloads.
Improved the UI action system to focus on data-relevant events and provide better form submission data:
- Enhanced form submission to include complete form data in the action payload
- Implemented smart logging that prioritizes data-relevant events (clicked, changed, submitted)
- Added detailed form data logging for form submissions
- Used INFO level for important events and DEBUG level for less important ones
- Improved checkbox handling in form data collection
- Maintained backward compatibility with existing event system

- Added SSE extension script to base template
- Added SSE connection and swap targets to page template
- Added individual component swap targets for granular updates
- Wrapped components in div containers with unique IDs for targeted updates
# UI Component Action Endpoint

Added a generic event handler system for UI components that sends events to a REST endpoint:
- Created new `/api/ui-action` endpoint to receive component actions
- Added JavaScript function to send component actions to the server
- Updated all UI components to use the new action system
- Actions include component ID, action type, and optional data
- Server logs all actions for monitoring and debugging
- Maintained backward compatibility with existing console logging

# Added GitHub Pull Request Listing Command

Added a new command to list pull requests from GitHub repositories:
- Created list-github-pull-requests command with support for filtering by state, assignee, author, labels, and base branch
- Added draft PR filtering support
- Included comprehensive JSON output options for PR-specific fields
73 changes: 70 additions & 3 deletions cmd/ui-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ func NewServer(dir string) (*Server, error) {
// Set up UI update endpoint
s.mux.Handle("/api/ui-update", s.handleUIUpdate())

// Set up UI action endpoint
s.mux.Handle("/api/ui-action", s.handleUIAction())

// Set up UI update page
s.mux.HandleFunc("/ui", s.handleUIUpdatePage())

Expand Down Expand Up @@ -421,12 +424,76 @@ func (s *Server) handleUIUpdate() http.HandlerFunc {
}
}

// handleUIAction handles POST requests to /api/ui-action
func (s *Server) handleUIAction() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Only accept POST requests
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

// Parse JSON body
var action struct {
ComponentID string `json:"componentId"`
Action string `json:"action"`
Data map[string]interface{} `json:"data"`
}

if err := json.NewDecoder(r.Body).Decode(&action); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}

// Determine if this is an important event to log at INFO level
isImportantEvent := false
switch action.Action {
case "clicked", "changed", "submitted":
isImportantEvent = true
}

// Log the action at appropriate level
logger := log.Debug()
if isImportantEvent {
logger = log.Info()
}

// Create log entry with component and action info
logger = logger.
Str("componentId", action.ComponentID).
Str("action", action.Action)

// Add data to log if it exists and is relevant
if len(action.Data) > 0 {
// For form submissions, log the form data in detail
if action.Action == "submitted" && action.Data["formData"] != nil {
logger = logger.Interface("formData", action.Data["formData"])
} else if action.Action == "changed" {
// For changed events, log the new value
logger = logger.Interface("data", action.Data)
} else {
// For other events, just log that data exists
logger = logger.Bool("hasData", true)
}
}

// Output the log message
logger.Msg("UI action received")

// Return success response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(map[string]string{"status": "success"})
if err != nil {
http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError)
}
}
}

// handleUIUpdatePage renders the UI update page
func (s *Server) handleUIUpdatePage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
component := uiUpdateTemplate()
if err := component.Render(r.Context(), w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
_ = component.Render(r.Context(), w)
}
}
95 changes: 66 additions & 29 deletions cmd/ui-server/templates.templ
Original file line number Diff line number Diff line change
Expand Up @@ -47,38 +47,64 @@ templ base(title string) {
}
}

function logFormSubmit(formId, formData) {
const data = {};
console.log('Form elements:', formData.elements);
function sendUIAction(componentId, action, data = {}) {
logToConsole(`Component ${componentId} ${action}`);

// Get all form elements
Array.from(formData.elements).forEach(element => {
console.log('Processing element:', element.tagName, element.type, element.id);
// If this is a form submission, collect all form data
if (action === 'submitted' && document.getElementById(componentId)) {
const form = document.getElementById(componentId);
const formData = new FormData(form);
const formValues = {};

if (element.tagName === 'BUTTON') return; // Skip buttons, we handle them separately
// Convert FormData to a plain object
for (const [key, value] of formData.entries()) {
formValues[key] = value;
}

// Add checkbox values (FormData doesn't include unchecked boxes)
form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
formValues[checkbox.id] = checkbox.checked;
});

// Add all input values by ID (in case name attributes are missing)
form.querySelectorAll('input:not([type="checkbox"]), textarea, select').forEach(input => {
if (input.id) {
formValues[input.id] = input.value;
}
});

if (element.type === 'checkbox') {
data[element.id] = element.checked;
} else if (element.id && element.value) {
data[element.id] = element.value;
// Add the clicked button info if available
if (form._lastClickedButton) {
formValues['_clicked_button'] = form._lastClickedButton;
}
});

// Add the clicked button info if available
if (formData._lastClickedButton) {
data['_clicked_button'] = formData._lastClickedButton;

// Merge with any existing data
data = { ...data, formData: formValues };

// Log form submission for debugging
console.log('Form submission data:', formValues);
logToConsole(`Form ${componentId} submitted with data`);
}

console.log('Collected data:', data);

const yaml = Object.entries(data)
.map(([key, value]) => {
const valueStr = typeof value === 'boolean' ? value : `"${value}"`;
return ` ${key}: ${valueStr}`;
})
.join('\n');

logToConsole(`Form ${formId} submitted with data:\n\`\`\`yaml\n${yaml}\n\`\`\``);
fetch('/api/ui-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
componentId: componentId,
action: action,
data: data
})
})
.then(response => response.json())
.then(data => {
console.log('Action response:', data);
})
.catch(error => {
console.error('Error sending action:', error);
logToConsole(`Error sending action: ${error.message}`);
});
}
</script>
<style>
Expand Down Expand Up @@ -237,7 +263,7 @@ templ renderComponent(typ string, props map[string]interface{}) {
<div id={ fmt.Sprintf("component-%s", id) }>
<button
id={ id }
data-hx-on:click={ fmt.Sprintf("logToConsole('%s clicked')", id) }
data-hx-on:click={ fmt.Sprintf("sendUIAction('%s', 'clicked')", id) }
if disabled, ok := props["disabled"].(bool); ok && disabled {
disabled="disabled"
}
Expand All @@ -260,6 +286,7 @@ templ renderComponent(typ string, props map[string]interface{}) {
<div id={ fmt.Sprintf("component-%s", id) }>
<h1
id={ id }
data-hx-on:click={ fmt.Sprintf("sendUIAction('%s', 'clicked')", id) }
>
if content, ok := props["content"].(string); ok {
{ content }
Expand All @@ -272,6 +299,7 @@ templ renderComponent(typ string, props map[string]interface{}) {
<div id={ fmt.Sprintf("component-%s", id) }>
<p
id={ id }
data-hx-on:click={ fmt.Sprintf("sendUIAction('%s', 'clicked')", id) }
>
if content, ok := props["content"].(string); ok {
{ content }
Expand All @@ -284,6 +312,9 @@ templ renderComponent(typ string, props map[string]interface{}) {
<div id={ fmt.Sprintf("component-%s", id) }>
<input
id={ id }
data-hx-on:change={ fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id) }
data-hx-on:focus={ fmt.Sprintf("sendUIAction('%s', 'focused')", id) }
data-hx-on:blur={ fmt.Sprintf("sendUIAction('%s', 'blurred')", id) }
if typ, ok := props["type"].(string); ok {
type={ typ }
}
Expand All @@ -296,6 +327,9 @@ templ renderComponent(typ string, props map[string]interface{}) {
if required, ok := props["required"].(bool); ok && required {
required="required"
}
if name, ok := props["id"].(string); ok {
name={ name }
}
class="form-control"
/>
</div>
Expand All @@ -305,6 +339,9 @@ templ renderComponent(typ string, props map[string]interface{}) {
<div id={ fmt.Sprintf("component-%s", id) }>
<textarea
id={ id }
data-hx-on:change={ fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id) }
data-hx-on:focus={ fmt.Sprintf("sendUIAction('%s', 'focused')", id) }
data-hx-on:blur={ fmt.Sprintf("sendUIAction('%s', 'blurred')", id) }
if rows, ok := props["rows"].(int); ok {
rows={ fmt.Sprint(rows) }
}
Expand All @@ -329,7 +366,7 @@ templ renderComponent(typ string, props map[string]interface{}) {
<input
type="checkbox"
id={ id }
data-hx-on:change={ fmt.Sprintf("logToConsole('%s ' + (this.checked ? 'checked' : 'unchecked'))", id) }
data-hx-on:change={ fmt.Sprintf("sendUIAction('%s', 'changed', {checked: this.checked})", id) }
if name, ok := props["name"].(string); ok {
name={ name }
}
Expand Down Expand Up @@ -397,7 +434,7 @@ templ renderComponent(typ string, props map[string]interface{}) {
<div id={ fmt.Sprintf("component-%s", id) }>
<form
id={ id }
data-hx-on:submit={ fmt.Sprintf("event.preventDefault(); logFormSubmit('%s', this)", id) }
data-hx-on:submit={ fmt.Sprintf("event.preventDefault(); sendUIAction('%s', 'submitted')", id) }
class="needs-validation"
novalidate
>
Expand Down
Loading

0 comments on commit 9256bac

Please sign in to comment.