@harnessio/forms 0.10.0 → 0.11.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.
package/README.md CHANGED
@@ -1,182 +1,492 @@
1
- # Harness forms
1
+ # @harnessio/forms
2
2
 
3
- This repository contains components and utilities for creating forms in the harness applications.
3
+ A type-safe, configuration-driven form library for React applications.
4
4
 
5
- ## Intro
5
+ ## Overview
6
6
 
7
- The library uses **form configuration schema** and **inputs** to generate form. For **json schema** driven forms, we are use parser to convert input data to **form configuration schema** data model. This means that for any kind of form we have to create **form configuration schema** either explicitly or implicitly by transforming input data.
7
+ `@harnessio/forms` provides a declarative approach to building forms using configuration schemas. The library generates forms from type-safe definitions, handles validation, and manages form state through React Hook Form integration.
8
8
 
9
- ## Principles
9
+ **Key Features:**
10
10
 
11
- - Form is generated from configuration.
12
- - Validation is part of configuration (per input).
13
- - Default values are part of configuration (per input).
14
- - Each input defines its configuration interface.
15
- - Input define default validation as utility function - optional.
11
+ - 🎯 Type-safe form definitions with TypeScript generics
12
+ - 📋 Configuration-driven form generation
13
+ - Built-in validation with Zod integration
14
+ - 🔢 Tuple support for fixed-position arrays
15
+ - 🎨 Flexible input component system
16
+ - 🔄 Conditional visibility and dynamic forms
16
17
 
17
- ### Step by step guide
18
+ ## Core Principles
18
19
 
19
- #### 1. Input type
20
+ - Forms are generated from configuration
21
+ - Each input defines its own configuration interface
22
+ - Validation schemas are part of input configuration
23
+ - Type safety through generic type parameters
24
+ - Extensible through custom input components
20
25
 
21
- Each input has a unique type.
26
+ ## Type System
22
27
 
23
- ```
28
+ The library uses generic type parameters for type safety:
29
+
30
+ ### `IInputDefinition<TConfig, TValue, TInputType>`
31
+
32
+ - `TConfig` - Input-specific configuration type
33
+ - `TValue` - Value type for the input
34
+ - `TInputType` - String literal type for input type
35
+
36
+ ### `InputProps<TValue, TConfig>`
37
+
38
+ - `TValue` - Value type for the input
39
+ - `TConfig` - Input-specific configuration type
40
+
41
+ ## Quick Start
42
+
43
+ ### 1. Define Input Types
44
+
45
+ Each input has a unique type identifier:
46
+
47
+ ```typescript
24
48
  export enum InputType {
25
- text = "text",
26
- number = "number",
27
- checkbox = "checkbox",
28
- connector = "connector"
29
- ...
49
+ text = 'text',
50
+ number = 'number',
51
+ checkbox = 'checkbox',
52
+ array = 'array',
53
+ list = 'list'
54
+ // ... more types
30
55
  }
31
56
  ```
32
57
 
33
- #### 2. Create inputs
58
+ ### 2. Create Input Components
34
59
 
35
- Examples of input can be found in the `playgorund`:
36
- [Text input example](./playground/src/implementation/inputs/text-input.tsx)
37
-
38
- Minimal implementation:
60
+ Define input configuration and component:
39
61
 
40
62
  ```typescript
41
- import { InputComponent, InputProps, useController, type AnyFormValue } from '@harnessio/forms'
63
+ import { InputComponent, InputProps, useController } from '@harnessio/forms'
42
64
 
65
+ // 1. Define input config type
43
66
  export interface TextInputConfig {
44
- inputType: InputType.text
67
+ placeholder?: string
45
68
  }
46
69
 
