@dynamic-field-kit/angular 1.2.10 → 1.4.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 (53) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +113 -4
  3. package/dist/README.md +113 -4
  4. package/dist/components/BaseInput.d.ts +23 -11
  5. package/dist/components/DynamicInput.d.ts +19 -8
  6. package/dist/components/FieldInput.d.ts +15 -7
  7. package/dist/components/MultiFieldInput.d.ts +30 -4
  8. package/dist/esm2022/components/BaseInput.mjs +17 -18
  9. package/dist/esm2022/components/DynamicInput.mjs +166 -84
  10. package/dist/esm2022/components/FieldInput.mjs +54 -26
  11. package/dist/esm2022/components/MultiFieldInput.mjs +282 -29
  12. package/dist/esm2022/fieldRegistryToken.mjs +12 -0
  13. package/dist/esm2022/layout/defaultLayouts.mjs +70 -23
  14. package/dist/esm2022/layout/index.mjs +2 -1
  15. package/dist/esm2022/layout/layoutRegistry.mjs +14 -0
  16. package/dist/esm2022/public-api.mjs +6 -2
  17. package/dist/esm2022/types/layout.mjs +1 -1
  18. package/dist/fesm2022/dynamic-field-kit-angular.mjs +604 -173
  19. package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +1 -1
  20. package/dist/fieldRegistryToken.d.ts +3 -0
  21. package/dist/layout/defaultLayouts.d.ts +21 -3
  22. package/dist/layout/index.d.ts +1 -0
  23. package/dist/layout/layoutRegistry.d.ts +12 -0
  24. package/dist/public-api.d.ts +5 -2
  25. package/dist/types/layout.d.ts +4 -1
  26. package/package.json +19 -8
  27. package/src/components/BaseInput.ts +36 -10
  28. package/src/components/DynamicInput.ts +196 -104
  29. package/src/components/FieldInput.ts +43 -18
  30. package/src/components/MultiFieldInput.ts +275 -19
  31. package/src/fieldRegistryToken.ts +15 -0
  32. package/src/layout/defaultLayouts.ts +42 -10
  33. package/src/layout/index.ts +1 -0
  34. package/src/layout/layoutRegistry.ts +25 -0
  35. package/src/public-api.ts +40 -24
  36. package/src/types/layout.ts +14 -1
  37. package/test/DynamicInput.spec.ts +230 -0
  38. package/test/FieldInput.spec.ts +146 -0
  39. package/test/MultiFieldInput.spec.ts +256 -0
  40. package/test/helpers/renderers.ts +89 -0
  41. package/test/layout.spec.ts +119 -0
  42. package/test/publicApi.spec.ts +64 -0
  43. package/test/setup.ts +12 -0
  44. package/test/smoke.spec.ts +27 -0
  45. package/tsconfig.json +13 -13
  46. package/tsconfig.spec.json +9 -0
  47. package/vitest.config.ts +30 -0
  48. package/src/adapters/tailwind.ts +0 -13
  49. package/src/components/imports.ts +0 -3
  50. package/src/examples/index.ts +0 -4
  51. package/src/examples/registerExample.ts +0 -7
  52. package/src/examples/text-field.component.ts +0 -18
  53. package/src/types.ts +0 -1
