ngx-dynamic-entity 1.6.0 → 1.7.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, ChangeDetectorRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output, ElementRef, afterNextRender } from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output, ElementRef, afterNextRender } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { ReactiveFormsModule, Validators, FormBuilder, FormArray, FormGroup } from '@angular/forms';
5
5
  import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, migrateRecord, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, ambiguousFieldIds, parseFieldRef, toRefToken, collectFieldScopes, fieldRefFor, refOf, getTabPath, applyAutoPatch, applyPatchOnTrue } from '@dynamic-entity/core';
@@ -152,6 +152,25 @@ const SYSTEM_DEFAULT_CAN_EDIT = new InjectionToken('SYSTEM_DEFAULT_CAN_EDIT');
152
152
  * Enables cross-entity field referencing (`isReferenced`, `referencedEntityKey`, `referencedFieldId`, `hasDrift`).
153
153
  */
154
154
  const CONFIG_SOURCE = new InjectionToken('CONFIG_SOURCE');
155
+ /**
156
+ * Turns the markdown a `markdown` field stores into HTML for display.
157
+ *
158
+ * Optional, and deliberately not bundled: these packages declare no runtime dependencies
159
+ * beyond `tslib`, and a markdown parser is a large one to force on every consumer. Without
160
+ * a renderer the field still works — it stores and shows the source text, with line breaks
161
+ * preserved and nothing interpreted.
162
+ *
163
+ * The returned HTML is bound through `[innerHTML]`, so Angular's sanitizer strips scripts
164
+ * and event handlers before it reaches the DOM. That is a backstop, not a licence: a
165
+ * renderer should still be configured to escape raw HTML in its input, because sanitizing
166
+ * removes the dangerous parts silently rather than telling the author their content was
167
+ * altered.
168
+ *
169
+ * @example
170
+ * import { marked } from 'marked';
171
+ * { provide: MARKDOWN_RENDERER, useValue: (src: string) => marked.parse(src) as string }
172
+ */
173
+ const MARKDOWN_RENDERER = new InjectionToken('MARKDOWN_RENDERER');
155
174
 
