@dynamic-field-kit/angular 1.2.4 → 1.3.3

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 (67) hide show
  1. package/README.md +116 -46
  2. package/angular.json +27 -0
  3. package/dist/README.md +188 -0
  4. package/dist/components/BaseInput.d.ts +40 -0
  5. package/dist/components/DynamicInput.d.ts +31 -0
  6. package/dist/components/FieldInput.d.ts +18 -0
  7. package/dist/components/MultiFieldInput.d.ts +40 -0
  8. package/dist/esm2022/components/BaseInput.mjs +68 -0
  9. package/dist/esm2022/components/DynamicInput.mjs +191 -0
  10. package/dist/esm2022/components/FieldInput.mjs +80 -0
  11. package/dist/esm2022/components/MultiFieldInput.mjs +264 -0
  12. package/{esm2020 → dist/esm2022}/dynamic-field-kit-angular.mjs +4 -4
  13. package/dist/esm2022/layout/defaultLayouts.mjs +114 -0
  14. package/dist/esm2022/layout/index.mjs +3 -0
  15. package/dist/esm2022/layout/layoutRegistry.mjs +14 -0
  16. package/dist/esm2022/lib/dynamic-field-kit.module.mjs +52 -0
  17. package/{esm2020 → dist/esm2022}/public-api.mjs +14 -14
  18. package/dist/esm2022/types/layout.mjs +2 -0
  19. package/dist/fesm2022/dynamic-field-kit-angular.mjs +771 -0
  20. package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +1 -0
  21. package/{index.d.ts → dist/index.d.ts} +5 -5
  22. package/dist/layout/defaultLayouts.d.ts +31 -0
  23. package/dist/layout/index.d.ts +2 -0
  24. package/dist/layout/layoutRegistry.d.ts +12 -0
  25. package/{lib → dist/lib}/dynamic-field-kit.module.d.ts +11 -11
  26. package/{public-api.d.ts → dist/public-api.d.ts} +9 -9
  27. package/dist/types/layout.d.ts +25 -0
  28. package/karma.conf.js +37 -0
  29. package/ng-package.json +7 -0
  30. package/package.json +44 -28
  31. package/src/components/BaseInput.ts +60 -0
  32. package/src/components/DynamicInput.ts +224 -0
  33. package/src/components/FieldInput.ts +67 -0
  34. package/src/components/MultiFieldInput.ts +238 -0
  35. package/src/layout/defaultLayouts.ts +70 -0
  36. package/src/layout/index.ts +2 -0
  37. package/src/layout/layoutRegistry.ts +25 -0
  38. package/src/lib/dynamic-field-kit.module.ts +29 -0
  39. package/src/public-api.ts +24 -0
  40. package/src/types/layout.ts +36 -0
  41. package/test/DynamicInput.spec.ts +30 -0
  42. package/test/FieldInput.spec.ts +44 -0
  43. package/test/angular.spec.ts +102 -0
  44. package/test/integration.spec.ts +57 -0
  45. package/test/layouts.spec.ts +26 -0
  46. package/test.ts +12 -0
  47. package/tsconfig.json +13 -0
  48. package/tsconfig.spec.json +21 -0
  49. package/components/BaseInput.d.ts +0 -22
  50. package/components/DynamicInput.d.ts +0 -28
  51. package/components/FieldInput.d.ts +0 -14
  52. package/components/MultiFieldInput.d.ts +0 -23
  53. package/esm2020/components/BaseInput.mjs +0 -46
  54. package/esm2020/components/DynamicInput.mjs +0 -146
  55. package/esm2020/components/FieldInput.mjs +0 -66
  56. package/esm2020/components/MultiFieldInput.mjs +0 -79
  57. package/esm2020/layout/defaultLayouts.mjs +0 -43
  58. package/esm2020/layout/index.mjs +0 -2
  59. package/esm2020/lib/dynamic-field-kit.module.mjs +0 -20
  60. package/esm2020/types/layout.mjs +0 -2
  61. package/fesm2015/dynamic-field-kit-angular.mjs +0 -393
  62. package/fesm2015/dynamic-field-kit-angular.mjs.map +0 -1
  63. package/fesm2020/dynamic-field-kit-angular.mjs +0 -390
  64. package/fesm2020/dynamic-field-kit-angular.mjs.map +0 -1
  65. package/layout/defaultLayouts.d.ts +0 -13
  66. package/layout/index.d.ts +0 -1
  67. package/types/layout.d.ts +0 -1
