diff --git a/examples/app/jsonschema_renderer.go b/examples/app/jsonschema_renderer.go new file mode 100644 index 0000000..c128696 --- /dev/null +++ b/examples/app/jsonschema_renderer.go @@ -0,0 +1,148 @@ +package main + +import ( + "bytes" + "fmt" + "html/template" + "sort" +) + +// FieldInfo represents metadata for a field extracted from JSONSchema +type FieldInfo struct { + Name string + Order int + Definition map[string]interface{} +} + +// JSONSchemaRenderer is responsible for rendering HTML fields based on JSONSchema +type JSONSchemaRenderer struct { + Schema map[string]interface{} + HTMLLayout string +} + +// NewJSONSchemaRenderer creates a new instance of JSONSchemaRenderer +func NewJSONSchemaRenderer(schema map[string]interface{}, htmlLayout string) *JSONSchemaRenderer { + return &JSONSchemaRenderer{ + Schema: schema, + HTMLLayout: htmlLayout, + } +} + +// RenderFields generates HTML for fields based on the JSONSchema +func (r *JSONSchemaRenderer) RenderFields() (string, error) { + fields := parseFieldsFromSchema(r.Schema) + requiredFields := make(map[string]bool) + if requiredList, ok := r.Schema["required"].([]interface{}); ok { + for _, field := range requiredList { + if fieldName, ok := field.(string); ok { + requiredFields[fieldName] = true + } + } + } + + sort.Slice(fields, func(i, j int) bool { + return fields[i].Order < fields[j].Order + }) + + var fieldHTML bytes.Buffer + for _, field := range fields { + fieldHTML.WriteString(renderField(field, requiredFields)) + } + + tmpl, err := template.New("layout").Funcs(template.FuncMap{ + "form_fields": func() template.HTML { + return template.HTML(fieldHTML.String()) + }, + }).Parse(r.HTMLLayout) + if err != nil { + return "", fmt.Errorf("failed to parse HTML layout: %w", err) + } + + var renderedHTML bytes.Buffer + err = tmpl.Execute(&renderedHTML, nil) + if err != nil { + return "", fmt.Errorf("failed to execute HTML template: %w", err) + } + + return fmt.Sprintf(`
%s
`, renderedHTML.String()), nil +} + +// parseFieldsFromSchema extracts and sorts fields from schema +func parseFieldsFromSchema(schema map[string]interface{}) []FieldInfo { + properties, ok := schema["properties"].(map[string]interface{}) + if !ok { + return nil + } + + var fields []FieldInfo + for name, definition := range properties { + order := 0 + if defMap, ok := definition.(map[string]interface{}); ok { + if ord, exists := defMap["order"].(float64); exists { + order = int(ord) // Ensure consistent type for sorting + } + fields = append(fields, FieldInfo{ + Name: name, + Order: order, + Definition: defMap, + }) + } + } + + sort.Slice(fields, func(i, j int) bool { + return fields[i].Order < fields[j].Order + }) + + return fields +} + +// renderField generates HTML for a single field +func renderField(field FieldInfo, requiredFields map[string]bool) string { + ui, ok := field.Definition["ui"].(map[string]interface{}) + if !ok { + return "" + } + + control, _ := ui["element"].(string) + class, _ := ui["class"].(string) + name, _ := ui["name"].(string) + title, _ := field.Definition["title"].(string) + placeholder, _ := field.Definition["placeholder"].(string) + + isRequired := requiredFields[name] + required := "" + if isRequired { + required = "required" + title += " *" // Add asterisk for required fields + } + + additionalAttributes := "" + for key, value := range field.Definition { + if key != "title" && key != "ui" && key != "placeholder" { + additionalAttributes += fmt.Sprintf(` %s="%v"`, key, value) + } + } + + switch control { + case "input": + return fmt.Sprintf(`
`, class, name, title, name, name, placeholder, required, additionalAttributes) + case "textarea": + return fmt.Sprintf(`
`, class, name, title, name, name, placeholder, required, additionalAttributes) + case "select": + options, _ := ui["options"].([]interface{}) + var optionsHTML bytes.Buffer + for _, option := range options { + optionsHTML.WriteString(fmt.Sprintf(``, option, option)) + } + return fmt.Sprintf(`
`, class, name, title, name, name, required, additionalAttributes, optionsHTML.String()) + case "h1", "h2", "h3", "h4", "h5", "h6": + return fmt.Sprintf(`<%s class="%s" id="%s" %s>%s`, control, class, name, additionalAttributes, title, control) + case "p": + return fmt.Sprintf(`

%s