156
175
  /**
157
176
  * Provider function for ngx-dynamic-entity.
@@ -426,6 +445,233 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
426
445
  type: Input
427
446
  }] } });
428
447
 
448
+ /**
449
+ * Markdown field: a long-form input that stores **markdown source**, never HTML.
450
+ *
451
+ * Storing the source rather than rendered HTML is the whole design. The record stays plain
452
+ * text — diffable, portable, safe to log, and impossible to turn into stored XSS by writing
453
+ * it into a database. Rendering happens at display time and only when a consumer asks for
454
+ * it, via `MARKDOWN_RENDERER`.
455
+ *
456
+ * Without a renderer this degrades to exactly what `textarea` does, plus preserved line
457
+ * breaks: the source is shown as text with nothing interpreted. That is the default because
458
+ * these packages declare no runtime dependencies beyond `tslib`, and a markdown parser is a
459
+ * large thing to force on a consumer who wanted a form library.
460
+ *
461
+ * With a renderer, its HTML is bound through `[innerHTML]`, which Angular sanitizes — see
462
+ * the token's own note on why that is a backstop rather than a licence.
463
+ */
464
+ class MarkdownFieldComponent {
465
+ field;
466
+ control;
467
+ language = 'en';
468
+ readonly = false;
469
+ masked = false;
470
+ /** Optional — the field is fully usable without one. */
471
+ render = inject(MARKDOWN_RENDERER, { optional: true });
472
+ cdr = inject(ChangeDetectorRef);
473
+ destroyRef = inject(DestroyRef);
474
+ previewing = signal(false, ...(ngDevMode ? [{ debugName: "previewing" }] : /* istanbul ignore next */ []));
475
+ valueSub;
476
+ /**
477
+ * Re-render when the value changes from outside this component.
478
+ *
479
+ * `rendered()` reads `control.value`, which under OnPush is neither an input nor a
480
+ * template event: typing in the textarea updates through `formControl`, but a host
481
+ * calling `patchValue`, a rule, or an `autoPatch` mapping does not mark this component
482
+ * dirty. The preview and the read-only view would keep showing the previous document
483
+ * while the record already held a different one.
484
+ */
485
+ ngOnChanges(changes) {
486
+ if (!changes['control'])
487
+ return;
488
+ this.valueSub?.unsubscribe();
489
+ this.valueSub = this.control?.valueChanges?.subscribe(() => this.cdr.markForCheck());
490
+ if (this.valueSub)
491
+ this.destroyRef.onDestroy(() => this.valueSub?.unsubscribe());
492
+ }
493
+ /** A Preview tab that can only ever show the source back is worse than no tab. */
494
+ get canPreview() {
495
+ return !!this.render;
496
+ }
497
+ get label() {
498
+ return resolveLabel(this.field?.label, this.language);
499
+ }
500
+ get placeholder() {
501
+ return resolveLabel(this.field?.placeholder, this.language);
502
+ }
503
+ /**
504
+ * Rendered HTML, or null when there is no renderer or nothing to render.
505
+ *
506
+ * A renderer is consumer code and may throw on input it does not like. Letting that
507
+ * escape would take down the whole form over one malformed field, so it falls back to the
508
+ * source text — which is what the field would have shown with no renderer at all.
509
+ */
510
+ rendered() {
511
+ const source = this.control?.value;
512
+ if (!this.render || typeof source !== 'string' || source === '')
513
+ return null;
514
+ try {
515
+ return this.render(source);
516
+ }
517
+ catch {
518
+ return null;
519
+ }
520
+ }
521
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MarkdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
522
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MarkdownFieldComponent, isStandalone: true, selector: "ngx-markdown-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, usesOnChanges: true, ngImport: i0, template: `
523
+ <div
524
+ class="ngx-field ngx-field--markdown"
525
+ [attr.data-testid]="'field-' + field.id"
526
+ [attr.data-field-type]="field.type"
527
+ [class.ngx-field--readonly]="readonly"
528
+ [class.ngx-field--masked]="masked"
529
+ >
530
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
531
+
532
+ @if (masked) {
533
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
534
+ } @else if (readonly) {
535
+ @if (rendered(); as html) {
536
+ <div class="ngx-field__value ngx-field__markdown" [attr.data-testid]="'field-' + field.id + '-value'" [innerHTML]="html"></div>
537
+ } @else {
538
+ <!-- No renderer: the source is the display. The stylesheet keeps its line
539
+ breaks with white-space: pre-wrap, and interpolation escapes it. -->
540
+ <span class="ngx-field__value ngx-field__markdown--source" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
541
+ }
542
+ } @else {
543
+ <div class="ngx-field__markdown-editor">
544
+ @if (canPreview) {
545
+ <div class="ngx-field__markdown-tabs" role="group" [attr.aria-label]="label + ' editor mode'">
546
+ <button
547
+ type="button"
548
+ class="ngx-field__markdown-tab"
549
+ [attr.data-testid]="'field-' + field.id + '-write'"
550
+ [attr.aria-pressed]="!previewing()"
551
+ (click)="previewing.set(false)"
552
+ >
553
+ Write
554
+ </button>
555
+ <button
556
+ type="button"
557
+ class="ngx-field__markdown-tab"
558
+ [attr.data-testid]="'field-' + field.id + '-preview'"
559
+ [attr.aria-pressed]="previewing()"
560
+ (click)="previewing.set(true)"
561
+ >
562
+ Preview
563
+ </button>
564
+ </div>
565
+ }
566
+
567
+ @if (previewing()) {
568
+ <div class="ngx-field__markdown ngx-field__markdown--preview" [attr.data-testid]="'field-' + field.id + '-preview-body'" [innerHTML]="rendered()"></div>
569
+ } @else {
570
+ <textarea
571
+ [id]="field.id"
572
+ class="ngx-field__input ngx-field__input--markdown"
573
+ [attr.data-testid]="'field-' + field.id + '-input'"
574
+ [formControl]="$any(control)"
575
+ [placeholder]="placeholder"
576
+ [attr.disabled]="field.disabled ? true : null"
577
+ rows="6"
578
+ ></textarea>
579
+ }
580
+ </div>
581
+
582
+ @if (control.invalid && control.touched) {
583
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
584
+ }
585
+ }
586
+ </div>
587
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
588
+ }
589
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MarkdownFieldComponent, decorators: [{
590
+ type: Component,
591
+ args: [{
592
+ changeDetection: ChangeDetectionStrategy.OnPush,
593
+ selector: 'ngx-markdown-field',
594
+ standalone: true,
595
+ imports: [ReactiveFormsModule],
596
+ template: `
597
+ <div
598
+ class="ngx-field ngx-field--markdown"
599
+ [attr.data-testid]="'field-' + field.id"
600
+ [attr.data-field-type]="field.type"
601
+ [class.ngx-field--readonly]="readonly"
602
+ [class.ngx-field--masked]="masked"
603
+ >
604
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
605
+
606
+ @if (masked) {
607
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
608
+ } @else if (readonly) {
609
+ @if (rendered(); as html) {
610
+ <div class="ngx-field__value ngx-field__markdown" [attr.data-testid]="'field-' + field.id + '-value'" [innerHTML]="html"></div>
611
+ } @else {
612
+ <!-- No renderer: the source is the display. The stylesheet keeps its line
613
+ breaks with white-space: pre-wrap, and interpolation escapes it. -->
614
+ <span class="ngx-field__value ngx-field__markdown--source" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
615
+ }
616
+ } @else {
617
+ <div class="ngx-field__markdown-editor">
618
+ @if (canPreview) {
619
+ <div class="ngx-field__markdown-tabs" role="group" [attr.aria-label]="label + ' editor mode'">
620
+ <button
621
+ type="button"
622
+ class="ngx-field__markdown-tab"
623
+ [attr.data-testid]="'field-' + field.id + '-write'"
624
+ [attr.aria-pressed]="!previewing()"
625
+ (click)="previewing.set(false)"
626
+ >
627
+ Write
628
+ </button>
629
+ <button
630
+ type="button"
631
+ class="ngx-field__markdown-tab"
632
+ [attr.data-testid]="'field-' + field.id + '-preview'"
633
+ [attr.aria-pressed]="previewing()"
634
+ (click)="previewing.set(true)"
635
+ >
636
+ Preview
637
+ </button>
638
+ </div>
639
+ }
640
+
641
+ @if (previewing()) {
642
+ <div class="ngx-field__markdown ngx-field__markdown--preview" [attr.data-testid]="'field-' + field.id + '-preview-body'" [innerHTML]="rendered()"></div>
643
+ } @else {
644
+ <textarea
645
+ [id]="field.id"
646
+ class="ngx-field__input ngx-field__input--markdown"
647
+ [attr.data-testid]="'field-' + field.id + '-input'"
648
+ [formControl]="$any(control)"
649
+ [placeholder]="placeholder"
650
+ [attr.disabled]="field.disabled ? true : null"
651
+ rows="6"
652
+ ></textarea>
653
+ }
654
+ </div>
655
+
656
+ @if (control.invalid && control.touched) {
657
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
658
+ }
659
+ }
660
+ </div>
661
+ `,
662
+ }]
663
+ }], propDecorators: { field: [{
664
+ type: Input
665
+ }], control: [{
666
+ type: Input
667
+ }], language: [{
668
+ type: Input
669
+ }], readonly: [{
670
+ type: Input
671
+ }], masked: [{
672
+ type: Input
673
+ }] } });
674
+
429
675
  class NumberFieldComponent {
430
676
  messages = inject(ValidationMessagesService);
431
677
  field;
@@ -3694,6 +3940,7 @@ function builtInFieldTypes() {
3694
3940
  return {
3695
3941
  text: TextFieldComponent,
3696
3942
  textarea: TextareaFieldComponent,
3943
+ markdown: MarkdownFieldComponent,
3697
3944
  number: NumberFieldComponent,
3698
3945
  currency: CurrencyFieldComponent,
3699
3946
  email: EmailFieldComponent,
@@ -5371,5 +5618,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
5371
5618
  * Generated bundle index. Do not edit.
5372
5619
  */
5373
5620
 
5374
- export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UPLOAD_HANDLER, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
5621
+ export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MARKDOWN_RENDERER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UPLOAD_HANDLER, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
5375
5622
  //# sourceMappingURL=ngx-dynamic-entity.mjs.map