projects/cobbler-frontend/src/app/common/multi-select/multi-select.component.ts
ControlValueAccessor
Validator
OnChanges
| providers |
MultiSelectComponent
MultiSelectComponent
|
| selector | cobbler-multi-select |
| standalone | true |
| imports |
MatFormFieldModule
MatSelectModule
MatListModule
MatCheckboxModule
MatButtonModule
MatIconModule
MatCardModule
MatInputModule
|
| styleUrls | ./multi-select.component.scss |
| templateUrl | ./multi-select.component.html |
| styleUrl | ./multi-select.component.scss |
MatFormFieldModule
MatSelectModule
ReactiveFormsModule
MatListModule
MatCheckboxModule
MatButtonModule
MatIconModule
MatCardModule
MatInputModule
HelpButtonComponent
ControlValueAccessor
Validator
OnChanges
Properties |
|
Methods |
Inputs |
| availableOptions | |
Type : string[] | null
|
|
Default value : null
|
|
| hint | |
Type : string
|
|
| label | |
Type : string
|
|
Default value : ''
|
|
| section | |
Type : string
|
|
Default value : ''
|
|
| addOption |
addOption()
|
|
Returns :
void
|
| buildFormGroup | ||||||||||||
buildFormGroup(options: string[], checked: unknown)
|
||||||||||||
|
Parameters :
Returns :
void
|
| changeValues | |||||||||
changeValues(event: MatCheckboxChange, option: string)
|
|||||||||
|
Parameters :
Returns :
void
|
| ngOnChanges | ||||||
ngOnChanges(changes: SimpleChanges)
|
||||||
|
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
|
| updateFormGroup | ||||||||||||
updateFormGroup(options: string[], checked: unknown)
|
||||||||||||
|
Parameters :
Returns :
void
|
| validate | ||||||
validate(control: AbstractControl)
|
||||||
|
Parameters :
Returns :
ValidationErrors | null
|
| writeValue | ||||||
writeValue(obj: string[])
|
||||||
|
Parameters :
Returns :
void
|
| Private currentValue |
Type : string[]
|
Default value : []
|
| Readonly dialog |
Type : unknown
|
Default value : inject(MatDialog)
|
| isDisabled |
Type : unknown
|
Default value : true
|
| matSelectOptionsFormGroup |
Type : FormGroup<{ }>
|
Default value : new FormGroup({})
|
| multiSelectOptions |
Type : Array<string>
|
Default value : []
|
| onChange |
Type : any
|
| onTouched |
Type : any
|
import {
AfterViewInit,
Component,
ElementRef,
inject,
Input,
OnChanges,
OnInit,
SimpleChanges,
ViewChild,
} 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 {
MatCheckboxChange,
MatCheckboxModule,
} from '@angular/material/checkbox';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatSelectModule } from '@angular/material/select';
import { DialogTextInputComponent } from '../dialog-text-input/dialog-text-input.component';
import { DialogBoxSelectComponent } from '../dialog-box-select/dialog-box-select.component';
import { filter, fromEvent, Subject, takeUntil } from 'rxjs';
import { HelpButtonComponent } from '../help-button/help-button.component';
@Component({
selector: 'cobbler-multi-select',
imports: [
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
MatListModule,
MatCheckboxModule,
MatButtonModule,
MatIconModule,
MatCardModule,
MatInputModule,
HelpButtonComponent,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: MultiSelectComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: MultiSelectComponent,
},
],
templateUrl: './multi-select.component.html',
styleUrl: './multi-select.component.scss',
})
export class MultiSelectComponent
implements ControlValueAccessor, Validator, OnChanges
{
@Input() label = '';
@Input() availableOptions: string[] | null = null; // options come from API response, user can't add any more (used for mgmt classes)
@Input() section: string = '';
@Input() hint?: string;
multiSelectOptions: Array<string> = [];
matSelectOptionsFormGroup: FormGroup<{}> = new FormGroup({});
private currentValue: string[] = [];
onChange: any;
onTouched: any;
readonly dialog = inject(MatDialog);
isDisabled = true;
ngOnChanges(changes: SimpleChanges): void {
if (
changes['availableOptions'] &&
!changes['availableOptions'].firstChange
) {
this.writeValue(this.currentValue);
}
}
buildFormGroup(options: string[], checked = false): void {
options.forEach((value) => {
this.matSelectOptionsFormGroup.addControl(
value,
new FormControl({ value: checked, disabled: this.isDisabled }),
);
});
}
updateFormGroup(options: string[], checked = false): void {
options.forEach((value) => {
this.matSelectOptionsFormGroup.get(value).setValue(checked);
});
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
if (this.isDisabled) {
this.matSelectOptionsFormGroup.disable();
} else {
this.matSelectOptionsFormGroup.enable();
}
}
writeValue(obj: string[]): void {
this.currentValue = obj || [];
this.multiSelectOptions = this.currentValue;
this.buildFormGroup(this.multiSelectOptions);
this.multiSelectOptions.forEach((opt) => {
this.matSelectOptionsFormGroup.get(opt)?.setValue(true);
});
}
registerOnValidatorChange(fn: () => void): void {}
validate(control: AbstractControl): ValidationErrors | null {
return undefined;
}
changeValues(event: MatCheckboxChange, option: string) {
if (!event.checked) {
// Remove the checkbox if it's clicked
this.matSelectOptionsFormGroup.removeControl(option);
this.multiSelectOptions = this.multiSelectOptions.filter(
(o) => o !== option,
);
this.currentValue = this.currentValue.filter((o) => o !== option);
}
const options = Object.keys(this.matSelectOptionsFormGroup.controls).filter(
(key) => {
const control = this.matSelectOptionsFormGroup.get(key);
return control instanceof FormControl && control.value;
},
);
this.onTouched(options);
this.onChange(options);
}
addOption(): void {
let dialogRef: MatDialogRef<any, any>;
// Dialog with select for management class
if (this.section === 'mgmt_classes') {
dialogRef = this.dialog.open(DialogBoxSelectComponent, {
data: {
// Filter the array to avoid duplication of options
options: (this.availableOptions ?? []).filter(
(o) => !this.currentValue.includes(o) && o !== undefined,
),
},
});
} else {
dialogRef = this.dialog.open(DialogTextInputComponent);
}
dialogRef.afterClosed().subscribe((result) => {
if (result) {
const newOptions = Array.from(this.currentValue);
newOptions.push(result);
this.onChange(newOptions);
this.onTouched();
this.writeValue(newOptions);
}
});
}
}
<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 (multiSelectOptions.length === 0) {
<p style="text-align: center" i18n="@@common.multi-select.empty">
Empty list of options
</p>
} @else {
<!-- TODO: See https://github.com/cobbler/cobbler-web/issues/270
<mat-form-field style="margin: 0 1em">
<mat-label>Search for option</mat-label>
<input matInput type="text">
</mat-form-field>
-->
<ng-container [formGroup]="matSelectOptionsFormGroup">
@for (option of multiSelectOptions; track option) {
<mat-checkbox
(change)="changeValues($event, option)"
[formControlName]="option"
[value]="option"
>{{ option }}</mat-checkbox
>
}
</ng-container>
}
<button
mat-button
[disabled]="isDisabled"
(click)="addOption()"
i18n="@@common.multi-select.add-option"
>
Add option
</button>
</mat-card>
./multi-select.component.scss
:host {
display: block;
margin-bottom: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}