projects/cobbler-frontend/src/app/common/key-value-editor/key-value-editor.component.ts
ControlValueAccessor
Validator
| providers |
KeyValueEditorComponent
KeyValueEditorComponent
|
| selector | cobbler-key-value-editor |
| standalone | true |
| imports |
MatCardModule
MatFormFieldModule
MatInputModule
MatIconModule
MatButtonModule
|
| styleUrls | ./key-value-editor.component.scss |
| templateUrl | ./key-value-editor.component.html |
| styleUrl | ./key-value-editor.component.scss |
MatCardModule
CdkDropList
CdkDrag
MatFormFieldModule
MatInputModule
MatIconModule
ReactiveFormsModule
MatButtonModule
HelpButtonComponent
ControlValueAccessor
Validator
Properties |
|
Methods |
Inputs |
| hint | |
Type : string
|
|
| label | |
Type : string
|
|
Default value : ''
|
|
| addOption |
addOption()
|
|
Returns :
void
|
| buildFormGroup |
buildFormGroup()
|
|
Returns :
void
|
| deleteKey | ||||||
deleteKey(key: string)
|
||||||
|
Parameters :
Returns :
void
|
| drop | ||||||
drop(event: CdkDragDrop
|
||||||
|
Parameters :
Returns :
void
|
| registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
|
Parameters :
Returns :
void
|
| registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
|
Parameters :
Returns :
void
|
| registerOnValidatorChange | ||||||
registerOnValidatorChange(fn: () => void)
|
||||||
|
Parameters :
Returns :
void
|
| setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| setFormGroupDisabledState | ||||||
setFormGroupDisabledState(isDisabled: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| validate | ||||||
validate(control: AbstractControl)
|
||||||
|
Parameters :
Returns :
ValidationErrors | null
|
| writeValue | ||||||
writeValue(obj: Map
|
||||||
|
Parameters :
Returns :
void
|
| Readonly dialog |
Type : unknown
|
Default value : inject<MatDialog>(MatDialog)
|
| isDisabled |
Type : unknown
|
Default value : true
|
| keyOrder |
Type : string[]
|
Default value : Array.from(this.keyValueOptions.keys())
|
| keyOrderFormGroup |
Type : unknown
|
Default value : new FormGroup({})
|
| keyValueOptions |
Type : Map<string | any>
|
Default value : new Map<string, any>()
|
| Protected Readonly Object |
Type : unknown
|
Default value : Object
|
| onChange |
Type : any
|
| onTouched |
Type : any
|
import {
CdkDrag,
CdkDragDrop,
CdkDropList,
moveItemInArray,
} from '@angular/cdk/drag-drop';
import { Component, Input, inject } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
ValidationErrors,
Validator,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialog } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import {
DialogKeyValueInputComponent,
DialogKeyValueInputReturnData,
} from '../dialog-key-value-input/dialog-key-value-input.component';
import { HelpButtonComponent } from '../help-button/help-button.component';
@Component({
selector: 'cobbler-key-value-editor',
imports: [
MatCardModule,
CdkDropList,
CdkDrag,
MatFormFieldModule,
MatInputModule,
MatIconModule,
ReactiveFormsModule,
MatButtonModule,
HelpButtonComponent,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: KeyValueEditorComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: KeyValueEditorComponent,
},
],
templateUrl: './key-value-editor.component.html',
styleUrl: './key-value-editor.component.scss',
})
export class KeyValueEditorComponent
implements ControlValueAccessor, Validator
{
readonly dialog = inject<MatDialog>(MatDialog);
@Input() label = '';
@Input() hint?: string;
keyValueOptions: Map<string, any> = new Map<string, any>();
onChange: any;
onTouched: any;
keyOrder: string[] = Array.from(this.keyValueOptions.keys());
keyOrderFormGroup = new FormGroup({});
isDisabled = true;
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
registerOnValidatorChange(fn: () => void): void {}
setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.setFormGroupDisabledState(isDisabled);
}
setFormGroupDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.keyOrderFormGroup.disable();
} else {
this.keyOrderFormGroup.enable();
}
}
validate(control: AbstractControl): ValidationErrors | null {
return undefined;
}
writeValue(obj: Map<string, any>): void {
if (!(obj instanceof Map)) {
throw new Error("obj wasn't of type Map!");
}
this.keyValueOptions = obj;
this.keyOrder = Array.from(this.keyValueOptions.keys());
this.buildFormGroup();
}
buildFormGroup(): void {
for (let key of this.keyOrder) {
const formGroupControls = {
key: new FormControl({ value: key, disabled: true }),
value: new FormControl({
value: this.keyValueOptions.get(key),
disabled: true,
}),
};
this.keyOrderFormGroup.addControl(
key + 'FormGroup',
new FormGroup(formGroupControls),
);
}
this.setFormGroupDisabledState(this.isDisabled);
}
deleteKey(key: string): void {
let newOptions = new Map<string, any>(this.keyValueOptions);
newOptions.delete(key);
this.onChange(newOptions);
this.onTouched();
this.writeValue(newOptions);
}
addOption(): void {
const dialogRef = this.dialog.open(DialogKeyValueInputComponent);
dialogRef
.afterClosed()
.subscribe((dialogResult: DialogKeyValueInputReturnData) => {
if (dialogResult && dialogResult.key !== '') {
let newOptions = new Map<string, any>(this.keyValueOptions);
newOptions.set(dialogResult.key, dialogResult.value);
this.onChange(newOptions);
this.onTouched();
this.writeValue(newOptions);
} else {
return;
}
});
}
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.keyOrder, event.previousIndex, event.currentIndex);
}
protected readonly Object = Object;
}
<mat-card appearance="outlined">
<mat-card-header class="card-header">
<mat-card-title>{{ label }}</mat-card-title>
@if (hint) {
<cobbler-help-button [hint]="hint" />
}
</mat-card-header>
@if (this.keyValueOptions.size === 0) {
<p style="text-align: center" i18n="@@common.key-value-editor.empty">
Empty list of options
</p>
} @else {
<div
cdkDropList
[cdkDropListDisabled]="isDisabled"
class="example-list"
[formGroup]="keyOrderFormGroup"
(cdkDropListDropped)="drop($event)"
>
@for (key of keyOrder; track key) {
<form class="example-box" formGroupName="{{ key }}FormGroup">
<mat-form-field>
<input
matInput
formControlName="key"
placeholder="Key"
i18n-placeholder="@@common.key-value-editor.placeholder.key"
value="{{ key }}"
/>
</mat-form-field>
=
<mat-form-field>
<input
matInput
formControlName="value"
placeholder="Value"
i18n-placeholder="@@common.key-value-editor.placeholder.value"
value="{{ keyValueOptions.get(key) }}"
/>
</mat-form-field>
<button
mat-icon-button
[disabled]="isDisabled"
(click)="deleteKey(key)"
>
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button [disabled]="isDisabled" cdkDrag>
<mat-icon>menu</mat-icon>
</button>
</form>
}
</div>
}
<button
mat-button
[disabled]="isDisabled"
(click)="addOption()"
i18n="@@common.key-value-editor.add-option"
>
Add option
</button>
</mat-card>
./key-value-editor.component.scss
:host {
display: block;
margin-bottom: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.example-list {
width: 80%;
max-width: 100%;
border: solid 1px #ccc;
min-height: 60px;
display: block;
background: white;
border-radius: 4px;
overflow: hidden;
}
.example-box {
margin: 20px 10px;
padding: 10px;
border: solid 1px #ccc;
color: rgba(0, 0, 0, 0.87);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
cursor: move;
background: white;
font-size: 14px;
}
.cdk-drag-preview {
border: none;
box-sizing: border-box;
border-radius: 4px;
box-shadow:
0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.example-box:last-child {
border: none;
}
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}