diff --git a/.changeset/silly-colts-make.md b/.changeset/silly-colts-make.md new file mode 100644 index 0000000000..3fa61867fe --- /dev/null +++ b/.changeset/silly-colts-make.md @@ -0,0 +1,5 @@ +--- +"@effect-app/vue-components": minor +--- + +Adds custom label feature to OmegaForm diff --git a/packages/vue-components/src/components/OmegaForm/OmegaFormStuff.ts b/packages/vue-components/src/components/OmegaForm/OmegaFormStuff.ts index d2afbd7282..cab49bb1c7 100644 --- a/packages/vue-components/src/components/OmegaForm/OmegaFormStuff.ts +++ b/packages/vue-components/src/components/OmegaForm/OmegaFormStuff.ts @@ -20,7 +20,11 @@ export type FlexibleArrayPath = T extends `${string}[${string} : never export type BaseProps = DeepKeys> = { - /** Will fallback to i18n when not specified */ + /** + * Will fallback to i18n when not specified. + * Can also be provided via #label slot for custom HTML labels. + * When using the slot, it receives bindings: { required, id, label } + */ label?: string validators?: FieldValidators // Use FlexibleArrayPath: if name contains [], just use TName; otherwise intersect with Leaves diff --git a/packages/vue-components/src/components/OmegaForm/OmegaInput.vue b/packages/vue-components/src/components/OmegaForm/OmegaInput.vue index 3acf610a66..9da3d19995 100644 --- a/packages/vue-components/src/components/OmegaForm/OmegaInput.vue +++ b/packages/vue-components/src/components/OmegaForm/OmegaInput.vue @@ -17,6 +17,15 @@ :label="label ?? i18n()" :meta="meta" > + @@ -42,8 +51,13 @@ import OmegaInternalInput from "./OmegaInternalInput.vue" const props = defineProps>() -// downgrade to DeepKeys to avoid useless and possible infinite recursion in TS -const propsName: Ref> = computed(() => props.name) +// downgrade to *as* DeepKeys to avoid useless and possible infinite recursion in TS +const propsName = computed(() => props.name as DeepKeys) + +defineSlots<{ + label?: (props: { required?: boolean; id: string; label: string }) => any + default?: (props: any) => any +}>() defineOptions({ inheritAttrs: false @@ -64,7 +78,7 @@ const getMetaFromArray = inject FieldMeta | null> | null>( ) const meta = computed(() => { - if (getMetaFromArray?.value && getMetaFromArray.value(props.name)) { + if (getMetaFromArray?.value && getMetaFromArray.value(props.name as DeepKeys)) { return getMetaFromArray.value(propsName.value) } return props.form.meta[propsName.value] diff --git a/packages/vue-components/src/components/OmegaForm/OmegaInputVuetify.vue b/packages/vue-components/src/components/OmegaForm/OmegaInputVuetify.vue index ad513f86fe..7a1ed9db82 100644 --- a/packages/vue-components/src/components/OmegaForm/OmegaInputVuetify.vue +++ b/packages/vue-components/src/components/OmegaForm/OmegaInputVuetify.vue @@ -16,7 +16,17 @@ v-bind="$attrs" :model-value="state.value" @change="(e: any) => field.handleChange(e.target.checked)" - /> + > + + + > + + + > + + + > + + diff --git a/packages/vue-components/src/components/OmegaForm/OmegaInternalInput.vue b/packages/vue-components/src/components/OmegaForm/OmegaInternalInput.vue index 69c971bab1..b099d85c99 100644 --- a/packages/vue-components/src/components/OmegaForm/OmegaInternalInput.vue +++ b/packages/vue-components/src/components/OmegaForm/OmegaInternalInput.vue @@ -4,7 +4,17 @@ + > + + @@ -15,7 +25,7 @@ generic="From extends Record, Name extends DeepKeys" > import { type DeepKeys, useStore } from "@tanstack/vue-form" -import { computed, type ComputedRef, getCurrentInstance, useAttrs, useId } from "vue" +import { computed, type ComputedRef, getCurrentInstance, useAttrs, useId, useSlots } from "vue" import type { InputProps, OmegaFieldInternalApi } from "./InputProps" import type { FieldValidators, MetaRecord, NestedKeyOf, TypeOverride } from "./OmegaFormStuff" import OmegaInputVuetify from "./OmegaInputVuetify.vue" @@ -60,6 +70,7 @@ const isRequired = computed(() => props.required ?? props?.meta?.required) const instance = getCurrentInstance() const vuetified = instance?.appContext.components["VTextField"] const attrs = useAttrs() +const slots = useSlots() // Create attrs without the class property to avoid duplication const attrsWithoutClass = computed(() => { @@ -148,7 +159,8 @@ const inputProps: ComputedRef> = computed(() => ({ errorMessages: errors.value, error: !!errors.value.length, type: fieldType.value, - label: `${props.label}${isRequired.value ? " *" : ""}`, + // Only add asterisk if label slot is not provided (slot has full control) + label: slots.label ? props.label : `${props.label}${isRequired.value ? " *" : ""}`, options: props.options, inputClass: props.inputClass }, diff --git a/packages/vue-components/src/components/OmegaForm/createUseFormWithCustomInput.ts b/packages/vue-components/src/components/OmegaForm/createUseFormWithCustomInput.ts index 18dba349e3..62c0ff9b38 100644 --- a/packages/vue-components/src/components/OmegaForm/createUseFormWithCustomInput.ts +++ b/packages/vue-components/src/components/OmegaForm/createUseFormWithCustomInput.ts @@ -20,7 +20,7 @@ export const createUseFormWithCustomInput = < const WrappedInput = { name: "WrappedInput", inheritAttrs: false, - setup(props: any, { attrs }: any) { + setup(props: any, { attrs, slots }: any) { return () => h(OmegaInput, { ...props, @@ -35,8 +35,17 @@ export const createUseFormWithCustomInput = < && key !== "form" ) ) - return h(CustomInputComponent, { ...filteredAttrs, field, state, inputProps }) - } + return h(CustomInputComponent, { ...filteredAttrs, field, state, inputProps }, { + // Pass through label slot if it exists + ...(slots.label && { + label: (labelProps: any) => slots.label(labelProps) + }) + }) + }, + // Pass through label slot to OmegaInput + ...(slots.label && { + label: (labelProps: any) => slots.label(labelProps) + }) }) } } diff --git a/packages/vue-components/src/components/OmegaForm/useOmegaForm.ts b/packages/vue-components/src/components/OmegaForm/useOmegaForm.ts index 7e7229119f..d771a20efb 100644 --- a/packages/vue-components/src/components/OmegaForm/useOmegaForm.ts +++ b/packages/vue-components/src/components/OmegaForm/useOmegaForm.ts @@ -236,7 +236,8 @@ export interface OmegaFormReturn< expose(exposed: import("vue").ShallowUnwrapRef<{}>): void attrs: any slots: { - default(props: MergedInputProps): void + default?(props: MergedInputProps): void + label?: (props: { required: boolean; id: string; label: string }) => void } emit: {} }> diff --git a/packages/vue-components/stories/OmegaForm.stories.ts b/packages/vue-components/stories/OmegaForm.stories.ts index 93fcc8c2af..420bfedf4e 100644 --- a/packages/vue-components/stories/OmegaForm.stories.ts +++ b/packages/vue-components/stories/OmegaForm.stories.ts @@ -10,6 +10,7 @@ import ClearableComponent from "./OmegaForm/Clearable.vue" import ComplexFormComponent from "./OmegaForm/ComplexForm.vue" import CreateUseFormWithCustomInputComponent from "./OmegaForm/createUseFormWIthCustomInput.vue" import CustomInputClassNameComponent from "./OmegaForm/CustomInputClassName.vue" +import CustomLabelSlotComponent from "./OmegaForm/CustomLabelSlot.vue" import DateComponent from "./OmegaForm/Date.vue" import DialogBlockingExamplesComponent from "./OmegaForm/DialogBlockingExamples.vue" import EmailFormComponent from "./OmegaForm/EmailForm.vue" @@ -230,3 +231,10 @@ export const CustomInputClassName: Story = { template: "" }) } + +export const CustomLabelSlot: Story = { + render: () => ({ + components: { CustomLabelSlotComponent }, + template: "" + }) +} diff --git a/packages/vue-components/stories/OmegaForm/CustomInput.vue b/packages/vue-components/stories/OmegaForm/CustomInput.vue index 6e7242e101..c522699bbc 100644 --- a/packages/vue-components/stories/OmegaForm/CustomInput.vue +++ b/packages/vue-components/stories/OmegaForm/CustomInput.vue @@ -1,7 +1,17 @@