Add Effect v4 support, Fast Refresh tooling, and revamped docs #56
@@ -163,6 +163,124 @@ Pass `initialEncodedValue` only when the first draft should differ from the
|
||||
encoded target. Otherwise `LensForm.service` obtains the initial draft by
|
||||
encoding the target through the schema.
|
||||
|
||||
## Example: local date input, UTC domain value
|
||||
|
||||
The encoded/decoded distinction is especially useful for dates. An HTML
|
||||
`datetime-local` input produces a wall-clock string such as
|
||||
`"2026-07-22T14:30"`. It contains no time-zone or UTC offset, while application
|
||||
state and APIs usually need an unambiguous UTC instant.
|
||||
|
||||
A schema can own that entire conversion. The following schema interprets the
|
||||
input in the current time zone and decodes it to `DateTime.Utc`:
|
||||
|
||||
```tsx title="src/DateTimeUtcFromZonedInput.ts"
|
||||
import { DateTime, Effect, Option, ParseResult, Schema } from "effect"
|
||||
|
||||
class DateTimeUtcFromZoned extends Schema.transformOrFail(
|
||||
Schema.DateTimeZonedFromSelf,
|
||||
Schema.DateTimeUtcFromSelf,
|
||||
{
|
||||
strict: true,
|
||||
decode: (input) => ParseResult.succeed(DateTime.toUtc(input)),
|
||||
encode: DateTime.setZoneCurrent,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class DateTimeUtcFromZonedInput extends Schema.transformOrFail(
|
||||
Schema.String,
|
||||
DateTimeUtcFromZoned,
|
||||
{
|
||||
strict: true,
|
||||
decode: (input, _options, ast) =>
|
||||
Effect.flatMap(DateTime.CurrentTimeZone, (timeZone) =>
|
||||
Option.match(
|
||||
DateTime.makeZoned(input, {
|
||||
timeZone,
|
||||
adjustForTimeZone: true,
|
||||
}),
|
||||
{
|
||||
onSome: ParseResult.succeed,
|
||||
onNone: () =>
|
||||
ParseResult.fail(
|
||||
new ParseResult.Type(
|
||||
ast,
|
||||
input,
|
||||
"Enter a valid date and time",
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
encode: (value) =>
|
||||
ParseResult.succeed(
|
||||
DateTime.formatIsoZoned(value).slice(0, 16),
|
||||
),
|
||||
},
|
||||
) {}
|
||||
```
|
||||
|
||||
Use it like any other field schema. The form and input work with a string, but
|
||||
the mutation receives UTC:
|
||||
|
||||
```tsx
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import { Component, Form, MutationForm, View } from "effect-view"
|
||||
import { DateTimeUtcFromZonedInput } from "./DateTimeUtcFromZonedInput"
|
||||
|
||||
const AppointmentSchema = Schema.Struct({
|
||||
startsAt: DateTimeUtcFromZonedInput,
|
||||
})
|
||||
|
||||
const AppointmentView = Component.make("Appointment")(function* () {
|
||||
const [form, startsAtField] = yield* Component.useOnMount(() =>
|
||||
Effect.gen(function* () {
|
||||
const form = yield* MutationForm.service({
|
||||
schema: AppointmentSchema,
|
||||
initialEncodedValue: { startsAt: "" },
|
||||
f: ([appointment]) =>
|
||||
Effect.log(
|
||||
`Saving ${DateTime.formatIso(appointment.startsAt)}`,
|
||||
),
|
||||
})
|
||||
|
||||
return [form, Form.focusObjectOn(form, "startsAt")] as const
|
||||
}),
|
||||
)
|
||||
|
||||
const startsAt = yield* Form.useInput(startsAtField)
|
||||
const [canCommit] = yield* View.useAll([form.canCommit])
|
||||
const runPromise = yield* Component.useRunPromise()
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
void runPromise(form.submit)
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={startsAt.value}
|
||||
onChange={(event) =>
|
||||
startsAt.setValue(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<button disabled={!canCommit}>Save appointment</button>
|
||||
</form>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
The schema requires `DateTime.CurrentTimeZone`; provide
|
||||
`DateTime.layerCurrentZoneLocal` in the application runtime. If the current
|
||||
zone is `Europe/Paris`, for example, entering `2026-07-22T14:30` decodes to the
|
||||
UTC instant `2026-07-22T12:30:00.000Z`.
|
||||
|
||||
Encoding works in the other direction. A `LensForm` whose target contains that
|
||||
UTC instant presents `2026-07-22T14:30` to the local input. The component never
|
||||
manually parses dates, applies offsets, or maintains a second representation;
|
||||
the schema defines both directions and the form keeps them synchronized.
|
||||
|
||||
## Focus into subforms
|
||||
|
||||
After creating either root implementation, focus it along the schema's
|
||||
|
||||
Reference in New Issue
Block a user