This commit is contained in:
fenris 2026-04-22 07:47:23 +02:00
commit 999c2981a6
24 changed files with 1222 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/.geany
/build/

24
misc/example-1.frm.json Normal file
View file

@ -0,0 +1,24 @@
{
"type": "group",
"members": {
"active": {
"type": "checkbox",
"label": "aktiv"
},
"name": {
"type": "text",
"label": "Name"
},
"ranks": {
"type": "list",
"element": {
"type": "number"
},
"label": "Rang"
},
"color": {
"type": "color",
"label": "Farbe"
}
}
}

3
readme.md Normal file
View file

@ -0,0 +1,3 @@
# formgen

1
source/logic/base.ts Normal file
View file

@ -0,0 +1 @@
type int = number;

75
source/logic/form.ts Normal file
View file

@ -0,0 +1,75 @@
namespace formgen
{
/**
*/
type type_action = {
label : string;
target : string;
};
/**
*/
export class class_form<type_value>
{
/**
*/
private input : formgen.input.interface_input<type_value>;
/**
*/
private actions : Array<type_action>;
/**
*/
public constructor(
input : formgen.input.interface_input<type_value>,
actions : Array<type_action>
)
{
this.input = input;
this.actions = actions;
}
/**
*/
public async setup(
element_target : Element
) : Promise<void>
{
const element_form : Element = document.createElement("form");
element_form.classList.add("formgen-form");
// element_form.setAttribute("method", "POST");
// input
{
const element_input : Element = document.createElement("div");
element_input.classList.add("formgen-form-input");
await this.input.setup(element_input);
element_form.appendChild(element_input);
}
// actions
{
const element_actions : Element = document.createElement("div");
element_actions.classList.add("formgen-form-actions");
for (const action of this.actions)
{
const element_action : Element = document.createElement("button");
element_action.classList.add("formgen-form-button");
element_action.setAttribute("formaction", action.target);
element_action.textContent = action.label;
element_actions.appendChild(element_action);
}
element_form.appendChild(element_actions);
}
element_target.appendChild(element_form);
return Promise.resolve<void>(undefined);
}
}
}

View file

@ -0,0 +1,19 @@
namespace formgen.helpers.call
{
/**
*/
export function convey(
x : unknown,
fs : Array<Function>
) : unknown
{
let y : unknown = x;
for (const f of fs)
{
y = f(y);
}
return y;
}
}

View file

@ -0,0 +1,17 @@
namespace formgen.helpers.file
{
/**
* @todo handle errors
*/
export function read_text(
path : string
) : Promise<string>
{
return (
fetch(path)
.then(x => x.text())
);
}
}

View file

@ -0,0 +1,14 @@
namespace formgen.helpers.json
{
/**
* @todo handle errors
*/
export function decode(
json : string
) : unknown
{
return JSON.parse(json);
}
}

View file

@ -0,0 +1,16 @@
namespace formgen.helpers.list
{
/**
*/
export function transform<type_element_from, type_element_to>(
list : Array<type_element_from>,
function_ : ((type_element_from) => type_element_to)
) : Array<type_element_to>
{
return list.map(
(element) => function_(element)
);
}
}

View file

@ -0,0 +1,42 @@
namespace formgen.helpers.map
{
/**
*/
export function read<type_value>(
map : Record<string, type_value>,
key : string,
fallback : type_value
) : type_value
{
return ((key in map) ? map[key] : fallback);
}
/**
*/
export function transform<type_value_from, type_value_to>(
map : Record<string, type_value_from>,
function_ : ((string, type_value_from) => type_value_to)
) : Record<string, type_value_to>
{
return Object.fromEntries(
Object.entries(map)
.map(([key, value]) => ([key, function_(key, value)]))
);
}
/**
*/
export function to_pairs<type_value>(
map : Record<string, type_value>,
) : Array<{key : string; value : type_value;}>
{
return (
Object.entries(map)
.map(([key, value]) => ({"key": key, "value": value}))
);
}
}

View file

