[invalid]="form.controls.label.invalid && (form.controls.label.dirty)"
[invalidText]="labelError"
cdRequiredField="Label"
+ cdDynamicInputCombobox
+ (updatedItems)="data.labels = $event"
i18n>
<cds-dropdown-list></cds-dropdown-list>
</cds-combo-box>
i18n-placeholder
[appendInline]="true"
[items]="labelsOption"
- itemValueKey="value"
+ itemValueKey="content"
id="labels"
+ cdDynamicInputCombobox
+ (updatedItems)="labelsOption = $event"
i18n>
<cds-dropdown-list></cds-dropdown-list>
</cds-combo-box>
import { HostService } from '~/app/shared/api/host.service';
import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
-import { SelectOption } from '~/app/shared/components/select/select-option.model';
import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
import { CdForm } from '~/app/shared/forms/cd-form';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
import { CdValidators } from '~/app/shared/forms/cd-validators';
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { ComboBoxItem } from '~/app/shared/models/combo-box.model';
import { FinishedTask } from '~/app/shared/models/finished-task';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
allLabels: string[];
pageURL: string;
hostPattern = false;
- labelsOption: Array<SelectOption> = [];
+ labelsOption: ComboBoxItem[] = [];
messages = new SelectMessages({
empty: $localize`There are no labels.`,
this.hostService.getLabels().subscribe((resp: string[]) => {
const uniqueLabels = new Set(resp.concat(this.hostService.predefinedLabels));
this.labelsOption = Array.from(uniqueLabels).map((label) => {
- return { enabled: true, name: label, content: label, selected: false, description: null };
+ return { name: label, content: label };
});
});
}
[formControlName]="field.name"
itemValueKey="content"
[id]="field.name"
- [items]="field?.typeConfig?.options"
[invalid]="getError(field)"
[invalidText]="getError(field)"
[appendInline]="false"
+ cdDynamicInputCombobox
+ (updatedItems)="field.typeConfig.options = $event"
+ [items]="field?.typeConfig?.options"
[cdRequiredField]="field?.required === true ? field.label : ''"
i18n>
<cds-dropdown-list></cds-dropdown-list>
import { ReactiveFormsModule } from '@angular/forms';
import { OptionalFieldDirective } from './optional-field.directive';
import { DimlessBinaryPerMinuteDirective } from './dimless-binary-per-minute.directive';
+import { DynamicInputComboboxDirective } from './dynamic-input-combobox.directive';
@NgModule({
imports: [ReactiveFormsModule],
AuthStorageDirective,
RequiredFieldDirective,
OptionalFieldDirective,
- DimlessBinaryPerMinuteDirective
+ DimlessBinaryPerMinuteDirective,
+ DynamicInputComboboxDirective
],
exports: [
AutofocusDirective,
AuthStorageDirective,
RequiredFieldDirective,
OptionalFieldDirective,
- DimlessBinaryPerMinuteDirective
+ DimlessBinaryPerMinuteDirective,
+ DynamicInputComboboxDirective
]
})
export class DirectivesModule {}
--- /dev/null
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { DEBOUNCE_TIMER, DynamicInputComboboxDirective } from './dynamic-input-combobox.directive';
+import { Subject } from 'rxjs';
+import { Component, EventEmitter } from '@angular/core';
+import { ComboBoxItem } from '../models/combo-box.model';
+
+@Component({
+ template: `<div cdDynamicInputCombobox
+ [items]="[]"></div>`,
+})
+class MockComponent {
+ items: ComboBoxItem[] = [{ content: 'Item1', name: 'Item1' }];
+ searchSubject = new Subject<string>();
+ selectedItems: ComboBoxItem[] = [];
+ updatedItems = new EventEmitter<ComboBoxItem[]>();
+}
+
+describe('DynamicInputComboboxDirective', () => {
+
+ let component: MockComponent;
+ let fixture: ComponentFixture<MockComponent>;
+ let directive: DynamicInputComboboxDirective;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [DynamicInputComboboxDirective, MockComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(MockComponent);
+ component = fixture.componentInstance;
+
+ directive = fixture.debugElement.children[0].injector.get(DynamicInputComboboxDirective);
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ directive.ngOnDestroy();
+ });
+
+ it('should create an instance', () => {
+ expect(directive).toBeTruthy();
+ });
+
+ it('should update items when input is given', fakeAsync(() => {
+ const newItem = 'NewItem';
+ directive.onInput(newItem);
+ tick(DEBOUNCE_TIMER);
+
+ expect(directive.items[0].content).toBe(newItem);
+ }));
+
+ it('should not unselect selected items', fakeAsync(() => {
+ const selectedItems: ComboBoxItem[] = [{
+ content: 'selectedItem',
+ name: 'selectedItem',
+ selected: true
+ }];
+
+ directive.items = selectedItems;
+
+ directive.onSelected(selectedItems);
+ tick(DEBOUNCE_TIMER);
+
+ directive.onInput(selectedItems[0].content);
+ tick(DEBOUNCE_TIMER);
+
+ expect(directive.items[0].content).toBe(selectedItems[0].content);
+ expect(directive.items[0].selected).toBeTruthy();
+ }))
+});
--- /dev/null
+import { Directive, Input, OnDestroy, OnInit, Output, EventEmitter, HostListener } from '@angular/core';
+import { ComboBoxItem } from '../models/combo-box.model';
+import { ComboBoxService } from '../services/combo-box.service';
+import { Subject, Subscription } from 'rxjs';
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
+
+export const DEBOUNCE_TIMER = 300;
+
+/**
+ * Directive to introduce adding custom items to the carbon combo-box component
+ * It takes the inputs of the combo-box and then append it with the searched item.
+ * Then it emits the updatedItems back to the <cds-combobox> element
+ */
+@Directive({
+ selector: '[cdDynamicInputCombobox]'
+})
+export class DynamicInputComboboxDirective implements OnInit, OnDestroy {
+ /**
+ * This input is the same as what we have in the <cds-combobox> element.
+ */
+ @Input() items: ComboBoxItem[];
+
+ /**
+ * This will hold the items of the combo-box appended with the searched items.
+ */
+ @Output() updatedItems: EventEmitter<ComboBoxItem[]> = new EventEmitter();
+
+ private searchSubscription: Subscription;
+ private searchSubject: Subject<string> = new Subject();
+ private selectedItems: ComboBoxItem[] = [];
+
+ constructor(
+ private combBoxService: ComboBoxService
+ ) { }
+
+ ngOnInit() {
+ this.searchSubscription = this.searchSubject
+ .pipe(
+ debounceTime(DEBOUNCE_TIMER),
+ distinctUntilChanged()
+ )
+ .subscribe((searchString) => {
+ // Already selected items should be selected in the dropdown
+ // even if the items are updated again
+ this.items = this.items.map((item: ComboBoxItem) => {
+ const selected = this.selectedItems.some(
+ (selectedItem: ComboBoxItem) => selectedItem.content === item.content
+ );
+ return { ...item, selected };
+ });
+
+ const exists = this.items.some(
+ (item: ComboBoxItem) => item.content === searchString
+ );
+
+ if (!exists) {
+ this.items = this.items.concat({ content: searchString, name: searchString });
+ }
+ this.updatedItems.emit(this.items );
+ this.combBoxService.emit({ searchString });
+ });
+ }
+
+ @HostListener('search', ['$event'])
+ onInput(event: string) {
+ if (event.length > 1) this.searchSubject.next(event);
+ }
+
+ @HostListener('selected', ['$event'])
+ onSelected(event: ComboBoxItem[]) {
+ this.selectedItems = event;
+ }
+
+ ngOnDestroy() {
+ this.searchSubscription.unsubscribe();
+ this.searchSubject.complete();
+ }
+}
--- /dev/null
+export type ComboBoxItem = {
+ content: string;
+ name: string;
+ selected?: boolean;
+};
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+
+import { ComboBoxService } from './combo-box.service';
+
+describe('ComboBoxService', () => {
+ let service: ComboBoxService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(ComboBoxService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
--- /dev/null
+import { Injectable } from '@angular/core';
+import { Subject } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ComboBoxService {
+ private searchSubject = new Subject<{ searchString: string }>();
+
+ constructor() {
+ }
+
+ emit(value: { searchString: string }) {
+ this.searchSubject.next(value);
+ }
+}