Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/whole-dogs-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-app/vue-components": minor
---

Improves type handling for Input Props Name, that now only accept leaves schema values and not every valid key of the schema
15 changes: 3 additions & 12 deletions packages/vue-components/src/components/OmegaForm/OmegaArray.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,11 @@
To extends Record<PropertyKey, any>
"
>
import { type DeepKeys, type DeepValue } from "@tanstack/vue-form"
import { type DeepKeys } from "@tanstack/vue-form"
import { computed, onMounted, provide } from "vue"
import { type OmegaInputProps } from "./OmegaFormStuff"
import { type OmegaArrayProps } from "./OmegaFormStuff"

const props = defineProps<
Omit<
OmegaInputProps<From, To>,
"validators" | "options" | "label" | "type" | "items"
> & {
defaultItems?: DeepValue<From, DeepKeys<From>>
// deprecated items, caused bugs in state update, use defaultItems instead. It's not a simple Never, because Volar explodes
items?: "please use `defaultItems` instead"
}
>()
const props = defineProps<OmegaArrayProps<From, To>>()

defineOptions({
inheritAttrs: false
Expand Down
33 changes: 30 additions & 3 deletions packages/vue-components/src/components/OmegaForm/OmegaFormStuff.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { type Effect, Option, type Record, S } from "effect-app"
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getMetadataFromSchema } from "@effect-app/vue/form"
import { type DeepKeys, type FieldAsyncValidateOrFn, type FieldValidateOrFn, type FormApi, type FormAsyncValidateOrFn, type FormOptions, type FormState, type FormValidateOrFn, type StandardSchemaV1, type VueFormApi } from "@tanstack/vue-form"
import { type DeepKeys, type DeepValue, type FieldAsyncValidateOrFn, type FieldValidateOrFn, type FormApi, type FormAsyncValidateOrFn, type FormOptions, type FormState, type FormValidateOrFn, type StandardSchemaV1, type VueFormApi } from "@tanstack/vue-form"
import { type RuntimeFiber } from "effect/Fiber"
import { getTransformationFrom, useIntl } from "../../utils"
import { type OmegaFieldInternalApi } from "./InputProps"
import { type OF, type OmegaFormReturn } from "./useOmegaForm"

export type BaseProps<From, TName extends DeepKeys<From>> = {
type Leaves<T, Path extends string = ""> = T extends ReadonlyArray<infer U> ? Leaves<U, `${Path}[number]`> & {}
: {
[K in keyof T]: T[K] extends string | boolean | number | null | undefined | symbol | bigint
? `${Path extends "" ? "" : `${Path}.`}${K & string}`
: Leaves<T[K], `${Path extends "" ? "" : `${Path}.`}${K & string}`> & {}
}[keyof T]

// Helper type to make array indices flexible - accepts both [number] and numeric literals [0], [1], etc.
type FlexibleArrayPath<T extends string> = T extends `${infer Before}[number]${infer After}`
? T | `${Before}[${number}]${FlexibleArrayPath<After>}`
: T

export type BaseProps<From, TName extends DeepKeys<From> = DeepKeys<From>> = {
/** Will fallback to i18n when not specified */
label?: string
validators?: FieldValidators<From>
name: TName
name: TName & FlexibleArrayPath<Leaves<From>>
}

export type TypesWithOptions = "radio" | "select" | "multiple" | "autocomplete" | "autocompletemultiple"
Expand Down Expand Up @@ -49,6 +61,21 @@ export type OmegaInputProps<
}
} & BaseProps<From, NestedKeyOf<From>>

export type OmegaArrayProps<
From extends Record<PropertyKey, any>,
To extends Record<PropertyKey, any>
> =
& Omit<
OmegaInputProps<From, To>,
"validators" | "options" | "label" | "type" | "items" | "name"
>
& {
name: DeepKeys<From>
defaultItems?: DeepValue<From, DeepKeys<From>>
// deprecated items, caused bugs in state update, use defaultItems instead. It's not a simple Never, because Volar explodes
items?: "please use `defaultItems` instead"
}

