Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F10352527
form.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
form.ts
View Options
import
{
BaseContext
}
from
"koa"
;
import
Router
from
"@koa/router"
;
import
{
Templatable
,
tempstream
}
from
"tempstream"
;
import
{
FormControl
}
from
"./controls/controls"
;
import
{
hasFieldOfType
,
hasShape
,
is
,
predicates
,
}
from
"@sealcode/ts-predicates"
;
import
{
Mountable
,
PageErrorMessage
}
from
"../page/page"
;
import
{
FormField
}
from
"./field"
;
export
type
FormDataPrimitive
=
string
|
string
[]
|
undefined
;
export
type
FormDataValue
=
|
FormDataPrimitive
|
Record
<
string
,
FormDataPrimitive
>
;
export
type
FieldValueType
<
F
extends
FormField
>
=
F
extends
FormField
<
infer
R
>
?
R
:
never
;
export
type
FormFieldsToValues
<
F
extends
Record
<
string
,
FormField
<
keyof
F
>>>
=
{
[
Property
in
keyof
F
]
:
FieldValueType
<
F
[
Property
]
>
;
};
export
type
FormMessage
=
{
type
:
"info"
|
"success"
|
"error"
;
text
:
string
};
export
type
FormData
<
Fieldnames
extends
string
=
string
>
=
{
raw_values
:
Record
<
Fieldnames
,
FormDataValue
>
;
messages
:
FormMessage
[];
};
export
abstract
class
Form
<
Fields
extends
Record
<
string
,
FormField
<
unknown
>>
=
Record
<
string
,
FormField
<
unknown
>
>
>
implements
Mountable
{
abstract
fields
:
Fields
;
abstract
controls
:
FormControl
[];
defaultSuccessMessage
=
"Done"
;
submitButtonText
=
"Wyślij"
;
constructor
()
{
this
.
init
();
}
init
()
:
void
{
for
(
const
[
name
,
field
]
of
Object
.
entries
(
this
.
fields
))
{
field
.
init
(
name
);
}
}
async
canAccess
(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_
:
BaseContext
)
:
Promise
<
{
canAccess
:
boolean
;
message
:
string
}
>
{
return
{
canAccess
:
true
,
message
:
""
};
}
async
renderError
(
_
:
BaseContext
,
error
:
PageErrorMessage
)
:
Promise
<
Templatable
>
{
return
tempstream
/* HTML */
`<div>
${
error
.
message
}
</div>`
;
}
async
validate
(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_
:
BaseContext
,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__
:
Record
<
string
,
unknown
>
)
:
Promise
<
{
valid
:
boolean
;
error
:
string
}
>
{
return
{
valid
:
true
,
error
:
""
,
};
}
private
async
_validate
(
ctx
:
BaseContext
,
values
:
Record
<
string
,
FormDataValue
>
)
:
Promise
<
{
valid
:
boolean
;
errors
:
Record
<
keyof
Fields
|
"form"
,
string
>
;
}
>
{
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const
errors
=
{}
as
Record
<
keyof
Fields
|
"form"
,
string
>
;
let
valid
=
true
;
await
Promise
.
all
(
Object
.
values
(
this
.
fields
).
map
(
async
(
field
)
=>
{
const
parsed_value
=
await
field
.
parse
(
values
[
field
.
name
]);
const
{
valid
:
fieldvalid
,
message
:
fieldmessage
}
=
await
field
.
_validate
(
ctx
,
parsed_value
);
if
(
!
fieldvalid
)
{
valid
=
false
;
errors
[
field
.
name
as
keyof
Fields
|
"form"
]
=
fieldmessage
;
}
})
);
const
formValidationResult
=
await
this
.
validate
(
ctx
,
values
);
if
(
!
formValidationResult
.
valid
)
{
valid
=
false
;
errors
[
"form"
as
keyof
Fields
|
"form"
]
=
formValidationResult
.
error
;
}
return
{
valid
,
errors
};
}
async
render
(
ctx
:
BaseContext
,
data
:
FormData
,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_
:
string
)
:
Promise
<
Templatable
>
{
return
tempstream
/* HTML */
`
${
this
.
makeFormTag
(
`
${
ctx
.
URL
.
pathname
}
/`
)
}
${
!
this
.
controls
.
some
((
control
)
=>
control
.
role
==
"messages"
)
?
this
.
renderMessages
(
ctx
,
data
)
:
""
}
${
this
.
renderControls
(
ctx
,
data
)
}
<input type="submit" value="
${
this
.
submitButtonText
}
"/></form>`
;
}
public
renderMessages
(
_
:
BaseContext
,
data
:
FormData
<
keyof
Fields
>
)
:
Templatable
{
return
tempstream
/* HTML */
`<div class="form-messages">
${
data
.
messages
.
map
(
(
message
)
=>
`<div class="form-message form-message--
${
message
.
type
}
">
${
message
.
text
}
</div>`
)
}
</div>`
;
}
public
renderControls
(
ctx
:
BaseContext
,
data
:
FormData
<
keyof
Fields
>
)
:
Templatable
{
return
tempstream
/* HTML */
`
${
this
.
controls
.
map
((
control
)
=>
control
.
render
(
ctx
,
data
.
raw_values
,
data
.
messages
)
)
}
`
;
}
public
makeFormTag
(
path
:
string
)
{
return
`<form method="POST" action="
${
path
}
">`
;
}
public
async
onValuesInvalid
(
ctx
:
BaseContext
,
form_path
:
string
)
{
ctx
.
status
=
422
;
ctx
.
body
=
await
this
.
render
(
ctx
,
{
raw_values
:
ctx
.
$body
,
messages
:
[{
type
:
"error"
,
text
:
"Some fields are invalid"
}],
},
form_path
);
}
public
async
onError
(
ctx
:
BaseContext
,
data
:
FormData
,
form_path
:
string
,
error
:
unknown
)
:
Promise
<
void
>
{
ctx
.
status
=
422
;
let
error_message
=
"Unknown error has occured"
;
if
(
is
(
error
,
predicates
.
object
)
&&
hasShape
({
message
:
predicates
.
string
},
error
)
)
{
error_message
=
error
.
message
;
}
ctx
.
body
=
await
this
.
render
(
ctx
,
{
raw_values
:
data
.
raw_values
,
messages
:
[{
type
:
"error"
,
text
:
error_message
}],
},
form_path
);
}
public
abstract
onSubmit
(
ctx
:
BaseContext
,
data
:
FormData
)
:
void
|
Promise
<
void
>
;
public
async
onSuccess
(
ctx
:
BaseContext
,
form_path
:
string
)
:
Promise
<
void
>
{
ctx
.
body
=
await
this
.
render
(
ctx
,
{
raw_values
:
ctx
.
$body
,
messages
:
[
{
type
:
"success"
,
text
:
this
.
defaultSuccessMessage
},
],
},
form_path
);
ctx
.
status
=
422
;
}
public
mount
(
router
:
Router
,
path
:
string
)
{
router
.
use
(
path
,
async
(
ctx
,
next
)
=>
{
const
result
=
await
this
.
canAccess
(
ctx
);
if
(
!
result
.
canAccess
)
{
ctx
.
body
=
this
.
renderError
(
ctx
,
{
type
:
"access"
,
message
:
result
.
message
,
});
ctx
.
status
=
403
;
return
;
}
await
next
();
});
router
.
get
(
path
,
async
(
ctx
)
=>
{
ctx
.
type
=
"html"
;
ctx
.
body
=
await
this
.
render
(
ctx
,
{
raw_values
:
{},
messages
:
[]
},
path
);
});
router
.
post
(
path
,
async
(
ctx
)
=>
{
const
{
valid
}
=
await
this
.
_validate
(
ctx
,
ctx
.
$body
);
if
(
!
valid
)
{
await
this
.
onValuesInvalid
(
ctx
,
path
);
return
;
}
try
{
await
this
.
onSubmit
(
ctx
,
{
raw_values
:
ctx
.
$body
,
messages
:
[],
});
await
this
.
onSuccess
(
ctx
,
path
);
}
catch
(
e
:
unknown
)
{
// eslint-disable-next-line no-console
console
.
dir
(
e
,
{
depth
:
5
});
const
message
=
is
(
e
,
predicates
.
object
)
&&
hasFieldOfType
(
e
,
"message"
,
predicates
.
string
)
?
e
?
.
message
:
is
(
e
,
predicates
.
string
)
?
e
:
"Wystąpił błąd"
;
await
this
.
onError
(
ctx
,
{
raw_values
:
ctx
.
$body
,
messages
:
[
{
type
:
"error"
,
text
:
message
,
},
],
},
path
,
e
);
}
});
}
}
File Metadata
Details
Attached
Mime Type
text/x-java
Expires
Sun, Nov 2, 16:30 (1 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1030436
Default Alt Text
form.ts (6 KB)
Attached To
Mode
rSGEN sealgen
Attached
Detach File
Event Timeline
Log In to Comment