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": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"name": "name",
|
||||
"class": "form-group",
|
||||
"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
|
||||
}
|
||||
|
||||
// Check if this field is required
|
||||
isRequired := r.isFieldRequired(fieldPath)
|
||||
// For nested paths like "company.address.street", we need to check if the final part
|
||||
// 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 schema.Properties != nil && len(*schema.Properties) > 0 {
|
||||
@@ -499,7 +515,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
||||
}
|
||||
|
||||
fields = append(fields, FieldInfo{
|
||||
Name: fieldPath,
|
||||
Name: fieldName,
|
||||
FieldPath: fullPath,
|
||||
Order: order,
|
||||
Schema: schema,
|
||||
@@ -512,7 +528,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
fullPath := propName
|
||||
@@ -520,8 +536,9 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
|
||||
fullPath = parentPath + "." + propName
|
||||
}
|
||||
|
||||
// Check if this nested field is required
|
||||
isRequired := parentRequired || contains(r.Schema.Required, propName)
|
||||
// Check if this field is required at its immediate parent level
|
||||
// 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 propSchema.Properties != nil && len(*propSchema.Properties) > 0 {
|
||||
@@ -558,24 +575,57 @@ func (r *JSONSchemaRenderer) getSchemaAtPath(path string) *jsonschema.Schema {
|
||||
parts := strings.Split(path, ".")
|
||||
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 {
|
||||
fmt.Printf("DEBUG: No properties at part %d ('%s')\n", i, part)
|
||||
return nil
|
||||
}
|
||||
|
||||
if propSchema, exists := (*currentSchema.Properties)[part]; exists {
|
||||
currentSchema = propSchema
|
||||
fmt.Printf("DEBUG: Found part '%s' at level %d\n", part, i)
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG: Successfully navigated to path '%s', found required: %v\n", path, currentSchema.Required)
|
||||
return currentSchema
|
||||
}
|
||||
|
||||
// isFieldRequired checks if a field is required at the current schema level
|
||||
func (r *JSONSchemaRenderer) isFieldRequired(fieldName string) bool {
|
||||
return contains(r.Schema.Required, fieldName)
|
||||
// isFieldRequiredAtPath checks if a field is required at a specific schema path
|
||||
func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string) bool {
|
||||
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
|
||||
@@ -736,10 +786,20 @@ func buildAllAttributesWithValidation(field FieldInfo) string {
|
||||
var builder strings.Builder
|
||||
builder.Grow(512) // Pre-allocate capacity
|
||||
|
||||
// Use the field path as the name attribute for nested fields
|
||||
fieldName := field.FieldPath
|
||||
// Check if UI specifies a custom name, otherwise use field path for nested fields
|
||||
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 == "" {
|
||||
fieldName = field.Name
|
||||
fieldName = field.FieldPath
|
||||
if fieldName == "" {
|
||||
fieldName = field.Name
|
||||
}
|
||||
}
|
||||
|
||||
// Add name attribute
|
||||
@@ -895,184 +955,6 @@ func getPlaceholder(schema *jsonschema.Schema) string {
|
||||
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 {
|
||||
// Check for content in UI first
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
if value, ok := m[key].(string); ok {
|
||||
return value
|
||||
@@ -1217,15 +1055,6 @@ func contains(slice []string, item string) bool {
|
||||
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
|
||||
func (r *JSONSchemaRenderer) renderButtons() string {
|
||||
if r.Schema.Form == nil {
|
||||
|
Reference in New Issue
Block a user