export type TypeOverride =
| "string"
| "text"
Expand Down
12 changes: 8 additions & 4 deletions packages/vue-components/src/components/OmegaForm/OmegaInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@
To extends Record<PropertyKey, any>
"
>
import { type DeepKeys } from "@tanstack/vue-form"
import { computed, inject, type Ref } from "vue"
import { useIntl } from "../../utils"
import { type FieldMeta, generateInputStandardSchemaFromFieldMeta, type OmegaInputPropsBase } from "./OmegaFormStuff"
import OmegaInternalInput from "./OmegaInternalInput.vue"

const props = defineProps<OmegaInputPropsBase<From, To>>()

// downgrade to DeepKeys<From> to avoid useless and possible infinite recursion in TS
const propsName: Ref<DeepKeys<From>> = computed(() => props.name)

defineOptions({
inheritAttrs: false
})
Expand All @@ -52,9 +56,9 @@ const getMetaFromArray = inject<Ref<(name: string) => FieldMeta | null> | null>(

const meta = computed(() => {
if (getMetaFromArray?.value && getMetaFromArray.value(props.name)) {
return getMetaFromArray.value(props.name)
return getMetaFromArray.value(propsName.value)
}
return props.form.meta[props.name]
return props.form.meta[propsName.value]
})

const schema = computed(() => {
Expand All @@ -72,9 +76,9 @@ const humanize = (str: string) => {
.replace(/^./, (char) => char.toUpperCase()) // Capitalize the first letter
.trim() // Remove leading/trailing spaces
}
const fallback = () => formatMessage({ id: `general.fields.${props.name}`, defaultMessage: humanize(props.name) })
const fallback = () => formatMessage({ id: `general.fields.${propsName.value}`, defaultMessage: humanize(props.name) })
const i18n = () =>
props.form.i18nNamespace
? formatMessage({ id: `${props.form.i18nNamespace}.fields.${props.name}`, defaultMessage: fallback() })
? formatMessage({ id: `${props.form.i18nNamespace}.fields.${propsName.value}`, defaultMessage: fallback() })
: fallback()
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MergedInputProps } from "./InputProps"
import OmegaArray from "./OmegaArray.vue"
import OmegaAutoGen from "./OmegaAutoGen.vue"
import OmegaErrorsInternal from "./OmegaErrorsInternal.vue"
import { BaseProps, DefaultTypeProps, type FormProps, generateMetaFromSchema, type MetaRecord, type NestedKeyOf, OmegaAutoGenMeta, OmegaError, type OmegaFormApi, OmegaFormState, OmegaInputProps } from "./OmegaFormStuff"
import { BaseProps, DefaultTypeProps, type FormProps, generateMetaFromSchema, type MetaRecord, type NestedKeyOf, OmegaArrayProps, OmegaAutoGenMeta, OmegaError, type OmegaFormApi, OmegaFormState } from "./OmegaFormStuff"
import OmegaInput from "./OmegaInput.vue"
import OmegaForm from "./OmegaWrapper.vue"

Expand Down Expand Up @@ -234,10 +234,7 @@ export interface OmegaFormReturn<
>,
never
>
& (Omit<OmegaInputProps<From, To>, "validators" | "options" | "label" | "type" | "items" | "form"> & {
defaultItems?: DeepValue<From, DeepKeys<From>>
items?: "please use `defaultItems` instead"
})
& (OmegaArrayProps<From, To>)
& {}
>
& import("vue").PublicProps
Expand Down
8 changes: 6 additions & 2 deletions packages/vue-components/stories/OmegaForm/SimpleForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div>values: {{ values }} {{ isDirty }}</div>
<form.Input
label="asder2"
name="asder2"
name="asder2.value"
>
<template #default="{ field, label, state }">
<label :for="field.name">{{ label }}</label>
Expand All @@ -28,7 +28,11 @@
import { S } from "effect-app"
import { useOmegaForm } from "../../src/components/OmegaForm"

const schema = S.Struct({ asder2: S.String })
const schema = S.Struct({
asder2: S.Struct({
value: S.String
})
})
const form = useOmegaForm(schema, {
onSubmit: async ({ value }) => {
console.log(value)
Expand Down