This commit is contained in:
sujit
2025-08-04 23:18:58 +05:45
parent 2786e64900
commit d2eb37ae19
6 changed files with 400 additions and 15 deletions

View File

@@ -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(`<form>%s</form>`, 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(`<div class="%s"><label for="%s">%s</label><input type="text" id="%s" name="%s" placeholder="%s" %s %s /></div>`, class, name, title, name, name, placeholder, required, additionalAttributes)
case "textarea":
return fmt.Sprintf(`<div class="%s"><label for="%s">%s</label><textarea id="%s" name="%s" placeholder="%s" %s %s></textarea></div>`, 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 value="%v">%v</option>`, option, option))
}
return fmt.Sprintf(`<div class="%s"><label for="%s">%s</label><select id="%s" name="%s" %s %s>%s</select></div>`, 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</%s>`, control, class, name, additionalAttributes, title, control)
case "p":
return fmt.Sprintf(`<p class="%s" id="%s" %s>%s</p>`, class, name, additionalAttributes, title)
case "a":
href, _ := ui["href"].(string)
return fmt.Sprintf(`<a class="%s" id="%s" href="%s" %s>%s</a>`, class, name, href, additionalAttributes, title)
default:
return ""
}
}

View File

@@ -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"
}
}
}

53
examples/app/server.go Normal file
View File

@@ -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)
}

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>Advanced Template</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="form.css">
<style>
.form-container {
padding: 20px;
border: 1px solid #ccc;
background-color: #f9f9f9;
}
</style>
</head>
<body class="bg-gray-200">
<div class="form-container rounded-lg">
<h1 class="text-xl font-bold mb-4">Form Fields</h1>
<div class="grid grid-cols-3 gap-3">
{{form_fields}}
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Basic Template</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="form.css">
</head>
<body class="bg-gray-100">
<div class="form-container p-4 bg-white shadow-md rounded">
{{form_fields}}
</div>
</body>
</html>

View File

@@ -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;
}