@serhiibudianskyi/wui 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.
@@ -0,0 +1,498 @@
1
+ import { z } from 'zod';
2
+ // Field class representing a form field
3
+ export class Field {
4
+ // Constructor to initialize the field with its configuration and schema
5
+ constructor(config, schema) {
6
+ // Configuration for the field
7
+ this._config = {};
8
+ // Zod schema for the field
9
+ this.schema = z.any();
10
+ this._config = config;
11
+ this.schema = schema;
12
+ }
13
+ get type() {
14
+ return this._config.type;
15
+ }
16
+ get name() {
17
+ return this._config.name;
18
+ }
19
+ get label() {
20
+ return this._config.label;
21
+ }
22
+ get placeholder() {
23
+ return this._config.placeholder || '';
24
+ }
25
+ get isReadOnly() {
26
+ return this._config.isReadOnly || false;
27
+ }
28
+ get isDisabled() {
29
+ return this._config.isDisabled || false;
30
+ }
31
+ get isRequired() {
32
+ return this._config.isRequired || false;
33
+ }
34
+ get min() {
35
+ return this._config.min;
36
+ }
37
+ get max() {
38
+ return this._config.max;
39
+ }
40
+ get revalidates() {
41
+ return this._config.revalidates || [];
42
+ }
43
+ get step() {
44
+ return this._config.step;
45
+ }
46
+ get options() {
47
+ return this._config.options || [];
48
+ }
49
+ get isMultiple() {
50
+ return this._config.isMultiple || false;
51
+ }
52
+ get maxSize() {
53
+ return this._config.maxSize;
54
+ }
55
+ get accept() {
56
+ return this._config.accept;
57
+ }
58
+ get maxFiles() {
59
+ return this.isMultiple ? this._config.maxFiles || +Infinity : 1;
60
+ }
61
+ get loadOptions() {
62
+ return this._config.loadOptions;
63
+ }
64
+ get className() {
65
+ return this._config.className || '';
66
+ }
67
+ get attrs() {
68
+ return this._config.attrs || {};
69
+ }
70
+ // Normalize the input value based on the field's configuration
71
+ getNormalizedValue(value) {
72
+ return this._config.normalize ? this._config.normalize(value) : value;
73
+ }
74
+ // Run custom validation logic for the field
75
+ runValidation(values, ctx) {
76
+ if (this._config.validate) {
77
+ this._config.validate(values, ctx);
78
+ }
79
+ }
80
+ // Get default value for the field based on its type
81
+ getDefaultValue() {
82
+ switch (this._config.type) {
83
+ case 'file':
84
+ return (this._config.isMultiple ? [] : null);
85
+ case 'number':
86
+ return (this._config.min ?? 0);
87
+ case 'checkbox':
88
+ return false;
89
+ case 'date':
90
+ case 'datetime-local':
91
+ return new Date();
92
+ case 'time':
93
+ return '00:00';
94
+ case 'select':
95
+ case 'async-select':
96
+ case 'creatable-select':
97
+ case 'async-creatable-select':
98
+ return (this._config.isMultiple ? [] : null);
99
+ case 'textarea':
100
+ case 'text':
101
+ case 'email':
102
+ case 'password':
103
+ case 'radio':
104
+ default:
105
+ return '';
106
+ }
107
+ }
108
+ }
109
+ export class FieldFactory {
110
+ // Create a Zod schema for string fields
111
+ static createStringSchema(config) {
112
+ let schema = z.string();
113
+ // Apply required validation if specified
114
+ if (config.isRequired) {
115
+ schema = schema.nonempty({ message: `${config.label} is required` });
116
+ }
117
+ return schema;
118
+ }
119
+ // Create a Zod schema for email fields
120
+ static createEmailSchema(config) {
121
+ let schema = this.createStringSchema(config).email('Invalid email address');
122
+ return schema;
123
+ }
124
+ // Create a text field with the given configuration
125
+ static text(name, label, config = {}) {
126
+ const fieldConfig = {
127
+ type: 'text',
128
+ name,
129
+ label,
130
+ ...config
131
+ };
132
+ return new Field(fieldConfig, this.createStringSchema(fieldConfig));
133
+ }
134
+ // Create an email field with the given configuration
135
+ static email(config) {
136
+ const fieldConfig = {
137
+ type: 'email',
138
+ name: config.name || 'email',
139
+ label: config.label || 'Email',
140
+ ...config
141
+ };
142
+ return new Field(fieldConfig, this.createEmailSchema(fieldConfig));
143
+ }
144
+ // Create a password field with the given configuration
145
+ static password(config) {
146
+ const fieldConfig = {
147
+ type: 'password',
148
+ name: config.name || 'password',
149
+ label: config.label || 'Password',
150
+ ...config
151
+ };
152
+ return new Field(fieldConfig, this.createStringSchema(fieldConfig));
153
+ }
154
+ // Create a textarea field with the given configuration
155
+ static textarea(name, label, config = {}) {
156
+ const fieldConfig = {
157
+ type: 'textarea',
158
+ name,
159
+ label,
160
+ ...config
161
+ };
162
+ return new Field(fieldConfig, this.createStringSchema(fieldConfig));
163
+ }
164
+ // Create a Zod schema for number fields
165
+ static createNumberSchema(config) {
166
+ // Preprocess to handle empty strings and convert to number
167
+ let schema = z.preprocess((val) => {
168
+ if (typeof val === 'string' && val.trim() === '' || isNaN(Number(val))) {
169
+ return undefined;
170
+ }
171
+ return Number(val);
172
+ }, z.number().optional() // It should be optional initially!
173
+ );
174
+ // Apply required and range validations if specified
175
+ if (config.isRequired) {
176
+ schema = schema.refine((val) => val !== undefined, {
177
+ message: `${config.label} is required`
178
+ });
179
+ }
180
+ // Minmax value validation
181
+ schema = schema
182
+ .refine((val) => {
183
+ if (val === undefined)
184
+ return true;
185
+ if (typeof config.min === 'number') {
186
+ return val >= config.min;
187
+ }
188
+ return true;
189
+ }, {
190
+ message: `${config.label} must be at least ${config.min}`
191
+ })
192
+ .refine((val) => {
193
+ if (val === undefined)
194
+ return true;
195
+ if (typeof config.max === 'number') {
196
+ return val <= config.max;
197
+ }
198
+ return true;
199
+ }, {
200
+ message: `${config.label} must be at most ${config.max}`
201
+ });
202
+ return schema;
203
+ }
204
+ // Create a number field with the given configuration
205
+ static number(name, label, config = {}) {
206
+ const fieldConfig = {
207
+ type: 'number',
208
+ name,
209
+ label,
210
+ ...config
211
+ };
212
+ return new Field(fieldConfig, this.createNumberSchema(fieldConfig));
213
+ }
214
+ // Create a Zod schema for checkbox fields
215
+ static createCheckboxSchema(config) {
216
+ let schema = z.boolean();
217
+ // Apply required validation if specified
218
+ if (config.isRequired) {
219
+ schema = schema.refine((val) => val === true, {
220
+ message: `${config.label} must be checked`
221
+ });
222
+ }
223
+ return schema;
224
+ }
225
+ // Create a checkbox field with the given configuration
226
+ static checkbox(name, label, config = {}) {
227
+ const fieldConfig = {
228
+ type: 'checkbox',
229
+ name,
230
+ label,
231
+ ...config
232
+ };
233
+ return new Field(fieldConfig, this.createCheckboxSchema(fieldConfig));
234
+ }
235
+ // Create a Zod schema for date fields
236
+ static createDateSchema(config) {
237
+ let schema = z.preprocess((val) => {
238
+ if (typeof val === 'string' || val instanceof Date) {
239
+ const date = new Date(val);
240
+ return isNaN(date.getTime()) ? undefined : date;
241
+ }
242
+ return undefined;
243
+ }, z.date().optional());
244
+ // Apply required validation if specified
245
+ if (config.isRequired) {
246
+ schema = schema.refine((val) => val !== undefined, {
247
+ message: `${config.label} is required`
248
+ });
249
+ }
250
+ // Minmax date validation
251
+ schema = schema
252
+ .refine((val) => {
253
+ if (val === undefined)
254
+ return true;
255
+ if (typeof config.min === 'string') {
256
+ return val >= new Date(config.min);
257
+ }
258
+ return true;
259
+ }, {
260
+ message: `${config.label} must be on or after ${config.min ? new Date(config.min).toLocaleDateString() : ''}`
261
+ })
262
+ .refine((val) => {
263
+ if (val === undefined)
264
+ return true;
265
+ if (typeof config.max === 'string') {
266
+ return val <= new Date(config.max);
267
+ }
268
+ return true;
269
+ }, {
270
+ message: `${config.label} must be on or before ${config.max ? new Date(config.max).toLocaleDateString() : ''}`
271
+ });
272
+ return schema;
273
+ }
274
+ // Create a date field with the given configuration
275
+ static date(name, label, config = {}) {
276
+ const fieldConfig = {
277
+ type: 'date',
278
+ name,
279
+ label,
280
+ ...config
281
+ };
282
+ return new Field(fieldConfig, this.createDateSchema(fieldConfig));
283
+ }
284
+ // Create a Zod schema for datetime-local fields
285
+ static createDateTimeLocalSchema(config) {
286
+ let schema = z.preprocess((val) => {
287
+ if (typeof val === 'string' || val instanceof Date) {
288
+ const date = new Date(val);
289
+ return isNaN(date.getTime()) ? undefined : date;
290
+ }
291
+ return undefined;
292
+ }, z.date().optional());
293
+ // Apply required validation if specified
294
+ if (config.isRequired) {
295
+ schema = schema.refine((val) => val !== undefined, {
296
+ message: `${config.label} is required`
297
+ });
298
+ }
299
+ // Minmax datetime validation
300
+ schema = schema
301
+ .refine((val) => {
302
+ if (val === undefined)
303
+ return true;
304
+ if (typeof config.min === 'string') {
305
+ return val >= new Date(config.min);
306
+ }
307
+ return true;
308
+ }, {
309
+ message: `${config.label} must be on or after ${config.min ? new Date(config.min).toLocaleString() : ''}`
310
+ })
311
+ .refine((val) => {
312
+ if (val === undefined)
313
+ return true;
314
+ if (typeof config.max === 'string') {
315
+ return val <= new Date(config.max);
316
+ }
317
+ return true;
318
+ }, {
319
+ message: `${config.label} must be on or before ${config.max ? new Date(config.max).toLocaleString() : ''}`
320
+ });
321
+ return schema;
322
+ }
323
+ // Create a datetime-local field with the given configuration
324
+ static datetimeLocal(name, label, config = {}) {
325
+ const fieldConfig = {
326
+ type: 'datetime-local',
327
+ name,
328
+ label,
329
+ ...config
330
+ };
331
+ return new Field(fieldConfig, this.createDateTimeLocalSchema(fieldConfig));
332
+ }
333
+ // Create a Zod schema for time fields
334
+ static createTimeSchema(config) {
335
+ let schema = z.string().optional();
336
+ // Apply required validation if specified
337
+ if (config.isRequired) {
338
+ schema = schema.refine((val) => val !== undefined && val !== '', {
339
+ message: `${config.label} is required`
340
+ });
341
+ }
342
+ // Validate time format (HH:MM)
343
+ schema = schema.refine((val) => !val || /^([01]\d|2[0-3]):([0-5]\d)$/.test(val), {
344
+ message: `${config.label} must be a valid time in HH:MM format`
345
+ });
346
+ // Minmax time validation
347
+ schema = schema
348
+ .refine((val) => {
349
+ if (!val)
350
+ return true;
351
+ if (typeof config.min === 'string') {
352
+ return val >= config.min;
353
+ }
354
+ return true;
355
+ }, {
356
+ message: `${config.label} must be at or after ${config.min}`
357
+ })
358
+ .refine((val) => {
359
+ if (!val)
360
+ return true;
361
+ if (typeof config.max === 'string') {
362
+ return val <= config.max;
363
+ }
364
+ return true;
365
+ }, {
366
+ message: `${config.label} must be at or before ${config.max}`
367
+ });
368
+ return schema;
369
+ }
370
+ // Create a time field with the given configuration
371
+ static time(name, label, config = {}) {
372
+ const fieldConfig = {
373
+ type: 'time',
374
+ name,
375
+ label,
376
+ ...config
377
+ };
378
+ return new Field(fieldConfig, this.createTimeSchema(fieldConfig));
379
+ }
380
+ // Create a radio field with the given configuration
381
+ static radio(name, label, options, config = {}) {
382
+ const fieldConfig = {
383
+ type: 'radio',
384
+ name,
385
+ label,
386
+ options,
387
+ ...config
388
+ };
389
+ return new Field(fieldConfig, this.createStringSchema(fieldConfig));
390
+ }
391
+ // Create a Zod schema for select fields
392
+ static createSelectSchema(config) {
393
+ if (config.isMultiple) {
394
+ // Array schema for multi-select
395
+ let schema = z.array(z.object({
396
+ label: z.string(),
397
+ value: z.string()
398
+ }));
399
+ // Apply required validation
400
+ if (config.isRequired) {
401
+ schema = schema.nonempty({ message: `${config.label} is required` });
402
+ }
403
+ return schema.default([]);
404
+ }
405
+ else {
406
+ // For single-select, return an Option object or null
407
+ let schema = z.object({
408
+ label: z.string(),
409
+ value: z.string()
410
+ }).nullable().default(null);
411
+ // Apply required validation
412
+ if (config.isRequired) {
413
+ schema = schema.refine((val) => val !== null, { message: `${config.label} is required` });
414
+ }
415
+ return schema;
416
+ }
417
+ }
418
+ // Create a select field with the given configuration
419
+ static select(name, label, options, config = {}) {
420
+ const fieldConfig = {
421
+ type: 'select',
422
+ name,
423
+ label,
424
+ options,
425
+ ...config
426
+ };
427
+ return new Field(fieldConfig, this.createSelectSchema(fieldConfig));
428
+ }
429
+ // Create an async-select field with the given configuration
430
+ static asyncSelect(name, label, loadOptions, config = {}) {
431
+ const fieldConfig = {
432
+ type: 'async-select',
433
+ name,
434
+ label,
435
+ loadOptions,
436
+ ...config
437
+ };
438
+ return new Field(fieldConfig, this.createSelectSchema(fieldConfig));
439
+ }
440
+ // Create a creatable-select field with the given configuration
441
+ static creatableSelect(name, label, options, config = {}) {
442
+ const fieldConfig = {
443
+ type: 'creatable-select',
444
+ name,
445
+ label,
446
+ options,
447
+ ...config
448
+ };
449
+ return new Field(fieldConfig, this.createSelectSchema(fieldConfig));
450
+ }
451
+ // Create an async-creatable-select field with the given configuration
452
+ static asyncCreatableSelect(name, label, loadOptions, config = {}) {
453
+ const fieldConfig = {
454
+ type: 'async-creatable-select',
455
+ name,
456
+ label,
457
+ loadOptions,
458
+ ...config
459
+ };
460
+ return new Field(fieldConfig, this.createSelectSchema(fieldConfig));
461
+ }
462
+ static createFileSchema(config) {
463
+ if (config.isMultiple) {
464
+ // Array schema for multiple files
465
+ let schema = z.array(z.string());
466
+ // Apply required validation
467
+ if (config.isRequired) {
468
+ schema = schema.nonempty({ message: `${config.label} is required` });
469
+ }
470
+ // Apply maxFiles validation
471
+ if (config.maxFiles) {
472
+ schema = schema.max(config.maxFiles, {
473
+ message: `${config.label} cannot have more than ${config.maxFiles} file${config.maxFiles > 1 ? 's' : ''}`
474
+ });
475
+ }
476
+ return schema.default([]);
477
+ }
478
+ else {
479
+ // For single file, return a string (file ID) or null
480
+ let schema = z.string().nullable().default(null);
481
+ // Apply required validation
482
+ if (config.isRequired) {
483
+ schema = schema.refine((val) => val !== null, { message: `${config.label} is required` });
484
+ }
485
+ return schema;
486
+ }
487
+ }
488
+ // Create a file field with the given configuration
489
+ static file(name, label, config = {}) {
490
+ const fieldConfig = {
491
+ type: 'file',
492
+ name,
493
+ label,
494
+ ...config
495
+ };
496
+ return new Field(fieldConfig, this.createFileSchema(fieldConfig));
497
+ }
498
+ }
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import { Field } from './Field';
3
+ export interface Section {
4
+ title?: string;
5
+ fields: Record<string, Field<any>>;
6
+ className?: string;
7
+ }
8
+ export declare class Form {
9
+ private _fields;
10
+ private _schema;
11
+ private _defaultValues;
12
+ private _sections;
13
+ constructor(sections: Section[]);
14
+ get sections(): Section[];
15
+ get schema(): z.ZodObject<any>;
16
+ get defaultValues(): Record<string, any>;
17
+ setDefaultValues(values: Record<string, any>): void;
18
+ private _buildSchema;
19
+ private _buildDefaultValues;
20
+ private _extractFieldsFromSections;
21
+ }
22
+ export declare class FormBuilder {
23
+ private _sections;
24
+ private _defaultValues;
25
+ section(section: Section): this;
26
+ sections(sections: Section[]): this;
27
+ defaultValues(values: Record<string, any>): this;
28
+ build(): Form;
29
+ }
@@ -0,0 +1,102 @@
1
+ import { z } from 'zod';
2
+ export class Form {
3
+ // Alternative constructor to initialize the form with sections
4
+ constructor(sections) {
5
+ // Form fields
6
+ this._fields = {};
7
+ // Zod schema for the form
8
+ this._schema = null;
9
+ // Default values for the form
10
+ this._defaultValues = {};
11
+ // Sections of the form
12
+ this._sections = [];
13
+ this._sections = sections;
14
+ this._fields = this._extractFieldsFromSections();
15
+ this._buildDefaultValues();
16
+ }
17
+ get sections() {
18
+ return this._sections;
19
+ }
20
+ get schema() {
21
+ // Build the schema if it hasn't been built yet
22
+ if (!this._schema) {
23
+ this._schema = this._buildSchema();
24
+ }
25
+ return this._schema;
26
+ }
27
+ // Get default values for the form
28
+ get defaultValues() {
29
+ return this._defaultValues;
30
+ }
31
+ // Set default values for the form
32
+ setDefaultValues(values) {
33
+ this._defaultValues = { ...this._defaultValues, ...values };
34
+ }
35
+ // Build the Zod schema for the form based on its fields
36
+ _buildSchema() {
37
+ const shape = {};
38
+ // Iterate over each field to build its schema
39
+ for (const [name, field] of Object.entries(this._fields)) {
40
+ let schema = field.schema;
41
+ // Wrap the schema with preprocessing to normalize the value
42
+ schema = z.preprocess(field.getNormalizedValue.bind(field), schema);
43
+ shape[name] = schema;
44
+ }
45
+ // Return the combined schema with custom refinements callback
46
+ return z.object(shape).superRefine((values, ctx) => {
47
+ for (const field of Object.values(this._fields)) {
48
+ field.runValidation(values, ctx);
49
+ }
50
+ });
51
+ }
52
+ // Build default values for the form based on its fields
53
+ _buildDefaultValues() {
54
+ this._defaultValues = {};
55
+ for (const [key, field] of Object.entries(this._fields)) {
56
+ this._defaultValues[key] = field.getDefaultValue();
57
+ }
58
+ }
59
+ // Extract fields from sections
60
+ _extractFieldsFromSections() {
61
+ const fields = {};
62
+ for (const section of this._sections) {
63
+ Object.assign(fields, section.fields);
64
+ }
65
+ return fields;
66
+ }
67
+ }
68
+ export class FormBuilder {
69
+ constructor() {
70
+ // Set of sections of fields for the form
71
+ this._sections = [];
72
+ // Default values for the form
73
+ this._defaultValues = {};
74
+ }
75
+ // Add a section to the form
76
+ section(section) {
77
+ this._sections.push(section);
78
+ // Extract default values from section fields
79
+ for (const [name, field] of Object.entries(section.fields)) {
80
+ this._defaultValues[name] = field.getDefaultValue();
81
+ }
82
+ return this;
83
+ }
84
+ // Add multiple sections to the form
85
+ sections(sections) {
86
+ for (const section of sections) {
87
+ this.section(section);
88
+ }
89
+ return this;
90
+ }
91
+ // Set default values for the form
92
+ defaultValues(values) {
93
+ this._defaultValues = { ...this._defaultValues, ...values };
94
+ return this;
95
+ }
96
+ // Build and return the form instance
97
+ build() {
98
+ const form = new Form(this._sections);
99
+ form.setDefaultValues(this._defaultValues);
100
+ return form;
101
+ }
102
+ }