Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F1262277
multiform.ts
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
6 KB
Referenced Files
None
Subscribers
None
multiform.ts
View Options
/* eslint-disable @typescript-eslint/no-explicit-any */
import
Router
from
"@koa/router"
;
import
{
is
,
predicates
}
from
"@sealcode/ts-predicates"
;
import
{
Context
}
from
"koa"
;
import
{
FlatTemplatable
,
tempstream
}
from
"tempstream"
;
import
{
Fields
,
MountableWithFields
}
from
"../page/mountable-with-fields.js"
;
import
{
attribute
}
from
"../sanitize.js"
;
import
type
{
FormDataValue
,
FormMessage
}
from
"./form-types.js"
;
import
{
Form
}
from
"./form.js"
;
const
FIELD_PREFIX_SEPARATOR
=
"___"
;
export
class
Multiform
extends
MountableWithFields
{
controls
=
[];
public
name
:
string
;
public
forms
:
Record
<
string
,
Form
<
Fields
,
unknown
>>
;
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
=
async
(
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
}
'`
);
}
}
}
}
async
extractRawValues
(
ctx
:
Context
)
:
Promise
<
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
,
unknown
>
]
>
{
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
,
show_field_errors
:
boolean
)
:
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
:
await
this
.
getSubformRawValues
(
ctx
,
sub_form_name
),
messages
:
[],
},
show_field_errors
);
}
async
getSubformRawValues
(
ctx
:
Context
,
sub_form_name
:
string
)
:
Promise
<
Record
<
string
,
FormDataValue
>>
{
const
result
=
Object
.
fromEntries
(
Object
.
entries
(
await
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
>
=
{},
show_field_errors
:
boolean
)
:
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 actually 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
,
show_field_errors
)
}
<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
,
[],
{},
false
);
});
for
(
const
[
key
,
form
]
of
Object
.
entries
(
this
.
forms
))
{
router
.
post
(
path
+
(
path
.
endsWith
(
"/"
)
?
""
:
"/"
)
+
key
,
async
(
ctx
)
=>
{
const
result
=
await
form
.
canAccess
(
ctx
);
if
(
!
result
.
canAccess
)
{
ctx
.
body
=
this
.
renderError
(
ctx
,
{
type
:
"access"
,
message
:
result
.
message
,
});
ctx
.
status
=
403
;
return
;
}
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
,
},
true
);
}
else
if
(
reaction
.
action
==
"redirect"
)
{
ctx
.
status
=
303
;
ctx
.
redirect
(
reaction
.
url
);
}
}
);
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-java
Expires
Thu, Jan 23, 19:19 (20 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
599673
Default Alt Text
multiform.ts (6 KB)
Attached To
Mode
rSGEN sealgen
Attached
Detach File
Event Timeline
Log In to Comment