src/app/core/basic-datatypes/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.ts

Extends

CustomFormControlDirective< ConfigurableEnumValue | ConfigurableEnumValue[] >

Implements

OnInit EditComponent

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs

Inputs

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

Async addNewOption
addNewOption(name: string)
Parameters :
Name Type Optional
name string No
Returns : unknown
onContainerClick
onContainerClick(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
openSettings
openSettings(event: Event)
Parameters :
Name Type Optional
event Event No
Returns : void
blur
blur()
Returns : void
focus
focus()
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

autocompleteComponent
Type : BasicAutocompleteComponent<ConfigurableEnumValue | ConfigurableEnumValue>
Decorators :
@ViewChild(BasicAutocompleteComponent)
Readonly canEdit
Type : unknown
Default value : computed(() => { this.enumId(); const enumEntity = this.enumEntity(); return !!enumEntity && this.ability.can("update", enumEntity); })
Readonly createNewOption
Type : unknown
Default value : computed< ((input: string) => Promise<ConfigurableEnumValue | undefined>) | undefined >(() => (this.canEdit() ? this.addNewOption.bind(this) : undefined))
Readonly enumEntity
Type : unknown
Default value : computed<ConfigurableEnum | undefined>(() => { this.enumRefreshTick(); const enumId = this.enumId(); return enumId ? this.enumService.getEnum(enumId) : undefined; })
Readonly enumId
Type : unknown
Default value : computed(() => this.formFieldConfig()?.additional)
enumValueToString
Type : unknown
Default value : () => {...}
Readonly invalidOptions
Type : unknown
Default value : computed<ConfigurableEnumValue[]>(() => { const value = this.formControlValue(); if (!value) { return []; } if ( !this.multi() && !Array.isArray(value) && (value as any).isInvalidOption ) { return [value]; } if (this.multi() && Array.isArray(value)) { return value.filter((option) => (option as any).isInvalidOption); } return []; })
Readonly multi
Type : unknown
Default value : computed(() => this.formFieldConfig()?.isArray ?? false)
Readonly options
Type : unknown
Default value : computed<ConfigurableEnumValue[]>(() => { this.enumRefreshTick(); return [...(this.enumEntity()?.values ?? []), ...this.invalidOptions()]; })
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 {
  ChangeDetectionStrategy,
  Component,
  computed,
  DestroyRef,
  inject,
  input,
  OnInit,
  signal,
  ViewChild,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

import { ReactiveFormsModule } from "@angular/forms";
import { MatButtonModule } from "@angular/material/button";
import { MatDialog } from "@angular/material/dialog";
import { MatFormFieldControl } from "@angular/material/form-field";
import { MatSelectModule } from "@angular/material/select";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { BasicAutocompleteComponent } from "../../../common-components/basic-autocomplete/basic-autocomplete.component";
import { CustomFormControlDirective } from "../../../common-components/basic-autocomplete/custom-form-control.directive";
import { ConfirmationDialogService } from "../../../common-components/confirmation-dialog/confirmation-dialog.service";
import { OkButton } from "../../../common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component";
import { FormFieldConfig } from "../../../common-components/entity-form/FormConfig";
import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator";
import { EditComponent } from "../../../entity/entity-field-edit/dynamic-edit/edit-component.interface";
import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
import { EntityAbility } from "../../../permissions/ability/entity-ability";
import {
  ConfigurableEnum,
  InvalidEnumOptionException,
} from "../configurable-enum";
import { ConfigurableEnumService } from "../configurable-enum.service";
import { ConfigurableEnumValue } from "../configurable-enum.types";
import { ConfigureEnumPopupComponent } from "../configure-enum-popup/configure-enum-popup.component";
import { DisplayConfigurableEnumComponent } from "../display-configurable-enum/display-configurable-enum.component";

@DynamicComponent("EditConfigurableEnum")
@Component({
  selector: "app-edit-configurable-enum",
  templateUrl: "./edit-configurable-enum.component.html",
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: [
    "./edit-configurable-enum.component.scss",
    "../../../common-components/basic-autocomplete/basic-autocomplete-dropdown.component.scss",
  ],
  imports: [
    ReactiveFormsModule,
    MatSelectModule,
    BasicAutocompleteComponent,
    FontAwesomeModule,
    MatButtonModule,
    DisplayConfigurableEnumComponent,
  ],
  providers: [
    {
      provide: MatFormFieldControl,
      useExisting: EditConfigurableEnumComponent,
    },
  ],
})
export class EditConfigurableEnumComponent
  extends CustomFormControlDirective<
    ConfigurableEnumValue | ConfigurableEnumValue[]
  >
  implements OnInit, EditComponent
{
  formFieldConfig = input<FormFieldConfig>();
  @ViewChild(BasicAutocompleteComponent)
  autocompleteComponent: BasicAutocompleteComponent<
    ConfigurableEnumValue,
    ConfigurableEnumValue
  >;

  private readonly enumService = inject(ConfigurableEnumService);
  private readonly entityMapper = inject(EntityMapperService);
  private readonly ability = inject(EntityAbility);
  private readonly dialog = inject(MatDialog);
  private readonly confirmation = inject(ConfirmationDialogService);
  private readonly destroyRef = inject(DestroyRef);
  private readonly enumRefreshTick = signal(0);
  private readonly formControlValue = signal<
    ConfigurableEnumValue | ConfigurableEnumValue[] | undefined
  >(undefined);

  readonly multi = computed(() => this.formFieldConfig()?.isArray ?? false);
  readonly enumId = computed(() => this.formFieldConfig()?.additional);
  readonly enumEntity = computed<ConfigurableEnum | undefined>(() => {
    this.enumRefreshTick();
    const enumId = this.enumId();
    return enumId ? this.enumService.getEnum(enumId) : undefined;
  });
  readonly canEdit = computed(() => {
    this.enumId();
    const enumEntity = this.enumEntity();
    return !!enumEntity && this.ability.can("update", enumEntity);
  });
  enumValueToString = (v: ConfigurableEnumValue) => v?.label;
  readonly createNewOption = computed<
    ((input: string) => Promise<ConfigurableEnumValue | undefined>) | undefined
  >(() => (this.canEdit() ? this.addNewOption.bind(this) : undefined));
  readonly invalidOptions = computed<ConfigurableEnumValue[]>(() => {
    const value = this.formControlValue();
    if (!value) {
      return [];
    }
    if (
      !this.multi() &&
      !Array.isArray(value) &&
      (value as any).isInvalidOption
    ) {
      return [value];
    }
    if (this.multi() && Array.isArray(value)) {
      return value.filter((option) => (option as any).isInvalidOption);
    }
    return [];
  });
  readonly options = computed<ConfigurableEnumValue[]>(() => {
    this.enumRefreshTick();
    return [...(this.enumEntity()?.values ?? []), ...this.invalidOptions()];
  });

  ngOnInit() {
    if (this.formControl) {
      this.formControlValue.set(this.formControl.value);
      this.formControl.valueChanges
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe((value) => this.formControlValue.set(value));
    }
  }

  async addNewOption(name: string) {
    const enumEntity = this.enumEntity();
    if (!enumEntity) {
      return undefined;
    }

    const prevValues = JSON.stringify(enumEntity.values);
    let addedOption: ConfigurableEnumValue;

    try {
      addedOption = enumEntity.addOption(name);
    } catch (error) {
      const message =
        error instanceof InvalidEnumOptionException
          ? $localize`Couldn't create the option "${name}" because it doesn't contain any usable characters for an ID, please include some letters or numbers. You can rename options more freely afterwards.`
          : $localize`Couldn't create this new option. Please check if the value already exists.`;
      await this.confirmation.getConfirmation(
        $localize`Failed to create new option`,
        message,
        OkButton,
      );
      return undefined;
    }

    if (!addedOption) {
      return undefined;
    }

    const userConfirmed = await this.confirmation.getConfirmation(
      $localize`Create new option`,
      $localize`Do you want to create the new option "${addedOption.label}"?`,
    );
    if (!userConfirmed) {
      enumEntity.values = JSON.parse(prevValues);
      return undefined;
    }

    await this.entityMapper.save(enumEntity);
    this.enumRefreshTick.update((version) => version + 1);
    return addedOption;
  }

  openSettings(event: Event) {
    event.stopPropagation();
    const enumEntity = this.enumEntity();
    if (!enumEntity) {
      return;
    }

    this.dialog
      .open(ConfigureEnumPopupComponent, { data: enumEntity })
      .afterClosed()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        this.enumRefreshTick.update((version) => version + 1);
        this.reconcileSingleSelectedValue();
      });
  }

  private reconcileSingleSelectedValue() {
    const value = this.formControl.value;
    const enumEntity = this.enumEntity();

    if (
      this.multi() ||
      !value ||
      Array.isArray(value) ||
      (value as any).isInvalidOption ||
      !enumEntity
    ) {
      return;
    }

    if (!enumEntity.values.some((enumValue) => enumValue.id === value.id)) {
      this.formControl.setValue(undefined);
    }
  }

  override onContainerClick(event: MouseEvent) {
    this.autocompleteComponent?.onContainerClick(event);
  }
}
<div class="flex-row gap-small">
  <app-basic-autocomplete
    (click)="onContainerClick($event)"
    class="flex-grow enum-container"
    #autocomplete
    [formControl]="formControl"
    [multi]="multi()"
    [options]="options()"
    [optionToString]="enumValueToString"
    [createOption]="createNewOption()"
    [displayFullLengthOptionLabel]="
      formFieldConfig()?.displayFullLengthOptionLabel
    "
    display="chips"
  >
    <ng-template let-option>
      <app-display-configurable-enum
        [value]="option"
      ></app-display-configurable-enum>
    </ng-template>
  </app-basic-autocomplete>

  @if (enabled()) {
    <fa-icon
      icon="caret-down"
      class="form-field-icon-suffix"
      (click)="autocomplete.showAutocomplete()"
    ></fa-icon>
  }
</div>

@if (enabled() && !!createNewOption() && canEdit()) {
  <fa-icon
    class="form-field-icon-suffix settings-icon"
    (click)="openSettings($event)"
    icon="wrench"
  ></fa-icon>
}

./edit-configurable-enum.component.scss

.enum-container {
  min-width: 0;
  overflow: hidden;
}

:host {
  position: relative;
}

.settings-icon {
  position: absolute;
  top: -15px;
  right: -15px;
  z-index: 1;
  font-size: 0.5em;
  padding: 0px 4px !important;
  transition: all 0.2s ease;

  &::before {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    width: 28px;
    height: 28px;
    background: linear-gradient(
      225deg,
      rgba(0, 0, 0, 0.08) 50%,
      transparent 50%
    );
    border-radius: 0 4px 0 0;
    z-index: -1;
    transition: all 0.2s ease;
  }

  &:hover {
    font-size: 1em;
    padding: 4px 6px !important;

    &::before {
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.08);
      border-radius: 4px;
    }
  }
}

../../../common-components/basic-autocomplete/basic-autocomplete-dropdown.component.scss

.autocomplete-container {
  position: relative;
}

.icon-container {
  position: absolute;
  top: 0;
  right: 0;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""