@codeleap/form 4.3.8 → 5.0.0

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.
@@ -0,0 +1,172 @@
1
+ import { createStateSlice, GlobalState, globalState } from "@codeleap/store"
2
+
3
+ import { TypeGuards } from "@codeleap/types"
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
5
+ import { FieldPaths, FieldPropertyTuples, FieldTuples, FormDef, FormValues, PropertyForKeys, ValidationResult } from "../types"
6
+
7
+
8
+
9
+
10
+ function buildState<T extends FormDef>(def: T) {
11
+ const stateArg = {}
12
+
13
+ for(const [name, field] of Object.entries(def)) {
14
+ stateArg[name] = field.value
15
+ }
16
+
17
+ return stateArg as FormValues<T>
18
+ }
19
+
20
+
21
+ type FormSelector<T extends FormDef, S> = (form: Form<T>) => S
22
+
23
+
24
+
25
+ class Form<T extends FormDef> {
26
+ id: string
27
+ fields: T
28
+ state: GlobalState<FormValues<T>>
29
+
30
+
31
+ constructor(id: string, shape: T) {
32
+ this.id = id
33
+ this.fields = shape
34
+
35
+ this.state = globalState(
36
+ buildState(this.fields)
37
+ )
38
+
39
+ this.attachState()
40
+
41
+ }
42
+
43
+ get values(){
44
+ return this.state.get()
45
+ }
46
+
47
+ get isValid(){
48
+ const res = this.validate()
49
+
50
+ return Object.values(res).every((result: ValidationResult<any, any>) => result.isValid)
51
+ }
52
+
53
+ slice<K extends FieldPaths<T>>(field: K) {
54
+
55
+ const fieldSlice = createStateSlice(
56
+ this.state,
57
+ (v) => v[field],
58
+ (value) => {
59
+ return {
60
+ [field]: value
61
+ } as FormValues<T>
62
+ }
63
+
64
+ )
65
+
66
+ return fieldSlice
67
+ }
68
+
69
+ iterFields<V>(cb: (field: FieldTuples<T>, index: number) => V){
70
+ const results:V[] = []
71
+ let index = 0
72
+
73
+ for(const [name, field] of Object.entries(this.fields)) {
74
+ const result = cb([ name, field ] as FieldTuples<T>, index)
75
+
76
+ results.push(result)
77
+
78
+ index++
79
+ }
80
+
81
+ return results
82
+ }
83
+
84
+ attachState(){
85
+ this.iterFields(([name, field]) => {
86
+ field.options.name = name
87
+
88
+ field.attach(
89
+ this.slice(name)
90
+ )
91
+ })
92
+ }
93
+
94
+ firstInvalid() {
95
+ for(const [fieldName, field] of Object.entries(this.fields)){
96
+ const validation = field.validate()
97
+
98
+ if(!validation.isValid) return {
99
+ field,
100
+ validation,
101
+ }
102
+ }
103
+ }
104
+
105
+ validate<Fields extends FieldPaths<T>[] = FieldPaths<T>[]>(fields?: Fields): PropertyForKeys<T, Fields[number], '__validationRes'> {
106
+
107
+ const validateFields = fields ?? Object.keys(this.fields)
108
+
109
+ const results = this.iterFields(([name, field]) => {
110
+ if(!validateFields.includes(name)) return null
111
+
112
+ return [name, field.validate()] as FieldPropertyTuples<T, '__validationRes'>
113
+ })
114
+
115
+ const resultMap = Object.fromEntries(
116
+ results.filter(v => !TypeGuards.isNil(v))
117
+ )
118
+
119
+ return resultMap as unknown as PropertyForKeys<T, Fields[number], '__validationRes'>
120
+ }
121
+
122
+
123
+
124
+ register(field: FieldPaths<T>) {
125
+ if(!this.fields[field]){
126
+ throw new Error(`Field "${field}" not found in "${this.id}" form`)
127
+ }
128
+ return this.fields[field].props()
129
+ }
130
+
131
+
132
+ use<Selected>(selector: FormSelector<T, Selected>): Selected {
133
+
134
+
135
+ const [selected, setSelected] = useState(() => selector(this))
136
+
137
+
138
+ const reselect = useCallback(() => {
139
+ setSelected(selector(this))
140
+ }, [selector])
141
+
142
+ useEffect(() => {
143
+ return this.state.listen((value, previous) => {
144
+ if(value != previous){
145
+ reselect()
146
+ }
147
+ })
148
+ }, [reselect])
149
+
150
+
151
+
152
+ return selected
153
+ }
154
+
155
+
156
+
157
+
158
+ }
159
+
160
+
161
+ export function useForm<T extends FormDef>(name: string, def: T) {
162
+ const form = useMemo(() => {
163
+ return new Form(name, def)
164
+ }, [name])
165
+
166
+
167
+ return form
168
+ }
169
+
170
+ export function form<Def extends FormDef>(...args: ConstructorParameters<typeof Form<Def>>) {
171
+ return new Form(...args)
172
+ }
@@ -0,0 +1,19 @@
1
+
2
+ import { FormDef, Validator } from "../types"
3
+ import { Field } from "./Field"
4
+
5
+ type FieldBuilder<T, Validate extends Validator<any,any,any>> = typeof Field<T, Validate>
6
+
7
+ export function fieldFactory<
8
+ T extends FieldBuilder<any,any>,
9
+ Value = T extends FieldBuilder<infer V, any> ? V : never,
10
+ Validator = T extends FieldBuilder<infer VL, any> ? VL : never,
11
+ >(cls: T) {
12
+ return <A extends ConstructorParameters<T>[0]>(options?: A): Field<Value, A['validate']> => {
13
+
14
+ // @ts-expect-error
15
+ return new cls(options ?? {})
16
+ }
17
+ }
18
+
19
+
@@ -0,0 +1,2 @@
1
+ export * from './Form'
2
+ export {Field} from './Field'
@@ -0,0 +1,40 @@
1
+ import { useImperativeHandle } from "react"
2
+ import { IFieldRef } from "../types"
3
+
4
+ export function useFieldBinding<T>(ref: React.Ref<IFieldRef<T>>, impl: Partial<IFieldRef<T>>, deps = []){
5
+
6
+ const notImplemented = (method: string) => {
7
+ throw new Error(`ref.${method} not implemented for ${this._type} field`)
8
+ }
9
+
10
+
11
+ useImperativeHandle(ref, ( ) => ({
12
+ blur: () => {
13
+ notImplemented('blur')
14
+ },
15
+ emit: () => {
16
+ notImplemented('emit')
17
+ },
18
+ focus: () => {
19
+ notImplemented('focus')
20
+ },
21
+ // @ts-expect-error
22
+ getValue: () => {
23
+ notImplemented('getValue')
24
+ },
25
+ scrollIntoView: async () => {
26
+ notImplemented('scrollIntoView')
27
+ },
28
+ hideValue() {
29
+ notImplemented('hideValue')
30
+ },
31
+ revealValue(){
32
+ notImplemented('revealValue')
33
+ },
34
+ toggleValueVisibility(){
35
+ notImplemented('toggleValueVisibility')
36
+ },
37
+ ...impl
38
+ }), deps)
39
+
40
+ }
@@ -0,0 +1,38 @@
1
+ import { WritableStore } from 'nanostores'
2
+ import { Validator } from './validation'
3
+
4
+
5
+
6
+ export type FieldState<T> = WritableStore<T>
7
+
8
+ export interface ExtraFieldOptions {
9
+
10
+ }
11
+
12
+ export type FieldOptions<
13
+ T,
14
+ Validate extends Validator<T, any, any>
15
+ > = {
16
+ name?: string
17
+ defaultValue?: T | null
18
+ state?: FieldState<T>
19
+
20
+ validate?: Validate
21
+
22
+ loader?: (form: any) => Partial<
23
+ Omit<FieldOptions<T, Validate >, 'loader'>
24
+ >
25
+
26
+ onValueChange?: (newValue: T) => void
27
+
28
+ } & ExtraFieldOptions
29
+
30
+
31
+ export type FieldMeasureResult = {
32
+ x?: number
33
+ y?: number
34
+ width?: number
35
+ height?: number
36
+ pageX?: number
37
+ pageY?: number
38
+ }
@@ -0,0 +1,36 @@
1
+ import { Field } from "../lib/Field"
2
+ import { ValidationResult } from "./validation"
3
+
4
+ export type FormDef = Record<string, Field<any,any>>
5
+
6
+ export type NarrowKeyof<T> = Extract<keyof T, string>
7
+
8
+ export type FormValues<T extends FormDef> = {
9
+ [K in NarrowKeyof<T>]: T[K]['value']
10
+ }
11
+
12
+ export type FormErrors<T extends FormDef> = {
13
+ [K in NarrowKeyof<T>]: ReturnType<T[K]['validate']> extends ValidationResult<any, infer E> ? E : never
14
+ }
15
+
16
+ export type FormResults<T extends FormDef> = {
17
+ [K in NarrowKeyof<T>]: ReturnType<T[K]['validate']> extends ValidationResult<infer R, any> ? R : never
18
+ }
19
+
20
+ export type FieldTuples<T extends FormDef> = {
21
+ [K in NarrowKeyof<T>]: [K, T[K]]
22
+ }[NarrowKeyof<T>]
23
+
24
+ export type PropertyForKeys<T extends FormDef, Keys extends FieldPaths<T>, Property extends keyof Field<any,any>> = {
25
+ [K in Keys]: T[K][Property]
26
+ }
27
+
28
+ export type FieldPropertyTuples<T extends FormDef, Property extends keyof Field<any,any>> = {
29
+ [K in NarrowKeyof<T>]: [K, T[K][Property]]
30
+ }[NarrowKeyof<T>]
31
+
32
+
33
+
34
+ export type FieldPaths<T extends FormDef> = ({
35
+ [K in NarrowKeyof<T>]: K
36
+ })[NarrowKeyof<T>]
@@ -0,0 +1,15 @@
1
+ export interface IFieldRef<T> {
2
+ getValue(): T
3
+ scrollIntoView(): Promise<void>
4
+ focus(): void
5
+ blur(): void
6
+ revealValue(): void
7
+ toggleValueVisibility(): void
8
+ hideValue(): void
9
+ emit(event: string, ...args: any[]): void
10
+ }
11
+
12
+
13
+ export interface IFieldProps {
14
+
15
+ }
@@ -0,0 +1,4 @@
1
+ export type * from './field'
2
+ export type * from './form'
3
+ export type * from './validation'
4
+ export type * from './globals'
@@ -0,0 +1,10 @@
1
+ export type ValidationResult<Result, Err> = {
2
+ isValid: boolean
3
+ result?: Result
4
+ error?: Err
5
+ readableError?: string
6
+ }
7
+
8
+ export type ValidatorFunction<Value, Result, Err> = (value: Value, form: any) => ValidationResult<Result, Err>
9
+
10
+ export type Validator<Value, Result, Err> = ValidatorFunction<Value, Result, Err>
@@ -0,0 +1 @@
1
+ export * from './zod'
@@ -0,0 +1,37 @@
1
+ import { z, ZodIssue } from 'zod'
2
+ import { ValidationResult } from '../types'
3
+ import { TypeGuards } from '@codeleap/types'
4
+
5
+ type ZodValidationResult<T extends z.ZodType> = ValidationResult<z.infer<T>, z.ZodError['issues']>
6
+
7
+ export function zodValidator<T extends z.ZodType>(model: T) {
8
+ return (value): ZodValidationResult<T> => {
9
+ const result = model.safeParse(value)
10
+
11
+ return {
12
+ isValid: result.success,
13
+ error: result.error?.issues,
14
+ result: result.data
15
+ }
16
+ }
17
+ }
18
+
19
+
20
+ const isZodIssue = (val: any): val is ZodIssue => {
21
+ return ['code','expected','received','path','message'].every(x => x in val)
22
+ }
23
+
24
+ export function isZodValidationResult(val: any): val is ZodValidationResult<any> {
25
+ const isValidABoolean = TypeGuards.isBoolean(val.isValid)
26
+
27
+ if(isValidABoolean) {
28
+ if(!val.isValid) {
29
+ return TypeGuards.isArray(val.error) && val.error.every(isZodIssue)
30
+ } else {
31
+ return 'result' in val
32
+ }
33
+ }
34
+
35
+ return false
36
+
37
+ }
package/src/constants.ts DELETED
@@ -1,33 +0,0 @@
1
- import * as Form from './types'
2
-
3
- export const defaultFieldValues: Partial<Record<Form.FormField['type'], any>> =
4
- {
5
- checkbox: false,
6
- text: '',
7
-
8
- file: null,
9
- multipleFile: [],
10
- 'range-slider': [0, 100],
11
- slider: 1,
12
- number: '',
13
- list: [],
14
- switch: false,
15
- date: null,
16
- }
17
-
18
- export const changeEventNames: Partial<Record<Form.FormField['type'], string>> =
19
- {
20
- checkbox: 'onValueChange',
21
- 'range-slider': 'onValueChange',
22
- slider: 'onValueChange',
23
- select: 'onValueChange',
24
- radio: 'onValueChange',
25
- text: 'onChangeText',
26
- file: 'onFileSelect',
27
- multipleFile: 'onFileSelect',
28
- number: 'onChangeText',
29
- list: 'onValueChange',
30
- switch: 'onValueChange',
31
- date: 'onValueChange',
32
- }
33
-
package/src/createForm.ts DELETED
@@ -1,105 +0,0 @@
1
- import { defaultFieldValues } from './constants'
2
- import * as Form from './types'
3
- import * as yup from 'yup'
4
- import { humanizeCamelCase } from '../../utils'
5
- import { changeEventNames } from './constants'
6
-
7
- function getDefaultValue(field: Partial<Form.FormField>) {
8
- switch (field.type) {
9
- case 'radio':
10
- case 'select':
11
- return field.options?.[0]?.value || ''
12
- default:
13
- return defaultFieldValues[field.type]
14
- }
15
- }
16
-
17
- export function getValidator(validate: Form.Validator<any>): Form.ValidatorFunction {
18
- if (!validate) return undefined
19
-
20
- if (typeof validate === 'function') {
21
- return validate
22
- }
23
-
24
- const yupModel = validate as yup.StringSchema
25
-
26
- return (value) => {
27
- try {
28
- yupModel.validateSync(value)
29
- return { valid: true, message: '' }
30
- } catch (e) {
31
- return { valid: false, message: e?.errors?.join(' ') || '' }
32
- }
33
- }
34
- }
35
-
36
- function buildInitialFormState<T extends Form.FieldsMap>(
37
- name: string,
38
- form: T,
39
- inside = [],
40
- ) {
41
- const state = {} as Form.MapValues<T>
42
- const props = {}
43
- let numberOfTextFields = 0
44
- for (const [k, value] of Object.entries(form)) {
45
- const { defaultValue, label, validate, type, ...fieldConfig } = value
46
-
47
- const key = k as keyof Form.MapValues<T>
48
-
49
- const fieldPathParts = [...inside, key]
50
- const fieldPath = fieldPathParts.join('.')
51
-
52
- let fieldValue = null
53
- if (type === 'text') numberOfTextFields += 1
54
- if (false) {
55
- // const { props: subFieldProps, state } = buildInitialFormState(
56
- // name,
57
- // value.fields,
58
- // fieldPathParts,
59
- // )
60
- // fieldValue = state
61
-
62
- // props = {
63
- // ...props,
64
- // ...subFieldProps,
65
- // }
66
- } else {
67
- fieldValue =
68
- typeof defaultValue !== 'undefined'
69
- ? defaultValue
70
- : getDefaultValue(value)
71
- }
72
-
73
- const fieldProps: any = {
74
- id: `form-${name}:${fieldPath}`,
75
- label: label || humanizeCamelCase(k),
76
- changeEventName: changeEventNames[type],
77
- type,
78
- ...fieldConfig,
79
- }
80
-
81
- delete fieldProps.fields
82
-
83
- if (validate) fieldProps.validate = getValidator(validate)
84
-
85
- props[fieldPath] = fieldProps
86
- state[key] = fieldValue
87
- }
88
-
89
- return { state, props, numberOfTextFields }
90
- }
91
-
92
- export function createForm<T extends Form.FieldsMap>(
93
- name: string,
94
- formArgs: Form.FormConfig<T>,
95
- ): Form.CreateFormReturn<T> {
96
- const { state, props, numberOfTextFields } = buildInitialFormState(name, formArgs)
97
-
98
- return {
99
- config: formArgs as T,
100
- defaultValue: state,
101
- staticFieldProps: props,
102
- name,
103
- numberOfTextFields,
104
- }
105
- }
package/src/presets.ts DELETED
@@ -1,14 +0,0 @@
1
- export const inputPresets = {
2
- email: () => ({
3
-
4
- }),
5
- name: () => ({
6
- autoCapialize: 'words',
7
- autoComplete: 'name',
8
-
9
- }),
10
- chat: () => ({
11
- returnKeyType: 'send',
12
-
13
- }),
14
- }