@@ -0,0 +1,67 @@
1
+ import { NgIf } from '@angular/common';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
5
+ Component,
6
+ EventEmitter,
7
+ Input,
8
+ OnChanges,
9
+ Output,
10
+ SimpleChanges,
11
+ } from '@angular/core';
12
+ import { FieldDescription } from '@dynamic-field-kit/core';
13
+ import { DynamicInput } from './DynamicInput';
14
+
15
+ @Component({
16
+ selector: 'dfk-field-input',
17
+ standalone: true,
18
+ imports: [NgIf, DynamicInput],
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ template: `
21
+ <dfk-dynamic-input
22
+ *ngIf="shouldRender"
23
+ [type]="fieldDescription!.type"
24
+ [value]="getFieldValue()"
25
+ [label]="fieldDescription!.label"
26
+ [placeholder]="fieldDescription!.placeholder"
27
+ [required]="fieldDescription!.required"
28
+ [description]="$any(fieldDescription!.description)"
29
+ [options]="fieldDescription!.options"
30
+ [className]="fieldDescription!.className"
31
+ (valueChange)="
32
+ onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
33
+ "
34
+ [disabled]="false"
35
+ [errorMessage]="''"
36
+ ></dfk-dynamic-input>
37
+ `,
38
+ })
39
+ export class FieldInput implements OnChanges {
40
+ @Input() fieldDescription?: FieldDescription;
41
+ @Input() value?: unknown;
42
+ @Output() onValueChangeField = new EventEmitter<{
43
+ value: unknown;
44
+ key: string;
45
+ }>();
46
+
47
+ shouldRender = false;
48
+
49
+ constructor(private cdr: ChangeDetectorRef) {}
50
+
51
+ ngOnChanges(_changes: SimpleChanges): void {
52
+ this.shouldRender = !!this.fieldDescription;
53
+ this.cdr.markForCheck();
54
+ }
55
+
56
+ getFieldValue(): unknown {
57
+ if (!this.fieldDescription) {
58
+ return undefined;
59
+ }
60
+
61
+ if (this.value === undefined && this.fieldDescription.type === 'text') {
62
+ return '';
63
+ }
64
+
65
+ return this.value;
66
+ }
67
+ }
@@ -0,0 +1,238 @@
1
+ import { NgClass, NgFor, NgIf } from '@angular/common';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
5
+ Component,
6
+ EventEmitter,
7
+ HostListener,
8
+ Input,
9
+ OnChanges,
10
+ OnInit,
11
+ Output,
12
+ SimpleChanges,
13
+ } from '@angular/core';
14
+ import {
15
+ applyComputedValues,
16
+ canAddGroupItem,
17
+ canRemoveGroupItem,
18
+ createGroupItem,
19
+ FieldDescription,
20
+ Properties,
21
+ } from '@dynamic-field-kit/core';
22
+ import { BaseLayoutConfig, LayoutConfig } from '../types/layout';
23
+ import { FieldInput } from './FieldInput';
24
+
25
+ const DEFAULT_BREAKPOINT = 768;
26
+
27
+ @Component({
28
+ selector: 'dfk-multi-field-input',
29
+ standalone: true,
30
+ // Repeatable field groups render a nested <dfk-multi-field-input> per item
31
+ // (see the #groupTpl below), so this component imports itself. That's the
32
+ // supported way to give a standalone component recursive template usage
33
+ // without a cross-file circular import between FieldInput and a separate
34
+ // group component (whose `imports` arrays are evaluated eagerly at module
35
+ // load time and would deadlock on an actual circular module reference).
36
+ imports: [NgClass, NgFor, NgIf, FieldInput, MultiFieldInput],
37
+ changeDetection: ChangeDetectionStrategy.OnPush,
38
+ template: `
39
+ <div
40
+ class="p-4 border rounded-lg bg-gray-50"
41
+ [ngClass]="{
42
+ 'flex flex-col': resolvedLayoutType === 'column',
43
+ 'flex flex-row': resolvedLayoutType === 'row',
44
+ grid: resolvedLayoutType === 'grid'
45
+ }"
46
+ [style.gap.px]="gap"
47
+ [style.gridTemplateColumns]="
48
+ resolvedLayoutType === 'grid' ? 'repeat(' + columns + ', 1fr)' : null
49
+ "
50
+ >
51
+ <ng-container *ngFor="let field of visibleFields; trackBy: trackByFn">
52
+ <dfk-field-input
53
+ *ngIf="!field.fields"
54
+ [fieldDescription]="field"
55
+ [value]="data[field.name]"
56
+ (onValueChangeField)="onFieldChange($event)"
57
+ ></dfk-field-input>
58
+
59
+ <div *ngIf="field.fields" [class]="field.className">
60
+ <div *ngIf="field.label">{{ field.label }}</div>
61
+ <div
62
+ *ngFor="
63
+ let item of getItems(field);
64
+ let i = index;
65
+ trackBy: trackByIndex
66
+ "
67
+ style="display: flex; align-items: flex-start; gap: 8px;"
68
+ >
69
+ <div style="flex: 1">
70
+ <dfk-multi-field-input
71
+ [fieldDescriptions]="field.fields"
72
+ [properties]="item"
73
+ (onChange)="onGroupItemChange(field, i, $event)"
74
+ ></dfk-multi-field-input>
75
+ </div>
76
+ <button
77
+ type="button"
78
+ (click)="onGroupItemRemove(field, i)"
79
+ [disabled]="!canRemoveItem(field)"
80
+ >
81
+ {{ field.removeLabel || 'Remove' }}
82
+ </button>
83
+ </div>
84
+ <button
85
+ type="button"
86
+ (click)="onGroupItemAdd(field)"
87
+ [disabled]="!canAddItem(field)"
88
+ >
89
+ {{ field.addLabel || 'Add' }}
90
+ </button>
91
+ </div>
92
+ </ng-container>
93
+ </div>
94
+ `,
95
+ })
96
+ export class MultiFieldInput implements OnInit, OnChanges {
97
+ @Input() fieldDescriptions: FieldDescription[] = [];
98
+ @Input() properties?: Properties;
99
+ @Output() onChange = new EventEmitter<Properties>();
100
+ @Input() layout: LayoutConfig = 'column';
101
+
102
+ data: Properties = {};
103
+ visibleFields: FieldDescription[] = [];
104
+ private isMobile = false;
105
+
106
+ constructor(private cdr: ChangeDetectorRef) {}
107
+
108
+ @HostListener('window:resize')
109
+ onWindowResize(): void {
110
+ const wasMobile = this.isMobile;
111
+ this.updateIsMobile();
112
+ if (wasMobile !== this.isMobile) {
113
+ this.cdr.markForCheck();
114
+ }
115
+ }
116
+
117
+ get resolvedLayout(): BaseLayoutConfig {
118
+ if (typeof this.layout === 'object' && this.layout.type === 'responsive') {
119
+ return this.isMobile ? this.layout.mobile : this.layout.desktop;
120
+ }
121
+ return this.layout as BaseLayoutConfig;
122
+ }
123
+
124
+ get resolvedLayoutType(): string {
125
+ const resolved = this.resolvedLayout;
126
+ return typeof resolved === 'string' ? resolved : resolved.type;
127
+ }
128
+
129
+ get gap(): number {
130
+ const resolved = this.resolvedLayout;
131
+ if (typeof resolved === 'object' && resolved.gap !== undefined) {
132
+ return resolved.gap;
133
+ }
134
+ return 12;
135
+ }
136
+
137
+ get columns(): number {
138
+ const resolved = this.resolvedLayout;
139
+ if (typeof resolved === 'object' && resolved.type === 'grid') {
140
+ return resolved.columns ?? 2;
141
+ }
142
+ return 2;
143
+ }
144
+
145
+ trackByFn(index: number, field: FieldDescription): string | number {
146
+ return field.name || index;
147
+ }
148
+
149
+ trackByIndex(index: number): number {
150
+ return index;
151
+ }
152
+
153
+ ngOnInit() {
154
+ this.updateIsMobile();
155
+ this.init();
156
+ }
157
+
158
+ ngOnChanges(_changes: SimpleChanges) {
159
+ this.init();
160
+ }
161
+
162
+ private updateIsMobile(): void {
163
+ const breakpoint =
164
+ typeof this.layout === 'object' && this.layout.type === 'responsive'
165
+ ? this.layout.breakpoint ?? DEFAULT_BREAKPOINT
166
+ : DEFAULT_BREAKPOINT;
167
+ this.isMobile =
168
+ typeof window !== 'undefined' && window.innerWidth < breakpoint;
169
+ }
170
+
171
+ private init() {
172
+ if (this.properties) {
173
+ this.data = applyComputedValues(this.fieldDescriptions, {
174
+ ...this.properties,
175
+ });
176
+ }
177
+ this.updateVisibleFields();
178
+ }
179
+
180
+ private updateVisibleFields() {
181
+ this.visibleFields = this.fieldDescriptions.filter(
182
+ (f) => !f.appearCondition || f.appearCondition(this.data)
183
+ );
184
+ }
185
+
186
+ onFieldChange(event: { value: unknown; key: string }): void {
187
+ this.commitData({ ...this.data, [event.key]: event.value });
188
+ }
189
+
190
+ getItems(field: FieldDescription): Properties[] {
191
+ const value = this.data[field.name];
192
+ return Array.isArray(value) ? (value as Properties[]) : [];
193
+ }
194
+
195
+ canAddItem(field: FieldDescription): boolean {
196
+ return canAddGroupItem(field, this.getItems(field));
197
+ }
198
+
199
+ canRemoveItem(field: FieldDescription): boolean {
200
+ return canRemoveGroupItem(field, this.getItems(field));
201
+ }
202
+
203
+ onGroupItemAdd(field: FieldDescription): void {
204
+ if (!this.canAddItem(field)) {
205
+ return;
206
+ }
207
+ const items = this.getItems(field);
208
+ this.commitData({
209
+ ...this.data,
210
+ [field.name]: [...items, createGroupItem(field)],
211
+ });
212
+ }
213
+
214
+ onGroupItemRemove(field: FieldDescription, index: number): void {
215
+ if (!this.canRemoveItem(field)) {
216
+ return;
217
+ }
218
+ const items = this.getItems(field).filter((_, i) => i !== index);
219
+ this.commitData({ ...this.data, [field.name]: items });
220
+ }
221
+
222
+ onGroupItemChange(
223
+ field: FieldDescription,
224
+ index: number,
225
+ next: Properties
226
+ ): void {
227
+ const items = this.getItems(field).slice();
228
+ items[index] = next;
229
+ this.commitData({ ...this.data, [field.name]: items });
230
+ }
231
+
232
+ private commitData(nextData: Properties): void {
233
+ this.data = applyComputedValues(this.fieldDescriptions, nextData);
234
+ this.updateVisibleFields();
235
+ this.onChange.emit(this.data);
236
+ this.cdr.markForCheck();
237
+ }
238
+ }
@@ -0,0 +1,70 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { Component, Input, TemplateRef } from '@angular/core';
3
+ import { layoutRegistry } from './layoutRegistry';
4
+
5
+ @Component({
6
+ selector: 'dfk-column-layout',
7
+ standalone: true,
8
+ imports: [CommonModule],
9
+ template: `<div
10
+ [style.display]="'flex'"
11
+ [style.flexDirection]="'column'"
12
+ [style.gap]="gap + 'px'"
13
+ >
14
+ <ng-container *ngTemplateOutlet="template"></ng-container>
15
+ </div>`,
16
+ })
17
+ export class ColumnLayout {
18
+ @Input() config?: { gap?: number };
19
+ @Input() template!: TemplateRef<unknown>;
20
+ get gap() {
21
+ return this.config?.gap ?? 12;
22
+ }
23
+ }
24
+
25
+ @Component({
26
+ selector: 'dfk-row-layout',
27
+ standalone: true,
28
+ imports: [CommonModule],
29
+ template: `<div
30
+ [style.display]="'flex'"
31
+ [style.flexDirection]="'row'"
32
+ [style.gap]="gap + 'px'"
33
+ >
34
+ <ng-container *ngTemplateOutlet="template"></ng-container>
35
+ </div>`,
36
+ })
37
+ export class RowLayout {
38
+ @Input() config?: { gap?: number };
39
+ @Input() template!: TemplateRef<unknown>;
40
+ get gap() {
41
+ return this.config?.gap ?? 12;
42
+ }
43
+ }
44
+
45
+ @Component({
46
+ selector: 'dfk-grid-layout',
47
+ standalone: true,
48
+ imports: [CommonModule],
49
+ template: `<div
50
+ [style.display]="'grid'"
51
+ [style.gridTemplateColumns]="'repeat(' + columns + ', 1fr)'"
52
+ [style.gap]="gap + 'px'"
53
+ >
54
+ <ng-container *ngTemplateOutlet="template"></ng-container>
55
+ </div>`,
56
+ })
57
+ export class GridLayout {
58
+ @Input() config?: { columns?: number; gap?: number };
59
+ @Input() template!: TemplateRef<unknown>;
60
+ get columns() {
61
+ return this.config?.columns ?? 2;
62
+ }
63
+ get gap() {
64
+ return this.config?.gap ?? 12;
65
+ }
66
+ }
67
+
68
+ layoutRegistry.register('column', ColumnLayout);
69
+ layoutRegistry.register('row', RowLayout);
70
+ layoutRegistry.register('grid', GridLayout);
@@ -0,0 +1,2 @@
1
+ export * from './defaultLayouts';
2
+ export { layoutRegistry, LayoutRegistry } from './layoutRegistry';
@@ -0,0 +1,25 @@
1
+ import { Type } from '@angular/core';
2
+
3
+ export type LayoutRenderer<C = unknown> = (props: {
4
+ children: unknown[];
5
+ config?: C;
6
+ }) => unknown;
7
+
8
+ export type AngularLayoutComponent = Type<unknown>;
9
+
10
+ export class LayoutRegistry {
11
+ private layouts = new Map<string, AngularLayoutComponent>();
12
+
13
+ register(type: string, component: AngularLayoutComponent): void {
14
+ if (this.layouts.has(type)) {
15
+ console.warn(`[dynamic-field-kit] Layout "${type}" already exists`);
16
+ }
17
+ this.layouts.set(type, component);
18
+ }
19
+
20
+ get(type: string): AngularLayoutComponent | undefined {
21
+ return this.layouts.get(type);
22
+ }
23
+ }
24
+
25
+ export const layoutRegistry = new LayoutRegistry();
@@ -0,0 +1,29 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { NgModule } from '@angular/core';
3
+
4
+ import { DynamicInput } from '../components/DynamicInput';
5
+ import { FieldInput } from '../components/FieldInput';
6
+ import { MultiFieldInput } from '../components/MultiFieldInput';
7
+
8
+ import { ColumnLayout, RowLayout, GridLayout } from '../layout/defaultLayouts';
9
+
10
+ @NgModule({
11
+ imports: [
12
+ CommonModule,
13
+ DynamicInput,
14
+ FieldInput,
15
+ MultiFieldInput,
16
+ ColumnLayout,
17
+ RowLayout,
18
+ GridLayout,
19
+ ],
20
+ exports: [
21
+ DynamicInput,
22
+ FieldInput,
23
+ MultiFieldInput,
24
+ ColumnLayout,
25
+ RowLayout,
26
+ GridLayout,
27
+ ],
28
+ })
29
+ export class DynamicFieldKitModule {}
@@ -0,0 +1,24 @@
1
+ // public-api.ts
2
+
3
+ // Components
4
+ export * from './components/BaseInput';
5
+ export * from './components/DynamicInput';
6
+ export * from './components/FieldInput';
7
+ export * from './components/MultiFieldInput';
8
+
9
+ // Layout
10
+ export * from './layout';
11
+ export * from './types/layout';
12
+
13
+ // Module
14
+ export * from './lib/dynamic-field-kit.module';
15
+
16
+ // Re-export types from core (only type, not export * )
17
+ export type {
18
+ FieldDescription,
19
+ FieldRendererProps,
20
+ FieldTypeKey,
21
+ } from '@dynamic-field-kit/core';
22
+
23
+ // Optional: expose registry for advanced use cases, but not required for basic usage
24
+ export { fieldRegistry } from '@dynamic-field-kit/core';
@@ -0,0 +1,36 @@
1
+ export interface GridLayoutConfig {
2
+ type: 'grid';
3
+ columns?: number;
4
+ gap?: number;
5
+ width?: string;
6
+ }
7
+
8
+ export interface RowLayoutConfig {
9
+ type: 'row';
10
+ gap?: number;
11
+ width?: string;
12
+ flex?: boolean;
13
+ }
14
+
15
+ export interface ColumnLayoutConfig {
16
+ type: 'column';
17
+ gap?: number;
18
+ width?: string;
19
+ }
20
+
21
+ export type BaseLayoutConfig =
22
+ | 'column'
23
+ | 'row'
24
+ | 'grid'
25
+ | GridLayoutConfig
26
+ | RowLayoutConfig
27
+ | ColumnLayoutConfig;
28
+
29
+ export interface ResponsiveLayoutConfig {
30
+ type: 'responsive';
31
+ mobile: BaseLayoutConfig;
32
+ desktop: BaseLayoutConfig;
33
+ breakpoint?: number;
34
+ }
35
+
36
+ export type LayoutConfig = BaseLayoutConfig | ResponsiveLayoutConfig;
@@ -0,0 +1,30 @@
1
+ describe('DynamicInput', () => {
2
+ it('should create DynamicInput', () => {
3
+ expect(true).toBe(true);
4
+ });
5
+
6
+ it('should accept type input', () => {
7
+ const type = 'text';
8
+ expect(type).toBe('text');
9
+ });
10
+
11
+ it('should have valueChange output', () => {
12
+ const hasOutput = true;
13
+ expect(hasOutput).toBe(true);
14
+ });
15
+
16
+ it('should have onChange output for backward compatibility', () => {
17
+ const hasOutput = true;
18
+ expect(hasOutput).toBe(true);
19
+ });
20
+
21
+ it('should handle unknown field type', () => {
22
+ const unknownType = 'unknownType';
23
+ expect(typeof unknownType).toBe('string');
24
+ });
25
+
26
+ it('should emit value on change', () => {
27
+ const value = 'test-value';
28
+ expect(value).toBe('test-value');
29
+ });
30
+ });
@@ -0,0 +1,44 @@
1
+ describe('FieldInput', () => {
2
+ it('should create FieldInput', () => {
3
+ expect(true).toBe(true);
4
+ });
5
+
6
+ it('should accept fieldDescription prop', () => {
7
+ const fieldDesc: any = {
8
+ name: 'username',
9
+ type: 'text',
10
+ label: 'Username',
11
+ placeholder: 'Enter name',
12
+ };
13
+ expect(fieldDesc.name).toBe('username');
14
+ expect(fieldDesc.type).toBe('text');
15
+ expect(fieldDesc.label).toBe('Username');
16
+ });
17
+
18
+ it('should pass options from fieldDescription', () => {
19
+ const fieldDesc: any = {
20
+ name: 'country',
21
+ type: 'text',
22
+ options: [{ label: 'USA' }, { label: 'VN' }],
23
+ };
24
+ expect(fieldDesc.options?.length).toBe(2);
25
+ });
26
+
27
+ it('should pass className from fieldDescription', () => {
28
+ const fieldDesc: any = {
29
+ name: 'test',
30
+ type: 'text',
31
+ className: 'my-custom-class',
32
+ };
33
+ expect(fieldDesc.className).toBe('my-custom-class');
34
+ });
35
+
36
+ it('should pass description from fieldDescription', () => {
37
+ const fieldDesc: any = {
38
+ name: 'test',
39
+ type: 'text',
40
+ description: 'This is a help text',
41
+ };
42
+ expect(fieldDesc.description).toBe('This is a help text');
43
+ });
44
+ });
@@ -0,0 +1,102 @@
1
+ describe('Angular Package', () => {
2
+ describe('Components', () => {
3
+ it('should have DynamicInput component', () => {
4
+ expect(true).toBe(true);
5
+ });
6
+
7
+ it('should have FieldInput component', () => {
8
+ expect(true).toBe(true);
9
+ });
10
+
11
+ it('should have MultiFieldInput component', () => {
12
+ expect(true).toBe(true);
13
+ });
14
+ });
15
+
16
+ describe('Field Description', () => {
17
+ it('should create field with name and type', () => {
18
+ const field = { name: 'username', type: 'text' };
19
+ expect(field.name).toBe('username');
20
+ expect(field.type).toBe('text');
21
+ });
22
+
23
+ it('should support optional label', () => {
24
+ const field = { name: 'email', type: 'text', label: 'Email' };
25
+ expect(field.label).toBe('Email');
26
+ });
27
+
28
+ it('should support placeholder', () => {
29
+ const field = { name: 'name', type: 'text', placeholder: 'Enter name' };
30
+ expect(field.placeholder).toBe('Enter name');
31
+ });
32
+
33
+ it('should support required flag', () => {
34
+ const field = { name: 'email', type: 'text', required: true };
35
+ expect(field.required).toBe(true);
36
+ });
37
+
38
+ it('should support options for select fields', () => {
39
+ const field = {
40
+ name: 'country',
41
+ type: 'text',
42
+ options: [{ label: 'Vietnam', value: 'vn' }],
43
+ };
44
+ expect(field.options?.length).toBe(1);
45
+ });
46
+
47
+ it('should support className', () => {
48
+ const field = { name: 'test', type: 'text', className: 'custom-class' };
49
+ expect(field.className).toBe('custom-class');
50
+ });
51
+
52
+ it('should support description', () => {
53
+ const field = { name: 'test', type: 'text', description: 'Help text' };
54
+ expect(field.description).toBe('Help text');
55
+ });
56
+ });
57
+
58
+ describe('Layouts', () => {
59
+ it('should support row layout', () => {
60
+ expect('row').toBe('row');
61
+ });
62
+
63
+ it('should support grid layout', () => {
64
+ expect('grid').toBe('grid');
65
+ });
66
+
67
+ it('should support column layout', () => {
68
+ expect('column').toBe('column');
69
+ });
70
+ });
71
+
72
+ describe('appearCondition', () => {
73
+ it('should evaluate appearCondition', () => {
74
+ const condition = (data: any) => data.show === true;
75
+ expect(condition({ show: true })).toBe(true);
76
+ expect(condition({ show: false })).toBe(false);
77
+ });
78
+
79
+ it('should filter fields based on appearCondition', () => {
80
+ const fields = [
81
+ { name: 'alwaysShow', type: 'text' },
82
+ {
83
+ name: 'conditional',
84
+ type: 'text',
85
+ appearCondition: (data: any) => data.accountType === 'business',
86
+ },
87
+ ];
88
+
89
+ const visibleForPersonal = fields.filter(
90
+ (f) =>
91
+ !f.appearCondition || f.appearCondition({ accountType: 'personal' })
92
+ );
93
+ expect(visibleForPersonal.length).toBe(1);
94
+
95
+ const visibleForBusiness = fields.filter(
96
+ (f) =>
97
+ !f.appearCondition || f.appearCondition({ accountType: 'business' })
98
+ );
99
+ expect(visibleForBusiness.length).toBe(2);
100
+ });
101
+ });
102
+ });