src/app/core/basic-datatypes/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.ts
CustomFormControlDirective<
ConfigurableEnumValue | ConfigurableEnumValue[]
>
| changeDetection | ChangeDetectionStrategy.OnPush |
| providers |
EditConfigurableEnumComponent
|
| selector | app-edit-configurable-enum |
| standalone | true |
| imports |
MatSelectModule
FontAwesomeModule
MatButtonModule
|
| styleUrls | ./edit-configurable-enum.component.scss, |
| templateUrl | ./edit-configurable-enum.component.html |
ReactiveFormsModule
MatSelectModule
BasicAutocompleteComponent
FontAwesomeModule
MatButtonModule
DisplayConfigurableEnumComponent
CustomFormControlDirective<
ConfigurableEnumValue | ConfigurableEnumValue[]
>
OnInit
EditComponent
Properties |
|
Methods |
Inputs |
Outputs |
| formFieldConfig | |
Type : FormFieldConfig
|
|
| aria-describedby | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
| disabled | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
| ngControl | |
Type : any
|
|
Default value : inject(NgControl, { optional: true, self: true })
|
|
|
Inherited from
CustomFormControlDirective
|
|
| placeholder | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
| required | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
| value | |
Type : T
|
|
|
Inherited from
CustomFormControlDirective
|
|
| valueChange | |
Type : EventEmitter
|
|
|
Inherited from
CustomFormControlDirective
|
|
| Async addNewOption | ||||||
addNewOption(name: string)
|
||||||
|
Parameters :
Returns :
unknown
|
| onContainerClick | ||||||
onContainerClick(event: MouseEvent)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| openSettings | ||||||
openSettings(event: Event)
|
||||||
|
Parameters :
Returns :
void
|
| blur |
blur()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| focus |
focus()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| setDescribedByIds | ||||||
setDescribedByIds(ids: string[])
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| writeValue | |||||||||||||||
writeValue(value: T, notifyFormControl: unknown)
|
|||||||||||||||
|
Inherited from
CustomFormControlDirective
|
|||||||||||||||
|
Implementation for Angular ControlValueAccessor interface that links the form control value to the component value
Parameters :
Returns :
void
|
| 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 enumId |
Type : unknown
|
Default value : computed(() => this.formFieldConfig()?.additional)
|
| enumValueToString |
Type : unknown
|
Default value : () => {...}
|
| 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"
|
|
Inherited from
CustomFormControlDirective
|
| elementRef |
Type : unknown
|
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
|
|
Inherited from
CustomFormControlDirective
|
| Readonly enabled |
Type : Signal<boolean>
|
Default value : computed(() => !this._disabled())
|
|
Inherited from
CustomFormControlDirective
|
|
Whether the control is currently enabled, as a signal (tracks disabled). |
| errorStateMatcher |
Type : unknown
|
Default value : inject(ErrorStateMatcher)
|
|
Inherited from
CustomFormControlDirective
|
| id |
Type : unknown
|
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
|
|
Inherited from
CustomFormControlDirective
|
| Static nextId |
Type : number
|
Default value : 0
|
|
Inherited from
CustomFormControlDirective
|
| onChange |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
| onTouched |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
| parentForm |
Type : unknown
|
Default value : inject(NgForm, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
| parentFormGroup |
Type : unknown
|
Default value : inject(FormGroupDirective, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
| stateChanges |
Type : unknown
|
Default value : new Subject<void>()
|
|
Inherited from
CustomFormControlDirective
|
| Readonly valueSignal |
Type : Signal<T>
|
Default value : computed(() => this._value())
|
|
Inherited from
CustomFormControlDirective
|
|
The current value of the control as a signal.
Authoritative in both modes: it reflects the bound |
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;
}