@ -0,0 +1,37 @@
namespace formgen.helpers.string
{
/**
*/
let _index : int = 0;
/**
*/
export function coin(
template : string,
arguments_ : Record<string,string>
) : string
{
let result = template;
for (const [key, value] of Object.entries(arguments_))
{
result = result.replace(
new RegExp("{{" + key + "}}", "g"),
value
);
}
return result;
}
/**
*/
export function generate(
) : string
{
_index += 1;
return _index.toFixed(0);
}
}

View file

@ -0,0 +1,94 @@
namespace formgen.input
{
/**
*/
export function from_raw(
raw : any
) : interface_input<unknown>
{
switch (raw.type)
{
case "checkbox":
{
return (
new class_input_checkbox(
{
"label": formgen.helpers.map.read<(null | string)>(raw, "label", null),
}
)
);
break;
}
case "number":
{
return (
new class_input_number(
{
"label": formgen.helpers.map.read<(null | string)>(raw, "label", null),
}
)
);
break;
}
case "text":
{
return (
new class_input_text(
{
"label": formgen.helpers.map.read<(null | string)>(raw, "label", null),
}
)
);
break;
}
case "color":
{
return (
new class_input_color(
{
"label": formgen.helpers.map.read<(null | string)>(raw, "label", null),
}
)
);
break;
}
case "list":
{
return (
new class_input_list(
() => from_raw(raw.element),
{
"label": formgen.helpers.map.read<(null | string)>(raw, "label", null),
}
)
);
break;
}
case "group":
{
return (
new class_input_group(
formgen.helpers.list.transform(
formgen.helpers.map.to_pairs(raw.members),
(pair) => ({
"name": pair.key,
"input": from_raw(pair.value),
})
),
{
"label": formgen.helpers.map.read<(null | string)>(raw, "label", null),
}
)
);
break;
}
default:
{
throw (new Error("unhandled type: " + raw.type));
break;
}
}
}
}

View file

@ -0,0 +1,33 @@
namespace formgen.input
{
/**
*/
export interface interface_input<type_value>
{
/**
*/
setup(
target : Element
) : Promise<void>
;
/**
*/
write(
value : type_value
) : Promise<void>
;
/**
*/
read(
) : Promise<type_value>
;
}
}

View file

@ -0,0 +1,113 @@
namespace formgen.input
{
/**
*/
export class class_input_checkbox implements interface_input<boolean>
{
/**
*/
private additional_classes : Array<string>;
/**
*/
private label : (null | string);
/**
*/
private element_input : (null | Element);
/**
*/
public constructor(
{
"additional_classes": additional_classes = [],
"label": label = null,
}
:
{
additional_classes ?: Array<string>,
label ?: (null | string);
}
=
{
}
)
{
this.additional_classes = ["formgen-input-checkbox"].concat(additional_classes);
this.label = label;
this.element_input = null;
}
/**
* [implementation]
*/
public setup(
target : Element
) : Promise<void>
{
const id : string = formgen.helpers.string.generate();
const element_container : Element = document.createElement("div");
element_container.classList.add("formgen-input");
for (const class_ of this.additional_classes)
{
element_container.classList.add(class_);
}
// label
{
if (this.label === null)
{
// do nothing
}
else
{
const element_label : Element = document.createElement("label");
element_label.setAttribute("for", id);
element_label.textContent = this.label;
element_container.appendChild(element_label);
}
}
// input
{
const element_input : Element = document.createElement("input");
element_input.setAttribute("type", "checkbox");
element_input.setAttribute("id", id);
element_container.appendChild(element_input);
this.element_input = element_input;
}
target.appendChild(element_container);
return Promise.resolve<void>(undefined);
}
/**
* [implementation]
*/
public read(
) : Promise<boolean>
{
const value : boolean = (this.element_input as HTMLInputElement).checked;
return Promise.resolve<boolean>(value);
}
/**
* [implementation]
*/
public write(
value : boolean
) : Promise<void>
{
(this.element_input as HTMLInputElement).checked = value;
return Promise.resolve<void>(undefined);
}
}
}

