@idem.agency/form-builder 0.0.9 → 0.0.11

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 (37) hide show
  1. package/CHANGELOG.md +1 -25
  2. package/dist/index.cjs +325 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +86 -0
  5. package/dist/index.d.mts +86 -0
  6. package/dist/index.mjs +292 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +12 -4
  9. package/{index.html → public/index.html} +1 -2
  10. package/public/main.tsx +90 -0
  11. package/src/app/debug.tsx +0 -89
  12. package/src/app/index.tsx +51 -40
  13. package/src/{widgets/form → entity/inputs}/ui/group/index.tsx +4 -3
  14. package/src/{widgets/form → entity/inputs}/ui/input/index.tsx +5 -3
  15. package/src/index.ts +2 -2
  16. package/src/shared/lib/validation/core.ts +3 -3
  17. package/src/shared/lib/validation/rules/confirm.ts +2 -2
  18. package/src/shared/lib/validation/rules/email.ts +1 -1
  19. package/src/shared/lib/validation/rules/require.ts +1 -1
  20. package/src/shared/model/builder/createContext.tsx +40 -0
  21. package/src/shared/model/builder/index.ts +6 -0
  22. package/src/shared/model/index.ts +0 -1
  23. package/src/shared/model/store/createStoreContext.tsx +74 -0
  24. package/src/shared/model/store/index.ts +46 -0
  25. package/src/shared/model/store/store.ts +27 -0
  26. package/src/shared/types/common.ts +1 -1
  27. package/src/widgets/dynamicBuilder/element.tsx +31 -0
  28. package/src/widgets/dynamicBuilder/index.tsx +28 -58
  29. package/tsconfig.json +22 -5
  30. package/tsdown.config.ts +10 -0
  31. package/vite.config.ts +7 -16
  32. package/src/main.tsx +0 -9
  33. package/src/shared/model/store.tsx +0 -52
  34. package/tsconfig.app.json +0 -26
  35. package/tsconfig.node.json +0 -24
  36. package/vite-env.d.ts +0 -1
  37. /package/src/{widgets/form → entity/inputs}/index.ts +0 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,292 @@
