@effect-app/vue-components 0.0.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.
Files changed (35) hide show
  1. package/README.md +184 -0
  2. package/dist/types/components/OmegaForm/OmegaErrors.vue.d.ts +2 -0
  3. package/dist/types/components/OmegaForm/OmegaErrorsContext.d.ts +35 -0
  4. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +85 -0
  5. package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +38 -0
  6. package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +25 -0
  7. package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +43 -0
  8. package/dist/types/components/OmegaForm/getOmegaStore.d.ts +3 -0
  9. package/dist/types/components/OmegaForm/index.d.ts +52 -0
  10. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +6 -0
  11. package/dist/types/components/TestComponent.vue.d.ts +6 -0
  12. package/dist/types/components/index.d.ts +3 -0
  13. package/dist/types/constants/index.d.ts +1 -0
  14. package/dist/types/index.d.ts +9 -0
  15. package/dist/types/utils/index.d.ts +7 -0
  16. package/dist/vue-components.css +1 -0
  17. package/dist/vue-components.es.js +624 -0
  18. package/package.json +48 -0
  19. package/src/assets/.keep +0 -0
  20. package/src/components/OmegaForm/OmegaErrors.vue +143 -0
  21. package/src/components/OmegaForm/OmegaErrorsContext.ts +64 -0
  22. package/src/components/OmegaForm/OmegaFormStuff.ts +575 -0
  23. package/src/components/OmegaForm/OmegaInput.vue +91 -0
  24. package/src/components/OmegaForm/OmegaInternalInput.vue +216 -0
  25. package/src/components/OmegaForm/OmegaWrapper.vue +137 -0
  26. package/src/components/OmegaForm/getOmegaStore.ts +32 -0
  27. package/src/components/OmegaForm/index.ts +29 -0
  28. package/src/components/OmegaForm/useOmegaForm.ts +61 -0
  29. package/src/components/TestComponent.vue +15 -0
  30. package/src/components/index.ts +6 -0
  31. package/src/components/style.css +3 -0
  32. package/src/constants/index.ts +1 -0
  33. package/src/env.d.ts +8 -0
  34. package/src/index.ts +17 -0
  35. package/src/utils/index.ts +12 -0