View file

@ -0,0 +1,40 @@
namespace formgen.input
{
/**
* @todo dedicated color type
*/
export class class_input_color extends class_input_simple<string>
{
/**
*/
public constructor(
{
"additional_classes": additional_classes = [],
"label": label = null,
}
:
{
additional_classes ?: Array<string>,
label ?: (null | string);
}
=
{
}
)
{
super(
"color",
(value) => value,
(raw) => raw,
{
"label": label,
"additional_classes": ["formgen-input-color"].concat(additional_classes),
}
);
}
}
}

View file

@ -0,0 +1,98 @@
namespace formgen.input
{
/**
*/
type type_field<type_value> = {
name : string;
input : interface_input<type_value>;
};
/**
*/
export class class_input_group implements interface_input<Record<string, any>>
{
/**
*/
private fields : Array<type_field<any>>;
/**
*/
private label : (null | string);
/**
*/
public constructor(
fields : Array<type_field<any>>,
{
"label": label = null,
}
:
{
label ?: (null | string);
}
=
{
}
)
{
this.fields = fields;
this.label = label;
}
/**
* [implementation]
*/
public async setup(
element_target : Element
) : Promise<void>
{
const element_container : Element = document.createElement("div");
element_container.classList.add("formgen-input");
element_container.classList.add("formgen-input-group");
for (const field of this.fields)
{
const element_field : Element = document.createElement("div");
element_field.classList.add("formgen-input-group-field");
element_field.setAttribute("rel", field.name);
await field.input.setup(element_field);
element_container.appendChild(element_field);
}
element_target.appendChild(element_container);
return Promise.resolve<void>(undefined);
}
/**
* [implementation]
*/
public async read(
) : Promise<Record<string, any>>
{
let result : Record<string, any> = {};
for (const field of this.fields)
{
result[field.name] = await field.input.read();
}
return result;
}
/**
* [implementation]
*/
public write(
value : Record<string, any>
) : Promise<void>
{
return Promise.reject(new Error("not implemented"));
}
}
}

191
source/logic/input/list.ts Normal file
View file

@ -0,0 +1,191 @@
namespace formgen.input
{
/**
*/
type type_entry<type_element> = {
id : string;
input : interface_input<type_element>;
element : Element;
};
/**
*/
export class class_input_list<type_element> implements interface_input<Array<type_element>>
{
/**
*/
private element_input_factory : (() => interface_input<type_element>);
/**
*/
private label : (null | string);
/**
*/
private element_entries : (null | Element);
/**
*/
private entries : Array<type_entry<type_element>>;
/**
*/
public constructor(
element_input_factory : (() => interface_input<type_element>),
{
"label": label = null,
}
:
{
label ?: (null | string);
}
=
{
}
)
{
this.element_input_factory = element_input_factory;
this.label = label;
this.element_entries = null;
this.entries = [];
}
/**
*/
private remove(
id : string
) : void
{
const index : int = this.entries.findIndex(entry => (entry.id === id));
this.entries[index].element.remove();
this.entries.splice(index, 1);
}
/**
*/
private async add(
) : Promise<void>
{
const id : string = formgen.helpers.string.generate();
const input : interface_input<type_element> = this.element_input_factory();
const element_entry : Element = document.createElement("div");
element_entry.classList.add("formgen-input-list-element");
element_entry.setAttribute("rel", id);
// remover
{
const element_remover : Element = document.createElement("button");
element_remover.classList.add("formgen-input-list-remover");
element_remover.textContent = "-";
element_remover.addEventListener(
"click",
(event) => {
event.preventDefault();
this.remove(id);
}
);
element_entry.appendChild(element_remover);
}
// input
{
await input.setup(element_entry);
}
this.entries.push(
{
"id": id,
"input": input,
"element": element_entry,
}
);
this.element_entries.appendChild(element_entry);
}
/**
* [implementation]
*/
public async setup(
element_target : Element
) : Promise<void>
{
const element_container : Element = document.createElement("div");
{
// label
{
if (this.label === null)
{
// do nothing
}
else
{
const element_label : Element = document.createElement("label");
element_label.textContent = this.label;
element_container.appendChild(element_label);
}
}
// entries
{
const element_entries : Element = document.createElement("div");
element_entries.classList.add("formgen-input-list-entries");
element_container.appendChild(element_entries);
this.element_entries = element_entries;
}
// adder
{
const element_adder : Element = document.createElement("div");
element_adder.classList.add("formgen-input-list-adder");
{
const element_adder_button : Element = document.createElement("button");
element_adder_button.textContent = "+";
element_adder.appendChild(element_adder_button);
element_adder.addEventListener(
"click",
(event) => {
event.preventDefault();
this.add();
}
);
}
element_container.appendChild(element_adder);
}
}
element_target.appendChild(element_container);
}
/**
* [implementation]
*/
public async read(
) : Promise<Array<type_element>>
{
let result : Array<type_element> = [];
for (const entry of this.entries)
{
result.push(await entry.input.read());
}
return result;
}
/**
* [implementation]
*/
public write(
value : Array<type_element>
) : Promise<void>
{
return Promise.reject(new Error("not implemented"));
}
}
}

