@dynamic-field-kit/angular 1.4.0 → 1.5.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -5
  4. package/dist/components/DynamicFormDevTools.d.ts +16 -0
  5. package/dist/components/DynamicInput.d.ts +2 -1
  6. package/dist/components/FieldInput.d.ts +7 -1
  7. package/dist/components/MultiFieldInput.d.ts +13 -2
  8. package/dist/fesm2022/dynamic-field-kit-angular.mjs +578 -34
  9. package/dist/index.d.ts +1 -1
  10. package/dist/layout/index.d.ts +2 -2
  11. package/dist/lib/dynamic-field-kit.module.d.ts +4 -4
  12. package/dist/lib/dynamic-form.store.d.ts +23 -0
  13. package/dist/public-api.d.ts +10 -8
  14. package/package.json +40 -21
  15. package/dist/README.md +0 -235
  16. package/dist/esm2022/components/BaseInput.mjs +0 -59
  17. package/dist/esm2022/components/DynamicInput.mjs +0 -244
  18. package/dist/esm2022/components/FieldInput.mjs +0 -99
  19. package/dist/esm2022/components/MultiFieldInput.mjs +0 -338
  20. package/dist/esm2022/dynamic-field-kit-angular.mjs +0 -5
  21. package/dist/esm2022/fieldRegistryToken.mjs +0 -12
  22. package/dist/esm2022/layout/defaultLayouts.mjs +0 -114
  23. package/dist/esm2022/layout/index.mjs +0 -3
  24. package/dist/esm2022/layout/layoutRegistry.mjs +0 -14
  25. package/dist/esm2022/lib/dynamic-field-kit.module.mjs +0 -52
  26. package/dist/esm2022/public-api.mjs +0 -18
  27. package/dist/esm2022/types/layout.mjs +0 -2
  28. package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +0 -1
  29. package/ng-package.json +0 -7
  30. package/src/components/BaseInput.ts +0 -57
  31. package/src/components/DynamicInput.ts +0 -280
  32. package/src/components/FieldInput.ts +0 -74
  33. package/src/components/MultiFieldInput.ts +0 -331
  34. package/src/fieldRegistryToken.ts +0 -15
  35. package/src/layout/defaultLayouts.ts +0 -70
  36. package/src/layout/index.ts +0 -2
  37. package/src/layout/layoutRegistry.ts +0 -25
  38. package/src/lib/dynamic-field-kit.module.ts +0 -29
  39. package/src/public-api.ts +0 -40
  40. package/src/types/layout.ts +0 -14
  41. package/test/DynamicInput.spec.ts +0 -230
  42. package/test/FieldInput.spec.ts +0 -146
  43. package/test/MultiFieldInput.spec.ts +0 -256
  44. package/test/helpers/renderers.ts +0 -89
  45. package/test/layout.spec.ts +0 -119
  46. package/test/publicApi.spec.ts +0 -64
  47. package/test/setup.ts +0 -12
  48. package/test/smoke.spec.ts +0 -27
  49. package/tsconfig.json +0 -13
  50. package/tsconfig.spec.json +0 -9
  51. package/vitest.config.ts +0 -30
