projects/cobbler-frontend/src/app/common/key-value-editor/key-value-editor.component.ts

Implements

ControlValueAccessor Validator

Metadata

Relationships

Index

Properties
Methods
Inputs

Inputs

hint
Type : string
label
Type : string
Default value : ''

Methods

addOption
addOption()
Returns : void
buildFormGroup
buildFormGroup()
Returns : void
deleteKey
deleteKey(key: string)
Parameters :
Name Type Optional
key string No
Returns : void
drop
drop(event: CdkDragDrop)
Parameters :
Name Type Optional
event CdkDragDrop<string[]> 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
registerOnValidatorChange
registerOnValidatorChange(fn: () => void)
Parameters :
Name Type Optional
fn function No
Returns : void
setDisabledState
setDisabledState(isDisabled: boolean)
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void
setFormGroupDisabledState
setFormGroupDisabledState(isDisabled: boolean)
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void
validate
validate(control: AbstractControl)
Parameters :
Name Type Optional
control AbstractControl No
Returns : ValidationErrors | null
writeValue
writeValue(obj: Map)
Parameters :
Name Type Optional
obj Map<string | any> No
Returns : void

Properties

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>
          &nbsp;=&nbsp;
          <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);
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""