View file

@ -0,0 +1,39 @@
namespace formgen.input
{
/**
*/
export class class_input_number extends class_input_simple<number>
{
/**
*/
public constructor(
{
"additional_classes": additional_classes = [],
"label": label = null,
}
:
{
additional_classes ?: Array<string>,
label ?: (null | string);
}
=
{
}
)
{
super(
"number",
(value) => value.toFixed(),
(raw) => parseInt(raw),
{
"label": label,
"additional_classes": ["formgen-input-number"].concat(additional_classes),
}
);
}
}
}

View file

@ -0,0 +1,136 @@
namespace formgen.input
{
/**
*/
export class class_input_simple<type_value> implements interface_input<type_value>
{
/**
*/
private type : string;
/**
*/
private value_encode : ((type_value) => string);
/**
*/
private value_decode : ((string) => type_value);
/**
*/
private additional_classes : Array<string>;
/**
*/
private label : (null | string);
/**
*/
private element_input : (null | Element);
/**
*/
public constructor(
type : string,
value_encode : ((type_value) => string),
value_decode : ((string) => type_value),
{
"additional_classes": additional_classes = [],
"label": label = null,
}
:
{
additional_classes ?: Array<string>,
label ?: (null | string);
}
=
{
}
)
{
this.type = type;
this.value_encode = value_encode;
this.value_decode = value_decode;
this.additional_classes = additional_classes;
this.label = label;
this.element_input = null;
}
/**
* [implementation]
*/
public setup(
element_target : Element
) : Promise<void>
{
const id : string = formgen.helpers.string.generate();
const element_container : Element = document.createElement("div");
element_container.classList.add("formgen-input");
for (const class_ of this.additional_classes)
{
element_container.classList.add(class_);
}
// label
{
if (this.label === null)
{
// do nothing
}
else
{
const element_label : Element = document.createElement("label");
element_label.setAttribute("for", id);
element_label.textContent = this.label;
element_container.appendChild(element_label);
}
}
// input
{
const element_input : Element = document.createElement("input");
element_input.setAttribute("type", this.type);
element_input.setAttribute("id", id);
element_container.appendChild(element_input);
this.element_input = element_input;
}
element_target.appendChild(element_container);
return Promise.resolve<void>(undefined);
}
/**
* [implementation]
*/
public read(
) : Promise<type_value>
{
const raw : string = (this.element_input as HTMLInputElement).value;
const value : type_value = this.value_decode(raw);
return Promise.resolve<type_value>(value);
}
/**
* [implementation]
*/
public write(
value : type_value
) : Promise<void>
{
const raw : string = this.value_encode(value);
(this.element_input as HTMLInputElement).value = raw;
return Promise.resolve<void>(undefined);
}
}
}

View file