@@ -1,230 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
- import { By } from '@angular/platform-browser';
3
- import { DynamicInput } from '../src/components/DynamicInput';
4
- import { FIELD_REGISTRY } from '../src/fieldRegistryToken';
5
- import { beforeEach, describe, expect, it } from 'vitest';
6
- import {
7
- DefaultsRendererComponent,
8
- makeRegistry,
9
- TextRendererComponent,
10
- LegacyOutputRendererComponent,
11
- fallbackRenderer,
12
- } from './helpers/renderers';
13
-
14
- describe('DynamicInput', () => {
15
- let registry: ReturnType<typeof makeRegistry>;
16
-
17
- beforeEach(() => {
18
- registry = makeRegistry();
19
- TestBed.configureTestingModule({
20
- imports: [DynamicInput],
21
- providers: [{ provide: FIELD_REGISTRY, useValue: registry }],
22
- });
23
- });
24
-
25
- it('renders a registered Angular component class', () => {
26
- registry.register('text', TextRendererComponent as never);
27
-
28
- const fixture = TestBed.createComponent(DynamicInput);
29
- fixture.componentRef.setInput('type', 'text');
30
- fixture.componentRef.setInput('value', 'hello');
31
- fixture.detectChanges();
32
-
33
- const input: HTMLInputElement =
34
- fixture.nativeElement.querySelector('input.txt');
35
- expect(input).not.toBeNull();
36
- expect(input.value).toBe('hello');
37
- });
38
-
39
- it('does not clobber a renderer input default when DynamicInput was never given that prop', () => {
40
- registry.register('defaults', DefaultsRendererComponent as never);
41
-
42
- const fixture = TestBed.createComponent(DynamicInput);
43
- fixture.componentRef.setInput('type', 'defaults');
44
- // Deliberately never setInput('label', ...): DynamicInput was never
45
- // given a value for `label`, so the renderer's own initializer
46
- // (`label = 'None'`) must survive applyProps untouched.
47
- fixture.detectChanges();
48
-
49
- const label: HTMLElement = fixture.nativeElement.querySelector('.label');
50
- expect(label.textContent).toBe('None');
51
- });
52
-
53
- it('forwards KNOWN_PROPS to the rendered instance', () => {
54
- registry.register('text', TextRendererComponent as never);
55
-
56
- const fixture = TestBed.createComponent(DynamicInput);
57
- fixture.componentRef.setInput('type', 'text');
58
- fixture.componentRef.setInput('placeholder', 'Your name');
59
- fixture.componentRef.setInput('disabled', true);
60
- fixture.detectChanges();
61
-
62
- const input: HTMLInputElement =
63
- fixture.nativeElement.querySelector('input.txt');
64
- expect(input.placeholder).toBe('Your name');
65
- expect(input.disabled).toBe(true);
66
- });
67
-
68
- it('forwards extraProps verbatim', () => {
69
- registry.register('text', TextRendererComponent as never);
70
-
71
- const fixture = TestBed.createComponent(DynamicInput);
72
- fixture.componentRef.setInput('type', 'text');
73
- fixture.componentRef.setInput('extraProps', { hint: 'be brief' });
74
- fixture.detectChanges();
75
-
76
- expect(fixture.nativeElement.querySelector('.hint').textContent).toBe(
77
- 'be brief'
78
- );
79
- });
80
-
81
- it('syncs prop changes to an already-rendered instance', () => {
82
- registry.register('text', TextRendererComponent as never);
83
-
84
- const fixture = TestBed.createComponent(DynamicInput);
85
- fixture.componentRef.setInput('type', 'text');
86
- fixture.componentRef.setInput('value', 'first');
87
- fixture.detectChanges();
88
-
89
- fixture.componentRef.setInput('value', 'second');
90
- fixture.detectChanges();
91
-
92
- const input: HTMLInputElement =
93
- fixture.nativeElement.querySelector('input.txt');
94
- expect(input.value).toBe('second');
95
- });
96
-
97
- it('emits valueChange and onChange when the renderer emits valueChange', () => {
98
- registry.register('text', TextRendererComponent as never);
99
-
100
- const fixture = TestBed.createComponent(DynamicInput);
101
- fixture.componentRef.setInput('type', 'text');
102
- fixture.detectChanges();
103
-
104
- const seen: unknown[] = [];
105
- const legacy: unknown[] = [];
106
- fixture.componentInstance.valueChange.subscribe((v) => seen.push(v));
107
- fixture.componentInstance.onChange.subscribe((v) => legacy.push(v));
108
-
109
- const input: HTMLInputElement =
110
- fixture.nativeElement.querySelector('input.txt');
111
- input.value = 'typed';
112
- input.dispatchEvent(new Event('input'));
113
-
114
- expect(seen).toEqual(['typed']);
115
- expect(legacy).toEqual(['typed']);
116
- });
117
-
118
- it('binds the legacy onValueChange output name', () => {
119
- registry.register('text', LegacyOutputRendererComponent as never);
120
-
121
- const fixture = TestBed.createComponent(DynamicInput);
122
- fixture.componentRef.setInput('type', 'text');
123
- fixture.detectChanges();
124
-
125
- const seen: unknown[] = [];
126
- fixture.componentInstance.valueChange.subscribe((v) => seen.push(v));
127
-
128
- fixture.nativeElement.querySelector('button.legacy-btn').click();
129
-
130
- expect(seen).toEqual(['legacy']);
131
- });
132
-
133
- it('renders a plain function renderer as fallback HTML', () => {
134
- registry.register('text', fallbackRenderer as never);
135
-
136
- const fixture = TestBed.createComponent(DynamicInput);
137
- fixture.componentRef.setInput('type', 'text');
138
- fixture.componentRef.setInput('label', 'Name');
139
- fixture.componentRef.setInput('value', 'Ada');
140
- fixture.detectChanges();
141
-
142
- expect(fixture.nativeElement.querySelector('.fallback').textContent).toBe(
143
- 'Name:Ada'
144
- );
145
- });
146
-
147
- it('rejects a subclass without its own @Component decorator instead of inheriting the parent renderer', () => {
148
- // Sub has no own `ɵcmp`: it only inherits TextRendererComponent's via
149
- // the prototype chain. isComponentType must use hasOwnProperty (not
150
- // `in`) so Sub is not mistaken for a renderable component in its own
151
- // right - the DOM must not show TextRendererComponent's template.
152
- class Sub extends TextRendererComponent {}
153
- registry.register('text', Sub as never);
154
-
155
- const fixture = TestBed.createComponent(DynamicInput);
156
- fixture.componentRef.setInput('type', 'text');
157
- fixture.detectChanges();
158
-
159
- expect(fixture.nativeElement.querySelector('input.txt')).toBeNull();
160
- expect(fixture.nativeElement.textContent).toContain(
161
- 'Failed to render field: text'
162
- );
163
- });
164
-
165
- it('renders an error for an unknown field type', () => {
166
- const fixture = TestBed.createComponent(DynamicInput);
167
- fixture.componentRef.setInput('type', 'nope');
168
- fixture.detectChanges();
169
-
170
- expect(fixture.nativeElement.textContent).toContain(
171
- 'Unknown field type: nope'
172
- );
173
- });
174
-
175
- it('re-renders when type changes', () => {
176
- registry.register('text', TextRendererComponent as never);
177
- registry.register('number', fallbackRenderer as never);
178
-
179
- const fixture = TestBed.createComponent(DynamicInput);
180
- fixture.componentRef.setInput('type', 'text');
181
- fixture.detectChanges();
182
- expect(fixture.nativeElement.querySelector('input.txt')).not.toBeNull();
183
-
184
- fixture.componentRef.setInput('type', 'number');
185
- fixture.detectChanges();
186
-
187
- expect(fixture.nativeElement.querySelector('input.txt')).toBeNull();
188
- expect(fixture.nativeElement.querySelector('.fallback')).not.toBeNull();
189
- });
190
-
191
- it('unsubscribes from renderer outputs on destroy', () => {
192
- registry.register('text', TextRendererComponent as never);
193
-
194
- const fixture = TestBed.createComponent(DynamicInput);
195
- fixture.componentRef.setInput('type', 'text');
196
- fixture.detectChanges();
197
-
198
- const rendererInstance = fixture.debugElement.query(
199
- By.directive(TextRendererComponent)
200
- ).componentInstance as TextRendererComponent;
201
-
202
- const seen: unknown[] = [];
203
- fixture.componentInstance.valueChange.subscribe((v) => seen.push(v));
204
-
205
- fixture.destroy();
206
- rendererInstance.valueChange.emit('after destroy'); // bypasses DOM entirely
207
-
208
- expect(seen).toEqual([]);
209
- });
210
-
211
- it('unsubscribes from the legacy onValueChange output on destroy', () => {
212
- registry.register('text', LegacyOutputRendererComponent as never);
213
-
214
- const fixture = TestBed.createComponent(DynamicInput);
215
- fixture.componentRef.setInput('type', 'text');
216
- fixture.detectChanges();
217
-
218
- const rendererInstance = fixture.debugElement.query(
219
- By.directive(LegacyOutputRendererComponent)
220
- ).componentInstance as LegacyOutputRendererComponent;
221
-
222
- const seen: unknown[] = [];
223
- fixture.componentInstance.valueChange.subscribe((v) => seen.push(v));
224
-
225
- fixture.destroy();
226
- rendererInstance.onValueChange.emit('after destroy');
227
-
228
- expect(seen).toEqual([]);
229
- });
230
- });
@@ -1,146 +0,0 @@
1
- import { ChangeDetectorRef } from '@angular/core';
2
- import { TestBed } from '@angular/core/testing';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { BaseInputComponent } from '../src/components/BaseInput';
5
- import { FieldInput } from '../src/components/FieldInput';
6
- import { FIELD_REGISTRY } from '../src/fieldRegistryToken';
7
- import {
8
- DefaultsRendererComponent,
9
- makeRegistry,
10
- TextRendererComponent,
11
- } from './helpers/renderers';
12
-
13
- describe('FieldInput', () => {
14
- let registry: ReturnType<typeof makeRegistry>;
15
-
16
- beforeEach(() => {
17
- registry = makeRegistry();
18
- registry.register('text', TextRendererComponent as never);
19
- TestBed.configureTestingModule({
20
- imports: [FieldInput],
21
- providers: [{ provide: FIELD_REGISTRY, useValue: registry }],
22
- });
23
- });
24
-
25
- it('renders nothing once fieldDescription is cleared', () => {
26
- // Mounting with no inputs at all passes on the `shouldRender = false`
27
- // field initializer without ever exercising ngOnChanges's guard. Mount
28
- // WITH a fieldDescription first, then clear it, so the assertion
29
- // actually depends on `shouldRender = !!this.fieldDescription`.
30
- const fixture = TestBed.createComponent(FieldInput);
31
- fixture.componentRef.setInput('fieldDescription', {
32
- name: 'first',
33
- type: 'text',
34
- });
35
- fixture.detectChanges();
36
- expect(fixture.nativeElement.querySelector('input.txt')).not.toBeNull();
37
-
38
- fixture.componentRef.setInput('fieldDescription', undefined);
39
- fixture.detectChanges();
40
-
41
- expect(fixture.nativeElement.querySelector('input.txt')).toBeNull();
42
- });
43
-
44
- it('renders the field described by fieldDescription', () => {
45
- const fixture = TestBed.createComponent(FieldInput);
46
- fixture.componentRef.setInput('fieldDescription', {
47
- name: 'first',
48
- type: 'text',
49
- placeholder: 'First name',
50
- });
51
- fixture.componentRef.setInput('value', 'Ada');
52
- fixture.detectChanges();
53
-
54
- const input: HTMLInputElement =
55
- fixture.nativeElement.querySelector('input.txt');
56
- expect(input.value).toBe('Ada');
57
- expect(input.placeholder).toBe('First name');
58
- });
59
-
60
- it('emits onValueChangeField with the field name as key', () => {
61
- const fixture = TestBed.createComponent(FieldInput);
62
- fixture.componentRef.setInput('fieldDescription', {
63
- name: 'first',
64
- type: 'text',
65
- });
66
- fixture.detectChanges();
67
-
68
- const seen: unknown[] = [];
69
- fixture.componentInstance.onValueChangeField.subscribe((e) => seen.push(e));
70
-
71
- const input: HTMLInputElement =
72
- fixture.nativeElement.querySelector('input.txt');
73
- input.value = 'Grace';
74
- input.dispatchEvent(new Event('input'));
75
-
76
- expect(seen).toEqual([{ value: 'Grace', key: 'first' }]);
77
- });
78
-
79
- it('forwards error, disabled and readOnly to the renderer', () => {
80
- const fixture = TestBed.createComponent(FieldInput);
81
- fixture.componentRef.setInput('fieldDescription', {
82
- name: 'email',
83
- type: 'text',
84
- });
85
- fixture.componentRef.setInput('error', ['Required', 'Invalid']);
86
- fixture.componentRef.setInput('disabled', true);
87
- fixture.componentRef.setInput('readOnly', true);
88
- fixture.detectChanges();
89
-
90
- const input: HTMLInputElement =
91
- fixture.nativeElement.querySelector('input.txt');
92
- expect(input.disabled).toBe(true);
93
- expect(input.readOnly).toBe(true);
94
- expect(fixture.nativeElement.querySelector('.err').textContent).toBe(
95
- 'Required, Invalid'
96
- );
97
- });
98
-
99
- it('forwards FieldDescription.props as extraProps', () => {
100
- const fixture = TestBed.createComponent(FieldInput);
101
- fixture.componentRef.setInput('fieldDescription', {
102
- name: 'first',
103
- type: 'text',
104
- props: { hint: 'keep it short' },
105
- });
106
- fixture.detectChanges();
107
-
108
- expect(fixture.nativeElement.querySelector('.hint').textContent).toBe(
109
- 'keep it short'
110
- );
111
- });
112
-
113
- it('documents current behaviour: a renderer default IS overwritten on the real FieldInput path', () => {
114
- // Unlike mounting DynamicInput directly, FieldInput's template binds
115
- // every KNOWN_PROP unconditionally, so DynamicInput sees a firstChange
116
- // SimpleChange for `label` even though fieldDescription never set one.
117
- // The `supplied` gate in DynamicInput.applyProps therefore does NOT
118
- // protect the renderer's own default here - this pins that fact rather
119
- // than asserting a guarantee that doesn't hold on this path.
120
- registry.register('defaults', DefaultsRendererComponent as never);
121
-
122
- const fixture = TestBed.createComponent(FieldInput);
123
- fixture.componentRef.setInput('fieldDescription', {
124
- name: 'first',
125
- type: 'defaults',
126
- // label intentionally omitted.
127
- });
128
- fixture.detectChanges();
129
-
130
- const label: HTMLElement = fixture.nativeElement.querySelector('.label');
131
- expect(label.textContent).toBe('');
132
- });
133
- });
134
-
135
- describe('BaseInputComponent', () => {
136
- class TestInput extends BaseInputComponent {}
137
-
138
- it('marks for check on input changes', () => {
139
- const cdr = { markForCheck: vi.fn() } as unknown as ChangeDetectorRef;
140
- const input = new TestInput(cdr);
141
-
142
- input.ngOnChanges({});
143
-
144
- expect(cdr.markForCheck).toHaveBeenCalledTimes(1);
145
- });
146
- });
@@ -1,256 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
- import type {
3
- FieldDescription,
4
- ValidationResult,
5
- } from '@dynamic-field-kit/core';
6
- import { beforeEach, describe, expect, it } from 'vitest';
7
- import { MultiFieldInput } from '../src/components/MultiFieldInput';
8
- import { FIELD_REGISTRY } from '../src/fieldRegistryToken';
9
- import { makeRegistry, TextRendererComponent } from './helpers/renderers';
10
-
11
- describe('MultiFieldInput', () => {
12
- let registry: ReturnType<typeof makeRegistry>;
13
-
14
- beforeEach(() => {
15
- registry = makeRegistry();
16
- registry.register('text', TextRendererComponent as never);
17
- TestBed.configureTestingModule({
18
- imports: [MultiFieldInput],
19
- providers: [{ provide: FIELD_REGISTRY, useValue: registry }],
20
- });
21
- });
22
-
23
- function mount(
24
- fields: FieldDescription[],
25
- properties: Record<string, unknown>
26
- ) {
27
- const fixture = TestBed.createComponent(MultiFieldInput);
28
- fixture.componentRef.setInput('fieldDescriptions', fields);
29
- fixture.componentRef.setInput('properties', properties);
30
- fixture.detectChanges();
31
- return fixture;
32
- }
33
-
34
- it('renders one input per field', () => {
35
- const fixture = mount(
36
- [
37
- { name: 'first', type: 'text' },
38
- { name: 'last', type: 'text' },
39
- ],
40
- { first: 'Ada', last: 'Lovelace' }
41
- );
42
-
43
- const inputs: HTMLInputElement[] = Array.from(
44
- fixture.nativeElement.querySelectorAll('input.txt')
45
- );
46
- expect(inputs.map((i) => i.value)).toEqual(['Ada', 'Lovelace']);
47
- });
48
-
49
- it('hides fields whose appearCondition is false', () => {
50
- const fields: FieldDescription[] = [
51
- { name: 'kind', type: 'text' },
52
- {
53
- name: 'company',
54
- type: 'text',
55
- appearCondition: (data) => data['kind'] === 'business',
56
- },
57
- ];
58
-
59
- expect(
60
- mount(fields, { kind: 'personal' }).nativeElement.querySelectorAll(
61
- 'input.txt'
62
- ).length
63
- ).toBe(1);
64
- expect(
65
- mount(fields, { kind: 'business' }).nativeElement.querySelectorAll(
66
- 'input.txt'
67
- ).length
68
- ).toBe(2);
69
- });
70
-
71
- it('applies computeValue to the data it emits', () => {
72
- const fields: FieldDescription[] = [
73
- { name: 'price', type: 'text' },
74
- {
75
- name: 'total',
76
- type: 'text',
77
- computeValue: (data) => `${Number(data['price']) * 2}`,
78
- },
79
- ];
80
-
81
- const fixture = mount(fields, { price: '5' });
82
-
83
- const inputs: HTMLInputElement[] = Array.from(
84
- fixture.nativeElement.querySelectorAll('input.txt')
85
- );
86
- expect(inputs[1].value).toBe('10');
87
- });
88
-
89
- it('emits onChange with the updated data when a field changes', () => {
90
- const fixture = mount([{ name: 'first', type: 'text' }], { first: 'Ada' });
91
-
92
- const seen: unknown[] = [];
93
- fixture.componentInstance.onChange.subscribe((d) => seen.push(d));
94
-
95
- const input: HTMLInputElement =
96
- fixture.nativeElement.querySelector('input.txt');
97
- input.value = 'Grace';
98
- input.dispatchEvent(new Event('input'));
99
-
100
- expect(seen).toEqual([{ first: 'Grace' }]);
101
- });
102
-
103
- it('passes validation errors down to the renderer', () => {
104
- const fixture = mount(
105
- [
106
- {
107
- name: 'email',
108
- type: 'text',
109
- validate: (value) =>
110
- String(value).includes('@') ? undefined : 'Invalid email',
111
- },
112
- ],
113
- { email: 'nope' }
114
- );
115
-
116
- expect(fixture.nativeElement.querySelector('.err').textContent).toBe(
117
- 'Invalid email'
118
- );
119
- });
120
-
121
- it('resolves disabledCondition and readOnlyCondition', () => {
122
- const fixture = mount(
123
- [
124
- {
125
- name: 'a',
126
- type: 'text',
127
- disabledCondition: (data) => data['frozen'] === true,
128
- },
129
- {
130
- name: 'b',
131
- type: 'text',
132
- readOnlyCondition: (data) => data['frozen'] === true,
133
- },
134
- ],
135
- { frozen: true }
136
- );
137
-
138
- const inputs: HTMLInputElement[] = Array.from(
139
- fixture.nativeElement.querySelectorAll('input.txt')
140
- );
141
- expect(inputs[0].disabled).toBe(true);
142
- expect(inputs[1].readOnly).toBe(true);
143
- });
144
-
145
- it('does not report errors for disabled fields', () => {
146
- const fixture = mount(
147
- [
148
- {
149
- name: 'email',
150
- type: 'text',
151
- disabled: true,
152
- validate: () => 'Invalid email',
153
- },
154
- ],
155
- { email: 'nope' }
156
- );
157
-
158
- expect(fixture.nativeElement.querySelector('.err')).toBeNull();
159
- });
160
-
161
- it('emits validityChange on init and on every change', () => {
162
- const seen: ValidationResult[] = [];
163
- const fixture = TestBed.createComponent(MultiFieldInput);
164
- fixture.componentInstance.validityChange.subscribe((r) => seen.push(r));
165
- fixture.componentRef.setInput('fieldDescriptions', [
166
- {
167
- name: 'email',
168
- type: 'text',
169
- validate: (value: unknown) =>
170
- String(value).includes('@') ? undefined : 'Invalid email',
171
- },
172
- ]);
173
- fixture.componentRef.setInput('properties', { email: 'nope' });
174
- fixture.detectChanges();
175
-
176
- expect(seen[seen.length - 1]).toEqual({
177
- valid: false,
178
- errors: { email: ['Invalid email'] },
179
- });
180
-
181
- const input: HTMLInputElement =
182
- fixture.nativeElement.querySelector('input.txt');
183
- input.value = 'ada@example.com';
184
- input.dispatchEvent(new Event('input'));
185
-
186
- expect(seen[seen.length - 1]).toEqual({ valid: true, errors: {} });
187
- });
188
-
189
- it('renders a repeatable group item per entry and adds one on Add', () => {
190
- const fields: FieldDescription[] = [
191
- {
192
- name: 'contacts',
193
- type: 'text',
194
- label: 'Contacts',
195
- fields: [{ name: 'email', type: 'text' }],
196
- },
197
- ];
198
-
199
- const fixture = mount(fields, {
200
- contacts: [{ email: 'a@x.com' }, { email: 'b@x.com' }],
201
- });
202
-
203
- expect(fixture.nativeElement.querySelectorAll('input.txt').length).toBe(2);
204
-
205
- const seen: unknown[] = [];
206
- fixture.componentInstance.onChange.subscribe((d) => seen.push(d));
207
-
208
- const addBtn: HTMLButtonElement = Array.from<HTMLButtonElement>(
209
- fixture.nativeElement.querySelectorAll('button')
210
- ).find((b) => b.textContent?.trim() === 'Add')!;
211
- addBtn.click();
212
- fixture.detectChanges();
213
-
214
- expect((seen[0] as Record<string, unknown[]>)['contacts'].length).toBe(3);
215
- });
216
-
217
- it('removes a group item on Remove', () => {
218
- const fields: FieldDescription[] = [
219
- {
220
- name: 'contacts',
221
- type: 'text',
222
- fields: [{ name: 'email', type: 'text' }],
223
- },
224
- ];
225
-
226
- const fixture = mount(fields, {
227
- contacts: [{ email: 'a@x.com' }, { email: 'b@x.com' }],
228
- });
229
-
230
- const seen: unknown[] = [];
231
- fixture.componentInstance.onChange.subscribe((d) => seen.push(d));
232
-
233
- const removeBtn: HTMLButtonElement = Array.from<HTMLButtonElement>(
234
- fixture.nativeElement.querySelectorAll('button')
235
- ).find((b) => b.textContent?.trim() === 'Remove')!;
236
- removeBtn.click();
237
- fixture.detectChanges();
238
-
239
- expect((seen[0] as Record<string, unknown[]>)['contacts']).toEqual([
240
- { email: 'b@x.com' },
241
- ]);
242
- });
243
-
244
- it('applies the grid layout', () => {
245
- const fixture = TestBed.createComponent(MultiFieldInput);
246
- fixture.componentRef.setInput('fieldDescriptions', [
247
- { name: 'a', type: 'text' },
248
- ]);
249
- fixture.componentRef.setInput('properties', { a: '1' });
250
- fixture.componentRef.setInput('layout', { type: 'grid', columns: 3 });
251
- fixture.detectChanges();
252
-
253
- const container: HTMLElement = fixture.nativeElement.firstElementChild;
254
- expect(container.style.gridTemplateColumns).toBe('repeat(3, 1fr)');
255
- });
256
- });