ngx-dynamic-entity 1.1.0 → 1.2.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/esm2022/lib/field-types/array-field.component.mjs +13 -5
  2. package/esm2022/lib/field-types/boolean-field.component.mjs +4 -3
  3. package/esm2022/lib/field-types/checkbox-field.component.mjs +4 -3
  4. package/esm2022/lib/field-types/currency-field.component.mjs +4 -3
  5. package/esm2022/lib/field-types/date-field.component.mjs +4 -3
  6. package/esm2022/lib/field-types/dropdown-field.component.mjs +9 -7
  7. package/esm2022/lib/field-types/email-field.component.mjs +4 -3
  8. package/esm2022/lib/field-types/entity-ref-field.component.mjs +21 -7
  9. package/esm2022/lib/field-types/field-component.contract.mjs +2 -0
  10. package/esm2022/lib/field-types/file-field.component.mjs +4 -3
  11. package/esm2022/lib/field-types/group-field.component.mjs +5 -5
  12. package/esm2022/lib/field-types/image-field.component.mjs +4 -3
  13. package/esm2022/lib/field-types/month-year-field.component.mjs +4 -3
  14. package/esm2022/lib/field-types/multi-select-field.component.mjs +4 -3
  15. package/esm2022/lib/field-types/number-field.component.mjs +7 -11
  16. package/esm2022/lib/field-types/password-field.component.mjs +4 -3
  17. package/esm2022/lib/field-types/radio-field.component.mjs +4 -3
  18. package/esm2022/lib/field-types/text-field.component.mjs +25 -18
  19. package/esm2022/lib/field-types/textarea-field.component.mjs +4 -3
  20. package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +53 -6
  21. package/esm2022/lib/form/dynamic-form.component.mjs +131 -15
  22. package/esm2022/lib/form/dynamic-record-form.component.mjs +2 -2
  23. package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +7 -2
  24. package/esm2022/lib/services/cascade-data.service.mjs +2 -2
  25. package/esm2022/lib/services/entity-ref-registry.service.mjs +2 -2
  26. package/esm2022/lib/services/lookup-registry.service.mjs +2 -2
  27. package/esm2022/lib/services/validation-messages.service.mjs +56 -0
  28. package/esm2022/lib/services/validator-registry.service.mjs +24 -2
  29. package/esm2022/lib/tokens/injection-tokens.mjs +19 -6
  30. package/esm2022/public-api.mjs +6 -1
  31. package/fesm2022/ngx-dynamic-entity.mjs +373 -81
  32. package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
  33. package/lib/field-types/array-field.component.d.ts +7 -1
  34. package/lib/field-types/dropdown-field.component.d.ts +1 -0
  35. package/lib/field-types/entity-ref-field.component.d.ts +11 -1
  36. package/lib/field-types/field-component.contract.d.ts +49 -0
  37. package/lib/field-types/group-field.component.d.ts +1 -1
  38. package/lib/field-types/number-field.component.d.ts +1 -0
  39. package/lib/field-types/text-field.component.d.ts +10 -2
  40. package/lib/form/dynamic-field/dynamic-field.component.d.ts +39 -4
  41. package/lib/form/dynamic-form.component.d.ts +62 -2
  42. package/lib/providers/provide-ngx-dynamic-entity.d.ts +17 -1
  43. package/lib/services/cascade-data.service.d.ts +1 -1
  44. package/lib/services/entity-ref-registry.service.d.ts +1 -1
  45. package/lib/services/lookup-registry.service.d.ts +1 -1
  46. package/lib/services/validation-messages.service.d.ts +43 -0
  47. package/lib/services/validator-registry.service.d.ts +13 -1
  48. package/lib/tokens/injection-tokens.d.ts +18 -5
  49. package/package.json +2 -2
  50. package/public-api.d.ts +2 -0
  51. package/styles.css +579 -0