@ -0,0 +1,39 @@
namespace formgen.input
{
/**
*/
export class class_input_text extends class_input_simple<string>
{
/**
*/
public constructor(
{
"additional_classes": additional_classes = [],
"label": label = null,
}
:
{
additional_classes ?: Array<string>,
label ?: (null | string);
}
=
{
}
)
{
super(
"text",
(value) => value,
(raw) => raw,
{
"label": label,
"additional_classes": ["formgen-input-text"].concat(additional_classes),
}
);
}
}
}

84
source/logic/main.ts Normal file
View file

@ -0,0 +1,84 @@
namespace formgen
{
/**
*/
let _input : (null | formgen.input.interface_input<unknown>) = null;
/**
*/
async function render(
raw : string,
element_target : Element
) : Promise<void>
{
element_target.innerHTML = "";
try
{
const description : any = formgen.helpers.json.decode(raw);
const input : formgen.input.interface_input<unknown> = formgen.input.from_raw(description);
/*
const form : formgen.class_form<unknown> = new formgen.class_form<unknown>(
input,
[
{"target": "/", "label": "abschicken"},
]
);
await form.setup(element_target)
*/
await input.setup(element_target);
_input = input;
}
catch (error)
{
console.error(error);
}
}
/**
*/
function main(
) : void
{
document.querySelector("#render").addEventListener(
"click",
() => {
render(
(document.querySelector("#input") as HTMLInputElement).value,
document.querySelector("#output")
);
}
);
document.querySelector("#read").addEventListener(
"click",
async () => {
if (_input === null)
{
console.warn("no input present");
}
else
{
const value = await _input.read();
console.info(value);
}
}
);
}
/**
*/
export function entry(
) : void
{
document.addEventListener(
"DOMContentLoaded",
() => {
main();
}
);
}
}

20
source/structure.html Normal file
View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script type="text/javascript" src="logic.js"></script>
<script type="text/javascript">formgen.entry();</script>
<link rel="stylesheet" type="text/css" href="style.css"/>
</head>
<body>
<textarea id="input">
</textarea>
<hr/>
<button id="render">render</button>
<hr/>
<div id="output">
</div>
<hr/>
<button id="read">lesen</button>
</body>
</html>

50
source/style.css Normal file
View file

@ -0,0 +1,50 @@
html
{
background-color: #000000;
color: #FFFFFF;
}
body
{
max-width: 960px;
margin: auto;
padding: 16px;
background-color: #202020;
color: #D0D0D0;
}
#input
{
width: 100%;
min-height: 240px;
}
#render
{
display: block;
}
#output
{
width: 100%;
min-height: 240px;
}
.formgen-input > label
{
display: block;
font-size: 80%;
font-weight: bold;
text-transform: uppercase;
}
.formgen-input-group-field
{
margin-bottom: 8px;
}
.formgen-form-button
{
text-transform: uppercase;
}

35
tools/build Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env sh
## consts
dir_source=source
dir_build=build
cmd_tsc=tsc
## exec
mkdir -p ${dir_build}
${cmd_tsc} \
--lib es2020,dom \
${dir_source}/logic/base.ts \
${dir_source}/logic/helpers/call.ts \
${dir_source}/logic/helpers/file.ts \
${dir_source}/logic/helpers/string.ts \
${dir_source}/logic/helpers/json.ts \
${dir_source}/logic/helpers/list.ts \
${dir_source}/logic/helpers/map.ts \
${dir_source}/logic/input/_interface.ts \
${dir_source}/logic/input/simple.ts \
${dir_source}/logic/input/text.ts \
${dir_source}/logic/input/number.ts \
${dir_source}/logic/input/color.ts \
${dir_source}/logic/input/checkbox.ts \
${dir_source}/logic/input/list.ts \
${dir_source}/logic/input/group.ts \
${dir_source}/logic/input/_factory.ts \
${dir_source}/logic/form.ts \
${dir_source}/logic/main.ts \
--outFile ${dir_build}/logic.js
cp ${dir_source}/structure.html ${dir_build}/index.html -u -v
cp ${dir_source}/style.css ${dir_build}/style.css -u -v