1
+ import { createContext, forwardRef, useContext, useId, useImperativeHandle, useRef, useSyncExternalStore } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import clsx from "clsx";
4
+ import { z } from "zod";
5
+
6
+ //#region src/shared/model/store/store.ts
7
+ function createStore(reducer, initialState) {
8
+ let state = initialState;
9
+ const listeners = /* @__PURE__ */ new Set();
10
+ return {
11
+ getState() {
12
+ return state;
13
+ },
14
+ dispatch(action) {
15
+ state = reducer(state, action);
16
+ listeners.forEach((l) => l());
17
+ },
18
+ subscribe(listener) {
19
+ listeners.add(listener);
20
+ return () => listeners.delete(listener);
21
+ }
22
+ };
23
+ }
24
+
25
+ //#endregion
26
+ //#region src/shared/model/store/createStoreContext.tsx
27
+ function createStoreContext$1(reducer, initialState) {
28
+ const StoreContext = createContext(null);
29
+ const Provider = ({ children }) => {
30
+ const storeRef = useRef(null);
31
+ if (!storeRef.current) storeRef.current = createStore(reducer, initialState);
32
+ return /* @__PURE__ */ jsx(StoreContext.Provider, {
33
+ value: storeRef.current,
34
+ children
35
+ });
36
+ };
37
+ function useStore(selector) {
38
+ const store = useContext(StoreContext);
39
+ if (!store) throw new Error("StoreProvider missing");
40
+ return useSyncExternalStore(store.subscribe, () => selector(store.getState()), () => selector(store.getState()));
41
+ }
42
+ function useDispatch() {
43
+ const store = useContext(StoreContext);
44
+ if (!store) throw new Error("StoreProvider missing");
45
+ return store.dispatch;
46
+ }
47
+ function useSubmit(onSubmit) {
48
+ const store = useContext(StoreContext);
49
+ return () => {
50
+ if (store) onSubmit(store.getState());
51
+ };
52
+ }
53
+ return {
54
+ Provider,
55
+ useStore,
56
+ useSubmit,
57
+ useDispatch
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ //#region src/shared/utils.ts
63
+ function updateNestedValue(obj, path, value) {
64
+ if (path.length === 0) return value;
65
+ const newObj = Array.isArray(obj) ? [...obj] : { ...obj };
66
+ const currentKey = path[0];
67
+ if (path.length === 1) newObj[currentKey] = value;
68
+ else {
69
+ const remainingPath = path.slice(1);
70
+ const nestedObj = newObj[currentKey];
71
+ if (typeof nestedObj === "undefined" || nestedObj === null) newObj[currentKey] = typeof remainingPath[0] === "number" ? [] : {};
72
+ newObj[currentKey] = updateNestedValue(newObj[currentKey], remainingPath, value);
73
+ }
74
+ return newObj;
75
+ }
76
+ function getNestedValue(obj, path) {
77
+ return path.reduce((acc, key) => acc && acc[key] !== void 0 ? acc[key] : void 0, obj);
78
+ }
79
+
80
+ //#endregion
81
+ //#region src/shared/model/store/index.ts
82
+ const initialState = {
83
+ formData: {},
84
+ errors: {}
85
+ };
86
+ function reducer(state, action) {
87
+ const newData = { ...state };
88
+ switch (action.type) {
89
+ case "setValue":
90
+ newData.formData = updateNestedValue(newData.formData, action.path, action.value);
91
+ break;
92
+ case "setError":
93
+ newData.errors = updateNestedValue(newData.errors, action.path, action.value);
94
+ break;
95
+ case "reset":
96
+ newData.formData = {};
97
+ newData.errors = {};
98
+ break;
99
+ case "setErrors":
100
+ newData.errors = { ...action.errors };
101
+ break;
102
+ }
103
+ return newData;
104
+ }
105
+ const { Provider: FormStoreProvider, useStore: useFormStore, useDispatch: useFormDispatch, useSubmit } = createStoreContext$1(reducer, initialState);
106
+
107
+ //#endregion
108
+ //#region src/widgets/dynamicBuilder/element.tsx
109
+ const BuilderElement = (props) => {
110
+ const Element = props.element;
111
+ const field = props.field;
112
+ const currentFieldPath = [...props.path, field.name];
113
+ const value = useFormStore(((s) => getNestedValue(s.formData, currentFieldPath)));
114
+ const errors = useFormStore(((s) => getNestedValue(s.errors, currentFieldPath)));
115
+ const dispatch = useFormDispatch();
116
+ return /* @__PURE__ */ jsx(Element, {
117
+ field: { ...field },
118
+ builder: props.DynamicBuilder,
119
+ path: currentFieldPath,
120
+ plugins: props.plugins,
121
+ value,
122
+ errors,
123
+ onChange: (value) => {
124
+ dispatch({
125
+ type: "setValue",
126
+ path: currentFieldPath,
127
+ value
128
+ });
129
+ dispatch({
130
+ type: "setError",
131
+ path: currentFieldPath,
132
+ value: ""
133
+ });
134
+ }
135
+ });
136
+ };
137
+
138
+ //#endregion
139
+ //#region src/widgets/dynamicBuilder/index.tsx
140
+ const DynamicBuilder = (props) => {
141
+ const path = props.path ?? [];
142
+ return props.layout.map((field, index) => {
143
+ const FormElement = props.plugins[field.type];
144
+ if (!FormElement) {
145
+ console.warn(`Неизвестный тип поля: ${field.type}. Проверьте formRegistry.`);
146
+ return null;
147
+ }
148
+ return /* @__PURE__ */ jsx(BuilderElement, {
149
+ element: FormElement,
150
+ path,
151
+ field
152
+ }, `${field.name}${index}`);
153
+ });
154
+ };
155
+
156
+ //#endregion
157
+ //#region src/shared/model/builder/createContext.tsx
158
+ function createStoreContext() {
159
+ const BuilderContext = createContext(null);
160
+ const Provider = ({ plugins, children }) => {
161
+ const storeRef = useRef(null);
162
+ if (!storeRef.current) storeRef.current = (layout, path, children) => {
163
+ return /* @__PURE__ */ jsx(DynamicBuilder, {
164
+ layout,
165
+ path,
166
+ plugins,
167
+ children
168
+ });
169
+ };
170
+ return /* @__PURE__ */ jsx(BuilderContext.Provider, {
171
+ value: storeRef.current,
172
+ children
173
+ });
174
+ };
175
+ function useBuilder() {
176
+ const store = useContext(BuilderContext);
177
+ if (!store) throw new Error("StoreProvider missing");
178
+ return store;
179
+ }
180
+ return {
181
+ Provider,
182
+ useBuilder
183
+ };
184
+ }
185
+
186
+ //#endregion
187
+ //#region src/shared/model/builder/index.ts
188
+ const { Provider: BuilderProvider, useBuilder } = createStoreContext();
189
+
190
+ //#endregion
191
+ //#region src/app/index.tsx
192
+ const FormBuilder = forwardRef((props, ref) => {
193
+ const submitHdl = useSubmit((data) => {
194
+ if (props.onSubmit) props.onSubmit(data);
195
+ });
196
+ useImperativeHandle(ref, () => ({
197
+ reset: () => {},
198
+ submit: () => {
199
+ submitHdl();
200
+ },
201
+ errors: () => {
202
+ return {};
203
+ }
204
+ }), [props.onSubmit]);
205
+ return /* @__PURE__ */ jsxs("form", {
206
+ onSubmit: (e) => {
207
+ e.preventDefault();
208
+ submitHdl();
209
+ },
210
+ className: props?.className,
211
+ children: [
212
+ /* @__PURE__ */ jsx(FormStoreProvider, { children: /* @__PURE__ */ jsx(BuilderProvider, {
213
+ plugins: props.plugins,
214
+ children: /* @__PURE__ */ jsx(DynamicBuilder, {
215
+ layout: props.layout,
216
+ plugins: props.plugins
217
+ })
218
+ }) }),
219
+ /* @__PURE__ */ jsx("input", {
220
+ type: "submit",
221
+ style: { display: "none" }
222
+ }),
223
+ props.children
224
+ ]
225
+ });
226
+ });
227
+
228
+ //#endregion
229
+ //#region src/entity/inputs/ui/input/index.tsx
230
+ function isTextFieldConfig(field) {
231
+ return field.type === "text" || field.type === "email" || field.type === "password";
232
+ }
233
+ const TextField = ({ field, value, errors, onChange }) => {
234
+ if (!isTextFieldConfig(field)) {
235
+ console.warn(`TextField received an invalid field config for type: ${field.type}`);
236
+ return null;
237
+ }
238
+ const id = useId();
239
+ return /* @__PURE__ */ jsxs("div", {
240
+ style: { marginBottom: "15px" },
241
+ children: [
242
+ /* @__PURE__ */ jsxs("label", {
243
+ htmlFor: id,
244
+ children: [field.label, ":"]
245
+ }),
246
+ /* @__PURE__ */ jsx("input", {
247
+ type: field.type,
248
+ id,
249
+ name: field.name,
250
+ placeholder: field.placeholder,
251
+ value: value || "",
252
+ onChange: (e) => onChange(e.target.value),
253
+ style: { borderColor: errors ? "red" : "#ccc" }
254
+ }),
255
+ errors && /* @__PURE__ */ jsx("p", {
256
+ style: {
257
+ color: "red",
258
+ fontSize: "0.8em"
259
+ },
260
+ children: Object.values(errors).join(", ")
261
+ })
262
+ ]
263
+ });
264
+ };
265
+
266
+ //#endregion
267
+ //#region src/entity/inputs/ui/group/index.tsx
268
+ function isGroupConfig(field) {
269
+ return field.fields;
270
+ }
271
+ const FormGroup = ({ field, path }) => {
272
+ if (!isGroupConfig(field)) return null;
273
+ const Builder = useBuilder()(field.fields, path);
274
+ const className = (field.variant ?? "col") == "col" ? "flex-col" : "flex-row";
275
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: field.label }), /* @__PURE__ */ jsx("div", {
276
+ className: clsx(className, "flex"),
277
+ children: Builder
278
+ })] });
279
+ };
280
+ FormGroup.fieldProps = ["fields"];
281
+
282
+ //#endregion
283
+ //#region src/shared/model/index.ts
284
+ const fieldShema = z.object({
285
+ name: z.string(),
286
+ label: z.string().optional(),
287
+ type: z.string()
288
+ });
289
+
290
+ //#endregion
291
+ export { FormBuilder, FormGroup, TextField, fieldShema };
292
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["createStoreContext","createStoreContext"],"sources":["../src/shared/model/store/store.ts","../src/shared/model/store/createStoreContext.tsx","../src/shared/utils.ts","../src/shared/model/store/index.ts","../src/widgets/dynamicBuilder/element.tsx","../src/widgets/dynamicBuilder/index.tsx","../src/shared/model/builder/createContext.tsx","../src/shared/model/builder/index.ts","../src/app/index.tsx","../src/entity/inputs/ui/input/index.tsx","../src/entity/inputs/ui/group/index.tsx","../src/shared/model/index.ts"],"sourcesContent":["export type Reducer<S, A> = (state: S, action: A) => S;\n\nexport function createStore<S, A>(\n reducer: Reducer<S, A>,\n initialState: S\n) {\n let state = initialState;\n const listeners = new Set<() => void>();\n \n return {\n getState(): S {\n return state;\n },\n \n dispatch(action: A) {\n state = reducer(state, action);\n listeners.forEach(l => l());\n },\n \n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n }\n };\n}\n\nexport type Store<S, A> = ReturnType<typeof createStore<S, A>>;\n","import {\n createContext,\n useContext,\n useRef,\n useSyncExternalStore\n} from \"react\";\nimport { createStore, type Reducer, type Store } from \"./store\";\n\nexport function createStoreContext<S, A>(\n reducer: Reducer<S, A>,\n initialState: S\n) {\n const StoreContext = createContext<Store<S, A> | null>(null);\n \n const Provider: React.FC<{ children: React.ReactNode }> = ({\n children\n }) => {\n const storeRef = useRef<Store<S, A>>(null);\n \n if (!storeRef.current) {\n storeRef.current = createStore(reducer, initialState);\n }\n\n return (\n <StoreContext.Provider value={storeRef.current}>\n {children}\n </StoreContext.Provider>\n );\n };\n \n function useStore<T>(\n selector: (state: S) => T\n ): T {\n const store = useContext(StoreContext);\n\n if (!store) {\n throw new Error(\"StoreProvider missing\");\n }\n \n return useSyncExternalStore(\n store.subscribe,\n () => selector(store.getState()),\n () => selector(store.getState())\n );\n }\n \n function useDispatch() {\n const store = useContext(StoreContext);\n \n if (!store) {\n throw new Error(\"StoreProvider missing\");\n }\n \n return store.dispatch;\n }\n \n function useSubmit(onSubmit: (state: S) => void) {\n const store = useContext(StoreContext);\n \n return () => {\n if (store) {\n const state = store.getState();\n onSubmit(state);\n }\n };\n }\n \n return {\n Provider,\n useStore,\n useSubmit,\n useDispatch\n };\n}\n","export function updateNestedValue(obj: any, path: string[], value: any): any {\n if (path.length === 0) {\n return value;\n }\n \n const newObj = Array.isArray(obj) ? [...obj] : { ...obj }; // Создаем копию\n const currentKey = path[0];\n \n if (path.length === 1) {\n // Последний элемент пути, просто обновляем значение\n newObj[currentKey] = value;\n } else {\n const remainingPath = path.slice(1);\n const nestedObj = newObj[currentKey];\n \n if (typeof nestedObj === 'undefined' || nestedObj === null) {\n newObj[currentKey] = typeof remainingPath[0] === 'number' ? [] : {};\n }\n newObj[currentKey] = updateNestedValue(newObj[currentKey], remainingPath, value);\n }\n return newObj;\n}\nexport function getNestedValue(obj: any, path: string[]): any {\n return path.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj);\n}\n","import { createStoreContext } from \"./createStoreContext\";\nimport type {FormData} from \"@/shared/types/common\";\nimport {updateNestedValue} from \"@/shared/utils\";\n\ntype State = {\n formData: FormData\n errors: Record<string, any>\n};\n\ntype Action =\n | { type: \"setValue\"; path: string[]; value: unknown }\n | { type: \"setError\"; path: string[]; value?: string }\n | { type: \"reset\"; }\n | { type: \"setErrors\"; errors?: object };\n\nconst initialState: State = {\n formData: {},\n errors: {}\n};\n\nfunction reducer(state: State, action: Action): State {\n const newData = {...state};\n switch (action.type) {\n case 'setValue':\n newData.formData = updateNestedValue(newData.formData, action.path, action.value);\n break;\n case 'setError':\n newData.errors = updateNestedValue(newData.errors, action.path, action.value);\n break;\n case 'reset':\n newData.formData = {};\n newData.errors = {};\n break;\n case 'setErrors':\n newData.errors = {...action.errors};\n break;\n }\n return newData;\n}\n\nexport const {\n Provider: FormStoreProvider,\n useStore: useFormStore,\n useDispatch: useFormDispatch,\n useSubmit,\n} = createStoreContext(reducer, initialState);\n","import {useFormStore, useFormDispatch} from \"@/shared/model/store\";\nimport {getNestedValue} from \"@/shared/utils\";\n\nexport const BuilderElement = (props: any) => {\n const Element = props.element;\n const field = props.field;\n const currentFieldPath = [...props.path, field.name];\n const value = useFormStore((s=> getNestedValue(s.formData, currentFieldPath)));\n const errors = useFormStore((s=> getNestedValue(s.errors, currentFieldPath)));\n const dispatch= useFormDispatch();\n return <Element\n field={{...field}}\n builder={props.DynamicBuilder}\n path={currentFieldPath}\n plugins={props.plugins}\n value={value}\n errors={errors}\n onChange={(value: any) => {\n dispatch({\n type: 'setValue',\n path: currentFieldPath,\n value\n });\n dispatch({\n type: 'setError',\n path: currentFieldPath,\n value: ''\n });\n }}\n />;\n}\n","import type {TDynamicBuilder} from \"@/shared/types/common\";\nimport {BuilderElement} from \"@/widgets/dynamicBuilder/element\";\n// import {getNestedValue} from \"@/shared/utils\";\n// import {useCallback} from \"react\";\n// import {VisibleCore} from \"@/shared/lib/VisibleCore\";\n// import {use} from \"@/shared/model/store\";\n\nexport const DynamicBuilder: TDynamicBuilder = (props) => {\n const path = props.path ?? [];\n \n // const isShow = useCallback((field: FormFieldConfig) => {\n // let result = true;\n //\n // if (field.viewConfig) {\n // result = VisibleCore.isVisible(field.viewConfig, state.formData);\n // }\n // return result;\n // }, [state]);\n \n \n return props.layout.map((field, index) => {\n const FormElement = props.plugins[field.type];\n \n if (!FormElement) {\n console.warn(`Неизвестный тип поля: ${field.type}. Проверьте formRegistry.`);\n return null;\n }\n \n // const currentValue = getNestedValue(state.formData, currentFieldPath);\n // const currentErrors = getNestedValue(state.errors, currentFieldPath) as (Record<string, string> | undefined);\n return <BuilderElement key={`${field.name}${index}`} element={FormElement} path={path} field={field}/>\n })\n}\n","import React, {createContext, type ReactNode, useContext, useRef,} from \"react\";\nimport {DynamicBuilder} from \"@/widgets/dynamicBuilder\";\n\nexport function createStoreContext() {\n const BuilderContext = createContext<((layout: any, path: string[]|undefined, children?: ReactNode) => ReactNode) | null>(null);\n \n const Provider: React.FC<{ children: React.ReactNode, plugins: any }> = ({\n plugins,\n children\n }) => {\n const storeRef = useRef<(layout: any, path: string[]|undefined, children?: ReactNode) => ReactNode>(null);\n \n if (!storeRef.current) {\n storeRef.current = (layout: any, path: string[]|undefined, children?: ReactNode) => {\n return <DynamicBuilder layout={layout} path={path} plugins={plugins}>{children}</DynamicBuilder>\n };\n }\n \n return (\n <BuilderContext.Provider value={storeRef.current}>\n {children}\n </BuilderContext.Provider>\n );\n };\n \n function useBuilder(): (layout: any, path: string[]|undefined, children?: ReactNode) => ReactNode {\n const store = useContext(BuilderContext);\n \n if (!store) {\n throw new Error(\"StoreProvider missing\");\n }\n \n return store\n }\n \n return {\n Provider,\n useBuilder\n };\n}\n","import {createStoreContext} from \"@/shared/model/builder/createContext\";\n\nexport const {\n Provider: BuilderProvider,\n useBuilder,\n} = createStoreContext();\n","'use client';\n\nimport type {FormBuilderRef, TFormBuilder} from \"../shared/types/common\";\n// import {StoreContext, storeReducer,} from \"@/shared/model\";\n\nimport {DynamicBuilder} from \"../widgets/dynamicBuilder\";\nimport { forwardRef, useImperativeHandle} from \"react\";\n// import {ValidationCore} from \"@/shared/lib/validation/core\";\n// import { useUpdateEffect } from '@/shared/hook/useUpdateEffect';\nimport {FormStoreProvider, useSubmit} from \"@/shared/model/store\";\nimport {BuilderProvider} from \"@/shared/model/builder\";\n\nexport const FormBuilder = forwardRef<FormBuilderRef, TFormBuilder>((props, ref) => {\n // const validator = useMemo(() => {\n // return (new ValidationCore(props.layout, props.plugins))\n // }, [props.layout, props.plugins]);\n\n // const [state, dispatch] = useReducer(storeReducer, {\n // formData: props.formData ?? {},\n // errors: {},\n // });\n //\n // useUpdateEffect(() => {\n // if (props.onChange) {\n // props.onChange(state.formData);\n // }\n // }, [state.formData, props.onChange]);\n const submitHdl = useSubmit((data) => {\n if (props.onSubmit) {\n props.onSubmit(data);\n }\n });\n \n // const submit = () => {\n //\n // // const errors = validator.validate(state.formData);\n // // if (Object.keys(errors).length == 0) {\n // // if (props.onSubmit) {\n // // props.onSubmit(state.formData)\n // // }\n // // } else {\n // // dispatch({\n // // type: 'setErrors',\n // // payload: {\n // // errors\n // // }\n // // });\n // // }\n // }\n\n useImperativeHandle(ref, () => ({\n reset: () => {\n // dispatch({type: 'reset'})\n },\n submit: () => {\n submitHdl();\n },\n errors: () => {\n return {}\n // return state?.errors ?? {};\n }\n }), [props.onSubmit]);\n\n return <form onSubmit={(e) => {\n e.preventDefault();\n submitHdl();\n }}\n className={props?.className}\n >\n <FormStoreProvider>\n <BuilderProvider plugins={props.plugins}>\n <DynamicBuilder layout={props.layout} plugins={props.plugins} />\n </BuilderProvider>\n </FormStoreProvider>\n <input type=\"submit\" style={{ display: 'none' }}/>\n {props.children}\n </form>\n});\n\nexport default FormBuilder;\n","import type { FormElementProps, FormFieldConfig, FormFieldBase, RC } from '../../../../shared/types/common';\nimport { useId } from \"react\";\n\nexport type TextFieldConfig = FormFieldBase & { type: 'text' | 'email' | 'password'; placeholder?: string; };\n\nfunction isTextFieldConfig(field: FormFieldConfig): field is TextFieldConfig {\n return field.type === 'text' || field.type === 'email' || field.type === 'password';\n}\n\nexport const TextField: RC<FormElementProps> = ({ field, value, errors, onChange }) => {\n if (!isTextFieldConfig(field)) {\n console.warn(`TextField received an invalid field config for type: ${field.type}`);\n return null;\n }\n const id = useId();\n return (\n <div style={{ marginBottom: '15px' }}>\n <label htmlFor={id}>{field.label}:</label>\n <input\n type={field.type}\n id={id}\n name={field.name}\n placeholder={field.placeholder}\n value={value || ''}\n onChange={(e) => onChange(e.target.value)}\n style={{ borderColor: errors ? 'red' : '#ccc' }}\n />\n {errors && <p style={{ color: 'red', fontSize: '0.8em' }}>{Object.values(errors).join(', ')}</p>}\n </div>\n );\n};\n","import type {FormElementProps, FormFieldBase, FormFieldConfig, RC} from \"../../../../shared/types/common.ts\";\nimport clsx from \"clsx\";\nimport {useBuilder} from \"@/shared/model/builder\";\nexport type FormGroupConfig = FormFieldBase & { variant?: 'row' | 'col', fields: FormFieldConfig[] };\n\nfunction isGroupConfig(field: FormFieldConfig): field is FormGroupConfig {\n return field.fields;\n}\n\n\nexport const FormGroup: RC<FormElementProps<FormGroupConfig>> = ({field, path}) => {\n if (!isGroupConfig(field)) {\n return null;\n }\n const Builder = useBuilder()(field.fields, path);\n const variant = field.variant ?? 'col';\n \n const className = variant == 'col' ? 'flex-col' : 'flex-row';\n\n return <div>\n <div>{field.label}</div>\n <div className={clsx(className, 'flex')}>\n {Builder}\n </div>\n </div>;\n};\n\nFormGroup.fieldProps = ['fields'];\n","import { z } from 'zod';\n\nconst fieldShema = z.object({\n name: z.string(),\n label: z.string().optional(),\n type: z.string(),\n});\n\ntype TField = z.infer<typeof fieldShema>;\n\nexport type { TField };\nexport { fieldShema };\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "@idem.agency/form-builder",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Построитель форм",
5
5
  "main": "src/index.ts",
6
6
  "author": "idem.agency",
7
7
  "license": "ISC",
8
8
  "type": "module",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
9
12
  "scripts": {
10
13
  "dev": "vite",
11
- "build": "tsc -b && vite build",
14
+ "build": "tsdown",
15
+ "dev:lib": "tsdown --watch",
12
16
  "lint": "eslint .",
13
17
  "preview": "vite preview",
14
18
  "test": "vitest run"
@@ -16,7 +20,8 @@
16
20
  "dependencies": {
17
21
  "clsx": "2.1.1",
18
22
  "react": "19.2.3",
19
- "react-dom": "19.2.3"
23
+ "react-dom": "19.2.3",
24
+ "zod": "4.3.6"
20
25
  },
21
26
  "devDependencies": {
22
27
  "@eslint/js": "9.39.2",
@@ -30,10 +35,13 @@
30
35
  "eslint-plugin-react-refresh": "0.4.26",
31
36
  "globals": "17.0.0",
32
37
  "tailwindcss": "4.1.18",
38
+ "tsdown": "0.20.3",
33
39
  "typescript": "5.9.3",
34
40
  "typescript-eslint": "8.53.0",
35
41
  "vite": "7.3.1",
36
42
  "vite-plugin-dts": "4.5.4",
43
+ "vite-tsconfig-paths": "6.0.5",
37
44
  "vitest": "4.0.17"
38
- }
45
+ },
46
+ "gitHead": "5c2212eb5f6bc9bf7e422e68b90700cfecd03f41"
39
47
  }
