This commit is contained in:
sujit
2025-08-05 07:18:15 +05:45
parent b1dad4dc8a
commit 4a55ffc47d
3 changed files with 1571 additions and 196 deletions

View File

@@ -1,113 +1,528 @@
{
"type": "object",
"properties": {
"page_title": {
"type": "string",
"title": "Main Page Title",
"order": 1,
"ui": {
"element": "h1",
"class": "text-3xl font-bold text-center mb-6",
"name": "page_title",
"content": "Comprehensive HTML Form Demo"
}
},
"intro_paragraph": {
"type": "string",
"title": "Introduction",
"order": 2,
"ui": {
"element": "p",
"class": "text-gray-600 mb-4",
"name": "intro",
"content": "This form demonstrates all supported HTML DOM elements in the renderer."
}
},
"divider": {
"type": "string",
"order": 3,
"ui": {
"element": "hr",
"class": "my-6 border-gray-300"
}
},
"first_name": {
"type": "string",
"title": "First Name",
"order": 1,
"placeholder": "Enter your first name",
"order": 10,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"name": "first_name"
"name": "first_name",
"autocomplete": "given-name",
"maxlength": "50"
}
},
"last_name": {
"type": "string",
"title": "Last Name",
"order": 2,
"placeholder": "Enter your last name",
"order": 11,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"name": "last_name"
"name": "last_name",
"autocomplete": "family-name",
"maxlength": "50"
}
},
"email": {
"type": "email",
"title": "Email Address",
"order": 3,
"placeholder": "your.email@example.com",
"order": 12,
"ui": {
"element": "input",
"type": "email",
"class": "form-group",
"name": "email"
"name": "email",
"autocomplete": "email"
}
},
"user_type": {
"type": "string",
"title": "User Type",
"order": 4,
"ui": {
"element": "select",
"class": "form-group",
"name": "user_type",
"options": [ "new", "premium", "standard" ]
}
},
"priority": {
"type": "string",
"title": "Priority Level",
"order": 5,
"ui": {
"element": "select",
"class": "form-group",
"name": "priority",
"options": [ "low", "medium", "high", "urgent" ]
}
},
"subject": {
"type": "string",
"title": "Subject",
"order": 6,
"password": {
"type": "password",
"title": "Password",
"placeholder": "Enter a secure password",
"order": 13,
"ui": {
"element": "input",
"type": "password",
"class": "form-group",
"name": "subject"
"name": "password",
"minlength": "8"
}
},
"message": {
"type": "textarea",
"title": "Message",
"order": 7,
"age": {
"type": "number",
"title": "Age",
"order": 14,
"ui": {
"element": "input",
"type": "number",
"class": "form-group",
"name": "age",
"min": "18",
"max": "120"
}
},
"birth_date": {
"type": "date",
"title": "Birth Date",
"order": 15,
"ui": {
"element": "input",
"type": "date",
"class": "form-group",
"name": "birth_date"
}
},
"website": {
"type": "url",
"title": "Personal Website",
"placeholder": "https://example.com",
"order": 16,
"ui": {
"element": "input",
"type": "url",
"class": "form-group",
"name": "website"
}
},
"phone": {
"type": "tel",
"title": "Phone Number",
"placeholder": "+1-555-123-4567",
"order": 17,
"ui": {
"element": "input",
"type": "tel",
"class": "form-group",
"name": "phone"
}
},
"favorite_color": {
"type": "color",
"title": "Favorite Color",
"order": 18,
"ui": {
"element": "input",
"type": "color",
"class": "form-group",
"name": "favorite_color",
"value": "#3B82F6"
}
},
"satisfaction": {
"type": "range",
"title": "Satisfaction Level",
"order": 19,
"ui": {
"element": "input",
"type": "range",
"class": "form-group",
"name": "satisfaction",
"min": "1",
"max": "10",
"value": "5"
}
},
"profile_picture": {
"type": "file",
"title": "Profile Picture",
"order": 20,
"ui": {
"element": "input",
"type": "file",
"class": "form-group",
"name": "profile_picture",
"accept": "image/*"
}
},
"newsletter": {
"type": "boolean",
"title": "Subscribe to Newsletter",
"order": 21,
"ui": {
"element": "input",
"type": "checkbox",
"class": "form-group",
"name": "newsletter",
"value": "yes"
}
},
"gender": {
"type": "string",
"title": "Gender",
"order": 22,
"ui": {
"element": "select",
"class": "form-group",
"name": "gender",
"options": [
{ "value": "", "text": "Select Gender", "selected": true },
{ "value": "male", "text": "Male" },
{ "value": "female", "text": "Female" },
{ "value": "other", "text": "Other" },
{ "value": "prefer_not_to_say", "text": "Prefer not to say" }
]
}
},
"country": {
"type": "string",
"title": "Country",
"order": 23,
"ui": {
"element": "select",
"class": "form-group",
"name": "country",
"options": [
{ "value": "us", "text": "United States" },
{ "value": "ca", "text": "Canada" },
{ "value": "uk", "text": "United Kingdom" },
{ "value": "de", "text": "Germany" },
{ "value": "fr", "text": "France" },
{ "value": "jp", "text": "Japan" },
{ "value": "au", "text": "Australia" }
]
}
},
"skills": {
"type": "string",
"title": "Skills",
"order": 24,
"ui": {
"element": "select",
"class": "form-group",
"name": "skills",
"multiple": true,
"options": [
{ "value": "javascript", "text": "JavaScript" },
{ "value": "python", "text": "Python" },
{ "value": "go", "text": "Go" },
{ "value": "rust", "text": "Rust" },
{ "value": "java", "text": "Java" },
{ "value": "csharp", "text": "C#" }
]
}
},
"bio": {
"type": "string",
"title": "Biography",
"placeholder": "Tell us about yourself...",
"order": 25,
"ui": {
"element": "textarea",
"class": "form-group",
"name": "message"
"name": "bio",
"rows": "4",
"cols": "50",
"maxlength": "500"
}
},
"section_header": {
"type": "string",
"order": 30,
"ui": {
"element": "h2",
"class": "text-2xl font-semibold mt-8 mb-4",
"content": "Additional Information"
}
},
"experience_fieldset": {
"type": "object",
"order": 31,
"ui": {
"element": "fieldset",
"class": "border border-gray-300 rounded p-4 mb-4",
"children": [
{
"ui": {
"element": "legend",
"class": "font-medium px-2",
"content": "Work Experience"
}
},
{
"title": "Years of Experience",
"ui": {
"element": "input",
"type": "number",
"name": "years_experience",
"class": "form-group",
"min": "0",
"max": "50"
}
},
{
"title": "Current Position",
"ui": {
"element": "input",
"type": "text",
"name": "current_position",
"class": "form-group",
"placeholder": "e.g., Software Engineer"
}
}
]
}
},
"technologies_datalist": {
"type": "string",
"title": "Preferred Technology",
"order": 32,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"name": "preferred_tech",
"list": "technologies",
"placeholder": "Start typing..."
}
},
"tech_datalist": {
"type": "string",
"order": 33,
"ui": {
"element": "datalist",
"id": "technologies",
"options": [
"React", "Vue.js", "Angular", "Node.js", "Express",
"Django", "Flask", "Spring Boot", "ASP.NET", "Laravel"
]
}
},
"completion_progress": {
"type": "string",
"order": 34,
"ui": {
"element": "div",
"class": "mb-4",
"contentHTML": "<label for='form_progress'>Form Completion:</label><progress id='form_progress' value='70' max='100' class='w-full'>70%</progress>"
}
},
"rating_meter": {
"type": "string",
"order": 35,
"ui": {
"element": "div",
"class": "mb-4",
"contentHTML": "<label for='rating'>Overall Rating:</label><meter id='rating' value='8' min='0' max='10' optimum='9' class='w-full'>8 out of 10</meter>"
}
},
"media_section": {
"type": "string",
"order": 40,
"ui": {
"element": "section",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>Media Examples</h3>"
}
},
"demo_image": {
"type": "string",
"order": 41,
"ui": {
"element": "figure",
"class": "mb-4",
"contentHTML": "<img src='https://placehold.co/300x200/3B82F6/ffffff?text=Demo+Image' alt='Demo placeholder image' class='rounded border' /><figcaption class='text-sm text-gray-600 mt-2'>Sample image with caption</figcaption>"
}
},
"table_section": {
"type": "string",
"order": 50,
"ui": {
"element": "div",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>Data Table Example</h3><table class='min-w-full border border-gray-300'><thead class='bg-gray-50'><tr><th class='border border-gray-300 px-4 py-2'>Name</th><th class='border border-gray-300 px-4 py-2'>Role</th><th class='border border-gray-300 px-4 py-2'>Experience</th></tr></thead><tbody><tr><td class='border border-gray-300 px-4 py-2'>John Doe</td><td class='border border-gray-300 px-4 py-2'>Developer</td><td class='border border-gray-300 px-4 py-2'>5 years</td></tr><tr><td class='border border-gray-300 px-4 py-2'>Jane Smith</td><td class='border border-gray-300 px-4 py-2'>Designer</td><td class='border border-gray-300 px-4 py-2'>3 years</td></tr></tbody></table>"
}
},
"list_examples": {
"type": "string",
"order": 60,
"ui": {
"element": "div",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>List Examples</h3><div class='grid grid-cols-1 md:grid-cols-3 gap-4'><div><h4 class='font-medium mb-2'>Unordered List:</h4><ul class='list-disc list-inside space-y-1'><li>First item</li><li>Second item</li><li>Third item</li></ul></div><div><h4 class='font-medium mb-2'>Ordered List:</h4><ol class='list-decimal list-inside space-y-1'><li>Step one</li><li>Step two</li><li>Step three</li></ol></div><div><h4 class='font-medium mb-2'>Description List:</h4><dl class='space-y-2'><dt class='font-medium'>Term 1:</dt><dd class='ml-4 text-gray-600'>Definition 1</dd><dt class='font-medium'>Term 2:</dt><dd class='ml-4 text-gray-600'>Definition 2</dd></dl></div></div>"
}
},
"interactive_details": {
"type": "string",
"order": 70,
"ui": {
"element": "details",
"class": "border border-gray-300 rounded p-4 mb-4",
"contentHTML": "<summary class='font-medium cursor-pointer'>Click to expand advanced options</summary><div class='mt-4 space-y-4'><div class='form-group'><label for='advanced_setting_1'>Advanced Setting 1:</label><input type='text' id='advanced_setting_1' name='advanced_setting_1' class='w-full border rounded px-3 py-2' /></div><div class='form-group'><label for='advanced_setting_2'>Advanced Setting 2:</label><select id='advanced_setting_2' name='advanced_setting_2' class='w-full border rounded px-3 py-2'><option value='option1'>Option 1</option><option value='option2'>Option 2</option></select></div></div>"
}
},
"code_example": {
"type": "string",
"order": 80,
"ui": {
"element": "div",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>Code Example</h3><pre class='bg-gray-100 p-4 rounded overflow-x-auto'><code class='text-sm'>function greetUser(name) {\n return `Hello, ${name}!`;\n}\n\nconsole.log(greetUser('World'));</code></pre>"
}
},
"text_formatting": {
"type": "string",
"order": 90,
"ui": {
"element": "div",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>Text Formatting Examples</h3><p class='mb-4'>This paragraph contains various text formatting: <strong>bold text</strong>, <em>italic text</em>, <mark>highlighted text</mark>, <small>small text</small>, <del>deleted text</del>, <ins>inserted text</ins>, <sup>superscript</sup>, <sub>subscript</sub>, and <abbr title='HyperText Markup Language'>HTML</abbr> abbreviation.</p><blockquote class='border-l-4 border-blue-500 pl-4 italic text-gray-600 mb-4'>This is a blockquote that can contain longer quoted text with proper styling and indentation.</blockquote><address class='not-italic text-gray-600'>Contact: <a href='mailto:demo@example.com' class='text-blue-600 hover:underline'>demo@example.com</a></address>"
}
},
"break_line": {
"type": "string",
"order": 95,
"ui": {
"element": "br"
}
},
"final_divider": {
"type": "string",
"order": 96,
"ui": {
"element": "hr",
"class": "my-8 border-gray-300"
}
},
"footer_note": {
"type": "string",
"order": 97,
"ui": {
"element": "footer",
"class": "text-center text-gray-500 text-sm",
"contentHTML": "<p>This comprehensive form demonstrates the full capabilities of the enhanced JSON Schema renderer.</p>"
}
}
},
"required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ],
"required": [
"first_name", "last_name", "email", "age", "birth_date", "gender", "bio"
],
"form": {
"class": "form-horizontal",
"action": "/process?task_id={{task_id}}&next=true",
"class": "max-w-4xl mx-auto p-6 bg-white shadow-lg rounded-lg",
"action": "/process?task_id={{task_id}}&form=comprehensive",
"method": "POST",
"enctype": "application/x-www-form-urlencoded",
"enctype": "multipart/form-data",
"groups": [
{
"title": {
"text": "User Information",
"class": "text-lg font-semibold mb-2"
"text": "Header Section",
"class": "sr-only"
},
"fields": [ "first_name", "last_name", "email" ],
"class": "flex gap-2 items-center justify-between"
"fields": [ "page_title", "intro_paragraph", "divider" ],
"class": "mb-8"
},
{
"title": {
"text": "Details",
"class": "text-lg font-semibold mb-2"
"text": "Personal Information",
"class": "text-2xl font-semibold mb-6 text-gray-800"
},
"fields": [ "user_type", "priority", "subject", "message" ],
"class": "flex gap-2 items-center justify-between"
"fields": [
"first_name", "last_name", "email", "password", "age",
"birth_date", "website", "phone", "favorite_color",
"satisfaction", "profile_picture", "newsletter"
],
"class": "grid grid-cols-1 md:grid-cols-2 gap-4 mb-8"
},
{
"title": {
"text": "Preferences & Background",
"class": "text-2xl font-semibold mb-6 text-gray-800"
},
"fields": [ "gender", "country", "skills", "bio" ],
"class": "space-y-4 mb-8"
},
{
"title": {
"text": "Professional Details",
"class": "sr-only"
},
"fields": [
"section_header", "experience_fieldset", "technologies_datalist",
"tech_datalist", "completion_progress", "rating_meter"
],
"class": "mb-8"
},
{
"title": {
"text": "Media & Content Examples",
"class": "sr-only"
},
"fields": [ "media_section", "demo_image" ],
"class": "mb-8"
},
{
"title": {
"text": "Data & Text Examples",
"class": "sr-only"
},
"fields": [
"table_section", "list_examples", "interactive_details",
"code_example", "text_formatting"
],
"class": "mb-8"
},
{
"title": {
"text": "Footer",
"class": "sr-only"
},
"fields": [ "break_line", "final_divider", "footer_note" ],
"class": "mt-8"
}
],
"submit": {
"type": "submit",
"label": "Submit",
"class": "btn btn-primary px-2 py-1"
"label": "Submit Complete Form",
"class": "bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 mr-4"
},
"reset": {
"type": "reset",
"label": "Reset",
"class": "btn btn-secondary px-2 py-1"
"label": "Reset All Fields",
"class": "bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 mr-4"
},
"buttons": [
{
"type": "button",
"label": "Save Draft",
"class": "bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200",
"onclick": "saveDraft()"
}
]
}
}