package/styles.css ADDED
@@ -0,0 +1,579 @@
1
+ /**
2
+ * ngx-dynamic-entity — optional base stylesheet.
3
+ *
4
+ * The package ships no styles by default: the components emit stable BEM-style hooks and
5
+ * nothing else, so they drop into any design system without fighting it. That is the right
6
+ * default for a library, but it meant a consumer's first render was unstyled browser HTML
7
+ * with no starting point.
8
+ *
9
+ * This file is that starting point. Import it and the forms are legible; override the custom
10
+ * properties below and they match your product. It is deliberately plain — no shadows, no
11
+ * brand colour, no opinion about type — because it exists to be built on, not shipped as-is.
12
+ *
13
+ * // angular.json → styles, or a global stylesheet
14
+ * @import 'ngx-dynamic-entity/styles.css';
15
+ *
16
+ * Everything is driven by the custom properties in the :root block, which are scoped under
17
+ * .ngx-form / .ngx-record-editor so importing this cannot affect the rest of your app.
18
+ * Colours use `color-mix` sparingly and always with a plain fallback declared first.
19
+ */
20
+
21
+ .ngx-form,
22
+ .ngx-record-editor {
23
+ /* Palette */
24
+ --ngx-color-text: #16202a;
25
+ --ngx-color-muted: #65788a;
26
+ --ngx-color-border: #cfd8e0;
27
+ --ngx-color-surface: #ffffff;
28
+ --ngx-color-surface-alt: #f5f8fa;
29
+ --ngx-color-accent: #0b5d8a;
30
+ --ngx-color-error: #9e2a2b;
31
+ --ngx-color-warning: #8a5a12;
32
+ --ngx-color-success: #1c6b52;
33
+ --ngx-color-disabled-bg: #eef1f4;
34
+
35
+ /* Metrics */
36
+ --ngx-radius: 4px;
37
+ --ngx-gap: 16px;
38
+ --ngx-field-gap: 6px;
39
+ --ngx-control-height: 38px;
40
+ --ngx-control-padding: 8px 10px;
41
+
42
+ /* Type */
43
+ --ngx-font: inherit;
44
+ --ngx-font-size: 14px;
45
+ --ngx-label-size: 13px;
46
+ --ngx-hint-size: 12px;
47
+
48
+ color: var(--ngx-color-text);
49
+ font-family: var(--ngx-font);
50
+ font-size: var(--ngx-font-size);
51
+ }
52
+
53
+ /* ─── Layout ──────────────────────────────────────────────────────────────── */
54
+
55
+ .ngx-form__panel {
56
+ display: grid;
57
+ grid-template-columns: repeat(12, minmax(0, 1fr));
58
+ gap: var(--ngx-gap);
59
+ align-items: start;
60
+ }
61
+
62
+ /* colSpan is applied inline by the renderer; this is the fallback for fields without one. */
63
+ .ngx-form__field-slot {
64
+ grid-column: span 12;
65
+ min-width: 0; /* lets long values shrink instead of forcing the grid wider */
66
+ position: relative;
67
+ }
68
+
69
+ .ngx-form__tabpanel:focus-visible {
70
+ outline: 2px solid var(--ngx-color-accent);
71
+ outline-offset: 2px;
72
+ }
73
+
74
+ .ngx-form__module-panel {
75
+ padding: var(--ngx-gap) 0;
76
+ }
77
+
78
+ /* ─── Tabs ────────────────────────────────────────────────────────────────── */
79
+
80
+ .ngx-form__tabs {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ gap: 4px;
84
+ border-bottom: 1px solid var(--ngx-color-border);
85
+ margin-bottom: var(--ngx-gap);
86
+ }
87
+
88
+ .ngx-form__tab {
89
+ appearance: none;
90
+ background: none;
91
+ border: 0;
92
+ border-bottom: 2px solid transparent;
93
+ padding: 8px 12px;
94
+ font: inherit;
95
+ color: var(--ngx-color-muted);
96
+ cursor: pointer;
97
+ }
98
+
99
+ .ngx-form__tab:hover {
100
+ color: var(--ngx-color-text);
101
+ }
102
+
103
+ .ngx-form__tab--active {
104
+ color: var(--ngx-color-accent);
105
+ border-bottom-color: var(--ngx-color-accent);
106
+ }
107
+
108
+ .ngx-form__tabs--sub {
109
+ border-bottom: 0;
110
+ }
111
+
112
+ .ngx-form__tab--sub {
113
+ font-size: var(--ngx-hint-size);
114
+ padding: 4px 10px;
115
+ }
116
+
117
+ /* ─── Fields ──────────────────────────────────────────────────────────────── */
118
+
119
+ .ngx-field {
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: var(--ngx-field-gap);
123
+ }
124
+
125
+ .ngx-field__label {
126
+ font-size: var(--ngx-label-size);
127
+ color: var(--ngx-color-muted);
128
+ }
129
+
130
+ .ngx-field__req {
131
+ color: var(--ngx-color-error);
132
+ margin-left: 2px;
133
+ }
134
+
135
+ .ngx-field__input,
136
+ .ngx-field__file-input {
137
+ width: 100%;
138
+ min-height: var(--ngx-control-height);
139
+ padding: var(--ngx-control-padding);
140
+ font: inherit;
141
+ color: inherit;
142
+ background: var(--ngx-color-surface);
143
+ border: 1px solid var(--ngx-color-border);
144
+ border-radius: var(--ngx-radius);
145
+ box-sizing: border-box;
146
+ }
147
+
148
+ textarea.ngx-field__input {
149
+ min-height: calc(var(--ngx-control-height) * 2);
150
+ resize: vertical;
151
+ }
152
+
153
+ .ngx-field__input:focus-visible {
154
+ outline: 2px solid var(--ngx-color-accent);
155
+ outline-offset: 1px;
156
+ border-color: var(--ngx-color-accent);
157
+ }
158
+
159
+ .ngx-field__input:disabled,
160
+ .ngx-field__input[disabled] {
161
+ background: var(--ngx-color-disabled-bg);
162
+ color: var(--ngx-color-muted);
163
+ cursor: not-allowed;
164
+ }
165
+
166
+ .ngx-field--invalid .ngx-field__input {
167
+ border-color: var(--ngx-color-error);
168
+ }
169
+
170
+ .ngx-field__error {
171
+ font-size: var(--ngx-hint-size);
172
+ color: var(--ngx-color-error);
173
+ }
174
+
175
+ /* A referenced field whose source definition has changed since it was linked. */
176
+ .ngx-field__drift {
177
+ font-size: var(--ngx-hint-size);
178
+ color: var(--ngx-color-warning);
179
+ }
180
+
181
+ .ngx-field__hint {
182
+ font-size: var(--ngx-hint-size);
183
+ color: var(--ngx-color-muted);
184
+ }
185
+
186
+ /* Read-only and masked renderings are text, not controls. */
187
+ .ngx-field__value {
188
+ min-height: var(--ngx-control-height);
189
+ display: flex;
190
+ align-items: center;
191
+ padding: var(--ngx-control-padding);
192
+ background: var(--ngx-color-surface-alt);
193
+ border: 1px solid transparent;
194
+ border-radius: var(--ngx-radius);
195
+ word-break: break-word;
196
+ }
197
+
198
+ .ngx-field__value--masked {
199
+ color: var(--ngx-color-muted);
200
+ letter-spacing: 0.12em;
201
+ }
202
+
203
+ /* ─── Composite controls ──────────────────────────────────────────────────── */
204
+
205
+ .ngx-field__currency-wrap,
206
+ .ngx-field__password-wrap,
207
+ .ngx-field__month-year-wrap,
208
+ .ngx-field__file-wrap {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 6px;
212
+ }
213
+
214
+ .ngx-field__currency-symbol {
215
+ color: var(--ngx-color-muted);
216
+ }
217
+
218
+ .ngx-field__eye-btn,
219
+ .ngx-field__upload-btn,
220
+ .ngx-field__remove-btn,
221
+ .ngx-field__array-add-btn,
222
+ .ngx-field__array-remove-btn,
223
+ .ngx-form__lock {
224
+ appearance: none;
225
+ font: inherit;
226
+ cursor: pointer;
227
+ background: var(--ngx-color-surface);
228
+ color: inherit;
229
+ border: 1px solid var(--ngx-color-border);
230
+ border-radius: var(--ngx-radius);
231
+ padding: 6px 10px;
232
+ }
233
+
234
+ .ngx-field__eye-btn:focus-visible,
235
+ .ngx-field__upload-btn:focus-visible,
236
+ .ngx-field__remove-btn:focus-visible,
237
+ .ngx-field__array-add-btn:focus-visible,
238
+ .ngx-field__array-remove-btn:focus-visible,
239
+ .ngx-form__lock:focus-visible,
240
+ .ngx-form__tab:focus-visible,
241
+ .ngx-form__submit:focus-visible,
242
+ .ngx-form__reset:focus-visible {
243
+ outline: 2px solid var(--ngx-color-accent);
244
+ outline-offset: 2px;
245
+ }
246
+
247
+ .ngx-form__lock {
248
+ position: absolute;
249
+ top: 0;
250
+ right: 0;
251
+ padding: 2px 6px;
252
+ line-height: 1;
253
+ }
254
+
255
+ /* Radio */
256
+ .ngx-field__radio-group {
257
+ display: flex;
258
+ flex-wrap: wrap;
259
+ gap: 12px;
260
+ }
261
+
262
+ .ngx-field__radio-option {
263
+ display: inline-flex;
264
+ align-items: center;
265
+ gap: 6px;
266
+ }
267
+
268
+ .ngx-field__radio-label {
269
+ cursor: pointer;
270
+ }
271
+
272
+ /* Boolean switch */
273
+ .ngx-field__toggle-wrap,
274
+ .ngx-field__toggle-label {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ gap: 8px;
278
+ cursor: pointer;
279
+ }
280
+
281
+ .ngx-field__toggle-track {
282
+ position: relative;
283
+ width: 38px;
284
+ height: 20px;
285
+ border-radius: 999px;
286
+ background: var(--ngx-color-border);
287
+ transition: background 0.15s ease;
288
+ flex: none;
289
+ }
290
+
291
+ .ngx-field__toggle-thumb {
292
+ position: absolute;
293
+ top: 2px;
294
+ left: 2px;
295
+ width: 16px;
296
+ height: 16px;
297
+ border-radius: 50%;
298
+ background: var(--ngx-color-surface);
299
+ transition: transform 0.15s ease;
300
+ }
301
+
302
+ .ngx-field__toggle-input:checked + .ngx-field__toggle-track {
303
+ background: var(--ngx-color-accent);
304
+ }
305
+
306
+ .ngx-field__toggle-input:checked + .ngx-field__toggle-track .ngx-field__toggle-thumb {
307
+ transform: translateX(18px);
308
+ }
309
+
310
+ .ngx-field__toggle-input:focus-visible + .ngx-field__toggle-track {
311
+ outline: 2px solid var(--ngx-color-accent);
312
+ outline-offset: 2px;
313
+ }
314
+
315
+ @media (prefers-reduced-motion: reduce) {
316
+ .ngx-field__toggle-track,
317
+ .ngx-field__toggle-thumb {
318
+ transition: none;
319
+ }
320
+ }
321
+
322
+ /* Group and array containers */
323
+ .ngx-field__fieldset {
324
+ border: 1px solid var(--ngx-color-border);
325
+ border-radius: var(--ngx-radius);
326
+ padding: 12px;
327
+ margin: 0;
328
+ min-width: 0;
329
+ }
330
+
331
+ .ngx-field__legend {
332
+ padding: 0 6px;
333
+ font-size: var(--ngx-label-size);
334
+ color: var(--ngx-color-muted);
335
+ }
336
+
337
+ .ngx-field__group-children,
338
+ .ngx-field__array-item-fields {
339
+ display: grid;
340
+ grid-template-columns: repeat(12, minmax(0, 1fr));
341
+ gap: var(--ngx-gap);
342
+ }
343
+
344
+ .ngx-field__array-list {
345
+ display: flex;
346
+ flex-direction: column;
347
+ gap: 10px;
348
+ }
349
+
350
+ .ngx-field__array-item {
351
+ border: 1px solid var(--ngx-color-border);
352
+ border-radius: var(--ngx-radius);
353
+ padding: 10px;
354
+ }
355
+
356
+ .ngx-field__array-header,
357
+ .ngx-field__array-item-header {
358
+ display: flex;
359
+ align-items: center;
360
+ justify-content: space-between;
361
+ gap: 8px;
362
+ margin-bottom: 8px;
363
+ }
364
+
365
+ .ngx-field__array-item-title {
366
+ font-size: var(--ngx-label-size);
367
+ color: var(--ngx-color-muted);
368
+ }
369
+
370
+ .ngx-field__array-empty,
371
+ .ngx-field__image-placeholder,
372
+ .ngx-record-editor__row-empty {
373
+ color: var(--ngx-color-muted);
374
+ font-size: var(--ngx-hint-size);
375
+ }
376
+
377
+ /* Image and file */
378
+ .ngx-field__image-wrap,
379
+ .ngx-field__image-actions {
380
+ display: flex;
381
+ align-items: center;
382
+ gap: 10px;
383
+ flex-wrap: wrap;
384
+ }
385
+
386
+ .ngx-field__image-preview {
387
+ max-width: 140px;
388
+ max-height: 140px;
389
+ border-radius: var(--ngx-radius);
390
+ border: 1px solid var(--ngx-color-border);
391
+ }
392
+
393
+ .ngx-field__file-link,
394
+ .ngx-field__email-link {
395
+ color: var(--ngx-color-accent);
396
+ }
397
+
398
+ .ngx-field__file-name,
399
+ .ngx-field__file-selected {
400
+ font-size: var(--ngx-hint-size);
401
+ color: var(--ngx-color-muted);
402
+ word-break: break-all;
403
+ }
404
+
405
+ /* ─── Banners and actions ─────────────────────────────────────────────────── */
406
+
407
+ .ngx-form__error-banner,
408
+ .ngx-form__warning-banner,
409
+ .ngx-form__info-banner,
410
+ .ngx-form__denied,
411
+ .ngx-record-editor__banner {
412
+ padding: 10px 12px;
413
+ border-radius: var(--ngx-radius);
414
+ border: 1px solid var(--ngx-color-border);
415
+ background: var(--ngx-color-surface-alt);
416
+ margin-bottom: 10px;
417
+ }
418
+
419
+ .ngx-form__error-banner,
420
+ .ngx-record-editor__banner--error {
421
+ color: var(--ngx-color-error);
422
+ border-color: currentColor;
423
+ }
424
+
425
+ .ngx-form__warning-banner,
426
+ .ngx-record-editor__banner--warning {
427
+ color: var(--ngx-color-warning);
428
+ border-color: currentColor;
429
+ }
430
+
431
+ .ngx-form__info-banner,
432
+ .ngx-record-editor__banner--info {
433
+ color: var(--ngx-color-accent);
434
+ border-color: currentColor;
435
+ }
436
+
437
+ .ngx-form__actions {
438
+ display: flex;
439
+ gap: 10px;
440
+ margin-top: var(--ngx-gap);
441
+ }
442
+
443
+ .ngx-form__submit,
444
+ .ngx-form__reset {
445
+ appearance: none;
446
+ font: inherit;
447
+ cursor: pointer;
448
+ min-height: var(--ngx-control-height);
449
+ padding: 0 18px;
450
+ border-radius: var(--ngx-radius);
451
+ border: 1px solid var(--ngx-color-border);
452
+ background: var(--ngx-color-surface);
453
+ color: inherit;
454
+ }
455
+
456
+ .ngx-form__submit {
457
+ background: var(--ngx-color-accent);
458
+ border-color: var(--ngx-color-accent);
459
+ color: #fff;
460
+ }
461
+
462
+ .ngx-form__submit:disabled,
463
+ .ngx-form__reset:disabled {
464
+ opacity: 0.55;
465
+ cursor: not-allowed;
466
+ }
467
+
468
+ /* ─── Record editor ───────────────────────────────────────────────────────── */
469
+
470
+ .ngx-record-editor__header {
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 12px;
474
+ margin-bottom: var(--ngx-gap);
475
+ }
476
+
477
+ .ngx-record-editor__avatar {
478
+ width: 40px;
479
+ height: 40px;
480
+ border-radius: 50%;
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: center;
484
+ background: var(--ngx-color-surface-alt);
485
+ border: 1px solid var(--ngx-color-border);
486
+ flex: none;
487
+ }
488
+
489
+ .ngx-record-editor__title {
490
+ margin: 0;
491
+ font-size: 18px;
492
+ }
493
+
494
+ .ngx-record-editor__subtitle,
495
+ .ngx-record-editor__section-hint,
496
+ .ngx-record-editor__summary-label {
497
+ color: var(--ngx-color-muted);
498
+ font-size: var(--ngx-hint-size);
499
+ }
500
+
501
+ .ngx-record-editor__summary-panel,
502
+ .ngx-record-editor__drawer {
503
+ border: 1px solid var(--ngx-color-border);
504
+ border-radius: var(--ngx-radius);
505
+ padding: 12px;
506
+ background: var(--ngx-color-surface-alt);
507
+ margin-bottom: var(--ngx-gap);
508
+ }
509
+
510
+ .ngx-record-editor__summary-item {
511
+ display: flex;
512
+ flex-direction: column;
513
+ gap: 2px;
514
+ padding: 4px 0;
515
+ }
516
+
517
+ .ngx-record-editor__section-bar {
518
+ display: flex;
519
+ align-items: center;
520
+ gap: 10px;
521
+ flex-wrap: wrap;
522
+ margin-bottom: 10px;
523
+ }
524
+
525
+ .ngx-record-editor__rows {
526
+ display: flex;
527
+ flex-direction: column;
528
+ gap: 6px;
529
+ }
530
+
531
+ .ngx-record-editor__row {
532
+ display: flex;
533
+ align-items: center;
534
+ justify-content: space-between;
535
+ gap: 10px;
536
+ padding: 8px 10px;
537
+ border: 1px solid var(--ngx-color-border);
538
+ border-radius: var(--ngx-radius);
539
+ }
540
+
541
+ .ngx-record-editor__row-text {
542
+ min-width: 0;
543
+ word-break: break-word;
544
+ }
545
+
546
+ .ngx-record-editor__banner-dismiss,
547
+ .ngx-record-editor__row-btn,
548
+ .ngx-record-editor__header-toggle {
549
+ appearance: none;
550
+ font: inherit;
551
+ cursor: pointer;
552
+ background: none;
553
+ border: 1px solid var(--ngx-color-border);
554
+ border-radius: var(--ngx-radius);
555
+ padding: 4px 8px;
556
+ color: inherit;
557
+ }
558
+
559
+ .ngx-record-editor__banner-dismiss:focus-visible,
560
+ .ngx-record-editor__row-btn:focus-visible,
561
+ .ngx-record-editor__header-toggle:focus-visible {
562
+ outline: 2px solid var(--ngx-color-accent);
563
+ outline-offset: 2px;
564
+ }
565
+
566
+ /* ─── Narrow screens ──────────────────────────────────────────────────────── */
567
+
568
+ @media (max-width: 640px) {
569
+ /* colSpan is set inline per field; below this width a 12-column grid is unreadable. */
570
+ .ngx-form__panel,
571
+ .ngx-field__group-children,
572
+ .ngx-field__array-item-fields {
573
+ grid-template-columns: minmax(0, 1fr);
574
+ }
575
+
576
+ .ngx-form__field-slot {
577
+ grid-column: 1 / -1 !important;
578
+ }
579
+ }