@idem.agency/form-builder 0.0.12 → 0.0.13

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 (56) hide show
  1. package/package.json +5 -2
  2. package/CHANGELOG.md +0 -16
  3. package/eslint.config.js +0 -23
  4. package/public/index.html +0 -13
  5. package/public/main.tsx +0 -97
  6. package/src/app/index.test.tsx +0 -137
  7. package/src/app/index.tsx +0 -34
  8. package/src/app/test.css +0 -1
  9. package/src/entity/dynamicBuilder/element.tsx +0 -71
  10. package/src/entity/dynamicBuilder/index.tsx +0 -21
  11. package/src/entity/dynamicBuilder/model/createBuilderContext.tsx +0 -52
  12. package/src/entity/dynamicBuilder/model/index.ts +0 -7
  13. package/src/entity/inputs/index.ts +0 -2
  14. package/src/entity/inputs/ui/group/index.test.tsx +0 -46
  15. package/src/entity/inputs/ui/group/index.tsx +0 -26
  16. package/src/entity/inputs/ui/input/index.test.tsx +0 -73
  17. package/src/entity/inputs/ui/input/index.tsx +0 -31
  18. package/src/plugins/validation/index.ts +0 -39
  19. package/src/plugins/validation/rules/confirm.test.ts +0 -23
  20. package/src/plugins/validation/rules/confirm.ts +0 -11
  21. package/src/plugins/validation/rules/email.test.ts +0 -20
  22. package/src/plugins/validation/rules/email.ts +0 -10
  23. package/src/plugins/validation/rules/index.ts +0 -5
  24. package/src/plugins/validation/rules/required.test.ts +0 -24
  25. package/src/plugins/validation/rules/required.ts +0 -9
  26. package/src/plugins/validation/types/index.ts +0 -7
  27. package/src/plugins/validation/validation.test.ts +0 -69
  28. package/src/plugins/validation/validation.ts +0 -57
  29. package/src/plugins/visibility/core.ts +0 -56
  30. package/src/plugins/visibility/index.ts +0 -25
  31. package/src/plugins/visibility/types.ts +0 -20
  32. package/src/shared/hook/useUpdateEffect.tsx +0 -23
  33. package/src/shared/model/index.ts +0 -12
  34. package/src/shared/model/plugins/EventBus.ts +0 -15
  35. package/src/shared/model/plugins/FieldRegistry.ts +0 -15
  36. package/src/shared/model/plugins/HookRegistry.ts +0 -21
  37. package/src/shared/model/plugins/MiddlewareRegistry.ts +0 -17
  38. package/src/shared/model/plugins/PipelineRegistry.ts +0 -73
  39. package/src/shared/model/plugins/PluginManager.ts +0 -84
  40. package/src/shared/model/plugins/context.tsx +0 -10
  41. package/src/shared/model/plugins/hooks.ts +0 -22
  42. package/src/shared/model/plugins/index.ts +0 -23
  43. package/src/shared/model/plugins/types.ts +0 -99
  44. package/src/shared/model/store/createStoreContext.tsx +0 -70
  45. package/src/shared/model/store/index.test.tsx +0 -113
  46. package/src/shared/model/store/index.tsx +0 -96
  47. package/src/shared/model/store/store.test.ts +0 -64
  48. package/src/shared/model/store/store.ts +0 -38
  49. package/src/shared/test-utils.tsx +0 -33
  50. package/src/shared/types/common.ts +0 -45
  51. package/src/shared/utils.test.ts +0 -55
  52. package/src/shared/utils.ts +0 -36
  53. package/src/widgets/form/form.tsx +0 -59
  54. package/tsconfig.json +0 -24
  55. package/tsdown.config.ts +0 -10
  56. package/vite.config.ts +0 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idem.agency/form-builder",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Построитель форм",
5
5
  "main": "src/index.ts",
6
6
  "author": "idem.agency",
@@ -23,6 +23,9 @@
23
23
  "react-dom": "19.2.3",
24
24
  "zod": "4.3.6"
