src/app/features/public-form/edit-public-form-columns/edit-public-form-columns.component.ts

Extends

CustomFormControlDirective<FieldGroup[]>

Implements

OnInit EditComponent

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs

Inputs

entity
Type : Entity
formFieldConfig
Type : FormFieldConfig
aria-describedby
Type : string
disabled
Type : boolean
ngControl
Type : any
Default value : inject(NgControl, { optional: true, self: true })
placeholder
Type : string
required
Type : boolean
value
Type : T

Outputs

valueChange
Type : EventEmitter

Methods

updateValue
updateValue(newConfig: FormConfig)
Parameters :
Name Type Optional
newConfig FormConfig No
Returns : void
blur
blur()
Returns : void
focus
focus()
Returns : void
onContainerClick
onContainerClick(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
setDescribedByIds
setDescribedByIds(ids: string[])
Parameters :
Name Type Optional
ids string[] No
Returns : void
setDisabledState
setDisabledState(isDisabled: boolean)
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void
writeValue
writeValue(value: T, notifyFormControl: unknown)

Implementation for Angular ControlValueAccessor interface that links the form control value to the component value

Parameters :
Name Type Optional Default value Description
value T No

The new value to set

notifyFormControl unknown No false

Whether to notify the FormControl of this change (for internal updates)

Returns : void

Properties

Optional entityConstructor
Type : EntityConstructor

not set for configs in the multi form format, which hold no top-level entity type

Optional entityForm
Type : AdminEntityFormComponent
Decorators :
@ViewChild(AdminEntityFormComponent)
formConfig
Type : FormConfig
isDisabled
Type : unknown
Default value : signal(false)
controlType
Type : string
Default value : "custom-control"
elementRef
Type : unknown
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
Readonly enabled
Type : Signal<boolean>
Default value : computed(() => !this._disabled())

Whether the control is currently enabled, as a signal (tracks disabled).

errorStateMatcher
Type : unknown
Default value : inject(ErrorStateMatcher)
id
Type : unknown
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
Static nextId
Type : number
Default value : 0
onChange
Type : unknown
Default value : () => {...}
onTouched
Type : unknown
Default value : () => {...}
parentForm
Type : unknown
Default value : inject(NgForm, { optional: true })
parentFormGroup
Type : unknown
Default value : inject(FormGroupDirective, { optional: true })
stateChanges
Type : unknown
Default value : new Subject<void>()
Readonly valueSignal
Type : Signal<T>
Default value : computed(() => this._value())

The current value of the control as a signal. Authoritative in both modes: it reflects the bound FormControl (synced in ngDoCheck) as well as [(value)] / writeValue updates.

import { EditComponent } from "#src/app/core/entity/entity-field-edit/dynamic-edit/edit-component.interface";
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  input,
  OnInit,
  signal,
  ViewChild,
} from "@angular/core";
import { MatFormFieldControl } from "@angular/material/form-field";
import { AdminEntityFormComponent } from "app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component";
import { CustomFormControlDirective } from "app/core/common-components/basic-autocomplete/custom-form-control.directive";
import { FormFieldConfig } from "app/core/common-components/entity-form/FormConfig";
import { DynamicComponent } from "app/core/config/dynamic-components/dynamic-component.decorator";
import { FieldGroup } from "app/core/entity-details/form/field-group";
import { FormConfig } from "app/core/entity-details/form/form.component";
import { EntityRegistry } from "app/core/entity/database-entity.decorator";
import { Entity, EntityConstructor } from "app/core/entity/model/entity";
import { PublicFormConfig } from "../public-form-config";
import { migratePublicFormConfig } from "../public-form.component";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
import { EntityMapperService } from "app/core/entity/entity-mapper/entity-mapper.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { PublicFormsService } from "../public-forms.service";

