Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F3010134
stateful-page.ts
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
4 KB
Referenced Files
None
Subscribers
None
stateful-page.ts
View Options
import
Router
from
"@koa/router"
;
import
{
predicates
,
hasShape
,
is
}
from
"@sealcode/ts-predicates"
;
import
deepmerge
,
{
ArrayMergeOptions
}
from
"deepmerge"
;
import
{
BaseContext
}
from
"koa"
;
import
{
Templatable
,
tempstream
}
from
"tempstream"
;
import
{
from_base64
,
to_base64
}
from
"../utils/base64.js"
;
import
{
ExtractTail
}
from
"../utils/utils.js"
;
import
{
Mountable
}
from
"./mountable.js"
;
export
abstract
class
StatefulPage
<
State
extends
Record
<
string
,
unknown
>
,
Actions
extends
Record
<
string
,
(
state
:
State
,
inputs
:
Record
<
string
,
string
>
,
...
args
:
unknown
[]
)
=>
State
>
>
extends
Mountable
{
abstract
actions
:
Actions
;
abstract
getInitialState
()
:
State
;
abstract
render
(
ctx
:
BaseContext
,
state
:
State
,
inputs
:
Record
<
string
,
string
>
)
:
Templatable
|
Promise
<
Templatable
>
;
async
canAccess
()
{
return
<
const
>
{
canAccess
:
true
,
message
:
""
};
}
constructor
()
{
super
();
const
original_render
=
this
.
render
.
bind
(
this
);
this
.
render
=
async
(
ctx
:
BaseContext
,
state
:
State
,
inputs
:
Record
<
string
,
string
>
)
=>
{
return
this
.
wrapInLayout
(
ctx
,
this
.
wrapInForm
(
state
,
await
original_render
(
ctx
,
state
,
inputs
)
)
);
};
}
abstract
wrapInLayout
(
ctx
:
BaseContext
,
content
:
Templatable
)
:
Templatable
;
wrapInForm
(
state
:
State
,
content
:
Templatable
)
:
Templatable
{
return
tempstream
/* HTML */
`<form action="./" method="POST">
<input
name="state"
type="hidden"
value="
${
to_base64
(
JSON
.
stringify
(
state
))
}
"
/>
${
content
}
</form>`
;
}
makeActionButton
<
ActionName
extends
keyof
Actions
>
(
state
:
State
,
action_description
:
|
{
action
:
ActionName
;
label
?:
string
;
}
|
ActionName
,
...
args
:
ExtractTail
<
ExtractTail
<
Parameters
<
Actions
[
ActionName
]
>>>
)
{
let
label
,
action
:
string
;
if
(
is
(
action_description
,
predicates
.
object
))
{
action
=
action_description
.
action
.
toString
();
label
=
action_description
.
label
||
action
;
}
else
{
action
=
action_description
.
toString
();
label
=
action
;
}
return
/* HTML */
`
<input
type="submit"
value=
${
label
}
formaction="./?action=
${
action
}
&args=
${
to_base64
(
JSON
.
stringify
(
args
)
)
}
"
/>
`
;
}
extractState
(
ctx
:
BaseContext
)
:
|
{
state
:
State
;
inputs
:
Record
<
string
,
string
>
;
action
:
keyof
Actions
;
action_args
:
string
;
}
|
{
state
:
State
;
inputs
:
Record
<
string
,
string
>
;
action
:
null
;
action_args
:
null
;
}
{
if
(
!
hasShape
(
{
action
:
predicates
.
maybe
(
predicates
.
string
),
state
:
predicates
.
string
,
args
:
predicates
.
maybe
(
predicates
.
string
),
$
:
predicates
.
maybe
(
predicates
.
object
),
},
ctx
.
$body
)
)
{
console
.
error
(
"Wrong data: "
,
ctx
.
$body
);
throw
new
Error
(
"wrong formdata shape"
);
}
const
inputs
=
Object
.
fromEntries
(
Object
.
entries
(
ctx
.
$body
).
filter
(
([
key
])
=>
!
[
"action"
,
"state"
,
"args"
,
"$"
].
includes
(
key
)
)
)
as
Record
<
string
,
string
>
;
// the "$" key is parsed as dot notation and overrides the state
const
original_state
=
JSON
.
parse
(
from_base64
(
ctx
.
$body
.
state
));
const
modified_state
=
deepmerge
(
original_state
,
ctx
.
$body
.
$
||
{},
{
arrayMerge
:
(
_target
:
any
[],
source
:
any
[],
_options
:
ArrayMergeOptions
)
=>
{
// https://github.com/TehShrike/deepmerge#user-content-arraymerge-example-combine-arrays
return
source
;
},
})
as
State
;
if
(
ctx
.
$body
.
action
&&
ctx
.
$body
.
args
)
{
return
{
state
:
modified_state
,
action
:
ctx
.
$body
.
action
as
string
,
inputs
,
action_args
:
ctx
.
$body
.
args
,
};
}
else
{
return
{
state
:
modified_state
,
action
:
null
,
action_args
:
null
,
inputs
,
};
}
}
mount
(
router
:
Router
,
path
:
string
)
{
router
.
get
(
path
,
(
ctx
)
=>
{
ctx
.
body
=
this
.
render
(
ctx
,
this
.
getInitialState
(),
{});
});
router
.
post
(
path
,
(
ctx
)
=>
{
const
{
action
,
state
,
inputs
,
action_args
}
=
this
.
extractState
(
ctx
);
if
(
action
)
{
ctx
.
body
=
this
.
render
(
ctx
,
this
.
actions
[
action
](
state
,
inputs
,
...(
JSON
.
parse
(
from_base64
(
action_args
as
string
)
)
as
unknown
[])
),
inputs
);
}
else
{
ctx
.
body
=
this
.
render
(
ctx
,
state
,
inputs
);
}
ctx
.
status
=
433
;
});
}
}
File Metadata
Details
Attached
Mime Type
text/x-java
Expires
Wed, May 7, 19:37 (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
663660
Default Alt Text
stateful-page.ts (4 KB)
Attached To
Mode
rSGEN sealgen
Attached
Detach File
Event Timeline
Log In to Comment