mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-18 23:24:31 +08:00
update
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"element": "input",
|
"element": "input",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
"name": "name",
|
||||||
"class": "form-group",
|
"class": "form-group",
|
||||||
"order": 1
|
"order": 1
|
||||||
}
|
}
|
||||||
|
@@ -1,103 +0,0 @@
|
|||||||
# JSON Schema Validation Examples
|
|
||||||
|
|
||||||
This directory contains comprehensive examples demonstrating all the validation features implemented in the JSON Schema form builder.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
### Schema Examples
|
|
||||||
|
|
||||||
1. **`comprehensive-validation.json`** - Complete example showcasing all JSON Schema 2020-12 validation features:
|
|
||||||
- Personal information with string validations (minLength, maxLength, pattern, format)
|
|
||||||
- Address with conditional validations based on country
|
|
||||||
- Preferences with array validations and enum constraints
|
|
||||||
- Financial information with numeric validations and conditional requirements
|
|
||||||
- Skills array with object validation
|
|
||||||
- Portfolio with anyOf compositions
|
|
||||||
- Terms acceptance with const validation
|
|
||||||
|
|
||||||
2. **`validation-features.json`** - Organized examples by validation category:
|
|
||||||
- **String Validations**: Basic length limits, pattern matching, format validation (email, URL, date), enum selection
|
|
||||||
- **Numeric Validations**: Integer ranges, number with exclusive bounds, multipleOf, range sliders
|
|
||||||
- **Array Validations**: Multi-select with constraints, dynamic object arrays
|
|
||||||
- **Conditional Validations**: if/then/else logic based on user type selection
|
|
||||||
- **Dependent Fields**: dependentRequired, conditional field requirements
|
|
||||||
- **Composition Validations**: anyOf for flexible contact methods, oneOf for exclusive payment methods
|
|
||||||
- **Advanced Features**: const fields, boolean checkboxes, read-only fields, textarea with maxLength
|
|
||||||
|
|
||||||
3. **`complex.json`** - Original company registration form with nested objects and grouped fields
|
|
||||||
|
|
||||||
### Demo Server
|
|
||||||
|
|
||||||
**`main.go`** - HTTP server that renders the schema examples into interactive HTML forms with:
|
|
||||||
- Real-time client-side validation
|
|
||||||
- HTML5 form controls with validation attributes
|
|
||||||
- Tailwind CSS styling
|
|
||||||
- JavaScript validation feedback
|
|
||||||
- Form submission handling
|
|
||||||
|
|
||||||
## Validation Features Demonstrated
|
|
||||||
|
|
||||||
### String Validations
|
|
||||||
- `minLength` / `maxLength` - Length constraints
|
|
||||||
- `pattern` - Regular expression validation
|
|
||||||
- `format` - Built-in formats (email, uri, date, etc.)
|
|
||||||
- `enum` - Predefined value lists
|
|
||||||
|
|
||||||
### Numeric Validations
|
|
||||||
- `minimum` / `maximum` - Inclusive bounds
|
|
||||||
- `exclusiveMinimum` / `exclusiveMaximum` - Exclusive bounds
|
|
||||||
- `multipleOf` - Value must be multiple of specified number
|
|
||||||
- Integer vs Number types
|
|
||||||
|
|
||||||
### Array Validations
|
|
||||||
- `minItems` / `maxItems` - Size constraints
|
|
||||||
- `uniqueItems` - No duplicate values
|
|
||||||
- `items` - Schema for array elements
|
|
||||||
- Multi-select form controls
|
|
||||||
|
|
||||||
### Object Validations
|
|
||||||
- `required` - Required properties
|
|
||||||
- `dependentRequired` - Fields required based on other fields
|
|
||||||
- `dependentSchemas` - Schema changes based on other fields
|
|
||||||
- Nested object structures
|
|
||||||
|
|
||||||
### Conditional Logic
|
|
||||||
- `if` / `then` / `else` - Conditional schema application
|
|
||||||
- `allOf` - Must satisfy all sub-schemas
|
|
||||||
- `anyOf` - Must satisfy at least one sub-schema
|
|
||||||
- `oneOf` - Must satisfy exactly one sub-schema
|
|
||||||
|
|
||||||
### Advanced Features
|
|
||||||
- `const` - Constant values
|
|
||||||
- `default` - Default values
|
|
||||||
- `readOnly` - Read-only fields
|
|
||||||
- Boolean validation
|
|
||||||
- HTML form grouping and styling
|
|
||||||
|
|
||||||
## Running the Demo
|
|
||||||
|
|
||||||
1. Navigate to the validation-demo directory:
|
|
||||||
```bash
|
|
||||||
cd examples/validation-demo
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run the server:
|
|
||||||
```bash
|
|
||||||
go run main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Open your browser to `http://localhost:8080`
|
|
||||||
|
|
||||||
4. Explore the different validation examples:
|
|
||||||
- Comprehensive Validation Demo - Complete real-world form
|
|
||||||
- Validation Features Demo - Organized by validation type
|
|
||||||
- Complex Form Demo - Original nested object example
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
The validation system is implemented in:
|
|
||||||
- `/renderer/validation.go` - Core validation logic and HTML attribute generation
|
|
||||||
- `/renderer/schema.go` - Form rendering with validation integration
|
|
||||||
- Client-side JavaScript - Real-time validation feedback
|
|
||||||
|
|
||||||
All examples demonstrate both server-side schema validation and client-side HTML5 validation attributes working together.
|
|
@@ -1,173 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/oarkflow/jsonschema"
|
|
||||||
"github.com/oarkflow/mq/renderer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Setup routes
|
|
||||||
http.HandleFunc("/", indexHandler)
|
|
||||||
http.HandleFunc("/comprehensive", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
renderFormHandler(w, r, "comprehensive-validation.json", "Comprehensive Validation Demo")
|
|
||||||
})
|
|
||||||
http.HandleFunc("/features", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
renderFormHandler(w, r, "validation-features.json", "Validation Features Demo")
|
|
||||||
})
|
|
||||||
http.HandleFunc("/complex", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
renderFormHandler(w, r, "complex.json", "Complex Form Demo")
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Println("Server starting on :8080")
|
|
||||||
fmt.Println("Available examples:")
|
|
||||||
fmt.Println(" - http://localhost:8080/comprehensive - Comprehensive validation demo")
|
|
||||||
fmt.Println(" - http://localhost:8080/features - Validation features demo")
|
|
||||||
fmt.Println(" - http://localhost:8080/complex - Complex form demo")
|
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderFormHandler(w http.ResponseWriter, r *http.Request, schemaFile, title string) {
|
|
||||||
// Load and compile schema
|
|
||||||
schemaData, err := os.ReadFile(schemaFile)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("Error reading schema file: %v", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JSON schema
|
|
||||||
var schemaMap map[string]interface{}
|
|
||||||
if err := json.Unmarshal(schemaData, &schemaMap); err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("Error parsing JSON schema: %v", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile schema
|
|
||||||
compiler := jsonschema.NewCompiler()
|
|
||||||
schema, err := compiler.Compile(schemaData)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("Error compiling schema: %v", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create renderer with basic template
|
|
||||||
basicTemplate := `
|
|
||||||
<form {{if .Form.Action}}action="{{.Form.Action}}"{{end}} {{if .Form.Method}}method="{{.Form.Method}}"{{end}} {{if .Form.Class}}class="{{.Form.Class}}"{{end}}>
|
|
||||||
{{.FieldsHTML}}
|
|
||||||
<div class="form-buttons">
|
|
||||||
{{.ButtonsHTML}}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
`
|
|
||||||
|
|
||||||
renderer := renderer.NewJSONSchemaRenderer(schema, basicTemplate)
|
|
||||||
|
|
||||||
// Render form with empty data
|
|
||||||
html, err := renderer.RenderFields(map[string]any{})
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("Error rendering form: %v", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create complete HTML page
|
|
||||||
fullHTML := createFullHTMLPage(title, html)
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
w.Write([]byte(fullHTML))
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
html := "<!DOCTYPE html>" +
|
|
||||||
"<html>" +
|
|
||||||
"<head>" +
|
|
||||||
"<title>JSON Schema Form Builder - Validation Examples</title>" +
|
|
||||||
"<style>" +
|
|
||||||
"body { font-family: Arial, sans-serif; margin: 40px; }" +
|
|
||||||
"h1 { color: #333; }" +
|
|
||||||
".example-list { list-style: none; padding: 0; }" +
|
|
||||||
".example-list li { margin: 15px 0; }" +
|
|
||||||
".example-list a { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; }" +
|
|
||||||
".example-list a:hover { background: #0056b3; }" +
|
|
||||||
".description { color: #666; margin-top: 5px; }" +
|
|
||||||
"</style>" +
|
|
||||||
"</head>" +
|
|
||||||
"<body>" +
|
|
||||||
"<h1>JSON Schema Form Builder - Validation Examples</h1>" +
|
|
||||||
"<p>This demo showcases comprehensive JSON Schema 2020-12 validation support with:</p>" +
|
|
||||||
"<ul>" +
|
|
||||||
"<li>String validations (minLength, maxLength, pattern, format)</li>" +
|
|
||||||
"<li>Numeric validations (minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf)</li>" +
|
|
||||||
"<li>Array validations (minItems, maxItems, uniqueItems)</li>" +
|
|
||||||
"<li>Conditional validations (if/then/else, allOf, anyOf, oneOf)</li>" +
|
|
||||||
"<li>Dependent validations (dependentRequired, dependentSchemas)</li>" +
|
|
||||||
"<li>Client-side and server-side validation</li>" +
|
|
||||||
"<li>Advanced HTML form generation with validation attributes</li>" +
|
|
||||||
"</ul>" +
|
|
||||||
"<h2>Available Examples:</h2>" +
|
|
||||||
"<ul class=\"example-list\">" +
|
|
||||||
"<li>" +
|
|
||||||
"<a href=\"/comprehensive\">Comprehensive Validation Demo</a>" +
|
|
||||||
"<div class=\"description\">Complete example with personal info, address, preferences, financial data, skills, portfolio, and complex conditional logic</div>" +
|
|
||||||
"</li>" +
|
|
||||||
"<li>" +
|
|
||||||
"<a href=\"/features\">Validation Features Demo</a>" +
|
|
||||||
"<div class=\"description\">Focused examples of specific validation features organized by category</div>" +
|
|
||||||
"</li>" +
|
|
||||||
"<li>" +
|
|
||||||
"<a href=\"/complex\">Complex Form Demo</a>" +
|
|
||||||
"<div class=\"description\">Original company registration form with nested objects and grouped fields</div>" +
|
|
||||||
"</li>" +
|
|
||||||
"</ul>" +
|
|
||||||
"</body>" +
|
|
||||||
"</html>"
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
w.Write([]byte(html))
|
|
||||||
}
|
|
||||||
|
|
||||||
func createFullHTMLPage(title, formHTML string) string {
|
|
||||||
template := "<!DOCTYPE html>" +
|
|
||||||
"<html>" +
|
|
||||||
"<head>" +
|
|
||||||
"<title>" + title + "</title>" +
|
|
||||||
"<meta charset=\"utf-8\">" +
|
|
||||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" +
|
|
||||||
"<script src=\"https://cdn.tailwindcss.com\"></script>" +
|
|
||||||
"<style>" +
|
|
||||||
".form-group { margin-bottom: 1rem; }" +
|
|
||||||
".form-group label { display: block; margin-bottom: 0.25rem; font-weight: 600; }" +
|
|
||||||
".form-group input, .form-group select, .form-group textarea { width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem; }" +
|
|
||||||
".btn { padding: 0.5rem 1rem; border-radius: 0.375rem; font-weight: 600; cursor: pointer; border: none; margin-right: 0.5rem; }" +
|
|
||||||
".btn-primary { background-color: #3b82f6; color: white; }" +
|
|
||||||
".validation-error { color: #dc2626; font-size: 0.875rem; margin-top: 0.25rem; }" +
|
|
||||||
".back-link { display: inline-block; margin-bottom: 2rem; color: #3b82f6; text-decoration: none; }" +
|
|
||||||
"</style>" +
|
|
||||||
"</head>" +
|
|
||||||
"<body class=\"bg-gray-50 min-h-screen py-8\">" +
|
|
||||||
"<div class=\"max-w-4xl mx-auto px-4\">" +
|
|
||||||
"<a href=\"/\" class=\"back-link\">← Back to Examples</a>" +
|
|
||||||
"<div class=\"bg-white rounded-lg shadow-lg p-8\">" +
|
|
||||||
"<h1 class=\"text-3xl font-bold text-gray-900 mb-6\">" + title + "</h1>" +
|
|
||||||
"<p class=\"text-gray-600 mb-8\">This form demonstrates comprehensive JSON Schema validation.</p>" +
|
|
||||||
formHTML +
|
|
||||||
"</div></div>" +
|
|
||||||
"<script>" +
|
|
||||||
"document.addEventListener('DOMContentLoaded', function() {" +
|
|
||||||
"const form = document.querySelector('form');" +
|
|
||||||
"if (form) {" +
|
|
||||||
"form.addEventListener('submit', function(e) {" +
|
|
||||||
"e.preventDefault();" +
|
|
||||||
"alert('Form validation demo - would submit data in real application');" +
|
|
||||||
"});" +
|
|
||||||
"}" +
|
|
||||||
"});" +
|
|
||||||
"</script>" +
|
|
||||||
"</body></html>"
|
|
||||||
|
|
||||||
return template
|
|
||||||
}
|
|
@@ -481,8 +481,24 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
|||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this field is required
|
// For nested paths like "company.address.street", we need to check if the final part
|
||||||
isRequired := r.isFieldRequired(fieldPath)
|
// is required in its immediate parent's schema
|
||||||
|
var fieldName string
|
||||||
|
var parentSchemaPath string
|
||||||
|
|
||||||
|
pathParts := strings.Split(fieldPath, ".")
|
||||||
|
if len(pathParts) > 1 {
|
||||||
|
// Extract the field name (last part) and parent path (all but last)
|
||||||
|
fieldName = pathParts[len(pathParts)-1]
|
||||||
|
parentSchemaPath = strings.Join(pathParts[:len(pathParts)-1], ".")
|
||||||
|
} else {
|
||||||
|
// Single level field
|
||||||
|
fieldName = fieldPath
|
||||||
|
parentSchemaPath = parentPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this field is required at the parent level
|
||||||
|
isRequired := r.isFieldRequiredAtPath(fieldName, parentSchemaPath)
|
||||||
|
|
||||||
// If this schema has properties, it's a nested object
|
// If this schema has properties, it's a nested object
|
||||||
if schema.Properties != nil && len(*schema.Properties) > 0 {
|
if schema.Properties != nil && len(*schema.Properties) > 0 {
|
||||||
@@ -499,7 +515,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fields = append(fields, FieldInfo{
|
fields = append(fields, FieldInfo{
|
||||||
Name: fieldPath,
|
Name: fieldName,
|
||||||
FieldPath: fullPath,
|
FieldPath: fullPath,
|
||||||
Order: order,
|
Order: order,
|
||||||
Schema: schema,
|
Schema: schema,
|
||||||
@@ -512,7 +528,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractFieldsFromNestedSchema processes nested schema properties
|
// extractFieldsFromNestedSchema processes nested schema properties
|
||||||
func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, parentRequired bool) []FieldInfo {
|
func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, _ bool) []FieldInfo {
|
||||||
var fields []FieldInfo
|
var fields []FieldInfo
|
||||||
|
|
||||||
fullPath := propName
|
fullPath := propName
|
||||||
@@ -520,8 +536,9 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
|
|||||||
fullPath = parentPath + "." + propName
|
fullPath = parentPath + "." + propName
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this nested field is required
|
// Check if this field is required at its immediate parent level
|
||||||
isRequired := parentRequired || contains(r.Schema.Required, propName)
|
// The parent schema path is the current parentPath, not one level up
|
||||||
|
isRequired := r.isFieldRequiredAtPath(propName, parentPath)
|
||||||
|
|
||||||
// If this property has nested properties, recurse
|
// If this property has nested properties, recurse
|
||||||
if propSchema.Properties != nil && len(*propSchema.Properties) > 0 {
|
if propSchema.Properties != nil && len(*propSchema.Properties) > 0 {
|
||||||
@@ -558,24 +575,57 @@ func (r *JSONSchemaRenderer) getSchemaAtPath(path string) *jsonschema.Schema {
|
|||||||
parts := strings.Split(path, ".")
|
parts := strings.Split(path, ".")
|
||||||
currentSchema := r.Schema
|
currentSchema := r.Schema
|
||||||
|
|
||||||
for _, part := range parts {
|
fmt.Printf("DEBUG: Navigating to path '%s', parts: %v\n", path, parts)
|
||||||
|
|
||||||
|
for i, part := range parts {
|
||||||
if currentSchema.Properties == nil {
|
if currentSchema.Properties == nil {
|
||||||
|
fmt.Printf("DEBUG: No properties at part %d ('%s')\n", i, part)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if propSchema, exists := (*currentSchema.Properties)[part]; exists {
|
if propSchema, exists := (*currentSchema.Properties)[part]; exists {
|
||||||
currentSchema = propSchema
|
currentSchema = propSchema
|
||||||
|
fmt.Printf("DEBUG: Found part '%s' at level %d\n", part, i)
|
||||||
} else {
|
} else {
|
||||||
|
fmt.Printf("DEBUG: Part '%s' not found at level %d. Available: %v\n", part, i, func() []string {
|
||||||
|
var keys []string
|
||||||
|
for k := range *currentSchema.Properties {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("DEBUG: Successfully navigated to path '%s', found required: %v\n", path, currentSchema.Required)
|
||||||
return currentSchema
|
return currentSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
// isFieldRequired checks if a field is required at the current schema level
|
// isFieldRequiredAtPath checks if a field is required at a specific schema path
|
||||||
func (r *JSONSchemaRenderer) isFieldRequired(fieldName string) bool {
|
func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string) bool {
|
||||||
return contains(r.Schema.Required, fieldName)
|
var schema *jsonschema.Schema
|
||||||
|
|
||||||
|
if schemaPath == "" {
|
||||||
|
// Check at root level
|
||||||
|
schema = r.Schema
|
||||||
|
} else {
|
||||||
|
// Navigate to the schema at the given path
|
||||||
|
schema = r.getSchemaAtPath(schemaPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema == nil {
|
||||||
|
// Debug: schema not found
|
||||||
|
fmt.Printf("DEBUG: Schema not found for path '%s'\n", schemaPath)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isRequired := contains(schema.Required, fieldName)
|
||||||
|
// Debug: show what we're checking
|
||||||
|
fmt.Printf("DEBUG: Checking if '%s' is required in schema at path '%s'. Required fields: %v. Result: %v\n",
|
||||||
|
fieldName, schemaPath, schema.Required, isRequired)
|
||||||
|
|
||||||
|
return isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderGroup generates HTML for a single group
|
// renderGroup generates HTML for a single group
|
||||||
@@ -736,10 +786,20 @@ func buildAllAttributesWithValidation(field FieldInfo) string {
|
|||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.Grow(512) // Pre-allocate capacity
|
builder.Grow(512) // Pre-allocate capacity
|
||||||
|
|
||||||
// Use the field path as the name attribute for nested fields
|
// Check if UI specifies a custom name, otherwise use field path for nested fields
|
||||||
fieldName := field.FieldPath
|
var fieldName string
|
||||||
|
if field.Schema.UI != nil {
|
||||||
|
if customName, exists := field.Schema.UI["name"].(string); exists && customName != "" {
|
||||||
|
fieldName = customName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to field path or field name
|
||||||
if fieldName == "" {
|
if fieldName == "" {
|
||||||
fieldName = field.Name
|
fieldName = field.FieldPath
|
||||||
|
if fieldName == "" {
|
||||||
|
fieldName = field.Name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add name attribute
|
// Add name attribute
|
||||||
@@ -895,184 +955,6 @@ func getPlaceholder(schema *jsonschema.Schema) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderFieldOptimized uses pre-compiled templates and optimized attribute building
|
|
||||||
func renderFieldOptimized(field FieldInfo) string {
|
|
||||||
// Use the new comprehensive rendering logic
|
|
||||||
return renderField(field)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildAllAttributes(field FieldInfo) string {
|
|
||||||
var attributes []string
|
|
||||||
|
|
||||||
// Use the field path as the name attribute for nested fields
|
|
||||||
fieldName := field.FieldPath
|
|
||||||
if fieldName == "" {
|
|
||||||
fieldName = field.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add name attribute
|
|
||||||
attributes = append(attributes, fmt.Sprintf(`name="%s"`, fieldName))
|
|
||||||
|
|
||||||
// Add standard attributes from UI
|
|
||||||
if field.Schema.UI != nil {
|
|
||||||
element, _ := field.Schema.UI["element"].(string)
|
|
||||||
for _, attr := range standardAttrs {
|
|
||||||
if attr == "name" {
|
|
||||||
continue // Already handled above
|
|
||||||
}
|
|
||||||
if value, exists := field.Schema.UI[attr]; exists {
|
|
||||||
if attr == "class" && value == "" {
|
|
||||||
continue // Skip empty class
|
|
||||||
}
|
|
||||||
// For select, input, textarea, add class to element itself
|
|
||||||
if attr == "class" && (element == "select" || element == "input" || element == "textarea") {
|
|
||||||
attributes = append(attributes, fmt.Sprintf(`class="%v"`, value))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// For other elements, do not add class here (it will be handled in the wrapper div)
|
|
||||||
if attr == "class" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, attr, value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add data-* and aria-* attributes
|
|
||||||
for key, value := range field.Schema.UI {
|
|
||||||
if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") {
|
|
||||||
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle required field
|
|
||||||
if field.IsRequired {
|
|
||||||
if !containsAttribute(attributes, "required=") {
|
|
||||||
attributes = append(attributes, `required="required"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle input type based on field type
|
|
||||||
element, _ := field.Schema.UI["element"].(string)
|
|
||||||
if element == "input" {
|
|
||||||
if inputType := getInputType(field.Schema); inputType != "" {
|
|
||||||
if !containsAttribute(attributes, "type=") {
|
|
||||||
attributes = append(attributes, fmt.Sprintf(`type="%s"`, inputType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(attributes, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildAllAttributesOptimized uses string builder for better performance
|
|
||||||
func buildAllAttributesOptimized(field FieldInfo) string {
|
|
||||||
var builder strings.Builder
|
|
||||||
builder.Grow(256) // Pre-allocate reasonable capacity
|
|
||||||
|
|
||||||
// Use the field path as the name attribute for nested fields
|
|
||||||
fieldName := field.FieldPath
|
|
||||||
if fieldName == "" {
|
|
||||||
fieldName = field.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add name attribute
|
|
||||||
builder.WriteString(`name="`)
|
|
||||||
builder.WriteString(fieldName)
|
|
||||||
builder.WriteString(`"`)
|
|
||||||
|
|
||||||
// Add standard attributes from UI
|
|
||||||
if field.Schema.UI != nil {
|
|
||||||
element, _ := field.Schema.UI["element"].(string)
|
|
||||||
for _, attr := range standardAttrs {
|
|
||||||
if attr == "name" {
|
|
||||||
continue // Already handled above
|
|
||||||
}
|
|
||||||
if value, exists := field.Schema.UI[attr]; exists {
|
|
||||||
if attr == "class" && value == "" {
|
|
||||||
continue // Skip empty class
|
|
||||||
}
|
|
||||||
// For select, input, textarea, add class to element itself
|
|
||||||
if attr == "class" && (element == "select" || element == "input" || element == "textarea") {
|
|
||||||
builder.WriteString(` class="`)
|
|
||||||
builder.WriteString(fmt.Sprintf("%v", value))
|
|
||||||
builder.WriteString(`"`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// For other elements, do not add class here (it will be handled in the wrapper div)
|
|
||||||
if attr == "class" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
builder.WriteString(` `)
|
|
||||||
builder.WriteString(attr)
|
|
||||||
builder.WriteString(`="`)
|
|
||||||
builder.WriteString(fmt.Sprintf("%v", value))
|
|
||||||
builder.WriteString(`"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add data-* and aria-* attributes
|
|
||||||
for key, value := range field.Schema.UI {
|
|
||||||
if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") {
|
|
||||||
builder.WriteString(` `)
|
|
||||||
builder.WriteString(key)
|
|
||||||
builder.WriteString(`="`)
|
|
||||||
builder.WriteString(fmt.Sprintf("%v", value))
|
|
||||||
builder.WriteString(`"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle required field
|
|
||||||
if field.IsRequired {
|
|
||||||
currentAttrs := builder.String()
|
|
||||||
if !strings.Contains(currentAttrs, "required=") {
|
|
||||||
builder.WriteString(` required="required"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle input type based on field type
|
|
||||||
element, _ := field.Schema.UI["element"].(string)
|
|
||||||
if element == "input" {
|
|
||||||
if inputType := getInputType(field.Schema); inputType != "" {
|
|
||||||
currentAttrs := builder.String()
|
|
||||||
if !strings.Contains(currentAttrs, "type=") {
|
|
||||||
builder.WriteString(` type="`)
|
|
||||||
builder.WriteString(inputType)
|
|
||||||
builder.WriteString(`"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getInputType(schema *jsonschema.Schema) string {
|
|
||||||
// Check UI type first
|
|
||||||
if schema.UI != nil {
|
|
||||||
if uiType, ok := schema.UI["type"].(string); ok {
|
|
||||||
return uiType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var typeStr string
|
|
||||||
if len(schema.Type) > 0 {
|
|
||||||
typeStr = schema.Type[0]
|
|
||||||
} else {
|
|
||||||
typeStr = "string"
|
|
||||||
}
|
|
||||||
// Map schema types to input types
|
|
||||||
switch typeStr {
|
|
||||||
case "string", "text":
|
|
||||||
return "text"
|
|
||||||
case "number", "integer":
|
|
||||||
return "number"
|
|
||||||
case "boolean":
|
|
||||||
return "checkbox"
|
|
||||||
default:
|
|
||||||
return "text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFieldContent(field FieldInfo) string {
|
func getFieldContent(field FieldInfo) string {
|
||||||
// Check for content in UI first
|
// Check for content in UI first
|
||||||
if field.Schema.UI != nil {
|
if field.Schema.UI != nil {
|
||||||
@@ -1157,50 +1039,6 @@ func generateLabel(field FieldInfo) string {
|
|||||||
return fmt.Sprintf(`<label for="%s">%s%s</label>`, fieldName, title, requiredSpan)
|
return fmt.Sprintf(`<label for="%s">%s%s</label>`, fieldName, title, requiredSpan)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateOptions(ui map[string]interface{}) string {
|
|
||||||
if ui == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
options, ok := ui["options"].([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var optionsHTML strings.Builder
|
|
||||||
for _, option := range options {
|
|
||||||
if optionMap, ok := option.(map[string]interface{}); ok {
|
|
||||||
// Complex option with attributes
|
|
||||||
value := getMapValue(optionMap, "value", "")
|
|
||||||
text := getMapValue(optionMap, "text", value)
|
|
||||||
selected := ""
|
|
||||||
if isSelected, ok := optionMap["selected"].(bool); ok && isSelected {
|
|
||||||
selected = ` selected="selected"`
|
|
||||||
}
|
|
||||||
disabled := ""
|
|
||||||
if isDisabled, ok := optionMap["disabled"].(bool); ok && isDisabled {
|
|
||||||
disabled = ` disabled="disabled"`
|
|
||||||
}
|
|
||||||
optionsHTML.WriteString(fmt.Sprintf(`<option value="%s"%s%s>%s</option>`,
|
|
||||||
value, selected, disabled, text))
|
|
||||||
} else {
|
|
||||||
// Simple option (just value)
|
|
||||||
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, option, option))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return optionsHTML.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUIValue(ui map[string]interface{}, key string) string {
|
|
||||||
if ui == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if value, ok := ui[key].(string); ok {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMapValue(m map[string]interface{}, key, defaultValue string) string {
|
func getMapValue(m map[string]interface{}, key, defaultValue string) string {
|
||||||
if value, ok := m[key].(string); ok {
|
if value, ok := m[key].(string); ok {
|
||||||
return value
|
return value
|
||||||
@@ -1217,15 +1055,6 @@ func contains(slice []string, item string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsAttribute(attributes []string, prefix string) bool {
|
|
||||||
for _, attr := range attributes {
|
|
||||||
if strings.HasPrefix(attr, prefix) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderButtons generates HTML for form buttons
|
// renderButtons generates HTML for form buttons
|
||||||
func (r *JSONSchemaRenderer) renderButtons() string {
|
func (r *JSONSchemaRenderer) renderButtons() string {
|
||||||
if r.Schema.Form == nil {
|
if r.Schema.Form == nil {
|
||||||
|
Reference in New Issue
Block a user