@UntilDestroy()
@DynamicComponent("EditPublicFormColumns")
@Component({
  selector: "app-edit-public-form-columns",
  imports: [AdminEntityFormComponent, HintBoxComponent],
  templateUrl: "./edit-public-form-columns.component.html",
  styleUrl: "./edit-public-form-columns.component.scss",
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: MatFormFieldControl,
      useExisting: EditPublicFormColumnsComponent,
    },
  ],
})
export class EditPublicFormColumnsComponent
  extends CustomFormControlDirective<FieldGroup[]>
  implements OnInit, EditComponent
{
  formFieldConfig = input<FormFieldConfig>();
  entity = input<Entity>();

  @ViewChild(AdminEntityFormComponent) entityForm?: AdminEntityFormComponent;

  /** not set for configs in the multi form format, which hold no top-level entity type */
  entityConstructor?: EntityConstructor;
  formConfig: FormConfig;
  private originalFormConfig: FormConfig;

  // Signal to track disabled state for immediate UI updates
  isDisabled = signal(false);

  private entities = inject(EntityRegistry);
  private readonly entityMapper = inject(EntityMapperService);
  private readonly publicFormsService = inject(PublicFormsService);

  ngOnInit() {
    const entity = this.entity();
    if (entity) {
      // configs in the multi form format do not hold a top-level entity type
      this.entityConstructor = entity["entity"]
        ? this.entities.get(entity["entity"])
        : undefined;

      const publicFormConfig: PublicFormConfig = migratePublicFormConfig({
        columns: this.formControl.getRawValue(),
      } as Partial<PublicFormConfig> as PublicFormConfig);
      this.formConfig = {
        fieldGroups: publicFormConfig.columns,
      };
      this.originalFormConfig = JSON.parse(JSON.stringify(this.formConfig));
      this.setupFormStateDetection();
      // Set initial disabled state
      this.isDisabled.set(this.formControl.disabled);
      this.subscribeToPublicFormConfigSave();
    }
  }

  /**
   * Subscribe to entity save events to persist custom fields to global schema
   * after the PublicFormConfig is saved.
   */
  private subscribeToPublicFormConfigSave() {
    this.entityMapper
      .receiveUpdates(PublicFormConfig)
      .pipe(untilDestroyed(this))
      .subscribe(async (update) => {
        await this.publicFormsService.saveCustomFieldsToEntityConfig(
          update.entity,
        );
      });
  }

  updateValue(newConfig: FormConfig) {
    // setTimeout needed for change detection of disabling tabs
    setTimeout(() => this.formControl.setValue(newConfig.fieldGroups));
    this.formControl.markAsDirty();
  }

  /**
   * Setup form state detection for cancel vs save operations
   */
  private setupFormStateDetection(): void {
    let wasDirty = false;
    let lastValue: FieldGroup[] | null = null;

    // Listen to form control status and value changes to detect cancel operations
    this.formControl.statusChanges.subscribe(() => {
      const isDirty = this.formControl.dirty;
      const isPristine = this.formControl.pristine;
      const currentValue = this.formControl.value;

      // If form was dirty and now becomes pristine
      if (wasDirty && isPristine) {
        const normalizedCurrentValue = this.normalizeFieldGroups(
          currentValue || [],
        );
        const normalizedOriginalValue = this.normalizeFieldGroups(
          this.originalFormConfig.fieldGroups || [],
        );

        // Check if the value was reverted to original (cancel)
        const isValueRevertedToOriginal =
          JSON.stringify(normalizedCurrentValue) ===
          JSON.stringify(normalizedOriginalValue);

        if (isValueRevertedToOriginal) {
          // Reset UI to original configuration immediately
          this.formConfig = JSON.parse(JSON.stringify(this.originalFormConfig));
        }
      }
      wasDirty = isDirty;
      lastValue = currentValue;
      this.isDisabled.set(this.formControl.disabled);
    });
  }

  // Normalize both values for comparison (handle missing header property)
  private normalizeFieldGroups = (fieldGroups: FieldGroup[]) => {
    return fieldGroups.map((group) => ({
      ...group,
      header: group.header || null,
    }));
  };
}
<app-hint-box i18n>
  You can edit how users will see the details of this public form. To edit
  fields, click on the Edit button, make your changes, and then save. These
  changes will reflect on the public form.
  <br />
  Drag and drop fields and sections in this preview of a profile view. The
  editor below closely resembles how the form will look for users later. Forms
  show all fields below each other not in multiple columns, however.
  <br />
</app-hint-box>

<div class="column-container">
  <app-admin-entity-form
    [config]="formConfig"
    (configChange)="updateValue($event)"
    [entityType]="entityConstructor"
    [isDisabled]="isDisabled()"
    [updateEntitySchema]="false"
  >
  </app-admin-entity-form>
</div>

./edit-public-form-columns.component.scss

.column-container {
  overflow: hidden;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""