47
- function TextInputInternal(props: InputProps<AnyFormValue>): JSX.Element {
70
+ // 2. Define value type
71
+ export type TextInputValueType = string
72
+
73
+ // 3. Create component with typed props
74
+ function TextInputInternal(
75
+ props: InputProps<TextInputValueType, TextInputConfig>
76
+ ): JSX.Element {
48
77
  const { readonly, path, input } = props
49
- const { label = '', required, placeholder } = input
78
+ const { label, required, inputConfig } = input
50
79
 
51
- const { field, formState } = useController<{ [key: string]: boolean }>({
52
- name: path
53
- })
80
+ const { field } = useController({ name: path })
54
81
 
55
82
  return (
56
83
  <>
57
84
  <label>{label}</label>
58
- <input placeholder={placeholder} {...field} disabled={readonly} tabIndex={0} />
85
+ <input
86
+ placeholder={inputConfig?.placeholder}
87
+ {...field}
88
+ disabled={readonly}
89
+ />
59
90
  </>
60
91
  )
61
92
  }
62
93
 
63
- export class TextInput extends InputComponent<AnyFormValue> {
94
+ // 4. Register as InputComponent
95
+ export class TextInput extends InputComponent<TextInputValueType, TextInputConfig> {
64
96
  public internalType = InputType.text
65
97
 
66
- renderComponent(props: InputProps<AnyFormValue>): JSX.Element {
98
+ renderComponent(props: InputProps<TextInputValueType, TextInputConfig>): JSX.Element {
67
99
  return <TextInputInternal {...props} />
68
100
  }
69
101
  }
70
-
71
102
  ```
72
103
 
73
- #### 3. Register inputs
104
+ **See examples:** [playground/src/implementation/inputs/](./playground/src/implementation/inputs/)
105
+
106
+ ### 3. Register Inputs
74
107
 
75
- Use InputFactory to register inputs
108
+ Use `InputFactory` to register your input components:
76
109
 
77
- ```js
110
+ ```typescript
78
111
  import { InputFactory } from '@harnessio/forms'
79
112
 
80
- import { TextInput } from '../inputs/TextInput'
113
+ import { NumberInput } from './inputs/number-input'
114
+ import { TextInput } from './inputs/text-input'
81
115
 
82
116
  const inputComponentFactory = new InputFactory()
83
117
  inputComponentFactory.registerComponent(new TextInput())
118
+ inputComponentFactory.registerComponent(new NumberInput())
84
119
 
85
120
  export default inputComponentFactory
86
121
  ```
87
122
 
88
- #### 4. Create form model - IFormDefinition
123
+ ### 4. Define Form Schema
124
+
125
+ Create a type-safe form definition using `IFormDefinition`:
126
+
127
+ ```typescript
128
+ import { z } from 'zod'
89
129
 
90
- Form model is a blueprint for creating form.
130
+ import { IFormDefinition } from '@harnessio/forms'
91
131
 
92
- ```js
93
- export const formDefinition: IFormDefinition = {
132
+ // Basic form without custom input configs
133
+ export const basicFormDefinition: IFormDefinition = {
94
134
  inputs: [
95
135
  {
96
- inputType: InputType.string,
97
- path: "name",
98
- label: "Name",
136
+ inputType: 'text',
137
+ path: 'name',
138
+ label: 'Name',
139
+ required: true,
140
+ validation: {
141
+ schema: z.string().min(3, 'Name must be at least 3 characters')
142
+ }
99
143
  },
100
144
  {
101
- inputType: InputType.number,
102
- path: "age",
103
- label: "Age",
145
+ inputType: 'number',
146
+ path: 'age',
147
+ label: 'Age',
148
+ validation: {
149
+ schema: z.coerce.number().min(18, 'Must be 18 or older')
150
+ }
104
151
  }
105
152
  ]
106
153
  }
107
154
  ```
108
155
 
109
- NOTE: Input may contain configuration. In this case we have to provide a generic type to `IFormDefinition` in order to get intellisense for the form definition inputs.
156
+ **With custom input configs for type safety:**
110
157
 
111
158
  ```typescript
112
- // 1. Define input config type
159
+ // 1. Define config types for each custom input
113
160
  export interface ListInputConfig {
114
- inputType: InputType.list
161
+ inputType: 'list'
115
162
  inputConfig: {
116
- inputs: UIInputWithConfigsForList[]
163
+ inputs: IInputDefinition[]
117
164
  layout?: 'grid' | 'default'
118
165
  }
119
166
  }
120
167
 
121
- // 2. Use input config type for second generic of component props
122
- function ListInputInternal(props: InputProps<AnyFormValue, ListInputConfig>): JSX.Element ....
168
+ export interface TextInputConfig {
169
+ inputType: 'text'
170
+ inputConfig?: {
171
+ placeholder?: string
172
+ }
173
+ }
123
174
 
124
- // 3. Make union of all Input configs
125
- export type InputConfigType =
126
- | ListInputConfig
127
- | TextInputConfig ...
175
+ // 2. Create union type
176
+ export type InputConfigType = ListInputConfig | TextInputConfig
128
177
 
129
- // 4. Use union type when defining form
178
+ // 3. Use union type for type-safe form definition
130
179
  export const formDefinition: IFormDefinition<InputConfigType> = {
131
- inputs: [...]
180
+ inputs: [
181
+ {
182
+ inputType: 'text',
183
+ path: 'username',
184
+ label: 'Username',
185
+ inputConfig: {
186
+ placeholder: 'Enter username' // ✅ Type-safe
187
+ }
188
+ },
189
+ {
190
+ inputType: 'list',
191
+ path: 'items',
192
+ label: 'Items',
193
+ inputConfig: {
194
+ inputs: [...], // ✅ Type-safe
195
+ layout: 'grid'
196
+ }
197
+ }
198
+ ]
132
199
  }
133
200
  ```
134
201
 
135
- For more info check [List input example](../views/src/components/form-inputs/TextInput.tsx)
202
+ ### 5. Render Form
203
+
204
+ Use `RootForm` and `RenderForm` components:
136
205
 
137
- #### 5. Render form
206
+ ```typescript
207
+ import { RootForm, RenderForm, useZodValidationResolver } from '@harnessio/forms'
208
+ import { inputComponentFactory } from './factory'
209
+ import { formDefinition } from './form-definition'
210
+
211
+ function MyForm() {
212
+ const resolver = useZodValidationResolver(formDefinition)
138
213
 
139
- Use RootForm and RenderForm components.
214
+ const handleSubmit = (values: AnyFormValue) => {
215
+ console.log('Form values:', values)
216
+ }
140
217
 
141
- ```js
142
- <RootForm initialValues={{}} onSubmit={handleOnSubmit}>
143
- <RenderForm factory={inputComponentFactory} inputs={formDefinition} />
144
- </RootForm>
218
+ return (
219
+ <RootForm
220
+ defaultValues={{}}
221
+ onSubmit={handleSubmit}
222
+ resolver={resolver}
223
+ >
224
+ {rootForm => (
225
+ <>
226
+ <RenderForm
227
+ factory={inputComponentFactory}
228
+ inputs={formDefinition}
229
+ />
230
+ <button onClick={() => rootForm.submitForm()}>
231
+ Submit
232
+ </button>
233
+ </>
234
+ )}
235
+ </RootForm>
236
+ )
237
+ }
145
238
  ```
146
239
 
147
- ### Configure Required validation
240
+ ## Tuple Support (Fixed-Position Arrays)
148
241
 
149
- Required validation can be configured globally for all inputs or per input. Per input validation overrides the global validation.
242
+ The library supports **tuple paths** using numeric indices for fixed-position arrays where each position can have its own schema.
150
243
 
151
- When the library is generating validation, it tries to pick the first available validation for **required** check in this order:
244
+ ### When to Use Tuples
152
245
 
153
- - requiredSchemaPerInput
154
- - requiredSchema
155
- - default - if validation is not found, it uses the default built-in validation.
246
+ Use tuples for:
156
247
 
157
- ```js
158
- // Required validation config example
159
- const validationConfig: IGlobalValidationConfig = {
160
- requiredSchemaPerInput: {
161
- [InputType.string]: zod.string(),
162
- [InputType.number]: zod.number(),
163
- [InputType.myCustomInput]: zod.custom(....),
164
- },
165
- requiredSchema: zod.custom(....), // << used for validating all inputs except string, number and myCustomInput
166
- };
248
+ - Fixed number of elements (e.g., `[x, y]` coordinates)
249
+ - Position-specific schemas (e.g., `[primary, backup]` servers)
250
+ - Elements that shouldn't be added/removed dynamically
251
+
252
+ ❌ Use `array` or `list` input types for:
253
+
254
+ - Variable number of items
255
+ - Add/remove functionality
256
+ - Same schema for all items
257
+
258
+ ### Tuple Path Syntax
259
+
260
+ ```typescript
261
+ // Coordinates: [10, 20]
262
+ {
263
+ inputs: [
264
+ { inputType: 'number', path: 'coordinates.0', label: 'X' },
265
+ { inputType: 'number', path: 'coordinates.1', label: 'Y' }
266
+ ]
267
+ }
268
+
269
+ // Nested objects: [{ name: 'Primary', url: '...' }, { name: 'Backup', url: '...' }]
270
+ {
271
+ inputs: [
272
+ { inputType: 'text', path: 'servers.0.name', label: 'Primary Server' },
273
+ { inputType: 'text', path: 'servers.0.url', validation: { schema: z.string().url() } },
274
+ { inputType: 'text', path: 'servers.1.name', label: 'Backup Server' },
275
+ { inputType: 'text', path: 'servers.1.url', validation: { schema: z.string().url() } }
276
+ ]
277
+ }
278
+
279
+ // Different schemas per position
280
+ {
281
+ inputs: [
282
+ { inputType: 'text', path: 'owners.0.email', validation: { schema: z.string().email() } },
283
+ { inputType: 'select', path: 'owners.1.role', inputConfig: { options: [...] } }
284
+ ]
285
+ }
286
+
287
+ // Sparse indices: ['Critical', null, null, null, null, 'Low']
288
+ {
289
+ inputs: [
290
+ { inputType: 'text', path: 'priorities.0', label: 'High Priority' },
291
+ { inputType: 'text', path: 'priorities.5', label: 'Low Priority' }
292
+ ]
293
+ }
167
294
  ```
168
295
 
169
- If validation configuration is not found, default/built-in validation takes place.
170
- Message can be set globally or per input.
296
+ **Example:** See [playground/src/examples/tuple-example/](./playground/src/examples/tuple-example/)
297
+
298
+ ## Validation
171
299
 
172
- ```js
173
- // Required message config example
300
+ The library integrates with Zod for schema validation.
174
301
 
175
- const validationConfig: IGlobalValidationConfig = {
176
- requiredMessage: "Required field",
177
- requiredMessagePerInput: {
178
- [InputType.string]: "Field is required",
179
- [InputType.number]: "Required. Please enter a number",
302
+ ### Per-Input Validation
303
+
304
+ Define validation schemas directly in input definitions:
305
+
306
+ **Static validation:**
307
+ ```typescript
308
+ {
309
+ inputType: 'text',
310
+ path: 'email',
311
+ label: 'Email',
312
+ required: true,
313
+ validation: {
314
+ schema: z.string().email('Invalid email format')
315
+ }
316
+ }
317
+ ```
318
+
319
+ **Dynamic validation (depends on other form values):**
320
+ ```typescript
321
+ {
322
+ inputType: 'text',
323
+ path: 'password',
324
+ label: 'Password',
325
+ required: true
326
+ },
327
+ {
328
+ inputType: 'text',
329
+ path: 'confirmPassword',
330
+ label: 'Confirm Password',
331
+ required: true,
332
+ validation: {
333
+ schema: (values) =>
334
+ z.string().refine(
335
+ (val) => val === values.password,
336
+ { message: 'Passwords must match' }
337
+ )
338
+ }
339
+ }
340
+ ```
341
+
342
+ ### Global Validation Configuration
343
+
344
+ Configure validation globally using `useZodValidationResolver`:
345
+
346
+ ```typescript
347
+ const resolver = useZodValidationResolver(
348
+ formDefinition,
349
+ {
350
+ // Custom required message for all inputs
351
+ requiredMessage: 'This field is required',
352
+
353
+ // Custom required message per input type
354
+ requiredMessagePerInput: {
355
+ text: 'Text field is required',
356
+ number: 'Please enter a number'
357
+ },
358
+
359
+ // Custom required schema per input type
360
+ requiredSchemaPerInput: {
361
+ text: z.string().min(1),
362
+ number: z.number(),
363
+ myCustomInput: z.custom(...)
364
+ },
365
+
366
+ // Global validation function
367
+ globalValidation: (value, input, metadata) => {
368
+ // Custom validation logic
369
+ return { continue: true }
370
+ }
180
371
  },
181
- };
372
+ metadata
373
+ )
182
374
  ```
375
+
376
+ **Validation resolution order for required fields:**
377
+
378
+ 1. `requiredSchemaPerInput[inputType]`
379
+ 2. `requiredSchema`
380
+ 3. Built-in default validation
381
+
382
+ ## Additional Features
383
+
384
+ ### Conditional Visibility
385
+
386
+ Control input visibility based on form values:
387
+
388
+ ```typescript
389
+ {
390
+ inputType: 'select',
391
+ path: 'authType',
392
+ label: 'Authentication Type'
393
+ },
394
+ {
395
+ inputType: 'text',
396
+ path: 'apiToken',
397
+ label: 'API Token',
398
+ isVisible: (values) => values.authType === 'token'
399
+ }
400
+ ```
401
+
402
+ ### Default Values
403
+
404
+ Set default values using the `default` property or `collectDefaultValues`:
405
+
406
+ ```typescript
407
+ import { collectDefaultValues } from '@harnessio/forms'
408
+
409
+ // In input definition
410
+ {
411
+ inputType: 'text',
412
+ path: 'name',
413
+ default: 'John Doe'
414
+ }
415
+
416
+ // Collect all defaults from form definition
417
+ const defaultValues = collectDefaultValues(formDefinition)
418
+ ```
419
+
420
+ ### Value Transformers
421
+
422
+ Transform values between data model and form state:
423
+
424
+ ```typescript
425
+ {
426
+ inputType: 'text',
427
+ path: 'tags',
428
+ label: 'Tags',
429
+ // Transform incoming data (array) to form display (string)
430
+ inputTransform: (value: string[]) => ({
431
+ value: value.join(', ')
432
+ }),
433
+ // Transform form value (string) back to data model (array)
434
+ outputTransform: (value: string) => ({
435
+ value: value.split(',').map(s => s.trim())
436
+ })
437
+ }
438
+ ```
439
+
440
+ **Transformer functions:**
441
+ - `inputTransform` - Converts data model → form state (runs when data is loaded)
442
+ - `outputTransform` - Converts form state → data model (runs on submit)
443
+ - Both must return `{ value: any }` or `undefined`
444
+ - Can chain multiple transformers by providing an array
445
+
446
+ ## API Reference
447
+
448
+ ### Core Types
449
+
450
+ ```typescript
451
+ // Input definition with generic parameters
452
+ IInputDefinition<TConfig = unknown, TValue = unknown, TInputType extends string = string>
453
+
454
+ // Input component props
455
+ InputProps<TValue = unknown, TConfig = unknown>
456
+
457
+ // Form definition
458
+ IFormDefinition<TConfig = unknown>
459
+
460
+ // Value types
461
+ AnyFormValue // any form value type
462
+ ```
463
+
464
+ ### Main Components
465
+
466
+ - `RootForm` - Root form component with React Hook Form integration
467
+ - `RenderForm` - Renders inputs from form definition
468
+ - `InputFactory` - Registry for input components
469
+ - `InputComponent<TValue, TConfig>` - Base class for input components
470
+
471
+ ### Hooks
472
+
473
+ - `useZodValidationResolver(definition, config?, metadata?)` - Creates Zod validation resolver
474
+ - `useController(options)` - React Hook Form controller hook
475
+
476
+ ### Utilities
477
+
478
+ - `collectDefaultValues(definition)` - Extracts default values from form definition
479
+
480
+ ## Examples
481
+
482
+ Complete working examples can be found in [playground/src/examples/](./playground/src/examples/):
483
+
484
+ - [Basic Example](./playground/src/examples/basic-example/) - Simple form with text and number inputs
485
+ - [Tuple Example](./playground/src/examples/tuple-example/) - Fixed-position arrays
486
+ - [Array Example](./playground/src/examples/array-example/) - Dynamic arrays
487
+ - [List Example](./playground/src/examples/list-example/) - Dynamic object lists
488
+ - [Conditional Example](./playground/src/examples/conditional-example/) - Conditional visibility
489
+
490
+ ## License
491
+
492
+ See the main repository license.