src/app/core/common-components/edit-text-with-autocomplete/edit-text-with-autocomplete.component.ts
This component creates a normal text input with autocomplete. Compared to the EditEntityComponent this does not just assign the ID to the form control but instead completely overwrites the form with the values taken from the selected entity. This is especially useful when instead of creating a new entity, an existing one can also be selected (and extended).
When a value is already present the autocomplete is disabled, and it works like a normal text input.
E.g.
Example :{
"id": "title",
"editComponent": "EditTextWithAutocomplete",
"additional": {
"entityType": "RecurringActivity",
"relevantProperty": "linkedGroups",
"relevantValue": "some-id",
},
}
CustomFormControlDirective<string>
| changeDetection | ChangeDetectionStrategy.OnPush |
| providers |
EditTextWithAutocompleteComponent
|
| selector | app-edit-text-with-autocomplete |
| standalone | true |
| imports |
MatInputModule
MatAutocompleteModule
FontAwesomeModule
MatTooltipModule
|
| templateUrl | ./edit-text-with-autocomplete.component.html |
No results matching.
ReactiveFormsModule
MatInputModule
MatAutocompleteModule
EntityBlockComponent
FontAwesomeModule
MatTooltipModule
CustomFormControlDirective<string>
EditComponent
Properties |
Methods |
Inputs |
Outputs |
constructor()
|
| formFieldConfig | |
Type : FormFieldConfig
|
|
| Required : true | |
| 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 initAutocomplete |
initAutocomplete()
|
|
Returns :
any
|
| keyup |
keyup()
|
|
Returns :
void
|
| Async resetForm |
resetForm()
|
|
Returns :
any
|
| Async selectEntity | ||||||
selectEntity(selected: Entity)
|
||||||
|
Parameters :
Returns :
any
|
| updateAutocomplete |
updateAutocomplete()
|
|
Returns :
void
|
| blur |
blur()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| focus |
focus()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| onContainerClick | ||||||
onContainerClick(event: MouseEvent)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
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
|
| addedFormControls |
Type : []
|
Default value : []
|
| additional |
Type : { entityType: string; relevantProperty?: string; relevantValue?: string; relatedEntitiesParent?: Entity }
|
|
Config passed using component |
| autocompleteDisabled |
Type : unknown
|
Default value : signal(true)
|
| autocompleteEntities |
Type : unknown
|
Default value : signal<Entity[]>([])
|
| currentValues |
Type : unknown
|
| entities |
Type : unknown
|
Default value : signal<Entity[]>([])
|
| lastValue |
Type : string
|
Default value : ""
|
| originalValues |
Type : unknown
|
| selectedEntity |
Type : unknown
|
Default value : signal<Entity | null>(null)
|
| 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 { FormFieldConfig } from "#src/app/core/common-components/entity-form/FormConfig";
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
input,
signal,
} from "@angular/core";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
import { MatAutocompleteModule } from "@angular/material/autocomplete";
import { MatFormFieldControl } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatTooltipModule } from "@angular/material/tooltip";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { EntityBlockComponent } from "../../basic-datatypes/entity/entity-block/entity-block.component";
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 { Entity } from "../../entity/model/entity";
import { CustomFormControlDirective } from "../basic-autocomplete/custom-form-control.directive";
import { ConfirmationDialogService } from "../confirmation-dialog/confirmation-dialog.service";
/**
* This component creates a normal text input with autocomplete.
* Compared to the {@link EditEntityComponent} this does not just assign the ID to the form control
* but instead completely overwrites the form with the values taken from the selected entity.
* This is especially useful when instead of creating a new entity, an existing one can also be selected (and extended).
*
* When a value is already present the autocomplete is disabled, and it works like a normal text input.
*
* E.g.
* ```json
* {
* "id": "title",
* "editComponent": "EditTextWithAutocomplete",
* "additional": {
* "entityType": "RecurringActivity",
* "relevantProperty": "linkedGroups",
* "relevantValue": "some-id",
* },
* }
* ```
*/
@DynamicComponent("EditTextWithAutocomplete")
@Component({
selector: "app-edit-text-with-autocomplete",
templateUrl: "./edit-text-with-autocomplete.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReactiveFormsModule,
MatInputModule,
MatAutocompleteModule,
EntityBlockComponent,
FontAwesomeModule,
MatTooltipModule,
],
providers: [
{
provide: MatFormFieldControl,
useExisting: EditTextWithAutocompleteComponent,
},
],
})
export class EditTextWithAutocompleteComponent
extends CustomFormControlDirective<string>
implements EditComponent
{
private entityMapperService = inject(EntityMapperService);
private confirmationDialog = inject(ConfirmationDialogService);
formFieldConfig = input.required<FormFieldConfig>();
get parent(): FormGroup {
return this.formControl.parent as FormGroup;
}
/**
* Config passed using component
*/
declare additional: {
/**
* The entity type for which autofill should be created.
* This should be the same type as for which the form was created.
*/
entityType: string;
/**
* (optional) a property which should be filled with certain value, if an entity is selected.
*/
relevantProperty?: string;
/**
* @deprecated use `relatedEntitiesParent` instead.
*
* (optional) required if `relevantProperty` is set.
* The value to be filled in `selectedEntity[relevantProperty]`.
*/
relevantValue?: string;
/**
* Reference to the "parent" entity within which this is listed as a related entity.
* (automatically assigned by RelatedEntitiesComponent)
*/
relatedEntitiesParent?: Entity;
};
entities = signal<Entity[]>([]);
autocompleteEntities = signal<Entity[]>([]);
selectedEntity = signal<Entity | null>(null);
currentValues;
originalValues;
autocompleteDisabled = signal(true);
lastValue = "";
addedFormControls = [];
constructor() {
super();
effect(() => {
this.additional = this.formFieldConfig().additional;
if (!this.formControl.value) {
void this.initAutocomplete();
}
});
}
keyup() {
this.lastValue = this.formControl.value;
this.updateAutocomplete();
}
updateAutocomplete() {
let val = this.formControl.value;
if (
!this.autocompleteDisabled() &&
val !== this.currentValues[this.formFieldConfig().id]
) {
let filteredEntities = this.entities();
if (val) {
filteredEntities = this.entities().filter(
(entity) =>
entity !== this.selectedEntity() &&
entity.toString().toLowerCase().includes(val.toLowerCase()),
);
}
this.autocompleteEntities.set(filteredEntities);
}
}
async initAutocomplete() {
const entityType = this.additional.entityType;
const entities = await this.entityMapperService.loadType(entityType);
const sortedEntities = [...entities].sort((e1, e2) =>
e1.toString().localeCompare(e2.toString()),
);
this.entities.set(sortedEntities);
this.autocompleteEntities.set(sortedEntities);
this.autocompleteDisabled.set(false);
this.currentValues = this.parent.getRawValue();
this.originalValues = this.currentValues;
}
async selectEntity(selected: Entity) {
if (await this.userConfirmsOverwriteIfNecessary(selected)) {
this.selectedEntity.set(selected);
this.addRelevantValueToRelevantProperty(selected);
this.setAllFormValues(selected);
this.currentValues = this.parent.getRawValue();
this.autocompleteEntities.set([]);
} else {
this.formControl.setValue(this.lastValue);
}
}
private async userConfirmsOverwriteIfNecessary(entity: Entity) {
return (
!this.valuesChanged() ||
this.confirmationDialog.getConfirmation(
$localize`:Discard changes header:Discard Changes?`,
$localize`Do you want to discard the changes made to '${entity}'?`,
)
);
}
private valuesChanged() {
return Object.entries(this.currentValues).some(
([prop, value]) =>
prop !== this.formFieldConfig().id &&
value !== this.parent.controls[prop].value,
);
}
private addRelevantValueToRelevantProperty(selected: Entity) {
const relevantValue =
this.additional.relevantValue ??
this.additional?.relatedEntitiesParent?.getId();
if (!selected[this.additional.relevantProperty]) {
selected[this.additional.relevantProperty] = [];
}
if (
this.additional.relevantProperty &&
relevantValue &&
!selected[this.additional.relevantProperty].includes(relevantValue)
) {
selected[this.additional.relevantProperty].push(relevantValue);
}
}
private setAllFormValues(selected: Entity) {
Object.keys(selected)
.filter((key) => selected.getSchema().has(key))
.forEach((key) => {
if (this.parent.controls.hasOwnProperty(key)) {
this.parent.controls[key].setValue(selected[key]);
} else {
// adding missing controls so saving does not lose any data
this.parent.addControl(key, new FormControl(selected[key]));
this.addedFormControls.push(key);
}
});
}
async resetForm() {
const selectedEntity = this.selectedEntity();
if (!selectedEntity) return;
if (await this.userConfirmsOverwriteIfNecessary(selectedEntity)) {
this.addedFormControls.forEach((control) =>
this.parent.removeControl(control),
);
this.addedFormControls = [];
this.formControl.reset();
this.parent.patchValue(this.originalValues);
this.selectedEntity.set(null);
this.currentValues = this.originalValues;
}
}
}
<input
matInput
[readonly]="!!selectedEntity()"
[formControl]="formControl"
[matAutocomplete]="autoSuggestions"
[matAutocompleteDisabled]="autocompleteDisabled()"
(keyup)="keyup()"
(focusin)="updateAutocomplete()"
/>
<mat-autocomplete #autoSuggestions="matAutocomplete">
@for (entity of autocompleteEntities(); track entity.getId()) {
<mat-option
[value]="entity[formFieldConfig().id]"
(onSelectionChange)="$event.source.selected ? selectEntity(entity) : null"
>
<app-entity-block
[entity]="entity"
[linkDisabled]="true"
></app-entity-block>
</mat-option>
}
</mat-autocomplete>
@if (!autocompleteDisabled()) {
<div style="font-size: 0.75em; color: rgba(0, 0, 0, 0.6); margin-top: 4px">
@if (!selectedEntity()) {
<span i18n>Creating new record.</span>
} @else {
<span i18n>Editing existing record.</span>
}
</div>
}
@if (!autocompleteDisabled() && !selectedEntity()) {
<fa-icon
(click)="tooltipElement.show()"
#tooltipElement="matTooltip"
icon="cogs"
i18n-matTooltip="Tooltip help text"
matTooltip="You can create a new or load an existing record. Start to type an existing name and then select it from the dropdown to edit the existing record. Type any new text to create a new record."
style="
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
"
class="tooltip-suffix"
></fa-icon>
} @else if (!autocompleteDisabled() && !!selectedEntity()) {
<fa-icon
(click)="resetForm()"
icon="circle-xmark"
i18n-matTooltip="Tooltip for button to reset form"
matTooltip="Unload existing record and reset form."
class="tooltip-suffix"
style="
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
"
></fa-icon>
}