View File

@@ -1,18 +1,212 @@
/* Normalize and style form controls */
body {
font-family: 'Arial', sans-serif;
background-color: #f8f9fa;
/* CSS Reset and Base Styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
line-height: 1.6;
scroll-behavior: smooth;
}
body {
font-family: 'Arial', 'Helvetica', sans-serif;
background-color: #f8f9fa;
color: #333;
margin: 0;
padding: 0;
}
/* Typography and Text Elements */
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
line-height: 1.2;
margin-bottom: 0.5rem;
color: #2c3e50;
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.75rem; }
h4 { font-size: 1.5rem; }
h5 { font-size: 1.25rem; }
h6 { font-size: 1rem; }
p {
margin-bottom: 1rem;
line-height: 1.6;
}
/* Inline text elements */
strong, b {
font-weight: bold;
}
em, i {
font-style: italic;
}
small {
font-size: 0.875rem;
color: #666;
}
mark {
background-color: #fff3cd;
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
}
del {
text-decoration: line-through;
color: #dc3545;
}
ins {
text-decoration: underline;
color: #28a745;
background-color: #d4edda;
}
sub, sup {
font-size: 0.75rem;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub { bottom: -0.25rem; }
sup { top: -0.5rem; }
abbr {
text-decoration: underline dotted;
cursor: help;
}
code {
font-family: 'Courier New', monospace;
background-color: #f8f9fa;
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
font-size: 0.875rem;
color: #e83e8c;
}
pre {
font-family: 'Courier New', monospace;
background-color: #f8f9fa;
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
margin-bottom: 1rem;
border: 1px solid #dee2e6;
}
pre code {
background: none;
padding: 0;
font-size: inherit;
color: inherit;
}
blockquote {
margin: 1rem 0;
padding: 1rem;
border-left: 4px solid #007bff;
background-color: #f8f9fa;
font-style: italic;
}
cite {
font-style: italic;
color: #666;
}
address {
font-style: normal;
margin-bottom: 1rem;
}
time {
color: #666;
}
/* Links */
a {
color: #007bff;
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
a:visited {
color: #6f42c1;
}
/* Lists */
ul, ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
li {
margin-bottom: 0.25rem;
}
ul ul, ol ol, ul ol, ol ul {
margin-bottom: 0;
margin-top: 0.25rem;
}
/* Nested list styles */
ul ul { list-style-type: circle; }
ul ul ul { list-style-type: square; }
ol ol { list-style-type: lower-alpha; }
ol ol ol { list-style-type: lower-roman; }
/* Description lists */
dl {
margin-bottom: 1rem;
}
dt {
font-weight: bold;
margin-top: 0.5rem;
}
dd {
margin-left: 1rem;
margin-bottom: 0.5rem;
}
/* Form Elements */
.form-group {
margin-bottom: 1rem;
width: 100%;
}
.required {
color: #dc3545; /* Bootstrap's danger color */
color: #dc3545;
}
.form-group label {
@@ -22,10 +216,35 @@ body {
color: #333;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group select,
.form-group textarea {
/* Input elements */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
input[type="url"],
input[type="search"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"],
input[type="color"],
input[type="file"],
select,
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
font-size: 1rem;
font-family: inherit;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
/* Ensure form-group input styles apply to all types including password */
.form-group input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ccc;
@@ -34,8 +253,19 @@ body {
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
input:focus,
select:focus,
textarea:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}
/* Specific focus styles for form-group inputs */
.form-group input[type="password"]:focus,
.form-group input[type="text"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="number"]:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: #007bff;
@@ -43,51 +273,208 @@ body {
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}
.form-group select {
input:invalid {
border-color: #dc3545;
}
input:invalid:focus {
box-shadow: 0 0 5px rgba(220, 53, 69, 0.5);
}
/* Range input */
input[type="range"] {
width: 100%;
height: 0.5rem;
background: #ddd;
border-radius: 0.25rem;
outline: none;
padding: 0;
box-shadow: none;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 1.5rem;
height: 1.5rem;
background: #007bff;
border-radius: 50%;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 1.5rem;
height: 1.5rem;
background: #007bff;
border-radius: 50%;
cursor: pointer;
border: none;
}
/* Checkbox and radio */
input[type="checkbox"],
input[type="radio"] {
width: auto;
margin-right: 0.5rem;
padding: 0;
box-shadow: none;
}
input[type="checkbox"] {
border-radius: 0.125rem;
}
input[type="radio"] {
border-radius: 50%;
}
/* Select styling */
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;
padding-right: 2.5rem;
}
.form-group textarea {
select[multiple] {
background-image: none;
padding-right: 0.75rem;
height: auto;
min-height: 6rem;
}
optgroup {
font-weight: bold;
color: #666;
}
option {
padding: 0.25rem;
}
/* Textarea */
textarea {
resize: vertical;
min-height: 6rem;
}
.form-group .form-control-error {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
/* Fieldset and Legend */
fieldset {
border: 1px solid #ccc;
border-radius: 0.25rem;
padding: 1rem;
margin-bottom: 1rem;
}
legend {
font-weight: bold;
padding: 0 0.5rem;
color: #333;
}
/* Progress and Meter */
progress, meter {
width: 100%;
height: 1.5rem;
appearance: none;
border: none;
border-radius: 0.25rem;
background-color: #e9ecef;
}
progress::-webkit-progress-bar,
meter::-webkit-meter-bar {
background-color: #e9ecef;
border-radius: 0.25rem;
}
progress::-webkit-progress-value {
background-color: #007bff;
border-radius: 0.25rem;
}
meter::-webkit-meter-optimum-value {
background-color: #28a745;
}
meter::-webkit-meter-suboptimum-value {
background-color: #ffc107;
}
meter::-webkit-meter-even-less-good-value {
background-color: #dc3545;
}
/* Output */
output {
display: inline-block;
padding: 0.375rem 0.75rem;
background-color: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
/* Datalist styling (limited browser support) */
datalist {
display: none;
}
/* Buttons */
button {
button,
input[type="button"],
input[type="submit"],
input[type="reset"] {
display: inline-block;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: bold;
font-family: inherit;
border: none;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
text-align: center;
text-decoration: none;
transition: all 0.2s ease;
background-color: #6c757d;
color: white;
}
button:hover {
transform: scale(1.05);
button:hover,
input[type="button"]:hover,
input[type="submit"]:hover,
input[type="reset"]:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
button:active {
transform: scale(0.95);
button:active,
input[type="button"]:active,
input[type="submit"]:active,
input[type="reset"]:active {
transform: translateY(0);
}
button:disabled {
button:disabled,
input[type="button"]:disabled,
input[type="submit"]:disabled,
input[type="reset"]:disabled {
background-color: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Primary Button */
button:focus,
input[type="button"]:focus,
input[type="submit"]:focus,
input[type="reset"]:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
/* Button variants */
.btn-primary {
color: #fff;
background-color: #007bff;
color: white;
}
.btn-primary:hover {
@@ -98,10 +485,9 @@ button:disabled {
background-color: #004085;
}
/* Secondary Button */
.btn-secondary {
color: #fff;
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
@@ -112,40 +498,283 @@ button:disabled {
background-color: #4e555b;
}
/* Additional layout-specific styles */
.bg-gray-100 {
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-info:hover {
background-color: #138496;
}
.btn-light {
background-color: #f8f9fa;
color: #212529;
border: 1px solid #dee2e6;
}
.bg-white {
background-color: #fff;
.btn-light:hover {
background-color: #e2e6ea;
}
.bg-gray-200 {
background-color: #e9ecef;
.btn-dark {
background-color: #343a40;
color: white;
}
.shadow-md {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
.btn-dark:hover {
background-color: #23272b;
}
.rounded {
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
background-color: white;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #dee2e6;
vertical-align: top;
}
th {
font-weight: bold;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
}
tr:hover {
background-color: #f5f5f5;
}
caption {
font-weight: bold;
margin-bottom: 0.5rem;
color: #666;
}
thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
}
tbody + tbody {
border-top: 2px solid #dee2e6;
}
/* Table variants */
.table-striped tbody tr:nth-of-type(odd) {
background-color: #f9f9f9;
}
.table-bordered {
border: 1px solid #dee2e6;
}
.table-bordered th,
.table-bordered td {
border: 1px solid #dee2e6;
}
/* Images and Media */
img {
max-width: 100%;
height: auto;
border-radius: 0.25rem;
}
.rounded-lg {
border-radius: 0.5rem;
figure {
margin: 1rem 0;
text-align: center;
}
.text-xl {
font-size: 1.25rem;
font-weight: bold;
figcaption {
font-size: 0.875rem;
color: #666;
margin-top: 0.5rem;
font-style: italic;
}
.font-bold {
font-weight: bold;
audio, video {
width: 100%;
max-width: 100%;
}
.mb-4 {
canvas {
max-width: 100%;
height: auto;
}
svg {
max-width: 100%;
height: auto;
fill: currentColor;
}
/* Sectioning Elements */
article, section, nav, aside {
margin-bottom: 1rem;
}
header, footer {
padding: 1rem 0;
}
main {
min-height: calc(100vh - 200px);
}
/* Interactive Elements */
details {
margin-bottom: 1rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
}
summary {
font-weight: bold;
cursor: pointer;
margin-bottom: 0.5rem;
padding: 0.5rem;
background-color: #f8f9fa;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
}
summary:hover {
background-color: #e9ecef;
}
details[open] summary {
margin-bottom: 1rem;
}
dialog {
border: 1px solid #ccc;
border-radius: 0.5rem;
padding: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background: white;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
/* Embedded Content */
iframe {
width: 100%;
border: none;
border-radius: 0.25rem;
}
embed, object {
max-width: 100%;
}
/* Custom Layout Utilities (Non-Tailwind) */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.row {
display: flex;
flex-wrap: wrap;
margin: -0.5rem;
}
.col {
flex: 1;
padding: 0.5rem;
}
/* Error states */
.form-control-error {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.is-invalid {
border-color: #dc3545;
}
.is-valid {
border-color: #28a745;
}
/* Print styles */
@media print {
* {
box-shadow: none !important;
text-shadow: none !important;
}
a, a:visited {
text-decoration: underline;
}
abbr[title]:after {
content: " (" attr(title) ")";
}
pre, blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr, img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p, h2, h3 {
orphans: 3;
widows: 3;
}
h2, h3 {
page-break-after: avoid;
}
}

View File

@@ -5,8 +5,150 @@ import (
"fmt"
"html/template"
"sort"
"strings"
)
// A single template for the entire group structure
const groupTemplateStr = `
<div class="form-group-container">
{{if .Title.Text}}
{{if .Title.Class}}
<div class="{{.Title.Class}}">{{.Title.Text}}</div>
{{else}}
<h3 class="group-title">{{.Title.Text}}</h3>
{{end}}
{{end}}
<div class="{{.GroupClass}}">{{.FieldsHTML}}</div>
</div>
`
// Templates for field rendering - now supports all HTML DOM elements
var fieldTemplates = map[string]string{
// Form elements
"input": `<div class="{{.Class}}">{{.LabelHTML}}<input {{.AllAttributes}} />{{.ContentHTML}}</div>`,
"textarea": `<div class="{{.Class}}">{{.LabelHTML}}<textarea {{.AllAttributes}}>{{.Content}}</textarea>{{.ContentHTML}}</div>`,
"select": `<div class="{{.Class}}">{{.LabelHTML}}<select {{.AllAttributes}}>{{.OptionsHTML}}</select>{{.ContentHTML}}</div>`,
"button": `<button {{.AllAttributes}}>{{.Content}}</button>`,
"option": `<option {{.AllAttributes}}>{{.Content}}</option>`,
"optgroup": `<optgroup {{.AllAttributes}}>{{.OptionsHTML}}</optgroup>`,
"label": `<label {{.AllAttributes}}>{{.Content}}</label>`,
"fieldset": `<fieldset {{.AllAttributes}}>{{.ContentHTML}}</fieldset>`,
"legend": `<legend {{.AllAttributes}}>{{.Content}}</legend>`,
"datalist": `<datalist {{.AllAttributes}}>{{.OptionsHTML}}</datalist>`,
"output": `<output {{.AllAttributes}}>{{.Content}}</output>`,
"progress": `<progress {{.AllAttributes}}>{{.Content}}</progress>`,
"meter": `<meter {{.AllAttributes}}>{{.Content}}</meter>`,
// Text content elements
"h1": `<h1 {{.AllAttributes}}>{{.Content}}</h1>`,
"h2": `<h2 {{.AllAttributes}}>{{.Content}}</h2>`,
"h3": `<h3 {{.AllAttributes}}>{{.Content}}</h3>`,
"h4": `<h4 {{.AllAttributes}}>{{.Content}}</h4>`,
"h5": `<h5 {{.AllAttributes}}>{{.Content}}</h5>`,
"h6": `<h6 {{.AllAttributes}}>{{.Content}}</h6>`,
"p": `<p {{.AllAttributes}}>{{.Content}}</p>`,
"div": `<div {{.AllAttributes}}>{{.ContentHTML}}</div>`,
"span": `<span {{.AllAttributes}}>{{.Content}}</span>`,
"pre": `<pre {{.AllAttributes}}>{{.Content}}</pre>`,
"code": `<code {{.AllAttributes}}>{{.Content}}</code>`,
"blockquote": `<blockquote {{.AllAttributes}}>{{.ContentHTML}}</blockquote>`,
"cite": `<cite {{.AllAttributes}}>{{.Content}}</cite>`,
"strong": `<strong {{.AllAttributes}}>{{.Content}}</strong>`,
"em": `<em {{.AllAttributes}}>{{.Content}}</em>`,
"small": `<small {{.AllAttributes}}>{{.Content}}</small>`,
"mark": `<mark {{.AllAttributes}}>{{.Content}}</mark>`,
"del": `<del {{.AllAttributes}}>{{.Content}}</del>`,
"ins": `<ins {{.AllAttributes}}>{{.Content}}</ins>`,
"sub": `<sub {{.AllAttributes}}>{{.Content}}</sub>`,
"sup": `<sup {{.AllAttributes}}>{{.Content}}</sup>`,
"abbr": `<abbr {{.AllAttributes}}>{{.Content}}</abbr>`,
"address": `<address {{.AllAttributes}}>{{.ContentHTML}}</address>`,
"time": `<time {{.AllAttributes}}>{{.Content}}</time>`,
// List elements
"ul": `<ul {{.AllAttributes}}>{{.ContentHTML}}</ul>`,
"ol": `<ol {{.AllAttributes}}>{{.ContentHTML}}</ol>`,
"li": `<li {{.AllAttributes}}>{{.Content}}</li>`,
"dl": `<dl {{.AllAttributes}}>{{.ContentHTML}}</dl>`,
"dt": `<dt {{.AllAttributes}}>{{.Content}}</dt>`,
"dd": `<dd {{.AllAttributes}}>{{.Content}}</dd>`,
// Links and media
"a": `<a {{.AllAttributes}}>{{.Content}}</a>`,
"img": `<img {{.AllAttributes}} />`,
"figure": `<figure {{.AllAttributes}}>{{.ContentHTML}}</figure>`,
"figcaption": `<figcaption {{.AllAttributes}}>{{.Content}}</figcaption>`,
"audio": `<audio {{.AllAttributes}}>{{.ContentHTML}}</audio>`,
"video": `<video {{.AllAttributes}}>{{.ContentHTML}}</video>`,
"source": `<source {{.AllAttributes}} />`,
"track": `<track {{.AllAttributes}} />`,
// Table elements
"table": `<table {{.AllAttributes}}>{{.ContentHTML}}</table>`,
"caption": `<caption {{.AllAttributes}}>{{.Content}}</caption>`,
"thead": `<thead {{.AllAttributes}}>{{.ContentHTML}}</thead>`,
"tbody": `<tbody {{.AllAttributes}}>{{.ContentHTML}}</tbody>`,
"tfoot": `<tfoot {{.AllAttributes}}>{{.ContentHTML}}</tfoot>`,
"tr": `<tr {{.AllAttributes}}>{{.ContentHTML}}</tr>`,
"th": `<th {{.AllAttributes}}>{{.Content}}</th>`,
"td": `<td {{.AllAttributes}}>{{.Content}}</td>`,
"colgroup": `<colgroup {{.AllAttributes}}>{{.ContentHTML}}</colgroup>`,
"col": `<col {{.AllAttributes}} />`,
// Sectioning elements
"article": `<article {{.AllAttributes}}>{{.ContentHTML}}</article>`,
"section": `<section {{.AllAttributes}}>{{.ContentHTML}}</section>`,
"nav": `<nav {{.AllAttributes}}>{{.ContentHTML}}</nav>`,
"aside": `<aside {{.AllAttributes}}>{{.ContentHTML}}</aside>`,
"header": `<header {{.AllAttributes}}>{{.ContentHTML}}</header>`,
"footer": `<footer {{.AllAttributes}}>{{.ContentHTML}}</footer>`,
"main": `<main {{.AllAttributes}}>{{.ContentHTML}}</main>`,
// Interactive elements
"details": `<details {{.AllAttributes}}>{{.ContentHTML}}</details>`,
"summary": `<summary {{.AllAttributes}}>{{.Content}}</summary>`,
"dialog": `<dialog {{.AllAttributes}}>{{.ContentHTML}}</dialog>`,
// Embedded content
"iframe": `<iframe {{.AllAttributes}}>{{.Content}}</iframe>`,
"embed": `<embed {{.AllAttributes}} />`,
"object": `<object {{.AllAttributes}}>{{.ContentHTML}}</object>`,
"param": `<param {{.AllAttributes}} />`,
"picture": `<picture {{.AllAttributes}}>{{.ContentHTML}}</picture>`,
"canvas": `<canvas {{.AllAttributes}}>{{.Content}}</canvas>`,
"svg": `<svg {{.AllAttributes}}>{{.ContentHTML}}</svg>`,
// Meta elements
"br": `<br {{.AllAttributes}} />`,
"hr": `<hr {{.AllAttributes}} />`,
"wbr": `<wbr {{.AllAttributes}} />`,
// Generic template for any unlisted element
"generic": `<{{.Element}} {{.AllAttributes}}>{{.ContentHTML}}</{{.Element}}>`,
"void": `<{{.Element}} {{.AllAttributes}} />`,
}
// Void elements that don't have closing tags
var voidElements = map[string]bool{
"area": true, "base": true, "br": true, "col": true, "embed": true,
"hr": true, "img": true, "input": true, "link": true, "meta": true,
"param": true, "source": true, "track": true, "wbr": true,
}
var standardAttrs = []string{
"id", "class", "name", "type", "value", "placeholder", "href", "src",
"alt", "title", "target", "rel", "role", "tabindex", "accesskey",
"contenteditable", "draggable", "hidden", "spellcheck", "translate",
"autocomplete", "autofocus", "disabled", "readonly", "required",
"multiple", "checked", "selected", "defer", "async", "loop", "muted",
"controls", "autoplay", "preload", "poster", "width", "height",
"rows", "cols", "size", "maxlength", "minlength", "min", "max",
"step", "pattern", "accept", "capture", "form", "formaction",
"formenctype", "formmethod", "formnovalidate", "formtarget",
"colspan", "rowspan", "headers", "scope", "start", "reversed",
"datetime", "open", "label", "high", "low", "optimum", "span",
}
// FieldInfo represents metadata for a field extracted from JSONSchema
type FieldInfo struct {
Name string
@@ -198,21 +340,6 @@ func parseGroupsFromSchema(schema map[string]any) []GroupInfo {
// renderGroup generates HTML for a single group
func renderGroup(group GroupInfo) string {
var groupHTML bytes.Buffer
// A single template for the entire group structure
const groupTemplateStr = `
<div class="form-group-container">
{{if .Title.Text}}
{{if .Title.Class}}
<div class="{{.Title.Class}}">{{.Title.Text}}</div>
{{else}}
<h3 class="group-title">{{.Title.Text}}</h3>
{{end}}
{{end}}
<div class="{{.GroupClass}}">{{.FieldsHTML}}</div>
</div>
`
// Render fields
var fieldsHTML bytes.Buffer
for _, field := range group.Fields {
@@ -236,123 +363,327 @@ func renderGroup(group GroupInfo) string {
return groupHTML.String()
}
// Templates for field rendering
var fieldTemplates = map[string]string{
"input": `<div class="{{.Class}}"><label for="{{.Name}}">{{.Title}}</label><input type="{{.InputType}}" id="{{.Name}}" name="{{.Name}}" placeholder="{{.Placeholder}}" {{.Required}} {{.AdditionalAttributes}} /></div>`,
"textarea": `<div class="{{.Class}}"><label for="{{.Name}}">{{.Title}}</label><textarea id="{{.Name}}" name="{{.Name}}" placeholder="{{.Placeholder}}" {{.Required}} {{.AdditionalAttributes}}></textarea></div>`,
"select": `<div class="{{.Class}}"><label for="{{.Name}}">{{.Title}}</label><select id="{{.Name}}" name="{{.Name}}" {{.Required}} {{.AdditionalAttributes}}>{{.OptionsHTML}}</select></div>`,
"h": `<{{.Control}} class="{{.Class}}" id="{{.Name}}" {{.AdditionalAttributes}}>{{.Title}}</{{.Control}}>`,
"p": `<p class="{{.Class}}" id="{{.Name}}" {{.AdditionalAttributes}}>{{.Title}}</p>`,
"a": `<a class="{{.Class}}" id="{{.Name}}" href="{{.Href}}" {{.AdditionalAttributes}}>{{.Title}}</a>`,
"button": `<button type="{{.Type}}" class="{{.Class}}">{{.Label}}</button>`,
}
func renderField(field FieldInfo) string {
ui, ok := field.Definition["ui"].(map[string]any)
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, _ := field.Definition["isRequired"].(bool)
required := ""
titleHTML := title
if isRequired {
required = "required"
titleHTML += ` <span class="required">*</span>`
element, _ := ui["element"].(string)
if element == "" {
return ""
}
inputType := "text"
if uiType, ok := ui["type"].(string); ok {
inputType = uiType
} else if fieldType, ok := field.Definition["type"].(string); ok && fieldType == "email" {
inputType = "email"
}
// Build all attributes
allAttributes := buildAllAttributes(field, ui)
var additionalAttributes bytes.Buffer
for key, value := range field.Definition {
switch key {
case "title", "ui", "placeholder", "type", "order", "isRequired":
continue
default:
additionalAttributes.WriteString(fmt.Sprintf(` %s="%v"`, key, value))
}
}
// Get content
content := getFieldContent(field.Definition, ui)
contentHTML := getFieldContentHTML(field.Definition, ui)
// Generate label if needed
labelHTML := generateLabel(field, ui)
data := map[string]any{
"Class": class,
"Name": name,
"Title": template.HTML(titleHTML),
"Placeholder": placeholder,
"Required": required,
"AdditionalAttributes": template.HTML(additionalAttributes.String()),
"InputType": inputType,
"Control": control,
"Href": "",
"Element": element,
"AllAttributes": template.HTMLAttr(allAttributes),
"Content": content,
"ContentHTML": template.HTML(contentHTML),
"LabelHTML": template.HTML(labelHTML),
"Class": getUIValue(ui, "class"),
"OptionsHTML": template.HTML(generateOptions(ui)),
}
// Handle options for select
if control == "select" {
options, _ := ui["options"].([]any)
var optionsHTML bytes.Buffer
for _, option := range options {
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, option, option))
}
data["OptionsHTML"] = template.HTML(optionsHTML.String())
// Use specific template if available, otherwise use generic
var tmplStr string
if template, exists := fieldTemplates[element]; exists {
tmplStr = template
} else if voidElements[element] {
tmplStr = fieldTemplates["void"]
} else {
tmplStr = fieldTemplates["generic"]
}
// Handle href for links
if control == "a" {
if href, ok := ui["href"].(string); ok {
data["Href"] = href
}
}
if tmplStr, ok := fieldTemplates[control]; ok {
tmpl := template.Must(template.New(control).Parse(tmplStr))
tmpl := template.Must(template.New(element).Parse(tmplStr))
var buf bytes.Buffer
tmpl.Execute(&buf, data)
return buf.String()
if err := tmpl.Execute(&buf, data); err != nil {
return ""
}
// Handle generic 'h' template for h1-h6
if control[0] == 'h' && len(control) == 2 && control[1] >= '1' && control[1] <= '6' {
data["Control"] = control
tmpl := template.Must(template.New("h").Parse(fieldTemplates["h"]))
var buf bytes.Buffer
tmpl.Execute(&buf, data)
return buf.String()
}
func buildAllAttributes(field FieldInfo, ui map[string]any) string {
var attributes []string
// Add standard attributes from ui
for _, attr := range standardAttrs {
if value, exists := ui[attr]; exists {
if attr == "class" && value == "" {
continue // Skip empty class
}
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, attr, value))
}
}
// Handle required field
if isRequired, ok := field.Definition["isRequired"].(bool); ok && isRequired {
if !contains(attributes, "required=") {
attributes = append(attributes, `required="required"`)
}
}
// Handle input type based on field type
element, _ := ui["element"].(string)
if element == "input" {
if inputType := getInputType(field.Definition, ui); inputType != "" {
if !contains(attributes, "type=") {
attributes = append(attributes, fmt.Sprintf(`type="%s"`, inputType))
}
}
}
// Add data-* and aria-* attributes
for key, value := range ui {
if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") {
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value))
}
}
// Add custom attributes from field definition (excluding known schema properties)
excludeFields := map[string]bool{
"type": true, "title": true, "ui": true, "placeholder": true,
"order": true, "isRequired": true, "content": true, "children": true,
}
for key, value := range field.Definition {
if !excludeFields[key] && !strings.HasPrefix(key, "ui") {
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value))
}
}
return strings.Join(attributes, " ")
}
func getInputType(fieldDef map[string]any, ui map[string]any) string {
// Check ui type first
if uiType, ok := ui["type"].(string); ok {
return uiType
}
// Map schema types to input types
if fieldType, ok := fieldDef["type"].(string); ok {
switch fieldType {
case "email":
return "email"
case "password":
return "password"
case "number", "integer":
return "number"
case "boolean":
return "checkbox"
case "date":
return "date"
case "time":
return "time"
case "datetime":
return "datetime-local"
case "url":
return "url"
case "tel":
return "tel"
case "color":
return "color"
case "range":
return "range"
case "file":
return "file"
case "hidden":
return "hidden"
default:
return "text"
}
}
return "text"
}
func getFieldContent(fieldDef map[string]any, ui map[string]any) string {
// Check for content in ui first
if content, ok := ui["content"].(string); ok {
return content
}
// Check for content in field definition
if content, ok := fieldDef["content"].(string); ok {
return content
}
// Use title as fallback for some elements
if title, ok := fieldDef["title"].(string); ok {
return title
}
return ""
}
func getFieldContentHTML(fieldDef map[string]any, ui map[string]any) string {
// Check for HTML content in ui
if contentHTML, ok := ui["contentHTML"].(string); ok {
return contentHTML
}
// Check for children elements
if children, ok := ui["children"].([]any); ok {
return renderChildren(children)
}
return ""
}
func renderChildren(children []any) string {
var result strings.Builder
for _, child := range children {
if childMap, ok := child.(map[string]any); ok {
// Create a temporary field info for the child
childField := FieldInfo{
Name: getMapValue(childMap, "name", ""),
Definition: childMap,
}
result.WriteString(renderField(childField))
}
}
return result.String()
}
func generateLabel(field FieldInfo, ui map[string]any) string {
// Check if label should be generated
if showLabel, ok := ui["showLabel"].(bool); !showLabel && ok {
return ""
}
title, _ := field.Definition["title"].(string)
if title == "" {
return ""
}
name := getUIValue(ui, "name")
if name == "" {
name = field.Name
}
// Check if field is required
isRequired, _ := field.Definition["isRequired"].(bool)
requiredSpan := ""
if isRequired {
requiredSpan = ` <span class="required">*</span>`
}
return fmt.Sprintf(`<label for="%s">%s%s</label>`, name, title, requiredSpan)
}
func generateOptions(ui map[string]any) string {
options, ok := ui["options"].([]any)
if !ok {
return ""
}
var optionsHTML strings.Builder
for _, option := range options {
if optionMap, ok := option.(map[string]any); 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]any, key string) string {
if value, ok := ui[key].(string); ok {
return value
}
return ""
}
func getMapValue(m map[string]any, key, defaultValue string) string {
if value, ok := m[key].(string); ok {
return value
}
return defaultValue
}
func contains(slice []string, substr string) bool {
for _, item := range slice {
if strings.Contains(item, substr) {
return true
}
}
return false
}
// renderButtons generates HTML for form buttons
func renderButtons(formConfig map[string]any) string {
var buttonsHTML bytes.Buffer
tmpl := template.Must(template.New("button").Parse(fieldTemplates["button"]))
if submitConfig, ok := formConfig["submit"].(map[string]any); ok {
data := map[string]any{
"Type": submitConfig["type"],
"Class": submitConfig["class"],
"Label": submitConfig["label"],
}
tmpl.Execute(&buttonsHTML, data)
buttonHTML := renderButtonFromConfig(submitConfig, "submit")
buttonsHTML.WriteString(buttonHTML)
}
if resetConfig, ok := formConfig["reset"].(map[string]any); ok {
data := map[string]any{
"Type": resetConfig["type"],
"Class": resetConfig["class"],
"Label": resetConfig["label"],
buttonHTML := renderButtonFromConfig(resetConfig, "reset")
buttonsHTML.WriteString(buttonHTML)
}
// Support for additional custom buttons
if buttons, ok := formConfig["buttons"].([]any); ok {
for _, button := range buttons {
if buttonMap, ok := button.(map[string]any); ok {
buttonType := getMapValue(buttonMap, "type", "button")
buttonHTML := renderButtonFromConfig(buttonMap, buttonType)
buttonsHTML.WriteString(buttonHTML)
}
}
tmpl.Execute(&buttonsHTML, data)
}
return buttonsHTML.String()
}
func renderButtonFromConfig(config map[string]any, defaultType string) string {
var attributes []string
buttonType := getMapValue(config, "type", defaultType)
attributes = append(attributes, fmt.Sprintf(`type="%s"`, buttonType))
if class := getMapValue(config, "class", ""); class != "" {
attributes = append(attributes, fmt.Sprintf(`class="%s"`, class))
}
// Add other button attributes
for key, value := range config {
switch key {
case "type", "class", "label", "content":
continue // Already handled
default:
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value))
}
}
content := getMapValue(config, "label", getMapValue(config, "content", "Button"))
return fmt.Sprintf(`<button %s>%s</button>`,
strings.Join(attributes, " "), content)
}