25
25
  },
26
+ "files": [
27
+ "dist"
28
+ ],
26
29
  "devDependencies": {
27
30
  "@eslint/js": "9.39.2",
28
31
  "@tailwindcss/vite": "4.1.18",
@@ -43,5 +46,5 @@
43
46
  "vite-tsconfig-paths": "6.0.5",
44
47
  "vitest": "4.0.17"
45
48
  },
46
- "gitHead": "47f4057163e739a39cfd1616bfb6e720bdb7f592"
49
+ "gitHead": "700ffe97c5801f382c16129af4ecb85fd476e030"
47
50
  }
package/CHANGELOG.md DELETED
@@ -1,16 +0,0 @@
1
- # Change Log
2
-
3
- All notable changes to this project will be documented in this file.
4
- See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
-
6
- ## [0.0.12](https://gitlab.idem.agency/idem-project/front-package/compare/@idem.agency/form-builder@0.0.11...@idem.agency/form-builder@0.0.12) (2026-03-16)
7
-
8
- **Note:** Version bump only for package @idem.agency/form-builder
9
-
10
-
11
-
12
-
13
-
14
- ## [0.0.11](https://gitlab.idem.agency/idem-project/front-package/compare/@idem.agency/form-builder@0.0.10...@idem.agency/form-builder@0.0.11) (2026-02-09)
15
-
16
- **Note:** Version bump only for package @idem.agency/form-builder
package/eslint.config.js DELETED
@@ -1,23 +0,0 @@
1
- import js from '@eslint/js'
2
- import globals from 'globals'
3
- import reactHooks from 'eslint-plugin-react-hooks'
4
- import reactRefresh from 'eslint-plugin-react-refresh'
5
- import tseslint from 'typescript-eslint'
6
- import { globalIgnores } from 'eslint/config'
7
-
8
- export default tseslint.config([
9
- globalIgnores(['dist']),
10
- {
11
- files: ['**/*.{ts,tsx}'],
12
- extends: [
13
- js.configs.recommended,
14
- tseslint.configs.recommended,
15
- reactHooks.configs['recommended-latest'],
16
- reactRefresh.configs.vite,
17
- ],
18
- languageOptions: {
19
- ecmaVersion: 2020,
20
- globals: globals.browser,
21
- },
22
- },
23
- ])
package/public/index.html DELETED
@@ -1,13 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Виджет для генерации форм</title>
8
- </head>
9
- <body>
10
- <div id="root"></div>
11
- <script type="module" src="main.tsx"></script>
12
- </body>
13
- </html>
package/public/main.tsx DELETED
@@ -1,97 +0,0 @@
1
- import {StrictMode} from 'react'
2
- import {createRoot} from 'react-dom/client'
3
- import {plugins, FormBuilder, inputs} from "@/index";
4
- import type {FormBuilderRef, FormElementRegistry, FormFieldConfig} from "@/index";
5
- import {useRef} from "react";
6
-
7
- function Debug() {
8
-
9
- const fields: FormElementRegistry = {
10
- text: inputs.TextField,
11
- group: inputs.FormGroup,
12
- }
13
-
14
- const layout: FormFieldConfig[] = [
15
- {
16
- name: 'username',
17
- label: 'Имя пользователя',
18
- type: 'text',
19
- placeholder: 'Введите ваше имя',
20
- defaultValue: 'Мое имя',
21
- validation: ['required', 'confirm:personal.username_2,Имя 2 пользователя'],
22
- visibility: {
23
- logic: 'and',
24
- rules: [
25
- {
26
- field: 'personal.username_2',
27
- operator: 'in',
28
- value: ['Alex']
29
- },
30
- ]
31
- }
32
- },
33
- {
34
- name: 'personal',
35
- label: 'Группа',
36
- type: 'group',
37
- variant: 'col',
38
- fields: [
39
- {
40
- name: 'username_2',
41
- label: 'Имя 2 пользователя',
42
- type: 'text',
43
- placeholder: 'Введите ваше имя 2',
44
- defaultValue: 'Мое имя',
45
- validation: ['required'],
46
- },
47
- {
48
- name: 'username_3',
49
- label: 'Имя 3 пользователя',
50
- type: 'text',
51
- placeholder: 'Введите ваше имя 2',
52
- defaultValue: 'Мое имя',
53
- validation: ['required'],
54
- },
55
- {
56
- name: 'username_4',
57
- label: 'Имя 4 пользователя',
58
- type: 'text',
59
- placeholder: 'Введите ваше имя 2',
60
- defaultValue: 'Мое имя',
61
- validation: ['required'],
62
- },
63
- ],
64
- },
65
- ];
66
-
67
- const ref = useRef<FormBuilderRef>(null);
68
-
69
- const validation = plugins.createValidationPlugin();
70
- const visibility = plugins.createVisibilityPlugin();
71
-
72
- return <>
73
- <FormBuilder plugins={[validation, visibility]} ref={ref} layout={layout} onChange={(e) => {
74
- console.log(e);
75
- }} onSubmit={(e) => {
76
- console.log(e);
77
- }} fields={fields} formData={{
78
- personal: {
79
- username_2: "Alex"
80
- }
81
- }}></FormBuilder>
82
- <button onClick={() => {
83
- ref.current?.reset();
84
- }}>Сбросить
85
- </button>
86
- <button onClick={() => {
87
- ref.current?.submit();
88
- }}>Отправить
89
- </button>
90
- </>
91
- }
92
-
93
- createRoot(document.getElementById('root')!).render(
94
- <StrictMode>
95
- <Debug/>
96
- </StrictMode>,
97
- )
@@ -1,137 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { render, fireEvent, act } from '@testing-library/react'
3
- import { createRef } from 'react'
4
- import { FormBuilder } from './index'
5
- import type { FormBuilderRef, FormElementProps } from '@/shared/types/common'
6
-
7
- const TextField = ({ field, value, onChange }: FormElementProps) => (
8
- <input
9
- data-testid={`field-${field.name}`}
10
- name={field.name}
11
- value={value || ''}
12
- onChange={e => onChange(e.target.value)}
13
- />
14
- )
15
-
16
- const fields = { text: TextField }
17
-
18
- const layout = [
19
- { type: 'text', name: 'username', label: 'Username' },
20
- ]
21
-
22
- describe('FormBuilder (integration)', () => {
23
- it('renders fields from layout via registry', () => {
24
- const { getByTestId } = render(
25
- <FormBuilder layout={layout} fields={fields} />
26
- )
27
- expect(getByTestId('field-username')).toBeTruthy()
28
- })
29
-
30
- it('calls onChange when a field value changes', async () => {
31
- const onChange = vi.fn()
32
- const { getByTestId } = render(
33
- <FormBuilder layout={layout} fields={fields} onChange={onChange} />
34
- )
35
- await act(async () => {
36
- fireEvent.change(getByTestId('field-username'), { target: { value: 'John' } })
37
- })
38
- expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ username: 'John' }))
39
- })
40
-
41
- it('ref.reset() clears the form', async () => {
42
- const ref = createRef<FormBuilderRef>()
43
- const { getByTestId } = render(
44
- <FormBuilder ref={ref} layout={layout} fields={fields} />
45
- )
46
- await act(async () => {
47
- fireEvent.change(getByTestId('field-username'), { target: { value: 'John' } })
48
- })
49
- expect((getByTestId('field-username') as HTMLInputElement).value).toBe('John')
50
- act(() => ref.current!.reset())
51
- expect((getByTestId('field-username') as HTMLInputElement).value).toBe('')
52
- })
53
-
54
- it('ref.submit() calls onSubmit with formData when valid', async () => {
55
- const onSubmit = vi.fn()
56
- const ref = createRef<FormBuilderRef>()
57
- render(
58
- <FormBuilder ref={ref} layout={layout} fields={fields} onSubmit={onSubmit} />
59
- )
60
- await act(async () => { await ref.current!.submit() })
61
- expect(onSubmit).toHaveBeenCalledWith(expect.any(Object))
62
- })
63
-
64
- it('ref.submit() does NOT call onSubmit when validation fails and sets errors', async () => {
65
- const onSubmit = vi.fn()
66
- const ref = createRef<FormBuilderRef>()
67
- const layoutWithValidation = [
68
- { type: 'text', name: 'username', label: 'Username', validation: ['required'] },
69
- ]
70
- render(
71
- <FormBuilder ref={ref} layout={layoutWithValidation} fields={fields} onSubmit={onSubmit} />
72
- )
73
- await act(async () => { await ref.current!.submit() })
74
- expect(onSubmit).not.toHaveBeenCalled()
75
- const errors = ref.current!.errors()
76
- expect(errors.username).toBeDefined()
77
- })
78
-
79
- it('ref.errors() returns current errors', async () => {
80
- const ref = createRef<FormBuilderRef>()
81
- render(
82
- <FormBuilder ref={ref} layout={layout} fields={fields} />
83
- )
84
- const errors = ref.current!.errors()
85
- expect(errors).toEqual({})
86
- })
87
-
88
- it('formData prop initializes the form with initial values', () => {
89
- const { getByTestId } = render(
90
- <FormBuilder
91
- layout={layout}
92
- fields={fields}
93
- formData={{ username: 'InitialUser' }}
94
- />
95
- )
96
- expect((getByTestId('field-username') as HTMLInputElement).value).toBe('InitialUser')
97
- })
98
-
99
- it('plugins can intercept field:change pipeline', async () => {
100
- const stages: string[] = []
101
- const testPlugin = {
102
- name: 'test',
103
- install(ctx: any) {
104
- ctx.pipeline.use('field:change', (data: any, next: any) => {
105
- stages.push('plugin')
106
- return next({ ...data, value: data.value.toUpperCase() })
107
- })
108
- },
109
- }
110
- const onChange = vi.fn()
111
- const { getByTestId } = render(
112
- <FormBuilder layout={layout} fields={fields} onChange={onChange} plugins={[testPlugin]} />
113
- )
114
- await act(async () => {
115
- fireEvent.change(getByTestId('field-username'), { target: { value: 'john' } })
116
- })
117
- expect(stages).toContain('plugin')
118
- expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ username: 'JOHN' }))
119
- })
120
-
121
- it('plugins can tap form:submit:before hook', async () => {
122
- const hookCalled = vi.fn()
123
- const testPlugin = {
124
- name: 'test',
125
- install(ctx: any) {
126
- ctx.events.tap('form:submit:before', hookCalled)
127
- },
128
- }
129
- const onSubmit = vi.fn()
130
- const ref = createRef<FormBuilderRef>()
131
- render(
132
- <FormBuilder ref={ref} layout={layout} fields={fields} onSubmit={onSubmit} plugins={[testPlugin]} />
133
- )
134
- await act(async () => { await ref.current!.submit() })
135
- expect(hookCalled).toHaveBeenCalled()
136
- })
137
- })
package/src/app/index.tsx DELETED
@@ -1,34 +0,0 @@
1
- import type { FormBuilderRef, TFormBuilder } from "../shared/types/common";
2
- import { forwardRef, useEffect, useMemo } from "react";
3
- import { FormStoreProvider } from "@/shared/model/store";
4
- import { Form } from "@/widgets/form/form";
5
- import { PluginManager } from '@/shared/model/plugins/PluginManager';
6
- import { PluginSystemContext } from '@/shared/model/plugins/context';
7
- import { createValidationPlugin } from '@/plugins/validation';
8
-
9
- export const FormBuilder = forwardRef<FormBuilderRef, TFormBuilder>((props, ref) => {
10
- const { formData, ...innerProps } = props;
11
-
12
- // eslint-disable-next-line react-hooks/exhaustive-deps
13
- const pluginManager = useMemo(() => {
14
- const pm = new PluginManager([createValidationPlugin(props.validator), ...(props.plugins ?? [])]);
15
- pm.install();
16
- return pm;
17
- }, []);
18
-
19
- useEffect(() => {
20
- // Re-install after Strict Mode's simulated unmount clears subscriptions
21
- pluginManager.install();
22
- return () => pluginManager.uninstall();
23
- }, [pluginManager]);
24
-
25
- return (
26
- <PluginSystemContext.Provider value={pluginManager}>
27
- <FormStoreProvider formData={formData}>
28
- <Form {...innerProps} ref={ref} />
29
- </FormStoreProvider>
30
- </PluginSystemContext.Provider>
31
- );
32
- });
33
-
34
- export default FormBuilder;
package/src/app/test.css DELETED
@@ -1 +0,0 @@
1
- @import "tailwindcss";
@@ -1,71 +0,0 @@
1
- import { useEffect } from 'react';
2
- import { useFormStore, useFormDispatch, useFormStoreInstance } from "@/shared/model/store";
3
- import { getNestedValue } from "@/shared/utils";
4
- import type { FormFieldConfig, FormElementComponent } from "@/shared/types/common";
5
- import { useCallEvents, useRunPipeline, useRunPipelineSync } from '@/shared/model/plugins/hooks';
6
-
7
- type BuilderElementProps = {
8
- element: FormElementComponent;
9
- field: FormFieldConfig;
10
- path: string;
11
- };
12
-
13
- export const DynamicBuilderElement = (props: BuilderElementProps) => {
14
- const Element = props.element;
15
- const field = props.field;
16
- const currentFieldPath = props.path ? `${props.path}.${field.name}` : field.name;
17
- const value = useFormStore((s) => getNestedValue(s.formData, currentFieldPath));
18
- const errors = useFormStore((s) => s.errors[currentFieldPath]);
19
- const dispatch = useFormDispatch();
20
- const store = useFormStoreInstance();
21
- const callEvents = useCallEvents();
22
- const runPipeline = useRunPipeline();
23
- const runPipelineSync = useRunPipelineSync();
24
-
25
- useEffect(() => {
26
- callEvents('field:register', { path: currentFieldPath, field });
27
- }, [currentFieldPath]);
28
-
29
- const visible = useFormStore((s) =>
30
- runPipelineSync('field:visible', {
31
- visible: true,
32
- field,
33
- path: currentFieldPath,
34
- formData: s.formData,
35
- }).visible
36
- );
37
-
38
- if (!visible) return null;
39
-
40
- return (
41
- <Element
42
- field={{ ...field }}
43
- path={currentFieldPath}
44
- value={value}
45
- errors={errors}
46
- onChange={async (value: any) => {
47
- const data = store.getState().formData;
48
- const { value: finalValue, errors } = await runPipeline('field:change', {
49
- value,
50
- field,
51
- path: currentFieldPath,
52
- formData: data,
53
- errors: [],
54
- });
55
- dispatch({
56
- type: 'setFieldValue',
57
- path: currentFieldPath,
58
- value: finalValue,
59
- errors,
60
- });
61
- await callEvents('field:change', {
62
- value: finalValue,
63
- prevValue: value,
64
- field,
65
- path: currentFieldPath,
66
- formData: store.getState().formData,
67
- });
68
- }}
69
- />
70
- );
71
- };
@@ -1,21 +0,0 @@
1
- import type {TDynamicBuilder} from "@/shared/types/common";
2
- import {DynamicBuilderElement} from "./element";
3
- import { useFields } from "@/entity/dynamicBuilder/model";
4
-
5
- export const DynamicBuilder: TDynamicBuilder = (props) => {
6
- const path = props.path ?? '';
7
- const fields = useFields();
8
-
9
- return props.layout.map((field, index) => {
10
- const FormElement = fields[field.type];
11
-
12
- if (!FormElement) {
13
- console.warn(`Неизвестный тип поля: ${field.type}. Проверьте formRegistry.`);
14
- return null;
15
- }
16
-
17
- return <DynamicBuilderElement key={`${field.name}-${index}`} element={FormElement} path={path} field={field}/>
18
- })
19
- }
20
-
21
- export * from './model';
@@ -1,52 +0,0 @@
1
- import React, {createContext, type ReactNode, useCallback, useContext} from "react";
2
- import {DynamicBuilder} from "@/entity/dynamicBuilder";
3
- import type {FormElementRegistry} from "~/src";
4
-
5
- export type TFnBuilder = (layout: any, path: string | undefined, children?: ReactNode) => ReactNode;
6
- export function createBuilderContext() {
7
- const BuilderContext = createContext<{
8
- builder: TFnBuilder,
9
- fields: FormElementRegistry,
10
- } | null>(null);
11
-
12
- const Provider: React.FC<{ children: React.ReactNode, fields: FormElementRegistry }> = ({
13
- fields,
14
- children
15
- }) => {
16
- const builder = useCallback<TFnBuilder>((layout, path, children) => {
17
- return <DynamicBuilder layout={layout} path={path}>{children}</DynamicBuilder>
18
- }, [fields]);
19
-
20
- return (
21
- <BuilderContext.Provider value={{builder, fields}}>
22
- {children}
23
- </BuilderContext.Provider>
24
- );
25
- };
26
-
27
- function useBuilder(): TFnBuilder {
28
- const store = useContext(BuilderContext);
29
-
30
- if (!store) {
31
- throw new Error("StoreProvider missing");
32
- }
33
-
34
- return store.builder
35
- }
36
-
37
- function useFields(): FormElementRegistry {
38
- const store = useContext(BuilderContext);
39
-
40
- if (!store) {
41
- throw new Error("StoreProvider missing");
42
- }
43
-
44
- return store.fields
45
- }
46
-
47
- return {
48
- Provider,
49
- useBuilder,
50
- useFields,
51
- };
52
- }
@@ -1,7 +0,0 @@
1
- import { createBuilderContext } from "./createBuilderContext";
2
-
3
- export const {
4
- Provider: BuilderProvider,
5
- useBuilder,
6
- useFields,
7
- } = createBuilderContext();
@@ -1,2 +0,0 @@
1
- export * from './ui/input/index'
2
- export * from './ui/group/index'
@@ -1,46 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import React from 'react'
3
- import { renderWithProviders } from '@/shared/test-utils'
4
- import { FormGroup } from './index'
5
- import type { FormGroupConfig } from './index'
6
-
7
- const baseGroupField: FormGroupConfig = {
8
- type: 'group',
9
- name: 'myGroup',
10
- label: 'My Group',
11
- fields: [],
12
- }
13
-
14
- describe('FormGroup', () => {
15
- it('renders the label', () => {
16
- const { getByText } = renderWithProviders(
17
- <FormGroup field={baseGroupField} path="myGroup" value={undefined} onChange={() => {}} />
18
- )
19
- expect(getByText('My Group')).toBeTruthy()
20
- })
21
-
22
- it('applies flex-col by default', () => {
23
- const { container } = renderWithProviders(
24
- <FormGroup field={baseGroupField} path="myGroup" value={undefined} onChange={() => {}} />
25
- )
26
- const inner = container.querySelector('.flex-col')
27
- expect(inner).toBeTruthy()
28
- })
29
-
30
- it('applies flex-row when variant is row', () => {
31
- const rowField: FormGroupConfig = { ...baseGroupField, variant: 'row' }
32
- const { container } = renderWithProviders(
33
- <FormGroup field={rowField} path="myGroup" value={undefined} onChange={() => {}} />
34
- )
35
- const inner = container.querySelector('.flex-row')
36
- expect(inner).toBeTruthy()
37
- })
38
-
39
- it('returns null when fields is not an array', () => {
40
- const invalidField = { type: 'group', name: 'g', label: 'G' } as any
41
- const { container } = renderWithProviders(
42
- <FormGroup field={invalidField} path="g" value={undefined} onChange={() => {}} />
43
- )
44
- expect(container.firstChild).toBeNull()
45
- })
46
- })
@@ -1,26 +0,0 @@
1
- import type {FormElementProps, FormFieldBase, FormFieldConfig, RC} from "../../../../shared/types/common.ts";
2
- import clsx from "clsx";
3
- import {useBuilder} from "@/entity/dynamicBuilder";
4
- export type FormGroupConfig = FormFieldBase & { variant?: 'row' | 'col', fields: FormFieldConfig[] };
5
-
6
- function isGroupConfig(field: FormFieldConfig): field is FormGroupConfig {
7
- return Array.isArray(field.fields);
8
- }
9
-
10
-
11
- export const FormGroup: RC<FormElementProps<FormGroupConfig>> = ({field, path}) => {
12
- if (!isGroupConfig(field)) {
13
- return null;
14
- }
15
- const Builder = useBuilder()(field.fields, path);
16
- const variant = field.variant ?? 'col';
17
-
18
- const className = variant == 'col' ? 'flex-col' : 'flex-row';
19
-
20
- return <div>
21
- <div>{field.label}</div>
22
- <div className={clsx(className, 'flex')}>
23
- {Builder}
24
- </div>
25
- </div>;
26
- };
@@ -1,73 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { render, fireEvent } from '@testing-library/react'
3
- import React from 'react'
4
- import { TextField } from './index'
5
-
6
- const baseField = { type: 'text' as const, name: 'username', label: 'Username' }
7
-
8
- describe('TextField', () => {
9
- it('renders label and input', () => {
10
- const { getByText, getByRole } = render(
11
- <TextField field={baseField} path="username" value="" onChange={() => {}} />
12
- )
13
- expect(getByText('Username:')).toBeTruthy()
14
- expect(getByRole('textbox')).toBeTruthy()
15
- })
16
-
17
- it('displays the passed value', () => {
18
- const { getByRole } = render(
19
- <TextField field={baseField} path="username" value="John" onChange={() => {}} />
20
- )
21
- expect((getByRole('textbox') as HTMLInputElement).value).toBe('John')
22
- })
23
-
24
- it('calls onChange when input changes', () => {
25
- const onChange = vi.fn()
26
- const { getByRole } = render(
27
- <TextField field={baseField} path="username" value="" onChange={onChange} />
28
- )
29
- fireEvent.change(getByRole('textbox'), { target: { value: 'Jane' } })
30
- expect(onChange).toHaveBeenCalledWith('Jane')
31
- })
32
-
33
- it('shows error message and red border when errors provided', () => {
34
- const { getByText, getByRole } = render(
35
- <TextField
36
- field={baseField}
37
- path="username"
38
- value=""
39
- errors={{ required: 'Field is required' }}
40
- onChange={() => {}}
41
- />
42
- )
43
- expect(getByText('Field is required')).toBeTruthy()
44
- const input = getByRole('textbox') as HTMLInputElement
45
- expect(input.style.borderColor).toBe('red')
46
- })
47
-
48
- it('returns null for invalid field type', () => {
49
- const invalidField = { type: 'group' as any, name: 'g', label: 'G' }
50
- const { container } = render(
51
- <TextField field={invalidField} path="g" value="" onChange={() => {}} />
52
- )
53
- expect(container.firstChild).toBeNull()
54
- })
55
-
56
- it('works with email type', () => {
57
- const emailField = { type: 'email' as const, name: 'email', label: 'Email' }
58
- const { getByRole } = render(
59
- <TextField field={emailField} path="email" value="a@b.com" onChange={() => {}} />
60
- )
61
- expect((getByRole('textbox') as HTMLInputElement).type).toBe('email')
62
- })
63
-
64
- it('works with password type', () => {
65
- const passField = { type: 'password' as const, name: 'pass', label: 'Password' }
66
- const { container } = render(
67
- <TextField field={passField} path="pass" value="secret" onChange={() => {}} />
68
- )
69
- const input = container.querySelector('input[type="password"]') as HTMLInputElement
70
- expect(input).toBeTruthy()
71
- expect(input.value).toBe('secret')
72
- })
73
- })