create-foldkit-app 0.29.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/templates/base/AGENTS.md +5 -112
- package/dist/templates/base/FOLDKIT.md +144 -0
- package/dist/templates/examples/api-cache/src/main.ts +36 -40
- package/dist/templates/examples/auth/src/main.ts +16 -15
- package/dist/templates/examples/auth/src/page/loggedIn/update.ts +10 -11
- package/dist/templates/examples/auth/src/page/loggedOut/page/login.ts +44 -55
- package/dist/templates/examples/auth/src/page/loggedOut/update.ts +8 -12
- package/dist/templates/examples/auth/src/update.ts +67 -73
- package/dist/templates/examples/canvas-art/src/main.ts +32 -31
- package/dist/templates/examples/charting/src/init.test.ts +6 -3
- package/dist/templates/examples/charting/src/init.ts +4 -4
- package/dist/templates/examples/charting/src/update.ts +34 -38
- package/dist/templates/examples/counter/src/main.ts +13 -13
- package/dist/templates/examples/counters/src/counter.ts +9 -8
- package/dist/templates/examples/counters/src/main.ts +20 -26
- package/dist/templates/examples/crash-view/src/main.ts +5 -3
- package/dist/templates/examples/embedding/src/main.ts +11 -9
- package/dist/templates/examples/form/src/main.ts +79 -89
- package/dist/templates/examples/generative-art/src/main.ts +6 -4
- package/dist/templates/examples/generative-art/src/update.ts +26 -23
- package/dist/templates/examples/interrupting-commands/src/main.ts +68 -66
- package/dist/templates/examples/job-application/src/main.ts +3 -4
- package/dist/templates/examples/job-application/src/step/attachments.ts +16 -19
- package/dist/templates/examples/job-application/src/step/coverLetter.ts +5 -5
- package/dist/templates/examples/job-application/src/step/education/education.ts +12 -17
- package/dist/templates/examples/job-application/src/step/education/entry.ts +29 -42
- package/dist/templates/examples/job-application/src/step/personalInfo.ts +35 -44
- package/dist/templates/examples/job-application/src/step/skills/entry.ts +15 -21
- package/dist/templates/examples/job-application/src/step/skills/skills.ts +12 -17
- package/dist/templates/examples/job-application/src/step/workHistory/entry.ts +41 -58
- package/dist/templates/examples/job-application/src/step/workHistory/workHistory.ts +12 -17
- package/dist/templates/examples/job-application/src/update.ts +26 -29
- package/dist/templates/examples/kanban/src/main.ts +3 -4
- package/dist/templates/examples/kanban/src/update.ts +31 -31
- package/dist/templates/examples/managed-resource-layer/src/main.ts +30 -34
- package/dist/templates/examples/map/src/main.ts +48 -56
- package/dist/templates/examples/pixel-art/src/comparison.bench.test.ts +34 -13
- package/dist/templates/examples/pixel-art/src/main.bench.ts +3 -3
- package/dist/templates/examples/pixel-art/src/main.ts +3 -4
- package/dist/templates/examples/pixel-art/src/update.ts +109 -100
- package/dist/templates/examples/query-sync/src/main.ts +27 -25
- package/dist/templates/examples/route-transitions/src/main.ts +46 -43
- package/dist/templates/examples/route-transitions/src/story.test.ts +11 -11
- package/dist/templates/examples/routing/src/main.ts +16 -14
- package/dist/templates/examples/routing/src/page/people.ts +23 -28
- package/dist/templates/examples/shopping-cart/src/main.ts +41 -42
- package/dist/templates/examples/shopping-cart/src/page/products.ts +28 -35
- package/dist/templates/examples/slow-warnings/src/main.ts +23 -32
- package/dist/templates/examples/snake/src/main.ts +135 -143
- package/dist/templates/examples/ssg/src/main.ts +20 -17
- package/dist/templates/examples/ssr/src/main.ts +20 -24
- package/dist/templates/examples/state-machine/src/main.ts +24 -11
- package/dist/templates/examples/state-machine/src/story.test.ts +2 -2
- package/dist/templates/examples/stopwatch/src/main.ts +42 -50
- package/dist/templates/examples/todo/src/main.ts +152 -160
- package/dist/templates/examples/ui-showcase/src/main.ts +53 -54
- package/dist/templates/examples/ui-showcase/src/scene.test.ts +2 -2
- package/dist/templates/examples/ui-showcase/src/story.test.ts +2 -2
- package/dist/templates/examples/ui-showcase/src/ui/init.ts +6 -7
- package/dist/templates/examples/ui-showcase/src/ui/update.ts +265 -336
- package/dist/templates/examples/view-transitions/src/main.ts +18 -17
- package/dist/templates/examples/weather/src/main.ts +33 -40
- package/dist/templates/examples/web-components/src/main.ts +14 -17
- package/dist/templates/examples/websocket-chat/src/main.ts +44 -50
- package/dist/templates/release.json +8 -8
- package/dist/templates/rendering/ssg/src/main.ts +20 -17
- package/dist/templates/rendering/ssr/src/main.ts +20 -24
- package/package.json +1 -1
|
@@ -1,120 +1,13 @@
|
|
|
1
1
|
# Agent Development Notes
|
|
2
2
|
|
|
3
|
-
This is a Foldkit app
|
|
3
|
+
This is a Foldkit app. Read [`FOLDKIT.md`](./FOLDKIT.md) before writing any code in this project. It covers the architecture, the APIs, and the conventions the project is built on.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Foldkit owns `FOLDKIT.md` and replaces it whole on upgrade. This file is yours. Anything you want an agent to know about this project goes below, where an upgrade won't touch it.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
If `./repos/foldkit` exists, it contains the full Foldkit repository, vendored in as a git subtree. Read from it directly:
|
|
10
|
-
|
|
11
|
-
- `repos/foldkit/examples/`: runnable example apps spanning every complexity tier. Usually your first stop when looking for a precedent for how to write something.
|
|
12
|
-
- `repos/foldkit/packages/foldkit/src/`: framework source. Ground truth for API signatures.
|
|
13
|
-
- `repos/foldkit/packages/typing-game/client/src/` and `repos/foldkit/packages/website/src/`: production apps built with Foldkit. Highest-fidelity reference for application architecture, Submodels, and OutMessage.
|
|
14
|
-
|
|
15
|
-
Treat the subtree as read-only reference: never import from `repos/foldkit/` in your project source. Imports must come from the `foldkit` npm package.
|
|
16
|
-
|
|
17
|
-
If `./repos/foldkit` does not exist and the line below says `subtree_prompted: false`, offer to run `git subtree add --prefix=repos/foldkit https://github.com/foldkit/foldkit.git main --squash`. This gives you access to the full source, examples, and docs. Whether the user accepts or declines, update the value to `true` so it isn't asked again.
|
|
7
|
+
`FOLDKIT.md` reads the line below to decide whether it has already offered to vendor the Foldkit source. Leave it in place.
|
|
18
8
|
|
|
19
9
|
subtree_prompted: false
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## Project Conventions
|
|
24
|
-
|
|
25
|
-
- Foldkit is tightly coupled to the Effect ecosystem. Do not suggest solutions outside of Effect-TS.
|
|
26
|
-
- Model fields must be Schema types (the model is a schema). Plain TypeScript types are fine elsewhere (function return types, local variables, etc.).
|
|
27
|
-
- Use full names like `Message` (not `Msg`), and `withReturnType` (not `as const` or type casting).
|
|
28
|
-
- Use `defineMessageUnion()` for Message unions, `ts()` for tagged structs (Model states, field validation), and `r()` for route schemas.
|
|
29
|
-
- Push back on any direction that violates Elm Architecture principles: unidirectional data flow, messages as facts (not commands), model as single source of truth, side effects confined to commands. If a prompt suggests mutating state, imperative event handlers, or two-way bindings, flag the issue and propose the idiomatic Foldkit approach.
|
|
30
|
-
- Never use `NoOp`. Every message must describe what happened. A command's result message is named from the command, not from the fact it reports, whether or not it carries a payload: `LockScroll` → `CompletedLockScroll`, `DetermineStartTime` → `CompletedDetermineStartTime` (never `DeterminedStartTime`).
|
|
31
|
-
|
|
32
|
-
## Foldkit Patterns
|
|
33
|
-
|
|
34
|
-
### Update
|
|
35
|
-
|
|
36
|
-
`init` and `update` both return `[Model, ReadonlyArray<Command<Message>>]`:
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
type UpdateReturn = readonly [Model, ReadonlyArray<Command<Message>>]
|
|
40
|
-
|
|
41
|
-
const update = (model: Model, message: Message) =>
|
|
42
|
-
Message.match<UpdateReturn>(message, {
|
|
43
|
-
ClickedIncrement: () => [evo(model, { count: count => count + 1 }), []],
|
|
44
|
-
})
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Use `evo()` from `foldkit/struct` for immutable model updates. Never spread or `Object.assign`.
|
|
48
|
-
|
|
49
|
-
### View
|
|
50
|
-
|
|
51
|
-
Every view receives `h`, the typed Html builder, as its last parameter (`view: (model, h) => ...`; `Submodel.defineView` passes the child's own). Never construct a builder; reach for `h.div`, `h.OnClick`, etc. off the parameter, and give extracted view helpers an `h: HtmlBuilder<Message>` last parameter that callers thread through. Only where no builder is in scope, typically module scope, use `inertHtml` from `foldkit/html`, aliased `ih` so the two builders stay distinguishable. Use `h.empty` (not `null`) for conditional rendering, `M.value().pipe(M.tagsExhaustive({...}))` for discriminated unions, and `Array.match` for lists that may be empty.
|
|
52
|
-
|
|
53
|
-
Keys are for mapped list items only: key each row by a stable Model identifier (`h.keyed('li')(item.id, [], [...])`), never by array position, and never derive a key from displayed data. Never key branches; the build gives each view function's output its own identity, so branch switches replace DOM automatically. When switching an inline same-tag ternary must reset DOM state, extract each arm into its own named view function.
|
|
54
|
-
|
|
55
|
-
Omit the children argument when an element has none: `h.div([h.Class('divider')])`, never `h.div([h.Class('divider')], [])`. The same holds for `keyed`: `h.keyed('li')(key, [attrs])`, never `h.keyed('li')(key, [attrs], [])`. Attributes stay required on element builders, so `h.div([])` is an element with neither. Sibling elements that end up at different arities are expected and fine; void elements like `h.img` have always read that way.
|
|
56
|
-
|
|
57
|
-
### Commands
|
|
58
|
-
|
|
59
|
-
Define a Command with `Command.define(name, { args, messages, execute })`; omit `args` when the Command takes none. Assign definitions to PascalCase constants. Never inline in pipe chains. Name the effect `execute` performs, not the later Model transition caused when update handles its result: a timer that only waits before update starts a dismissal is `WaitBeforeDismissal`, not `DismissAfter`. Commands catch all errors via `Effect.catch(() => Effect.succeed(Message.FailedX(...)))` so side effects never crash the app. Definitions live colocated with the update function that returns them.
|
|
60
|
-
|
|
61
|
-
For the with-args shape, see `repos/foldkit/examples/weather/src/main.ts` or `repos/foldkit/examples/kanban/src/command.ts`. For an argless DOM-side-effect Command, the argless form in `kanban/src/command.ts` (`FocusAddCardInput`) is the canonical reference.
|
|
62
|
-
|
|
63
|
-
For DOM operations (focus, scroll, modals, scroll lock), Foldkit ships a `Dom` module. For time, randomness, UUIDs, and delays, use Effect's built-ins directly (`Clock`, `Random`, `Effect.uuid`, `Effect.sleep`). Don't reach for raw `document.querySelector`, `setTimeout`, `Date.now()`, or `Math.random()`.
|
|
64
|
-
|
|
65
|
-
### File Organization
|
|
66
|
-
|
|
67
|
-
The invariant: keep the runtime boot separate from the pure definitions. `src/entry.ts` calls `Runtime.makeApplication` and `Runtime.run`, and `index.html` references it. The definitions (Model, Messages, init, update, view, Commands) never call `Runtime.run`, so they stay importable from tests without booting a runtime as a side effect. Never call `Runtime.run` from `main.ts`.
|
|
68
|
-
|
|
69
|
-
For a small app the definitions all fit in one `src/main.ts`. Split a unit into its own file when it has _both_ a distinct reason to change _and_ a name you'd give it unprompted: the pure domain core into `timer.ts` or `domain.ts`, the view into `view.ts` (or a `components/` directory), a Command's owned resource into its own module. Split on that revealed seam, not on line count alone. A file that has grown large is _evidence_ a seam has formed, so treat its size as a prompt to re-check for one. Two splits are forced: extract Messages to `message.ts` when Commands need the constructors (this breaks the cycle between `command.ts` and `main.ts`), and colocate Commands with the update that returns them. Exemplars: counter and stopwatch are a single `main.ts`; kanban splits `domain` / `command` / `message` / `model`; typing-game splits views by page.
|
|
70
|
-
|
|
71
|
-
Use uppercase section headers (`// MODEL`, `// MESSAGE`, `// INIT`, `// UPDATE`, `// COMMAND`, `// VIEW`) for wayfinding.
|
|
72
|
-
|
|
73
|
-
### Testing
|
|
74
|
-
|
|
75
|
-
Test update functions with `foldkit/test`. Since update is pure, tests run without a runtime, DOM, or side effects. Use `story` for update-level tests (send Messages, assert on Model and Commands) and `scene` for feature-level testing through the view with accessible locators.
|
|
76
|
-
|
|
77
|
-
Import the steps as named imports from `foldkit/story` or `foldkit/scene`: `import { Command, given, message, model, story } from 'foldkit/story'`. A test file needs only one of the two modules. If a single file ever tests both, import the namespaces instead (`import { Scene, Story } from 'foldkit'`) so `Story.given` and `Scene.given` stay distinguishable.
|
|
78
|
-
|
|
79
|
-
Name each test file for its test style, beside the code under test: `story.test.ts` for the Story tests (which drive `update`) and `scene.test.ts` for the Scene tests (which drive the rendered view). The name describes how the test works, not a source file, so it stays correct whether `update` and `view` live in `main.ts` or in their own files. A test file lives in the folder that holds the code it drives, so in a multi-page app most of them sit in a page folder rather than at the root. When one folder holds more than one test of a kind (sibling pages, component variants), prefix with the subject: `login.story.test.ts`.
|
|
80
|
-
|
|
81
|
-
Scene runs at any level, since a page's own `update`/`view` pair drops into `scene` unmodified. Put a `scene.test.ts` in the page folder for behavior that page owns, which covers view states awkward to reach through the root Model, and keep a root-level `scene.test.ts` for flows that cross pages, which covers how the parent folds an OutMessage, a Command the parent lifts, a route change, and view inputs the parent computes. If the `repos/foldkit` subtree is available, study the `story.test.ts` and `scene.test.ts` files in `repos/foldkit/examples/`. `repos/foldkit/examples/auth` is the multi-page shape: a root `src/scene.test.ts` for the cross-page login flow alongside `src/page/loggedOut/page/login.scene.test.ts` and `login.story.test.ts` driving that page's own pair.
|
|
82
|
-
|
|
83
|
-
## Code Style
|
|
84
|
-
|
|
85
|
-
- Encode state in discriminated unions, not booleans or nullable fields. `Idle | Loading | Error | Ok`, not `isLoading: boolean`. Make impossible states unrepresentable.
|
|
86
|
-
- Use `Option` instead of `null` or `undefined`. Prefix Option-typed values with `maybe*`. Match with `Option.match`; don't unwrap with `Option.map(...)` + `Option.getOrElse(...)` when you can just match.
|
|
87
|
-
- Use Effect modules over native methods in `pipe` chains (`Array.map`, `String.startsWith`, `Array.findFirst`). Native methods are fine when calling directly on a named variable.
|
|
88
|
-
- Never cast Schema values with `as Type`. Use the callable constructor: `Message.SucceededLogin({ sessionId })`, not `{ _tag: 'SucceededLogin', sessionId } as Message`.
|
|
89
|
-
- Always `Array.isArrayEmpty` / `Array.isArrayNonEmpty` (not `.length === 0` / `.length > 0`). Use `Array.match` when handling both empty and non-empty cases.
|
|
90
|
-
- Never use `for` loops or `let` for iteration. Reach for `Array.map`, `Array.filterMap`, `Array.makeBy`, `Array.reduce`.
|
|
91
|
-
- Never use `T[]`. Always `Array<T>` or `ReadonlyArray<T>`.
|
|
92
|
-
- Use `Message.match` for exhaustive Message matching. Use Effect `Match` for other tagged unions, partial matching, fallbacks, and one handler shared across multiple tags. Never use `switch`.
|
|
93
|
-
- Always use braces for control flow: `if (foo) { return true }`.
|
|
94
|
-
- Don't add inline comments to explain code. Use better names instead. Reserve `// NOTE:` for behavior that would mislead a careful reader.
|
|
95
|
-
|
|
96
|
-
## Message Layout
|
|
97
|
-
|
|
98
|
-
Declare the whole Message union with `defineMessageUnion()`, then put `type Message = typeof Message.Type` on the next line:
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
const Message = defineMessageUnion({
|
|
102
|
-
ClickedSubmit: {},
|
|
103
|
-
UpdatedEmail: { value: S.String },
|
|
104
|
-
})
|
|
105
|
-
type Message = typeof Message.Type
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Keep the `defineMessageUnion()` declaration and `type Message` alias adjacent. Construct values through the namespace (`Message.ClickedSubmit()`) and handle the union with `Message.match`. Never destructure constructors from `Message` or `OutMessage`; the owning namespace stays visible at every call site.
|
|
109
|
-
|
|
110
|
-
Keep each case's payload object on one line when it fits. Let Prettier wrap payloads that need more space, so the declaration remains easy to scan as one variant per line.
|
|
111
|
-
|
|
112
|
-
Messages are verb-first past-tense. Common prefixes: `Clicked*`, `Updated*` (input changes and external state updates), `Submitted*`, `Pressed*`, `Selected*`, `Succeeded*` / `Failed*` (paired async results), `Completed*` (every other Command result), `Got*` (child OutMessage in the Submodel pattern).
|
|
113
|
-
|
|
114
|
-
## Debugging
|
|
115
|
-
|
|
116
|
-
This project ships with `@foldkit/devtools-mcp` pre-wired. When the dev server is running and the app is open in a browser, `foldkit_*` MCP tools let you inspect Model, Message history, and time-travel. Reach for them before adding `console.log` whenever the question is about state or Message flow.
|
|
117
|
-
|
|
118
|
-
## Going Deeper
|
|
11
|
+
## Project Notes
|
|
119
12
|
|
|
120
|
-
|
|
13
|
+
Domain vocabulary, deployment steps, local conventions that differ from Foldkit's defaults: write them here.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Foldkit Conventions
|
|
2
|
+
|
|
3
|
+
Foldkit owns this file. `create-foldkit-app` writes it and upgrades replace it whole, so anything added here is lost. Project-specific instructions go in `AGENTS.md`.
|
|
4
|
+
|
|
5
|
+
This is a Foldkit app, a framework built on Effect-TS, architected like Elm.
|
|
6
|
+
|
|
7
|
+
## Getting the canonical references
|
|
8
|
+
|
|
9
|
+
The live Foldkit code is the canonical reference for everything: which APIs exist, what idiomatic apps look like, what the current conventions are. Written docs (including this one) can drift; the code can't.
|
|
10
|
+
|
|
11
|
+
If `./repos/foldkit` exists, it contains the full Foldkit repository, vendored in as a git subtree. Read from it directly:
|
|
12
|
+
|
|
13
|
+
- `repos/foldkit/examples/`: runnable example apps spanning every complexity tier. Usually your first stop when looking for a precedent for how to write something.
|
|
14
|
+
- `repos/foldkit/packages/foldkit/src/`: framework source. Ground truth for API signatures.
|
|
15
|
+
- `repos/foldkit/packages/typing-game/client/src/` and `repos/foldkit/packages/website/src/`: production apps built with Foldkit. Highest-fidelity reference for application architecture, Submodels, and OutMessage.
|
|
16
|
+
|
|
17
|
+
Treat the subtree as read-only reference: never import from `repos/foldkit/` in your project source. Imports must come from the `foldkit` npm package.
|
|
18
|
+
|
|
19
|
+
If `./repos/foldkit` does not exist and `AGENTS.md` says `subtree_prompted: false`, offer to run `git subtree add --prefix=repos/foldkit https://github.com/foldkit/foldkit.git main --squash`. This gives you access to the full source, examples, and docs. Whether the user accepts or declines, set that line in `AGENTS.md` to `true` so it isn't asked again.
|
|
20
|
+
|
|
21
|
+
If `foldkit-skills` is installed as a Claude Code plugin, the `generate-program` and `audit-program` skills carry snapshot architecture and conventions guides synced from the live code.
|
|
22
|
+
|
|
23
|
+
## Keeping this file current
|
|
24
|
+
|
|
25
|
+
Foldkit's APIs and conventions change, and this file changes with them. A stale copy sends agents after APIs the installed packages no longer export.
|
|
26
|
+
|
|
27
|
+
Replace it whenever the project upgrades its Foldkit packages. If `./repos/foldkit` exists, copy `repos/foldkit/packages/create-foldkit-app/templates/base/FOLDKIT.md` over this file. Otherwise take the [current template on GitHub](https://github.com/foldkit/foldkit/blob/main/packages/create-foldkit-app/templates/base/FOLDKIT.md). There is nothing here to merge or preserve.
|
|
28
|
+
|
|
29
|
+
## Project Conventions
|
|
30
|
+
|
|
31
|
+
- Foldkit is tightly coupled to the Effect ecosystem. Do not suggest solutions outside of Effect-TS.
|
|
32
|
+
- Model fields must be Schema types (the model is a schema). Plain TypeScript types are fine elsewhere (function return types, local variables, etc.).
|
|
33
|
+
- Use full names like `Message` (not `Msg`), and `withReturnType` (not `as const` or type casting).
|
|
34
|
+
- Use `defineMessageUnion()` for Message unions, `ts()` for tagged structs (Model states, field validation), and `r()` for route schemas.
|
|
35
|
+
- Push back on any direction that violates Elm Architecture principles: unidirectional data flow, messages as facts (not commands), model as single source of truth, side effects confined to commands. If a prompt suggests mutating state, imperative event handlers, or two-way bindings, flag the issue and propose the idiomatic Foldkit approach.
|
|
36
|
+
- Never use `NoOp`. Every message must describe what happened. A command's result message is named from the command, not from the fact it reports, whether or not it carries a payload: `LockScroll` → `CompletedLockScroll`, `DetermineStartTime` → `CompletedDetermineStartTime` (never `DeterminedStartTime`).
|
|
37
|
+
|
|
38
|
+
## Foldkit Patterns
|
|
39
|
+
|
|
40
|
+
### Update
|
|
41
|
+
|
|
42
|
+
`init` and `update` both return a record with the next Model and optional Commands. Inline the return type when the matcher is its only use:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const update = (model: Model, message: Message) =>
|
|
46
|
+
Message.match<Update.Return<Model, Message>>(message, {
|
|
47
|
+
ClickedIncrement: () => ({
|
|
48
|
+
model: evo(model, { count: count => count + 1 }),
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Create an `UpdateReturn` alias when another matcher, helper, or exported signature reuses the type. The match generic constrains the whole update, so do not repeat the return annotation on the function.
|
|
54
|
+
|
|
55
|
+
Update, init, boot, and component helper producers return `{ model }` when they statically create no Commands. When they compute a Commands collection, return it directly without checking whether it is empty. Never write the literal `commands: []`; `foldkit/no-empty-commands-array` enforces this producer convention.
|
|
56
|
+
|
|
57
|
+
When composing one of those results, bind the whole result to a value named after the operation and access its fields through that value. Use `homeInit`, `dialogClose`, or a trailing underscore such as `init_` when the operation name collides with the function. Do not destructure or rename `model`, `commands`, or `outMessage`. Dot access does not prevent someone from ignoring `outMessage`; it keeps the operation and all of its returned fields visible together. Name a child fold's `write` parameter after the next child Model, such as `nextSettings`.
|
|
58
|
+
|
|
59
|
+
Pass optional Commands directly to APIs that accept them: `Command.mapMessages(homeInit.commands, toParentMessage)`. Use `result.commands ?? []` only when the next operation requires a concrete array for spreading, concatenating, execution, or an assertion.
|
|
60
|
+
|
|
61
|
+
`Update.Return<Model, Message>` rejects an OutMessage-producing result where an API would consume only its Model and Commands. `Update.ReturnWithOutMessage<Model, Message, OutMessage>` still accepts a result with no `outMessage`, because omission means the update emitted nothing.
|
|
62
|
+
|
|
63
|
+
Manual unpacking of a child result usually means the site should use `Update.foldChild` or `Update.foldChildStep`.
|
|
64
|
+
|
|
65
|
+
Use `Update.combine` when a later Step should receive the Model produced by an earlier Step. It takes two or more Steps. Do not wrap one Step in `Update.combine`; call that operation directly. Name an inline Step parameter `stepModel`; it contains the Model produced by the preceding Step.
|
|
66
|
+
|
|
67
|
+
When the OutMessage is already known while constructing a new result, include it directly: `{ model, commands, outMessage }`. Use `Update.withOutMessage` when attaching an OutMessage to an existing plain return or when the value has the type `OutMessage | undefined`. Pipe an existing return into the helper: `pipe(dialogClose, Update.withOutMessage(outMessage))`. When constructing the plain return in the same expression, pass it first: `Update.withOutMessage({ model, commands }, outMessage)`.
|
|
68
|
+
|
|
69
|
+
Add `toParentOutMessage` only when at least one child OutMessage is forwarded from the current Submodel to its parent. Omit it when no variant is forwarded. Local handling through `foldOutMessage` is independent, so a forwarded variant may also update the current parent. Never write `toParentOutMessage: () => undefined`.
|
|
70
|
+
|
|
71
|
+
Use `evo()` from `foldkit/struct` for immutable model updates. Never spread or `Object.assign`.
|
|
72
|
+
|
|
73
|
+
### View
|
|
74
|
+
|
|
75
|
+
Every view receives `h`, the typed Html builder, as its last parameter (`view: (model, h) => ...`; `Submodel.defineView` passes the child's own). Never construct a builder; reach for `h.div`, `h.OnClick`, etc. off the parameter, and give extracted view helpers an `h: HtmlBuilder<Message>` last parameter that callers thread through. Only where no builder is in scope, typically module scope, use `inertHtml` from `foldkit/html`, aliased `ih` so the two builders stay distinguishable. Use `h.empty` (not `null`) for conditional rendering, `M.value().pipe(M.tagsExhaustive({...}))` for discriminated unions, and `Array.match` for lists that may be empty.
|
|
76
|
+
|
|
77
|
+
Keys are for mapped list items only: key each row by a stable Model identifier (`h.keyed('li')(item.id, [], [...])`), never by array position, and never derive a key from displayed data. Never key branches; the build gives each view function's output its own identity, so branch switches replace DOM automatically. When switching an inline same-tag ternary must reset DOM state, extract each arm into its own named view function.
|
|
78
|
+
|
|
79
|
+
Omit the children argument when an element has none: `h.div([h.Class('divider')])`, never `h.div([h.Class('divider')], [])`. The same holds for `keyed`: `h.keyed('li')(key, [attrs])`, never `h.keyed('li')(key, [attrs], [])`. Attributes stay required on element builders, so `h.div([])` is an element with neither. Sibling elements that end up at different arities are expected and fine; void elements like `h.img` have always read that way.
|
|
80
|
+
|
|
81
|
+
### Commands
|
|
82
|
+
|
|
83
|
+
Define a Command with `Command.define(name, { args, messages, execute })`; omit `args` when the Command takes none. Assign definitions to PascalCase constants. Never inline in pipe chains. Name the effect `execute` performs, not the later Model transition caused when update handles its result: a timer that only waits before update starts a dismissal is `WaitBeforeDismissal`, not `DismissAfter`. Commands catch all errors via `Effect.catch(() => Effect.succeed(Message.FailedX(...)))` so side effects never crash the app. Definitions live colocated with the update function that returns them.
|
|
84
|
+
|
|
85
|
+
For the with-args shape, see `repos/foldkit/examples/weather/src/main.ts` or `repos/foldkit/examples/kanban/src/command.ts`. For an argless DOM-side-effect Command, the argless form in `kanban/src/command.ts` (`FocusAddCardInput`) is the canonical reference.
|
|
86
|
+
|
|
87
|
+
For DOM operations (focus, scroll, modals, scroll lock), Foldkit ships a `Dom` module. For time, randomness, UUIDs, and delays, use Effect's built-ins directly (`Clock`, `Random`, `Effect.uuid`, `Effect.sleep`). Don't reach for raw `document.querySelector`, `setTimeout`, `Date.now()`, or `Math.random()`.
|
|
88
|
+
|
|
89
|
+
### File Organization
|
|
90
|
+
|
|
91
|
+
The invariant: keep the runtime boot separate from the pure definitions. `src/entry.ts` calls `Runtime.makeApplication` and `Runtime.run`, and `index.html` references it. The definitions (Model, Messages, init, update, view, Commands) never call `Runtime.run`, so they stay importable from tests without booting a runtime as a side effect. Never call `Runtime.run` from `main.ts`.
|
|
92
|
+
|
|
93
|
+
For a small app the definitions all fit in one `src/main.ts`. Split a unit into its own file when it has _both_ a distinct reason to change _and_ a name you'd give it unprompted: the pure domain core into `timer.ts` or `domain.ts`, the view into `view.ts` (or a `components/` directory), a Command's owned resource into its own module. Split on that revealed seam, not on line count alone. A file that has grown large is _evidence_ a seam has formed, so treat its size as a prompt to re-check for one. Two splits are forced: extract Messages to `message.ts` when Commands need the constructors (this breaks the cycle between `command.ts` and `main.ts`), and colocate Commands with the update that returns them. Exemplars: counter and stopwatch are a single `main.ts`; kanban splits `domain` / `command` / `message` / `model`; typing-game splits views by page.
|
|
94
|
+
|
|
95
|
+
Use uppercase section headers (`// MODEL`, `// MESSAGE`, `// INIT`, `// UPDATE`, `// COMMAND`, `// VIEW`) for wayfinding.
|
|
96
|
+
|
|
97
|
+
### Testing
|
|
98
|
+
|
|
99
|
+
Test update functions with `foldkit/test`. Since update is pure, tests run without a runtime, DOM, or side effects. Use `story` for update-level tests (send Messages, assert on Model and Commands) and `scene` for feature-level testing through the view with accessible locators.
|
|
100
|
+
|
|
101
|
+
Import the steps as named imports from `foldkit/story` or `foldkit/scene`: `import { Command, given, message, model, story } from 'foldkit/story'`. A test file needs only one of the two modules. If a single file ever tests both, import the namespaces instead (`import { Scene, Story } from 'foldkit'`) so `Story.given` and `Scene.given` stay distinguishable.
|
|
102
|
+
|
|
103
|
+
Name each test file for its test style, beside the code under test: `story.test.ts` for the Story tests (which drive `update`) and `scene.test.ts` for the Scene tests (which drive the rendered view). The name describes how the test works, not a source file, so it stays correct whether `update` and `view` live in `main.ts` or in their own files. A test file lives in the folder that holds the code it drives, so in a multi-page app most of them sit in a page folder rather than at the root. When one folder holds more than one test of a kind (sibling pages, component variants), prefix with the subject: `login.story.test.ts`.
|
|
104
|
+
|
|
105
|
+
Scene runs at any level, since a page's own `update`/`view` pair drops into `scene` unmodified. Put a `scene.test.ts` in the page folder for behavior that page owns, which covers view states awkward to reach through the root Model, and keep a root-level `scene.test.ts` for flows that cross pages, which covers how the parent folds an OutMessage, a Command the parent lifts, a route change, and view inputs the parent computes. If the `repos/foldkit` subtree is available, study the `story.test.ts` and `scene.test.ts` files in `repos/foldkit/examples/`. `repos/foldkit/examples/auth` is the multi-page shape: a root `src/scene.test.ts` for the cross-page login flow alongside `src/page/loggedOut/page/login.scene.test.ts` and `login.story.test.ts` driving that page's own pair.
|
|
106
|
+
|
|
107
|
+
## Code Style
|
|
108
|
+
|
|
109
|
+
- Encode state in discriminated unions, not booleans or nullable fields. `Idle | Loading | Error | Ok`, not `isLoading: boolean`. Make impossible states unrepresentable.
|
|
110
|
+
- Use `Option` for absence in the Model and domain values instead of `null` or `undefined`. Foldkit return records are the exception: omit `commands` and `outMessage` when absent. A partial `toParentOutMessage` mapper returns `undefined` for each named child variant that stops at the current parent. Prefix Option-typed values with `maybe*`. Match with `Option.match`; don't unwrap with `Option.map(...)` + `Option.getOrElse(...)` when you can just match.
|
|
111
|
+
- Use Effect modules over native methods in `pipe` chains (`Array.map`, `String.startsWith`, `Array.findFirst`). Native methods are fine when calling directly on a named variable.
|
|
112
|
+
- Never cast Schema values with `as Type`. Use the callable constructor: `Message.SucceededLogin({ sessionId })`, not `{ _tag: 'SucceededLogin', sessionId } as Message`.
|
|
113
|
+
- Always `Array.isArrayEmpty` / `Array.isArrayNonEmpty` (not `.length === 0` / `.length > 0`). Use `Array.match` when handling both empty and non-empty cases.
|
|
114
|
+
- Never use `for` loops or `let` for iteration. Reach for `Array.map`, `Array.filterMap`, `Array.makeBy`, `Array.reduce`.
|
|
115
|
+
- Never use `T[]`. Always `Array<T>` or `ReadonlyArray<T>`.
|
|
116
|
+
- Use `Message.match` for exhaustive Message matching. Use Effect `Match` for other tagged unions, partial matching, fallbacks, and one handler shared across multiple tags. Never use `switch`.
|
|
117
|
+
- Always use braces for control flow: `if (foo) { return true }`.
|
|
118
|
+
- Don't add inline comments to explain code. Use better names instead. Reserve `// NOTE:` for behavior that would mislead a careful reader.
|
|
119
|
+
|
|
120
|
+
## Message Layout
|
|
121
|
+
|
|
122
|
+
Declare the whole Message union with `defineMessageUnion()`, then put `type Message = typeof Message.Type` on the next line:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const Message = defineMessageUnion({
|
|
126
|
+
ClickedSubmit: {},
|
|
127
|
+
UpdatedEmail: { value: S.String },
|
|
128
|
+
})
|
|
129
|
+
type Message = typeof Message.Type
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Keep the `defineMessageUnion()` declaration and `type Message` alias adjacent. Construct values through the namespace (`Message.ClickedSubmit()`) and handle the union with `Message.match`. Never destructure constructors from `Message` or `OutMessage`; the owning namespace stays visible at every call site.
|
|
133
|
+
|
|
134
|
+
Keep each case's payload object on one line when it fits. Let Prettier wrap payloads that need more space, so the declaration remains easy to scan as one variant per line.
|
|
135
|
+
|
|
136
|
+
Messages are verb-first past-tense. Common prefixes: `Clicked*`, `Updated*` (input changes and external state updates), `Submitted*`, `Pressed*`, `Selected*`, `Succeeded*` / `Failed*` (paired async results), `Completed*` (every other Command result), `Got*` (child OutMessage in the Submodel pattern).
|
|
137
|
+
|
|
138
|
+
## Debugging
|
|
139
|
+
|
|
140
|
+
This project ships with `@foldkit/devtools-mcp` pre-wired. When the dev server is running and the app is open in a browser, `foldkit_*` MCP tools let you inspect Model, Message history, and time-travel. Reach for them before adding `console.log` whenever the question is about state or Message flow.
|
|
141
|
+
|
|
142
|
+
## Going Deeper
|
|
143
|
+
|
|
144
|
+
For Submodels and OutMessage, Subscriptions, Mount / ManagedResource / CustomElement, field validation, routing, accessibility, and the full convention set, read the live Foldkit code in `repos/foldkit/`. The `examples/` directory and the production apps (`packages/typing-game/`, `packages/website/`) are the highest-fidelity references for any specific pattern. The `foldkit-skills` plugin's `generate-program` and `audit-program` skills carry written snapshot guides if you want a structured walkthrough.
|
|
@@ -90,18 +90,18 @@ export type Message = typeof Message.Type
|
|
|
90
90
|
|
|
91
91
|
// UPDATE
|
|
92
92
|
|
|
93
|
-
type UpdateReturn =
|
|
93
|
+
type UpdateReturn = Update.Return<Model, Message>
|
|
94
94
|
|
|
95
95
|
const applyPostsTransition = (
|
|
96
96
|
model: Model,
|
|
97
97
|
maybeNextPosts: Option.Option<PostsData>,
|
|
98
98
|
): UpdateReturn =>
|
|
99
99
|
Option.match(maybeNextPosts, {
|
|
100
|
-
onNone: () =>
|
|
101
|
-
onSome: nextPosts =>
|
|
102
|
-
evo(model, { posts: () => nextPosts }),
|
|
103
|
-
[FetchPosts()],
|
|
104
|
-
|
|
100
|
+
onNone: () => ({ model }),
|
|
101
|
+
onSome: nextPosts => ({
|
|
102
|
+
model: evo(model, { posts: () => nextPosts }),
|
|
103
|
+
commands: [FetchPosts()],
|
|
104
|
+
}),
|
|
105
105
|
})
|
|
106
106
|
|
|
107
107
|
const applyStatsTransition = (
|
|
@@ -109,11 +109,11 @@ const applyStatsTransition = (
|
|
|
109
109
|
maybeNextStats: Option.Option<StatsData>,
|
|
110
110
|
): UpdateReturn =>
|
|
111
111
|
Option.match(maybeNextStats, {
|
|
112
|
-
onNone: () =>
|
|
113
|
-
onSome: nextStats =>
|
|
114
|
-
evo(model, { stats: () => nextStats }),
|
|
115
|
-
[FetchStats()],
|
|
116
|
-
|
|
112
|
+
onNone: () => ({ model }),
|
|
113
|
+
onSome: nextStats => ({
|
|
114
|
+
model: evo(model, { stats: () => nextStats }),
|
|
115
|
+
commands: [FetchStats()],
|
|
116
|
+
}),
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
const setPostDetail = (postId: string, postDetail: PostDetailData) =>
|
|
@@ -168,20 +168,19 @@ export const update = (model: Model, message: Message) =>
|
|
|
168
168
|
})
|
|
169
169
|
|
|
170
170
|
return Option.match(HashMap.get(model.postDetailById, postId), {
|
|
171
|
-
onNone: () =>
|
|
172
|
-
evo(selectedModel, {
|
|
171
|
+
onNone: () => ({
|
|
172
|
+
model: evo(selectedModel, {
|
|
173
173
|
postDetailById: setPostDetail(postId, PostDetailData.Loading()),
|
|
174
174
|
}),
|
|
175
|
-
[FetchPostDetail({ postId })],
|
|
176
|
-
|
|
177
|
-
onSome: () =>
|
|
175
|
+
commands: [FetchPostDetail({ postId })],
|
|
176
|
+
}),
|
|
177
|
+
onSome: () => ({ model: selectedModel }),
|
|
178
178
|
})
|
|
179
179
|
},
|
|
180
180
|
|
|
181
|
-
ClickedBackToPosts: () =>
|
|
182
|
-
evo(model, { maybeSelectedPostId: () => Option.none() }),
|
|
183
|
-
|
|
184
|
-
],
|
|
181
|
+
ClickedBackToPosts: () => ({
|
|
182
|
+
model: evo(model, { maybeSelectedPostId: () => Option.none() }),
|
|
183
|
+
}),
|
|
185
184
|
|
|
186
185
|
ClickedInvalidatePosts: () =>
|
|
187
186
|
applyPostsTransition(model, AsyncData.revalidateOrLoad(model.posts)),
|
|
@@ -189,12 +188,12 @@ export const update = (model: Model, message: Message) =>
|
|
|
189
188
|
ClickedRetryPosts: () =>
|
|
190
189
|
applyPostsTransition(model, AsyncData.revalidateOrLoad(model.posts)),
|
|
191
190
|
|
|
192
|
-
ClickedRetryPostDetail: ({ postId }) =>
|
|
193
|
-
evo(model, {
|
|
191
|
+
ClickedRetryPostDetail: ({ postId }) => ({
|
|
192
|
+
model: evo(model, {
|
|
194
193
|
postDetailById: setPostDetail(postId, PostDetailData.Loading()),
|
|
195
194
|
}),
|
|
196
|
-
[FetchPostDetail({ postId })],
|
|
197
|
-
|
|
195
|
+
commands: [FetchPostDetail({ postId })],
|
|
196
|
+
}),
|
|
198
197
|
|
|
199
198
|
ClickedRefreshStats: () =>
|
|
200
199
|
applyStatsTransition(model, AsyncData.revalidateOrLoad(model.stats)),
|
|
@@ -205,28 +204,25 @@ export const update = (model: Model, message: Message) =>
|
|
|
205
204
|
TickedRevalidateStats: () =>
|
|
206
205
|
applyStatsTransition(model, AsyncData.revalidate(model.stats)),
|
|
207
206
|
|
|
208
|
-
SettledFetchPosts: ({ result }) =>
|
|
209
|
-
evo(model, { posts: AsyncData.settle(result) }),
|
|
210
|
-
|
|
211
|
-
],
|
|
207
|
+
SettledFetchPosts: ({ result }) => ({
|
|
208
|
+
model: evo(model, { posts: AsyncData.settle(result) }),
|
|
209
|
+
}),
|
|
212
210
|
|
|
213
|
-
SettledFetchPostDetail: ({ postId, result }) =>
|
|
214
|
-
evo(model, {
|
|
211
|
+
SettledFetchPostDetail: ({ postId, result }) => ({
|
|
212
|
+
model: evo(model, {
|
|
215
213
|
postDetailById: HashMap.modify(postId, AsyncData.settle(result)),
|
|
216
214
|
}),
|
|
217
|
-
|
|
218
|
-
],
|
|
215
|
+
}),
|
|
219
216
|
|
|
220
|
-
SettledFetchStats: ({ result }) =>
|
|
221
|
-
evo(model, { stats: AsyncData.settle(result) }),
|
|
222
|
-
|
|
223
|
-
],
|
|
217
|
+
SettledFetchStats: ({ result }) => ({
|
|
218
|
+
model: evo(model, { stats: AsyncData.settle(result) }),
|
|
219
|
+
}),
|
|
224
220
|
})
|
|
225
221
|
|
|
226
222
|
// INIT
|
|
227
223
|
|
|
228
|
-
export const init: Runtime.ApplicationInit<Model, Message> = () =>
|
|
229
|
-
{
|
|
224
|
+
export const init: Runtime.ApplicationInit<Model, Message> = () => ({
|
|
225
|
+
model: {
|
|
230
226
|
tabs: Tabs.init({ id: TABS_ID }),
|
|
231
227
|
activeTab: 'Posts',
|
|
232
228
|
posts: PostsData.Loading(),
|
|
@@ -234,8 +230,8 @@ export const init: Runtime.ApplicationInit<Model, Message> = () => [
|
|
|
234
230
|
maybeSelectedPostId: Option.none(),
|
|
235
231
|
stats: StatsData.Idle(),
|
|
236
232
|
},
|
|
237
|
-
[FetchPosts()],
|
|
238
|
-
|
|
233
|
+
commands: [FetchPosts()],
|
|
234
|
+
})
|
|
239
235
|
|
|
240
236
|
// COMMAND
|
|
241
237
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Match as M, Option, Schema as S } from 'effect'
|
|
2
2
|
import { KeyValueStore } from 'effect/unstable/persistence'
|
|
3
|
-
import {
|
|
3
|
+
import { Runtime, type Update } from 'foldkit'
|
|
4
4
|
import { Url } from 'foldkit/url'
|
|
5
5
|
|
|
6
6
|
import { BrowserKeyValueStore } from '@effect/platform-browser'
|
|
@@ -39,7 +39,7 @@ export type Flags = typeof Flags.Type
|
|
|
39
39
|
|
|
40
40
|
// INIT
|
|
41
41
|
|
|
42
|
-
type InitReturn =
|
|
42
|
+
type InitReturn = Update.Return<Model, Message>
|
|
43
43
|
const withInitReturn = M.withReturnType<InitReturn>()
|
|
44
44
|
|
|
45
45
|
export const init: Runtime.RoutingApplicationInit<Model, Message, Flags> = (
|
|
@@ -52,24 +52,25 @@ export const init: Runtime.RoutingApplicationInit<Model, Message, Flags> = (
|
|
|
52
52
|
onNone: () =>
|
|
53
53
|
M.value(route).pipe(
|
|
54
54
|
withInitReturn,
|
|
55
|
-
M.tag('Home', 'Login', 'NotFound', route =>
|
|
56
|
-
LoggedOut.init(route),
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
M.tag('Home', 'Login', 'NotFound', route => ({
|
|
56
|
+
model: LoggedOut.init(route),
|
|
57
|
+
})),
|
|
58
|
+
M.orElse(() => ({
|
|
59
|
+
model: LoggedOut.init(LoginRoute()),
|
|
60
|
+
commands: [RedirectToLogin()],
|
|
61
|
+
})),
|
|
60
62
|
),
|
|
61
63
|
|
|
62
64
|
onSome: session =>
|
|
63
65
|
M.value(route).pipe(
|
|
64
66
|
withInitReturn,
|
|
65
|
-
M.tag('Dashboard', 'Settings', 'NotFound', route =>
|
|
66
|
-
LoggedIn.init(route, session),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
]),
|
|
67
|
+
M.tag('Dashboard', 'Settings', 'NotFound', route => ({
|
|
68
|
+
model: LoggedIn.init(route, session),
|
|
69
|
+
})),
|
|
70
|
+
M.orElse(() => ({
|
|
71
|
+
model: LoggedIn.init(DashboardRoute(), session),
|
|
72
|
+
commands: [RedirectToDashboard()],
|
|
73
|
+
})),
|
|
73
74
|
),
|
|
74
75
|
})
|
|
75
76
|
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Command } from 'foldkit'
|
|
1
|
+
import { type Update } from 'foldkit'
|
|
3
2
|
|
|
4
3
|
import { Message, OutMessage } from './message'
|
|
5
4
|
import { Model } from './model'
|
|
6
5
|
|
|
7
|
-
type UpdateReturn = readonly [
|
|
8
|
-
Model,
|
|
9
|
-
ReadonlyArray<Command.Command<Message>>,
|
|
10
|
-
Option.Option<OutMessage>,
|
|
11
|
-
]
|
|
12
|
-
|
|
13
6
|
export const update = (model: Model, message: Message) =>
|
|
14
|
-
Message.match<
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
Message.match<Update.ReturnWithOutMessage<Model, Message, OutMessage>>(
|
|
8
|
+
message,
|
|
9
|
+
{
|
|
10
|
+
ClickedLogout: () => ({
|
|
11
|
+
model,
|
|
12
|
+
outMessage: OutMessage.RequestedLogout(),
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
)
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
String,
|
|
10
10
|
pipe,
|
|
11
11
|
} from 'effect'
|
|
12
|
-
import { Command, Submodel } from 'foldkit'
|
|
12
|
+
import { Command, Submodel, type Update } from 'foldkit'
|
|
13
13
|
import {
|
|
14
14
|
Field,
|
|
15
15
|
Invalid,
|
|
@@ -86,12 +86,6 @@ const isFormValid = (model: Model): boolean =>
|
|
|
86
86
|
|
|
87
87
|
// UPDATE
|
|
88
88
|
|
|
89
|
-
type UpdateReturn = readonly [
|
|
90
|
-
Model,
|
|
91
|
-
ReadonlyArray<Command.Command<Message>>,
|
|
92
|
-
Option.Option<OutMessage>,
|
|
93
|
-
]
|
|
94
|
-
|
|
95
89
|
export const SimulateAuthRequest = Command.define('SimulateAuthRequest', {
|
|
96
90
|
args: { email: S.String, password: S.String },
|
|
97
91
|
messages: [
|
|
@@ -122,59 +116,54 @@ export const SimulateAuthRequest = Command.define('SimulateAuthRequest', {
|
|
|
122
116
|
})
|
|
123
117
|
|
|
124
118
|
export const update = (model: Model, message: Message) =>
|
|
125
|
-
Message.match<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
ChangedPassword: ({ value }) => [
|
|
133
|
-
evo(model, { password: () => validatePassword(value) }),
|
|
134
|
-
[],
|
|
135
|
-
Option.none(),
|
|
136
|
-
],
|
|
137
|
-
|
|
138
|
-
SubmittedForm: () => {
|
|
139
|
-
if (model.isSubmitting) {
|
|
140
|
-
return [model, [], Option.none()]
|
|
141
|
-
}
|
|
119
|
+
Message.match<Update.ReturnWithOutMessage<Model, Message, OutMessage>>(
|
|
120
|
+
message,
|
|
121
|
+
{
|
|
122
|
+
ChangedEmail: ({ value }) => ({
|
|
123
|
+
model: evo(model, { email: () => validateEmail(value) }),
|
|
124
|
+
}),
|
|
142
125
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
126
|
+
ChangedPassword: ({ value }) => ({
|
|
127
|
+
model: evo(model, { password: () => validatePassword(value) }),
|
|
128
|
+
}),
|
|
146
129
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
130
|
+
SubmittedForm: () => {
|
|
131
|
+
if (model.isSubmitting) {
|
|
132
|
+
return { model }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!isFormValid(model)) {
|
|
136
|
+
return { model }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
model: evo(model, { isSubmitting: () => true }),
|
|
141
|
+
commands: [
|
|
142
|
+
SimulateAuthRequest({
|
|
143
|
+
email: model.email.value,
|
|
144
|
+
password: model.password.value,
|
|
145
|
+
}),
|
|
146
|
+
],
|
|
147
|
+
}
|
|
148
|
+
},
|
|
158
149
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
],
|
|
150
|
+
SucceededSimulateAuthRequest: ({ session }) => ({
|
|
151
|
+
model,
|
|
152
|
+
outMessage: OutMessage.SucceededLogin({ session }),
|
|
153
|
+
}),
|
|
164
154
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
155
|
+
FailedSimulateAuthRequest: ({ error }) => ({
|
|
156
|
+
model: evo(model, {
|
|
157
|
+
password: () =>
|
|
158
|
+
Invalid({
|
|
159
|
+
value: model.password.value,
|
|
160
|
+
errors: [error],
|
|
161
|
+
}),
|
|
162
|
+
isSubmitting: () => false,
|
|
163
|
+
}),
|
|
173
164
|
}),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
],
|
|
177
|
-
})
|
|
165
|
+
},
|
|
166
|
+
)
|
|
178
167
|
|
|
179
168
|
// VIEW
|
|
180
169
|
|