@@ -5,10 +5,9 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Виджет для генерации форм</title>
8
- <link href="/src/app/test.css" rel="stylesheet">
9
8
  </head>
10
9
  <body>
11
10
  <div id="root"></div>
12
- <script type="module" src="/src/main.tsx"></script>
11
+ <script type="module" src="main.tsx"></script>
13
12
  </body>
14
13
  </html>
@@ -0,0 +1,90 @@
1
+ import {StrictMode} from 'react'
2
+ import {createRoot} from 'react-dom/client'
3
+ import { FormBuilder, FormGroup, TextField } from "~/src/index";
4
+ import type {FormBuilderRef, FormElementRegistry, FormFieldConfig} from "~/src/index";
5
+ import {useRef} from "react";
6
+
7
+ function Debug() {
8
+
9
+ const plugins: FormElementRegistry = {
10
+ text: TextField,
11
+ group: 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
+ viewConfig: {
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
+ id: 'username_2',
41
+ name: 'username_2',
42
+ label: 'Имя 2 пользователя',
43
+ type: 'text',
44
+ placeholder: 'Введите ваше имя 2',
45
+ defaultValue: 'Мое имя',
46
+ validation: ['required'],
47
+ },
48
+ {
49
+ name: 'username_3',
50
+ label: 'Имя 3 пользователя',
51
+ type: 'text',
52
+ placeholder: 'Введите ваше имя 2',
53
+ defaultValue: 'Мое имя',
54
+ validation: ['required'],
55
+ },
56
+ {
57
+ name: 'username_4',
58
+ label: 'Имя 4 пользователя',
59
+ type: 'text',
60
+ placeholder: 'Введите ваше имя 2',
61
+ defaultValue: 'Мое имя',
62
+ validation: ['required'],
63
+ },
64
+ ],
65
+ },
66
+ ];
67
+
68
+ const ref = useRef<FormBuilderRef>(null);
69
+ return <div>
70
+ <FormBuilder ref={ref} layout={layout} plugins={plugins} formData={{
71
+ personal: {
72
+ username_2: "Alex"
73
+ }
74
+ }}></FormBuilder>
75
+ <button onClick={() => {
76
+ ref.current?.reset();
77
+ }}>Сбросить
78
+ </button>
79
+ <button onClick={() => {
80
+ ref.current?.submit();
81
+ }}>Отправить
82
+ </button>
83
+ </div>
84
+ }
85
+
86
+ createRoot(document.getElementById('root')!).render(
87
+ <StrictMode>
88
+ <Debug/>
89
+ </StrictMode>,
90
+ )
package/src/app/debug.tsx CHANGED
@@ -1,89 +0,0 @@
1
- import FormBuilder from "./index.tsx";
2
- import type {FormBuilderRef, FormElementRegistry, FormFieldConfig} from "../shared/types/common.ts";
3
- import {FormGroup, TextField} from "../widgets/form";
4
- import {useRef} from "react";
5
-
6
- function Debug() {
7
-
8
- const plugins: FormElementRegistry = {
9
- text: TextField,
10
- group: FormGroup,
11
- }
12
-
13
- const layout: FormFieldConfig[] = [
14
- {
15
- id: 'username',
16
- name: 'username',
17
- label: 'Имя пользователя',
18
- type: 'text',
19
- placeholder: 'Введите ваше имя',
20
- defaultValue: 'Мое имя',
21
- validation: ['required', 'confirm:personal.username_2,Имя 2 пользователя'],
22
- viewConfig: {
23
- logic: 'and',
24
- rules: [
25
- {
26
- field: 'personal.username_2',
27
- operator: 'in',
28
- value: ['Alex']
29
- },
30
- ]
31
- }
32
- },
33
- {
34
- id: 'personal',
35
- name: 'personal',
36
- label: 'Группа',
37
- type: 'group',
38
- variant: 'col',
39
- fields: [
40
- {
41
- id: 'username_2',
42
- name: 'username_2',
43
- label: 'Имя 2 пользователя',
44
- type: 'text',
45
- placeholder: 'Введите ваше имя 2',
46
- defaultValue: 'Мое имя',
47
- validation: ['required'],
48
- },
49
- {
50
- id: 'username_3',
51
- name: 'username_3',
52
- label: 'Имя 3 пользователя',
53
- type: 'text',
54
- placeholder: 'Введите ваше имя 2',
55
- defaultValue: 'Мое имя',
56
- validation: ['required'],
57
- },
58
- {
59
- id: 'username_4',
60
- name: 'username_4',
61
- label: 'Имя 4 пользователя',
62
- type: 'text',
63
- placeholder: 'Введите ваше имя 2',
64
- defaultValue: 'Мое имя',
65
- validation: ['required'],
66
- },
67
- ],
68
- },
69
- ];
70
-
71
- const ref = useRef<FormBuilderRef>(null);
72
- return <div>
73
- <FormBuilder ref={ref} layout={layout} plugins={plugins} formData={{
74
- personal: {
75
- username_2: "Alex"
76
- }
77
- }}></FormBuilder>
78
- <button onClick={() => {
79
- ref.current?.reset();
80
- }}>Сбросить
81
- </button>
82
- <button onClick={() => {
83
- ref.current?.submit();
84
- }}>Отправить
85
- </button>
86
- </div>
87
- }
88
-
89
- export default Debug