@@ -0,0 +1,89 @@
1
+ import { NgIf } from '@angular/common';
2
+ import { Component, EventEmitter, Input, Output } from '@angular/core';
3
+ import { FieldRegistry } from '@dynamic-field-kit/core';
4
+
5
+ // The field types the specs register. Without this augmentation `keyof
6
+ // FieldTypeMap` is `never`, so `registry.register('text', …)` fails to compile
7
+ // once vitest type-checking is enabled. Every spec imports this helper, so the
8
+ // augmentation applies across the whole test compilation.
9
+ declare module '@dynamic-field-kit/core' {
10
+ interface FieldTypeMap {
11
+ text: string;
12
+ number: number;
13
+ defaults: string;
14
+ }
15
+ }
16
+
17
+ @Component({
18
+ selector: 'dfk-test-text',
19
+ standalone: true,
20
+ imports: [NgIf],
21
+ template: `
22
+ <input
23
+ class="txt"
24
+ [value]="value ?? ''"
25
+ [disabled]="!!disabled"
26
+ [readOnly]="!!readOnly"
27
+ [placeholder]="placeholder ?? ''"
28
+ (input)="valueChange.emit($any($event.target).value)"
29
+ />
30
+ <span class="err" *ngIf="error">{{ errorText }}</span>
31
+ <span class="hint" *ngIf="hint">{{ hint }}</span>
32
+ `,
33
+ })
34
+ export class TextRendererComponent {
35
+ @Input() value?: unknown;
36
+ @Input() label?: string;
37
+ @Input() placeholder?: string;
38
+ @Input() required?: boolean;
39
+ @Input() disabled?: boolean;
40
+ @Input() readOnly?: boolean;
41
+ @Input() error?: string | string[];
42
+ @Input() options?: unknown[];
43
+ @Input() className?: string;
44
+ @Input() description?: string;
45
+ // Not a FieldRendererProps key: proves extraProps reach the instance.
46
+ @Input() hint?: string;
47
+
48
+ @Output() valueChange = new EventEmitter<unknown>();
49
+
50
+ get errorText(): string {
51
+ return ([] as string[]).concat(this.error ?? []).join(', ');
52
+ }
53
+ }
54
+
55
+ @Component({
56
+ selector: 'dfk-test-legacy',
57
+ standalone: true,
58
+ template: `<button class="legacy-btn" (click)="onValueChange.emit('legacy')">
59
+ go
60
+ </button>`,
61
+ })
62
+ export class LegacyOutputRendererComponent {
63
+ @Input() value?: unknown;
64
+ // Deliberately the legacy output name, to cover DynamicInput.bindOutputs.
65
+ @Output() onValueChange = new EventEmitter<unknown>();
66
+ }
67
+
68
+ @Component({
69
+ selector: 'dfk-test-defaults',
70
+ standalone: true,
71
+ template: `<span class="label">{{ label }}</span>`,
72
+ })
73
+ export class DefaultsRendererComponent {
74
+ // Initialized input: proves DynamicInput does not overwrite a renderer's
75
+ // own default with undefined for a prop it was never given.
76
+ @Input() label = 'None';
77
+ @Input() value?: unknown;
78
+ @Output() valueChange = new EventEmitter<unknown>();
79
+ }
80
+
81
+ export function fallbackRenderer(props: Record<string, unknown>): string {
82
+ return `<span class="fallback">${String(props['label'] ?? '')}:${String(
83
+ props['value'] ?? ''
84
+ )}</span>`;
85
+ }
86
+
87
+ export function makeRegistry(): FieldRegistry {
88
+ return new FieldRegistry();
89
+ }
@@ -0,0 +1,119 @@
1
+ import { Component, TemplateRef, ViewChild } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import {
5
+ ColumnLayout,
6
+ GridLayout,
7
+ RowLayout,
8
+ } from '../src/layout/defaultLayouts';
9
+ import { LayoutRegistry, layoutRegistry } from '../src/layout/layoutRegistry';
10
+
11
+ @Component({
12
+ standalone: true,
13
+ imports: [ColumnLayout, RowLayout, GridLayout],
14
+ template: `
15
+ <ng-template #tpl><span class="child">x</span></ng-template>
16
+ <dfk-column-layout
17
+ [template]="tpl"
18
+ [config]="{ gap: 20 }"
19
+ ></dfk-column-layout>
20
+ <dfk-row-layout [template]="tpl"></dfk-row-layout>
21
+ <dfk-grid-layout
22
+ [template]="tpl"
23
+ [config]="{ columns: 3 }"
24
+ ></dfk-grid-layout>
25
+ `,
26
+ })
27
+ class LayoutHost {
28
+ @ViewChild('tpl', { static: true }) tpl!: TemplateRef<unknown>;
29
+ }
30
+
31
+ @Component({
32
+ standalone: true,
33
+ imports: [GridLayout],
34
+ template: `
35
+ <ng-template #tpl><span class="child">x</span></ng-template>
36
+ <dfk-grid-layout [template]="tpl"></dfk-grid-layout>
37
+ `,
38
+ })
39
+ class GridDefaultsHost {
40
+ @ViewChild('tpl', { static: true }) tpl!: TemplateRef<unknown>;
41
+ }
42
+
43
+ describe('LayoutRegistry', () => {
44
+ afterEach(() => {
45
+ vi.restoreAllMocks();
46
+ });
47
+
48
+ it('registers and retrieves a layout', () => {
49
+ const registry = new LayoutRegistry();
50
+ registry.register('custom', ColumnLayout);
51
+
52
+ expect(registry.get('custom')).toBe(ColumnLayout);
53
+ });
54
+
55
+ it('returns undefined for an unknown layout', () => {
56
+ expect(new LayoutRegistry().get('nope')).toBeUndefined();
57
+ });
58
+
59
+ it('warns when a layout type is registered twice', () => {
60
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
61
+ const registry = new LayoutRegistry();
62
+ registry.register('custom', ColumnLayout);
63
+ registry.register('custom', RowLayout);
64
+
65
+ expect(warn).toHaveBeenCalledTimes(1);
66
+ expect(registry.get('custom')).toBe(RowLayout);
67
+ });
68
+
69
+ it('exports a shared registry instance', () => {
70
+ expect(layoutRegistry).toBeInstanceOf(LayoutRegistry);
71
+ });
72
+ });
73
+
74
+ describe('default layouts', () => {
75
+ it('renders the projected template with the configured gap', () => {
76
+ const fixture = TestBed.createComponent(LayoutHost);
77
+ fixture.detectChanges();
78
+
79
+ const column: HTMLElement = fixture.nativeElement.querySelector(
80
+ 'dfk-column-layout > div'
81
+ );
82
+ expect(column.style.flexDirection).toBe('column');
83
+ expect(column.style.gap).toBe('20px');
84
+ expect(column.querySelector('.child')).not.toBeNull();
85
+ });
86
+
87
+ it('defaults the gap to 12px', () => {
88
+ const fixture = TestBed.createComponent(LayoutHost);
89
+ fixture.detectChanges();
90
+
91
+ const row: HTMLElement = fixture.nativeElement.querySelector(
92
+ 'dfk-row-layout > div'
93
+ );
94
+ expect(row.style.flexDirection).toBe('row');
95
+ expect(row.style.gap).toBe('12px');
96
+ });
97
+
98
+ it('renders the grid layout with the configured column count', () => {
99
+ const fixture = TestBed.createComponent(LayoutHost);
100
+ fixture.detectChanges();
101
+
102
+ const grid: HTMLElement = fixture.nativeElement.querySelector(
103
+ 'dfk-grid-layout > div'
104
+ );
105
+ expect(grid.style.display).toBe('grid');
106
+ expect(grid.style.gridTemplateColumns).toBe('repeat(3, 1fr)');
107
+ expect(grid.style.gap).toBe('12px');
108
+ });
109
+
110
+ it('defaults the grid columns to 2', () => {
111
+ const fixture = TestBed.createComponent(GridDefaultsHost);
112
+ fixture.detectChanges();
113
+
114
+ const grid: HTMLElement = fixture.nativeElement.querySelector(
115
+ 'dfk-grid-layout > div'
116
+ );
117
+ expect(grid.style.gridTemplateColumns).toBe('repeat(2, 1fr)');
118
+ });
119
+ });
@@ -0,0 +1,64 @@
1
+ import { Component } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { FieldRegistry, fieldRegistry } from '@dynamic-field-kit/core';
4
+ import { describe, expect, it } from 'vitest';
5
+ import * as publicApi from '../src/public-api';
6
+ import { FIELD_REGISTRY } from '../src/fieldRegistryToken';
7
+ import { DynamicFieldKitModule } from '../src/lib/dynamic-field-kit.module';
8
+ import { makeRegistry, TextRendererComponent } from './helpers/renderers';
9
+
10
+ describe('public API', () => {
11
+ it('exports the components, registry and validation helpers', () => {
12
+ expect(publicApi.DynamicInput).toBeDefined();
13
+ expect(publicApi.FieldInput).toBeDefined();
14
+ expect(publicApi.MultiFieldInput).toBeDefined();
15
+ expect(publicApi.FIELD_REGISTRY).toBe(FIELD_REGISTRY);
16
+ expect(publicApi.FieldRegistry).toBe(FieldRegistry);
17
+ expect(typeof publicApi.validateField).toBe('function');
18
+ expect(typeof publicApi.validateFields).toBe('function');
19
+ expect(typeof publicApi.resolveDisabled).toBe('function');
20
+ expect(typeof publicApi.resolveReadOnly).toBe('function');
21
+ });
22
+ });
23
+
24
+ describe('FIELD_REGISTRY token', () => {
25
+ it('defaults to the process-wide singleton', () => {
26
+ TestBed.configureTestingModule({});
27
+
28
+ expect(TestBed.inject(FIELD_REGISTRY)).toBe(fieldRegistry);
29
+ });
30
+
31
+ it('can be overridden with a scoped registry', () => {
32
+ const scoped = makeRegistry();
33
+ TestBed.configureTestingModule({
34
+ providers: [{ provide: FIELD_REGISTRY, useValue: scoped }],
35
+ });
36
+
37
+ expect(TestBed.inject(FIELD_REGISTRY)).toBe(scoped);
38
+ expect(TestBed.inject(FIELD_REGISTRY)).not.toBe(fieldRegistry);
39
+ });
40
+ });
41
+
42
+ describe('DynamicFieldKitModule', () => {
43
+ @Component({
44
+ standalone: true,
45
+ imports: [DynamicFieldKitModule],
46
+ template: `<dfk-field-input
47
+ [fieldDescription]="{ name: 'a', type: 'text' }"
48
+ ></dfk-field-input>`,
49
+ })
50
+ class ModuleHost {}
51
+
52
+ it('exports the components for template use', () => {
53
+ const scoped = makeRegistry();
54
+ scoped.register('text', TextRendererComponent as never);
55
+ TestBed.configureTestingModule({
56
+ providers: [{ provide: FIELD_REGISTRY, useValue: scoped }],
57
+ });
58
+
59
+ const fixture = TestBed.createComponent(ModuleHost);
60
+ fixture.detectChanges();
61
+
62
+ expect(fixture.nativeElement.querySelector('input.txt')).not.toBeNull();
63
+ });
64
+ });
package/test/setup.ts ADDED
@@ -0,0 +1,12 @@
1
+ import '@analogjs/vitest-angular/setup-zone';
2
+
3
+ import { getTestBed } from '@angular/core/testing';
4
+ import {
5
+ BrowserDynamicTestingModule,
6
+ platformBrowserDynamicTesting,
7
+ } from '@angular/platform-browser-dynamic/testing';
8
+
9
+ getTestBed().initTestEnvironment(
10
+ BrowserDynamicTestingModule,
11
+ platformBrowserDynamicTesting()
12
+ );
@@ -0,0 +1,27 @@
1
+ import { Component } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { fieldRegistry } from '@dynamic-field-kit/core';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ @Component({
7
+ selector: 'dfk-smoke',
8
+ standalone: true,
9
+ template: `<span class="smoke">{{ label }}</span>`,
10
+ })
11
+ class SmokeComponent {
12
+ label = 'mounted';
13
+ }
14
+
15
+ describe('vitest + Angular infrastructure', () => {
16
+ it('imports @dynamic-field-kit/core', () => {
17
+ expect(typeof fieldRegistry.register).toBe('function');
18
+ });
19
+
20
+ it('mounts a component through TestBed', () => {
21
+ const fixture = TestBed.createComponent(SmokeComponent);
22
+ fixture.detectChanges();
23
+
24
+ const el: HTMLElement = fixture.nativeElement.querySelector('.smoke');
25
+ expect(el.textContent).toBe('mounted');
26
+ });
27
+ });
package/tsconfig.json CHANGED
@@ -1,13 +1,13 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "composite": false,
5
- "declaration": true,
6
- "outDir": "dist",
7
- "target": "ES2019",
8
- "module": "ES2020",
9
- "experimentalDecorators": true,
10
- "emitDecoratorMetadata": true
11
- },
12
- "include": ["src/**/*"]
13
- }
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": false,
5
+ "declaration": true,
6
+ "outDir": "dist",
7
+ "target": "ES2019",
8
+ "module": "ES2020",
9
+ "experimentalDecorators": true,
10
+ "emitDecoratorMetadata": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/spec",
5
+ "target": "ES2022",
6
+ "useDefineForClassFields": true
7
+ },
8
+ "include": ["src/**/*.ts", "test/**/*.ts"]
9
+ }
@@ -0,0 +1,30 @@
1
+ /// <reference types="vitest" />
2
+ import angular from '@analogjs/vite-plugin-angular';
3
+ import { defineConfig } from 'vite';
4
+
5
+ export default defineConfig({
6
+ // disableTypeChecking:false turns Angular semantic diagnostics (NG2007 etc.)
7
+ // back on for specs — otherwise only the ng-packagr build typechecks src, and
8
+ // spec files get no type checking at all.
9
+ plugins: [angular({ disableTypeChecking: false })],
10
+ test: {
11
+ globals: true,
12
+ environment: 'jsdom',
13
+ setupFiles: ['test/setup.ts'],
14
+ include: ['test/**/*.spec.ts'],
15
+ pool: 'forks',
16
+ coverage: {
17
+ provider: 'istanbul',
18
+ reportsDirectory: 'coverage',
19
+ reporter: ['lcov', 'text-summary'],
20
+ include: ['src/**/*.ts'],
21
+ // Coverage floor — fails the run when coverage drops below these numbers.
22
+ thresholds: {
23
+ statements: 85,
24
+ branches: 75,
25
+ functions: 85,
26
+ lines: 85,
27
+ },
28
+ },
29
+ },
30
+ });
@@ -1,13 +0,0 @@
1
- // Minimal Tailwind adapter placeholder for Angular package.
2
- // Mirrors the React adapter shape: expose helper class names.
3
-
4
- export const tailwind = {
5
- input: 'px-2 py-1 border rounded',
6
- label: 'text-sm font-medium mb-1',
7
- };
8
-
9
- export default tailwind;
10
- // Minimal Tailwind adapter placeholder for Angular package
11
- export function cx(...parts: Array<string | false | null | undefined>) {
12
- return parts.filter(Boolean).join(' ');
13
- }
@@ -1,3 +0,0 @@
1
- import { CommonModule } from '@angular/common';
2
-
3
- export const BaseImports = [CommonModule];
@@ -1,4 +0,0 @@
1
- export { TextFieldComponent } from './text-field.component';
2
- import './registerExample';
3
-
4
- export {};
@@ -1,7 +0,0 @@
1
- import { fieldRegistry } from '@dynamic-field-kit/core';
2
- import { TextFieldComponent } from './text-field.component';
3
-
4
- // Example: register Angular component class into shared registry
5
- fieldRegistry.register('text', TextFieldComponent as any);
6
-
7
- export {};
@@ -1,18 +0,0 @@
1
- import { Component, Input, Output, EventEmitter } from '@angular/core';
2
-
3
- @Component({
4
- selector: 'dfk-text-field',
5
- standalone: true,
6
- template: ` <input [value]="value ?? ''" (input)="onInput($event)" /> `,
7
- })
8
- export class TextFieldComponent {
9
- @Input() value?: any;
10
- @Output() valueChange = new EventEmitter<any>();
11
- @Output() onValueChange = new EventEmitter<any>();
12
-
13
- onInput(e: any) {
14
- const value = e.target.value;
15
- this.valueChange.emit(value);
16
- this.onValueChange.emit(value);
17
- }
18
- }
package/src/types.ts DELETED
@@ -1 +0,0 @@
1
- export * from '@dynamic-field-kit/core';