All Examples
Embedding
A Foldkit widget embedded in a plain TypeScript host page through Runtime.embed. The host seeds initial state with Flags, pushes a step value in through an inbound Port, mirrors the count the widget emits through an outbound Port, and mounts and unmounts the widget with dispose. All communication crosses one Schema-typed handle; the host never touches the Model.
Embedding
Ports
makeElement
Host Interop
/
import { Duration, Effect, Schema, Stream } from 'effect'
import { Command, Port, Runtime, Subscription, type Update } from 'foldkit'
import { Html, HtmlBuilder } from 'foldkit/html'
import { defineMessageUnion } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Button } from '@foldkit/ui'
// MODEL
export const Model = Schema.Struct({
count: Schema.Number,
step: Schema.Number,
})
export type Model = typeof Model.Type
// MESSAGE
export const Message = defineMessageUnion({
Ticked: {},
ClickedAdvance: {},
ChangedStep: { step: Schema.Number },
CompletedReportCount: {},
})
export type Message = typeof Message.Type
// PORT
export const ports = {
inbound: { stepChanged: Port.inbound(Schema.Number) },
outbound: { countChanged: Port.outbound(Schema.Number) },
}
// INIT
export const Flags = Schema.Struct({ initialCount: Schema.Number })
export type Flags = typeof Flags.Type
export const init: Runtime.ElementInit<Model, Message, Flags> = flags => ({
model: { count: flags.initialCount, step: 1 },
})
// COMMAND
export const ReportCount = Command.define('ReportCount', {
args: { count: Schema.Number },
messages: [Message.CompletedReportCount],
execute: ({ count }) =>
Port.emit(ports.outbound.countChanged, count).pipe(
Effect.as(Message.CompletedReportCount()),
),
})
// UPDATE
type UpdateReturn = Update.Return<Model, Message>
const advance = (model: Model): UpdateReturn => {
const count = model.count + model.step
return {
model: evo(model, { count: () => count }),
commands: [ReportCount({ count })],
}
}
export const update = (model: Model, message: Message) =>
Message.match<UpdateReturn>(message, {
Ticked: () => advance(model),
ClickedAdvance: () => advance(model),
ChangedStep: ({ step }) => ({ model: evo(model, { step: () => step }) }),
CompletedReportCount: () => ({ model }),
})
// SUBSCRIPTION
const TICK_INTERVAL = Duration.seconds(1)
export const subscriptions = Subscription.make<Model, Message>()(_entry => ({
tick: Subscription.persistent(
Stream.tick(TICK_INTERVAL).pipe(Stream.map(Message.Ticked)),
),
hostStep: Port.subscription(ports.inbound.stepChanged, step =>
Message.ChangedStep({ step }),
),
}))
// VIEW
export const view = (model: Model, h: HtmlBuilder<Message>): Html =>
h.div(
[
h.Class(
'flex flex-col items-center gap-4 rounded-xl border border-teal-200 bg-teal-50 p-6',
),
],
[
h.div(
[
h.Class(
'text-xs font-semibold uppercase tracking-wide text-teal-700',
),
],
['Foldkit widget'],
),
h.div(
[h.Class('text-5xl font-bold tabular-nums text-gray-900')],
[String(model.count)],
),
h.div(
[h.Class('text-sm text-gray-600')],
[`Ticking up by ${model.step} every second`],
),
Button.view(
{
onClick: Message.ClickedAdvance(),
toView: attributes =>
h.button(
[
...attributes.button,
h.Class(
'cursor-pointer rounded-lg bg-teal-600 px-4 py-2 text-sm font-semibold text-white hover:bg-teal-500',
),
],
[`Advance by ${model.step}`],
),
},
h,
),
],
)
// PROGRAM
export const makeElement = (container: HTMLElement, flags: Flags) =>
Runtime.makeElement({
Model,
Flags,
flags: Effect.succeed(flags),
init,
update,
view,
subscriptions,
ports,
container,
devTools: {
Message,
},
})