`, class, name, additionalAttributes, title) + case "a": + href, _ := ui["href"].(string) + return fmt.Sprintf(`%s`, class, name, href, additionalAttributes, title) + default: + return "" + } +} diff --git a/examples/schema.json b/examples/app/schema.json similarity index 56% rename from examples/schema.json rename to examples/app/schema.json index 3c711de..3d83812 100644 --- a/examples/schema.json +++ b/examples/app/schema.json @@ -3,30 +3,30 @@ "properties": { "first_name": { "type": "string", - "title": "👤 First Name", + "title": "First Name", "order": 1, "ui": { - "control": "input", + "element": "input", "class": "form-group", "name": "first_name" } }, "last_name": { "type": "string", - "title": "👤 Last Name", + "title": "Last Name", "order": 2, "ui": { - "control": "input", + "element": "input", "class": "form-group", "name": "last_name" } }, "email": { "type": "email", - "title": "📧 Email Address", + "title": "Email Address", "order": 3, "ui": { - "control": "input", + "element": "input", "type": "email", "class": "form-group", "name": "email" @@ -34,10 +34,10 @@ }, "user_type": { "type": "string", - "title": "👥 User Type", + "title": "User Type", "order": 4, "ui": { - "control": "select", + "element": "select", "class": "form-group", "name": "user_type", "options": [ "new", "premium", "standard" ] @@ -45,10 +45,10 @@ }, "priority": { "type": "string", - "title": "🚨 Priority Level", + "title": "Priority Level", "order": 5, "ui": { - "control": "select", + "element": "select", "class": "form-group", "name": "priority", "options": [ "low", "medium", "high", "urgent" ] @@ -56,24 +56,50 @@ }, "subject": { "type": "string", - "title": "📋 Subject", + "title": "Subject", "order": 6, "ui": { - "control": "input", + "element": "input", "class": "form-group", "name": "subject" } }, "message": { "type": "textarea", - "title": "💬 Message", + "title": "Message", "order": 7, "ui": { - "control": "textarea", + "element": "textarea", "class": "form-group", "name": "message" } } }, - "required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ] + "required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ], + "form": { + "class": "form-horizontal", + "action": "/submit", + "method": "POST", + "enctype": "application/x-www-form-urlencoded", + "groups": [ + { + "title": "User Information", + "fields": [ "first_name", "last_name", "email" ] + }, + { + "title": "Ticket Details", + "fields": [ "user_type", "priority", "subject", "message" ] + } + ], + "submit": { + "type": "submit", + "label": "Submit", + "class": "btn btn-primary" + }, + "reset": { + "type": "reset", + "label": "Reset", + "class": "btn btn-secondary" + } + } } diff --git a/examples/app/server.go b/examples/app/server.go new file mode 100644 index 0000000..6a07140 --- /dev/null +++ b/examples/app/server.go @@ -0,0 +1,53 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" +) + +func main() { + schemaContent, err := os.ReadFile("schema.json") + if err != nil { + fmt.Printf("Error reading schema file: %v\n", err) + return + } + var schema map[string]interface{} + if err := json.Unmarshal(schemaContent, &schema); err != nil { + fmt.Printf("Error parsing schema: %v\n", err) + return + } + http.Handle("/form.css", http.FileServer(http.Dir("templates"))) + http.HandleFunc("/render", func(w http.ResponseWriter, r *http.Request) { + templateName := r.URL.Query().Get("template") + if templateName == "" { + templateName = "basic" + } + + templatePath := fmt.Sprintf("templates/%s.html", templateName) + htmlLayout, err := os.ReadFile(templatePath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load template: %v", err), http.StatusInternalServerError) + return + } + + renderer := NewJSONSchemaRenderer(schema, string(htmlLayout)) + renderedHTML, err := renderer.RenderFields() + if err != nil { + http.Error(w, fmt.Sprintf("Failed to render fields: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(renderedHTML)) + }) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + fmt.Printf("Server running on port %s\n", port) + http.ListenAndServe(fmt.Sprintf(":%s", port), nil) +} diff --git a/examples/app/templates/advanced.html b/examples/app/templates/advanced.html new file mode 100644 index 0000000..6a49d7e --- /dev/null +++ b/examples/app/templates/advanced.html @@ -0,0 +1,26 @@ + + + + + Advanced Template + + + + + + +
+

Form Fields

+
+ {{form_fields}} +
+
+ + + diff --git a/examples/app/templates/basic.html b/examples/app/templates/basic.html new file mode 100644 index 0000000..7071666 --- /dev/null +++ b/examples/app/templates/basic.html @@ -0,0 +1,16 @@ + + + + + Basic Template + + + + + +
+ {{form_fields}} +
+ + + diff --git a/examples/app/templates/form.css b/examples/app/templates/form.css new file mode 100644 index 0000000..4ca4328 --- /dev/null +++ b/examples/app/templates/form.css @@ -0,0 +1,116 @@ +/* Normalize and style form controls */ +body { + font-family: 'Arial', sans-serif; + background-color: #f8f9fa; + margin: 0; + padding: 0; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + font-weight: bold; + margin-bottom: 0.5rem; + color: #333; +} + +.form-group input[type="text"], +.form-group input[type="email"], +.form-group select, +.form-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #ccc; + border-radius: 0.25rem; + font-size: 1rem; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.form-group input[type="text"]:focus, +.form-group input[type="email"]:focus, +.form-group select:focus, +.form-group textarea:focus { + border-color: #007bff; + outline: none; + box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); +} + +.form-group select { + appearance: none; + background: url('data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns%3D%22http%3A//www.w3.org/2000/svg%22 viewBox%3D%220 0 4 5%22%3E%3Cpath fill%3D%22%23000%22 d%3D%22M2 0L0 2h4z%22/%3E%3C/svg%3E') no-repeat right 0.75rem center; + background-size: 0.5rem; +} + +.form-group textarea { + resize: vertical; +} + +.form-group .form-control-error { + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.25rem; +} + +/* Buttons */ +button { + display: inline-block; + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: bold; + color: #fff; + background-color: #007bff; + border: none; + border-radius: 0.25rem; + cursor: pointer; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #0056b3; +} + +button:disabled { + background-color: #ccc; + cursor: not-allowed; +} + +/* Additional layout-specific styles */ +.bg-gray-100 { + background-color: #f8f9fa; +} + +.bg-white { + background-color: #fff; +} + +.bg-gray-200 { + background-color: #e9ecef; +} + +.shadow-md { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.text-xl { + font-size: 1.25rem; + font-weight: bold; +} + +.font-bold { + font-weight: bold; +} + +.mb-4 { + margin-bottom: 1rem; +}