Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F996199
multiform.ts
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
5 KB
Referenced Files
None
Subscribers
None
multiform.ts
View Options
import
Router
from
"@koa/router"
;
import
{
is
,
predicates
}
from
"@sealcode/ts-predicates"
;
import
{
Context
}
from
"koa"
;
import
{
FlatTemplatable
,
tempstream
}
from
"tempstream"
;
import
{
Form
,
FormDataValue
,
FormMessage
}
from
".."
;
import
{
Fields
,
MountableWithFields
}
from
"../page/mountable-with-fields"
;
import
{
attribute
}
from
"../sanitize"
;
const
FIELD_PREFIX_SEPARATOR
=
"___"
;
export
class
Multiform
extends
MountableWithFields
{
controls
=
[];
public
name
:
string
;
public
forms
:
Record
<
string
,
Form
<
Fields
>>
;
init
()
:
void
{
super
.
init
();
for
(
const
[
key
,
form
]
of
Object
.
entries
(
this
.
forms
))
{
if
(
key
!==
attribute
(
key
))
{
throw
new
Error
(
`Form name "
${
key
}
" is not url-safe. Try: "
${
attribute
(
key
)
}
"`
);
}
form
.
makeOpenFormTag
=
()
=>
`<div>`
;
form
.
makeCloseFormTag
=
()
=>
`</div>`
;
form
.
field_names_prefix
=
key
+
FIELD_PREFIX_SEPARATOR
;
form
.
form_id
=
this
.
name
;
form
.
action
=
"./"
+
key
;
form
.
init
();
form
.
extractRawValues
=
(
context
)
=>
this
.
getSubformRawValues
(
context
,
key
);
const
submit_button_id
=
this
.
name
+
"_"
+
key
+
"_submit"
;
form
.
makeSubmitButton
=
()
=>
/* HTML */
`<input
type="submit"
value="
${
form
.
submitButtonText
}
"
formaction="
${
form
.
action
}
"
id="
${
submit_button_id
}
"
${
form
.
form_id
?
`form="
${
form
.
form_id
}
"`
:
""
}
/> `
;
}
for
(
const
form
of
Object
.
values
(
this
.
forms
))
{
for
(
const
field
of
Object
.
values
(
form
.
fields
))
{
if
(
field
.
name
.
includes
(
FIELD_PREFIX_SEPARATOR
))
{
throw
new
Error
(
`A field name within multiform cannot contain '
${
FIELD_PREFIX_SEPARATOR
}
'`
);
}
}
}
}
extractRawValues
(
ctx
:
Context
)
:
Record
<
string
,
FormDataValue
>
{
return
ctx
.
$body
||
{};
}
async
canAccess
()
:
Promise
<
{
canAccess
:
boolean
;
message
:
string
}
>
{
return
{
canAccess
:
true
,
message
:
"this view includes multiple forms and each of them have their unique access rules"
,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getSubformsToRender
(
ctx
:
Context
)
:
Array
<
readonly
[
string
,
Form
<
any
>
]
>
{
const
requested_frame
=
ctx
.
headers
[
"turbo-frame"
];
if
(
!
is
(
requested_frame
,
predicates
.
or
(
predicates
.
undefined
,
predicates
.
string
)
)
)
{
throw
new
Error
(
"Wrong turbo-frame header value type"
);
}
const
forms_to_render
=
requested_frame
&&
this
.
forms
[
requested_frame
]
?
[
<
const
>
[
requested_frame
,
this
.
forms
[
requested_frame
]]]
:
Object
.
entries
(
this
.
forms
);
return
forms_to_render
;
}
async
renderSubform
(
ctx
:
Context
,
sub_form_name
:
string
)
:
Promise
<
FlatTemplatable
>
{
const
form
=
this
.
forms
[
sub_form_name
];
const
result
=
await
form
.
canAccess
(
ctx
);
if
(
!
result
.
canAccess
)
{
return
result
.
message
;
}
return
form
.
render
(
ctx
,
{
raw_values
:
this
.
getSubformRawValues
(
ctx
,
sub_form_name
),
messages
:
[],
});
}
getSubformRawValues
(
ctx
:
Context
,
sub_form_name
:
string
)
:
Record
<
string
,
FormDataValue
>
{
const
result
=
Object
.
fromEntries
(
Object
.
entries
(
this
.
extractRawValues
(
ctx
))
.
filter
(([
key
])
=>
key
.
startsWith
(
sub_form_name
+
FIELD_PREFIX_SEPARATOR
)
)
.
map
(([
key
,
value
])
=>
[
key
.
slice
((
sub_form_name
+
FIELD_PREFIX_SEPARATOR
).
length
),
value
,
])
);
return
result
;
}
async
render
(
ctx
:
Context
,
messages
:
FormMessage
[],
prerenderedForms
:
Record
<
string
,
FlatTemplatable
|
undefined
>
=
{}
)
:
Promise
<
FlatTemplatable
>
{
return
tempstream
/* HTML */
`
${
this
.
renderMessages
(
ctx
,
{
raw_values
:
{
}
,
messages,
})}
<div class="forms">
<form
id="
${
this
.
name
}
"
novalidate
${
/* novalidate is here because all the subforms are actuallly one form and errors in one will prevent submitting all of the forms */
""
}
method="POST"
></form>
${
this
.
getSubformsToRender
(
ctx
).
map
(([
form_name
])
=>
{
const
frame_form_id
=
this
.
name
+
"_"
+
form_name
+
"_frame_form"
;
return
tempstream
/* HTML */
`<turbo-frame
id="
${
form_name
}
"
contains-subform
subform-id="
${
frame_form_id
}
"
>
${
prerenderedForms
[
form_name
]
||
this
.
renderSubform
(
ctx
,
form_name
)
}
<form id="
${
frame_form_id
}
" method="POST"></form>
</turbo-frame>`
;
}
)}
</div>
${
this
.
makeBottomScript
()
}
`
;
}
makeBottomScript
()
:
string
{
// this script assigns each field to its corresponding form, instead of
// the html-only version where all fields are attached to one meta-form
return
/* HTML */
` <script>
(function () {
if (!window.subform_handlers) {
window.subform_handlers = {};
}
if (window.subform_handlers["
${
this
.
name
}
"]) {
return;
}
const handler = () => {
document
.querySelectorAll("turbo-frame[contains-subform]")
.forEach((frame) => {
frame_form_id = frame.getAttribute("subform-id");
frame.querySelectorAll("input").forEach((input) => {
input.setAttribute("form", frame_form_id);
});
});
};
window.subform_handlers["
${
this
.
name
}
"] = handler;
document.documentElement.addEventListener(
"turbo:load",
handler
);
document.documentElement.addEventListener(
"turbo:frame-render",
handler
);
})();
</script>`
;
}
mount
(
router
:
Router
,
path
:
string
)
:
void
{
router
.
get
(
path
,
async
(
ctx
)
=>
{
ctx
.
type
=
"html"
;
ctx
.
body
=
await
this
.
render
(
ctx
,
[],
{});
});
for
(
const
[
key
,
form
]
of
Object
.
entries
(
this
.
forms
))
{
router
.
post
(
path
+
(
path
.
endsWith
(
"/"
)
?
""
:
"/"
)
+
key
,
async
(
ctx
)
=>
{
const
reaction
=
await
form
.
handlePost
(
ctx
);
if
(
reaction
.
action
==
"stay"
)
{
ctx
.
status
=
422
;
const
form_content
=
reaction
.
content
;
ctx
.
body
=
this
.
render
(
ctx
,
[],
{
[
key
]
:
form_content
,
});
}
else
if
(
reaction
.
action
==
"redirect"
)
{
ctx
.
status
=
303
;
ctx
.
redirect
(
reaction
.
url
);
}
}
);
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-java
Expires
Tue, Dec 24, 14:02 (20 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
557103
Default Alt Text
multiform.ts (5 KB)
Attached To
Mode
rSGEN sealgen
Attached
Detach File
Event Timeline
Log In to Comment