@@ -0,0 +1,216 @@
1
+ <template>
2
+ <div class="omega-input">
3
+ <v-text-field
4
+ v-if="fieldType === 'email' || fieldType === 'text'"
5
+ :id="id"
6
+ :required="meta?.required"
7
+ :min-length="meta?.type === 'string' && meta?.minLength"
8
+ :max-length="meta?.type === 'string' && meta?.maxLength"
9
+ :type="fieldType"
10
+ :name="field.name"
11
+ :label="`${label}${meta?.required ? ' *' : ''}`"
12
+ :model-value="field.state.value"
13
+ :error-messages="showedErrors"
14
+ :error="!!showedErrors.length"
15
+ @update:model-value="field.handleChange"
16
+ @blur="setRealDirty"
17
+ />
18
+ <v-text-field
19
+ v-if="fieldType === 'number'"
20
+ :id="id"
21
+ :required="meta?.required"
22
+ :min="meta?.type === 'number' && meta.minimum"
23
+ :max="meta?.type === 'number' && meta.maximum"
24
+ :type="fieldType"
25
+ :name="field.name"
26
+ :label="`${label}${meta?.required ? ' *' : ''}`"
27
+ :model-value="field.state.value"
28
+ :error-messages="showedErrors"
29
+ :error="!!showedErrors.length"
30
+ @update:model-value="
31
+ (e: any) => {
32
+ field.handleChange(Number(e))
33
+ }
34
+ "
35
+ @blur="setRealDirty"
36
+ />
37
+ <div
38
+ v-if="fieldType === 'select' || fieldType === 'multiple'"
39
+ :class="fieldType !== 'multiple' && 'd-flex align-center'"
40
+ >
41
+ <v-select
42
+ :id="id"
43
+ :required="meta?.required"
44
+ :multiple="fieldType === 'multiple'"
45
+ :chips="fieldType === 'multiple'"
46
+ :name="field.name"
47
+ :model-value="field.state.value"
48
+ :label="`${label}${meta?.required ? ' *' : ''}`"
49
+ :items="options"
50
+ :error-messages="showedErrors"
51
+ :error="!!showedErrors.length"
52
+ @update:model-value="field.handleChange"
53
+ @blur="setRealDirty"
54
+ />
55
+ <v-btn
56
+ v-if="fieldType !== 'multiple'"
57
+ variant-btn="secondary"
58
+ :variant-icon="mdiRefresh"
59
+ class="mr-2"
60
+ title="Reset"
61
+ @click="field.handleChange(undefined)"
62
+ ></v-btn>
63
+ </div>
64
+ </div>
65
+ </template>
66
+
67
+ <script setup lang="ts" generic="To">
68
+ /* eslint-disable @typescript-eslint/no-explicit-any */
69
+ import { VTextField, VSelect } from "vuetify/components"
70
+ import { mdiRefresh } from "@mdi/js"
71
+ import { useStore, type FieldApi } from "@tanstack/vue-form"
72
+ import type {
73
+ FieldValidators,
74
+ MetaRecord,
75
+ NestedKeyOf,
76
+ TypeOverride,
77
+ } from "./OmegaFormStuff"
78
+ import { useOmegaErrors } from "./OmegaErrorsContext"
79
+ import { useId, computed, watch, onMounted, ref, watchEffect } from "vue"
80
+
81
+ const props = defineProps<{
82
+ field: FieldApi<
83
+ any,
84
+ any,
85
+ any,
86
+ any,
87
+ any,
88
+ any,
89
+ any,
90
+ any,
91
+ any,
92
+ any,
93
+ any,
94
+ any,
95
+ any,
96
+ any,
97
+ any,
98
+ any,
99
+ any,
100
+ any,
101
+ any
102
+ >
103
+ meta: MetaRecord<To>[NestedKeyOf<To>]
104
+ label: string
105
+ options?: { title: string; value: string }[]
106
+ type?: TypeOverride
107
+ validators?: FieldValidators<To>
108
+ }>()
109
+
110
+ const id = useId()
111
+
112
+ const fieldApi = props.field
113
+
114
+ const fieldState = useStore(fieldApi.store, state => state)
115
+
116
+ const fieldType = computed(() => {
117
+ if (props.type) return props.type
118
+ if (props.meta?.type === "string") {
119
+ if (props.meta.format === "email") return "email"
120
+ return "text"
121
+ }
122
+ return props.meta?.type || "unknown"
123
+ })
124
+
125
+ const fieldValue = computed(() => fieldState.value.value)
126
+ const errors = computed(() =>
127
+ fieldState.value.meta.errors.map((e: any) => e.message).filter(Boolean),
128
+ )
129
+
130
+ // we remove value and errors when the field is empty and not required
131
+ //watchEffect will trigger infinite times with both free fieldValue and errors, so bet to watch a stupid boolean
132
+ watch(
133
+ () => [!!fieldValue.value],
134
+ () => {
135
+ if (errors.value.length && !fieldValue.value && !props.meta?.required) {
136
+ fieldApi.setValue(
137
+ props.meta?.nullableOrUndefined === "undefined" ? undefined : null,
138
+ )
139
+ }
140
+ },
141
+ )
142
+
143
+ onMounted(() => {
144
+ if (
145
+ !fieldValue.value &&
146
+ !props.meta?.required &&
147
+ props.meta?.nullableOrUndefined === "null"
148
+ ) {
149
+ fieldApi.setValue(null)
150
+ }
151
+ })
152
+
153
+ const realDirty = ref(false)
154
+ const setRealDirty = () => {
155
+ realDirty.value = true
156
+ }
157
+
158
+ const { addError, formSubmissionAttempts, removeError } = useOmegaErrors()
159
+
160
+ watchEffect(() => {
161
+ if (formSubmissionAttempts.value > 0) {
162
+ realDirty.value = true
163
+ }
164
+ })
165
+
166
+ const showedErrors = computed(() => {
167
+ // single select field can be validated on change
168
+ if (!realDirty.value && fieldType.value !== "select") return []
169
+ return errors.value
170
+ })
171
+
172
+ watch(
173
+ () => fieldState.value.meta.errors,
174
+ () => {
175
+ if (fieldState.value.meta.errors.length) {
176
+ addError({
177
+ inputId: id,
178
+ errors: fieldState.value.meta.errors
179
+ .map((e: any) => e.message)
180
+ .filter(Boolean),
181
+ label: props.label,
182
+ })
183
+ } else {
184
+ removeError(id)
185
+ }
186
+ },
187
+ )
188
+ </script>
189
+
190
+ <style>
191
+ .omega-input {
192
+ .v-input__details:has(.v-messages:empty) {
193
+ grid-template-rows: 0fr;
194
+ transition: all 0.2s;
195
+ }
196
+
197
+ & .v-messages:empty {
198
+ min-height: 0;
199
+ }
200
+
201
+ & .v-input__details:has(.v-messages) {
202
+ transition: all 0.2s;
203
+ overflow: hidden;
204
+ min-height: 0;
205
+ display: grid;
206
+ grid-template-rows: 1fr;
207
+ }
208
+
209
+ & .v-messages {
210
+ transition: all 0.2s;
211
+ > * {
212
+ transition-duration: 0s !important;
213
+ }
214
+ }
215
+ }
216
+ </style>
@@ -0,0 +1,137 @@
1
+ <template>
2
+ <form @submit.prevent.stop="form.handleSubmit()">
3
+ <fieldset :disabled="formIsSubmitting">
4
+ <slot :form="form" :subscribed-values="subscribedValues" />
5
+ </fieldset>
6
+ </form>
7
+ </template>
8
+
9
+ <script
10
+ setup
11
+ lang="ts"
12
+ generic="
13
+ From extends Record<PropertyKey, any>,
14
+ To extends Record<PropertyKey, any>,
15
+ K extends keyof OmegaFormState<To, From> = keyof OmegaFormState<To, From>
16
+ "
17
+ >
18
+ /**
19
+ * Form component that wraps TanStack Form's useForm hook
20
+ *
21
+ * Usage:
22
+ * <Form :default-values="..." :on-submit="..." :validators="..." ...etc>
23
+ * <template #default="{ form }">
24
+ * <!-- Children with access to form -->
25
+ * <component :is="form.Field" name="fieldName">
26
+ * <template #default="{ field }">
27
+ * <input
28
+ * :value="field.state.value"
29
+ * @input="e => field.handleChange(e.target.value)"
30
+ * />
31
+ * </template>
32
+ * </component>
33
+ * </template>
34
+ * </Form>
35
+ *
36
+ * <Form :default-values="..." :on-submit="..." :validators="..." ...etc>
37
+ * <template #default="{ form }">
38
+ * <Input :form="form" name="foobar" />
39
+ * </template>
40
+ * </Form>
41
+ *
42
+ * <Form :schema="schema" :subscribe="['values', 'isSubmitting']">
43
+ * <template #default="{ form, subscribedValues }">
44
+ * <Input :form="form" name="foobar" />
45
+ * </template>
46
+ * </Form>
47
+ */
48
+ /* eslint-disable @typescript-eslint/no-explicit-any */
49
+ import { useStore, type StandardSchemaV1Issue } from "@tanstack/vue-form"
50
+ import { type S } from "effect-app"
51
+ import {
52
+ type FilterItems,
53
+ type FormProps,
54
+ type MetaRecord,
55
+ type OmegaFormApi,
56
+ type OmegaFormState,
57
+ } from "./OmegaFormStuff"
58
+ import { getOmegaStore } from "./getOmegaStore"
59
+ import { provideOmegaErrors } from "./OmegaErrorsContext"
60
+ import { useOmegaForm } from "./useOmegaForm"
61
+ import { watch } from "vue"
62
+
63
+ const props = defineProps<
64
+ {
65
+ subscribe?: K[]
66
+ } & (
67
+ | {
68
+ form: OmegaFormApi<To, From> & {
69
+ meta: MetaRecord<To>
70
+ filterItems?: FilterItems
71
+ }
72
+ schema?: undefined
73
+ }
74
+ | (FormProps<To, From> & {
75
+ form?: undefined
76
+ schema: S.Schema<From, To, never>
77
+ })
78
+ )
79
+ >()
80
+
81
+ const form = props.form ?? useOmegaForm<From, To>(props.schema, props)
82
+
83
+ const formIsSubmitting = useStore(form.store, state => state.isSubmitting)
84
+
85
+ defineExpose(form)
86
+
87
+ const subscribedValues = getOmegaStore(
88
+ form as OmegaFormApi<To, From>,
89
+ props.subscribe,
90
+ )
91
+
92
+ const formSubmissionAttempts = useStore(
93
+ form.store,
94
+ state => state.submissionAttempts,
95
+ )
96
+
97
+ const errors = form.useStore(state => state.errors)
98
+
99
+ watch(
100
+ () => [form.filterItems, errors.value],
101
+ () => {
102
+ const filterItems: FilterItems | undefined = form.filterItems
103
+ if (!filterItems) return {}
104
+ if (!errors.value) return {}
105
+ const errorList = Object.values(errors.value)
106
+ .filter(
107
+ (fieldErrors): fieldErrors is Record<string, StandardSchemaV1Issue[]> =>
108
+ Boolean(fieldErrors),
109
+ )
110
+ .flatMap(fieldErrors =>
111
+ Object.values(fieldErrors)
112
+ .flat()
113
+ .map((issue: StandardSchemaV1Issue) => issue.message),
114
+ )
115
+ if (errorList.some(e => e === filterItems.message)) {
116
+ filterItems.items.forEach((item: any) => {
117
+ const m: any = form.getFieldMeta(item)
118
+ form.setFieldMeta(item, {
119
+ ...m,
120
+ errorMap: {
121
+ onSubmit: [{ path: [item], message: filterItems.message }],
122
+ },
123
+ })
124
+ })
125
+ }
126
+ return {}
127
+ },
128
+ )
129
+
130
+ provideOmegaErrors(formSubmissionAttempts, errors)
131
+ </script>
132
+
133
+ <style scoped>
134
+ fieldset {
135
+ display: contents;
136
+ }
137
+ </style>
@@ -0,0 +1,32 @@
1
+ import { useStore } from "@tanstack/vue-form"
2
+ import { type Ref, computed } from "vue"
3
+ import type { OmegaFormState, OmegaFormApi } from "./OmegaFormStuff"
4
+
5
+ export function getOmegaStore<
6
+ To,
7
+ From,
8
+ K extends keyof OmegaFormState<To, From> = keyof OmegaFormState<To, From>,
9
+ >(
10
+ form: OmegaFormApi<To, From>,
11
+ subscribe?: K[],
12
+ ): Ref<
13
+ K[] extends undefined
14
+ ? Record<string, never>
15
+ : Pick<OmegaFormState<To, From>, K>
16
+ > {
17
+ return computed(() => {
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ if (!subscribe) return {} as any
20
+
21
+ const state = useStore(form.store, state => {
22
+ const result = {} as Pick<OmegaFormState<To, From>, K>
23
+ for (const key of subscribe) {
24
+ result[key] = state[key]
25
+ }
26
+ return result
27
+ })
28
+
29
+ return state.value
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ }) as any // Type assertion needed due to Vue's computed typing limitations
32
+ }
@@ -0,0 +1,29 @@
1
+ import { defineCustomElement } from "vue"
2
+ import { default as OmegaForm } from "./OmegaWrapper.vue"
3
+ import { default as OmegaInput } from "./OmegaInput.vue"
4
+ import { default as OmegaErrors } from "./OmegaErrors.vue"
5
+
6
+ export * as OmegaErrorsContext from "./OmegaErrorsContext"
7
+ export * from "./OmegaFormStuff"
8
+ export { useOmegaForm } from "./useOmegaForm"
9
+ export { default } from "./OmegaWrapper.vue"
10
+
11
+ export { OmegaForm, OmegaInput, OmegaErrors }
12
+
13
+ const OmegaFormCE = defineCustomElement(OmegaForm)
14
+ const OmegaInputCE = defineCustomElement(OmegaInput)
15
+ const OmegaErrorsCE = defineCustomElement(OmegaErrors)
16
+
17
+ export { OmegaFormCE, OmegaInputCE, OmegaErrorsCE }
18
+
19
+ export function registerOmegaForm() {
20
+ if (!customElements.get("omega-form")) {
21
+ customElements.define("omega-form", OmegaFormCE)
22
+ }
23
+ if (!customElements.get("omega-input")) {
24
+ customElements.define("omega-input", OmegaInputCE)
25
+ }
26
+ if (!customElements.get("omega-errors")) {
27
+ customElements.define("omega-errors", OmegaErrorsCE)
28
+ }
29
+ }
@@ -0,0 +1,61 @@
1
+ import {
2
+ useForm,
3
+ type FormValidateOrFn,
4
+ type FormAsyncValidateOrFn,
5
+ type StandardSchemaV1,
6
+ } from "@tanstack/vue-form"
7
+ import { S } from "effect-app"
8
+ import {
9
+ generateMetaFromSchema,
10
+ type FilterItems,
11
+ type FormProps,
12
+ type MetaRecord,
13
+ type OmegaFormApi,
14
+ } from "./OmegaFormStuff"
15
+ /* eslint-disable @typescript-eslint/no-explicit-any */
16
+ export const useOmegaForm = <
17
+ From extends Record<PropertyKey, any>,
18
+ To extends Record<PropertyKey, any>,
19
+ >(
20
+ schema: S.Schema<From, To, never>,
21
+ options?: NoInfer<FormProps<To, From>>,
22
+ ): OmegaFormApi<To, From> & {
23
+ meta: MetaRecord<To>
24
+ filterItems?: FilterItems
25
+ } => {
26
+ if (!schema) throw new Error("Schema is required")
27
+ const standardSchema = S.standardSchemaV1(schema)
28
+
29
+ const { filterItems, meta } = generateMetaFromSchema(schema)
30
+
31
+ const form = useForm<
32
+ To,
33
+ FormValidateOrFn<To> | undefined,
34
+ FormValidateOrFn<To> | undefined,
35
+ StandardSchemaV1<To, From>,
36
+ FormValidateOrFn<To> | undefined,
37
+ FormAsyncValidateOrFn<To> | undefined,
38
+ FormValidateOrFn<To> | undefined,
39
+ FormAsyncValidateOrFn<To> | undefined,
40
+ FormAsyncValidateOrFn<To> | undefined,
41
+ FormAsyncValidateOrFn<To> | undefined
42
+ >({
43
+ ...options,
44
+ validators: {
45
+ onSubmit: standardSchema,
46
+ ...(options?.validators || {}),
47
+ },
48
+ onSubmit: options?.onSubmit
49
+ ? ({ formApi, meta, value }) =>
50
+ options.onSubmit?.({
51
+ formApi: formApi as OmegaFormApi<To, From>,
52
+ meta,
53
+ value: value as unknown as From,
54
+ })
55
+ : undefined,
56
+ }) satisfies OmegaFormApi<To, From>
57
+
58
+ const exposed = Object.assign(form, { meta, filterItems })
59
+
60
+ return exposed
61
+ }
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div class="cool">I'm so <span class="super">{{ cool }}</span> {{ test }}</div>
3
+ </template>
4
+ <style scoped>
5
+ .super {
6
+ color: aqua;
7
+ }
8
+ </style>
9
+ <script setup lang="ts">
10
+ import "./style.css"
11
+ import { ref, defineProps } from "vue"
12
+ const cool = ref("COOL")
13
+
14
+ defineProps<{ test: string }>()
15
+ </script>
@@ -0,0 +1,6 @@
1
+ import TestComponent from './TestComponent.vue'
2
+
3
+ export * from "./OmegaForm"
4
+ export {
5
+ TestComponent
6
+ }
@@ -0,0 +1,3 @@
1
+ .cool {
2
+ color: pink;
3
+ }
@@ -0,0 +1 @@
1
+ export {}
package/src/env.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.vue' {
4
+ import { DefineComponent } from 'vue'
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
6
+ const component: DefineComponent<{}, {}, any>
7
+ export default component
8
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { App } from 'vue'
2
+ import * as components from './components'
3
+
4
+ function install (app: App) {
5
+ for (const key in components) {
6
+ // @ts-expect-error
7
+ app.component(key, components[key])
8
+ }
9
+ }
10
+
11
+ // import './assets/main.scss'
12
+
13
+ export default { install }
14
+
15
+ export * from './components'
16
+ export * from './constants'
17
+ export * from './utils'
@@ -0,0 +1,12 @@
1
+ import { inject, InjectionKey, provide, ref } from "vue";
2
+ import { makeIntl} from "@effect-app/vue"
3
+
4
+ const intlKey = Symbol() as InjectionKey<ReturnType<ReturnType<typeof makeIntl>["useIntl"]>>
5
+ export const useIntl = () => {
6
+ const intl = inject(intlKey)
7
+ if (!intl) {
8
+ throw new Error("useIntl must be used within a IntlProvider")
9
+ }
10
+ return intl
11
+ }
12
+ export const provideIntl = (intl: ReturnType<ReturnType<typeof makeIntl>["useIntl"]>) => provide(intlKey, intl)