From 1fe41cc5708dc14a5775036425f771ff038d9ea0 Mon Sep 17 00:00:00 2001 From: Moritz Terink <23279624+moterink@users.noreply.github.com> Date: Fri, 21 Jan 2022 10:36:15 +0100 Subject: [PATCH] feat(combobox): Add form control support for combobox. --- apps/demos/src/app-routing.module.ts | 14 +- apps/demos/src/nav-items.ts | 4 + .../experimental/combobox/README.md | 9 +- .../combobox/src/combobox-module.ts | 4 +- .../combobox/src/combobox.spec.ts | 134 ++++++++++++++++- .../experimental/combobox/src/combobox.ts | 135 +++++++++++++++--- libs/barista-components/form-field/README.md | 11 +- .../form-field/src/form-field-control.ts | 4 +- .../src/loading-spinner.scss | 3 +- .../src/combobox/combobox-examples.module.ts | 13 +- .../combobox-form-control-example.html | 17 --- .../combobox-form-field-example.html | 18 +++ .../combobox-form-field-example.ts} | 46 +++--- libs/examples/src/combobox/index.ts | 2 +- libs/examples/src/index.ts | 6 +- 15 files changed, 324 insertions(+), 96 deletions(-) delete mode 100644 libs/examples/src/combobox/combobox-form-control-example/combobox-form-control-example.html create mode 100644 libs/examples/src/combobox/combobox-form-field-example/combobox-form-field-example.html rename libs/examples/src/combobox/{combobox-form-control-example/combobox-form-control-example.ts => combobox-form-field-example/combobox-form-field-example.ts} (60%) diff --git a/apps/demos/src/app-routing.module.ts b/apps/demos/src/app-routing.module.ts index cfd7499cbb..53242bf700 100644 --- a/apps/demos/src/app-routing.module.ts +++ b/apps/demos/src/app-routing.module.ts @@ -86,7 +86,6 @@ import { DtExampleConsumptionError, DtExampleConsumptionWarning, DtExampleComboboxSimple, - DtExampleComboboxFormControl, DtExampleContainerBreakpointObserverDefault, DtExampleContainerBreakpointObserverIfElse, DtExampleContainerBreakpointObserverIf, @@ -333,6 +332,7 @@ import { DtExampleTreeTableProblemIndicator, DtExampleTreeTableSimple, DtExampleComboboxCustomOptionHeight, + DtExampleComboboxFormField, DtExampleSelectCustomValueTemplate, DtExampleCalendarMinMax, DtExampleTimepickerMinMax, @@ -499,14 +499,14 @@ const ROUTES: Routes = [ path: 'combobox-simple-example', component: DtExampleComboboxSimple, }, - { - path: 'combobox-form-control-example', - component: DtExampleComboboxFormControl, - }, { path: 'combobox-custom-option-height-example', component: DtExampleComboboxCustomOptionHeight, }, + { + path: 'combobox-form-field-example', + component: DtExampleComboboxFormField, + }, { path: 'confirmation-dialog-default-example', component: DtExampleConfirmationDialogDefault, @@ -1059,7 +1059,7 @@ const ROUTES: Routes = [ }, { path: 'stacked-series-chart-heat-field-example', - component: DtExampleStackedSeriesChartHeatField + component: DtExampleStackedSeriesChartHeatField, }, { path: 'stepper-default-example', component: DtExampleStepperDefault }, { path: 'stepper-editable-example', component: DtExampleStepperEditable }, @@ -1123,7 +1123,7 @@ const ROUTES: Routes = [ path: 'table-interactive-rows-example', component: DtExampleTableInteractiveRows, }, - { path: 'table-selection', component: DtExampleTableSelection}, + { path: 'table-selection', component: DtExampleTableSelection }, { path: 'table-loading-example', component: DtExampleTableLoading }, { path: 'table-observable-example', component: DtExampleTableObservable }, { path: 'table-order-column-example', component: DtExampleTableOrderColumn }, diff --git a/apps/demos/src/nav-items.ts b/apps/demos/src/nav-items.ts index 3ffad7c429..ab1ec1f018 100644 --- a/apps/demos/src/nav-items.ts +++ b/apps/demos/src/nav-items.ts @@ -323,6 +323,10 @@ export const DT_DEMOS_EXAMPLE_NAV_ITEMS = [ name: 'combobox-custom-option-height-example', route: '/combobox-custom-option-height-example', }, + { + name: 'combobox-form-field-example', + route: '/combobox-form-field-example', + }, ], }, { diff --git a/libs/barista-components/experimental/combobox/README.md b/libs/barista-components/experimental/combobox/README.md index b5ef3ab027..d0f613d70d 100644 --- a/libs/barista-components/experimental/combobox/README.md +++ b/libs/barista-components/experimental/combobox/README.md @@ -78,9 +78,10 @@ automatically change the height of the options. -## Combobox bound to a form control +## Form field -For now, in case the combobox needs to be bound to a form control, you can add -the DefaultValueAccessor directive as in the following example: +The combobox component supports the ``. These include error +messages, hint text, prefix & suffix. For additional information about these +features, see the [form field documentation](/components/form-field). - + diff --git a/libs/barista-components/experimental/combobox/src/combobox-module.ts b/libs/barista-components/experimental/combobox/src/combobox-module.ts index f95f90953d..498013ba9b 100644 --- a/libs/barista-components/experimental/combobox/src/combobox-module.ts +++ b/libs/barista-components/experimental/combobox/src/combobox-module.ts @@ -28,8 +28,6 @@ import { PortalModule } from '@angular/cdk/portal'; import { DtOptionModule } from '@dynatrace/barista-components/core'; @NgModule({ - exports: [DtCombobox, DtOptionModule], - declarations: [DtCombobox], imports: [ CommonModule, PortalModule, @@ -40,5 +38,7 @@ import { DtOptionModule } from '@dynatrace/barista-components/core'; DtLoadingDistractorModule, PortalModule, ], + exports: [DtCombobox, DtOptionModule], + declarations: [DtCombobox], }) export class DtComboboxModule {} diff --git a/libs/barista-components/experimental/combobox/src/combobox.spec.ts b/libs/barista-components/experimental/combobox/src/combobox.spec.ts index 77e217c0cf..b8195c615f 100644 --- a/libs/barista-components/experimental/combobox/src/combobox.spec.ts +++ b/libs/barista-components/experimental/combobox/src/combobox.spec.ts @@ -22,12 +22,22 @@ import { } from '@angular/core/testing'; import { Component, + ViewChild, //NgZone } from '@angular/core'; import { OverlayContainer } from '@angular/cdk/overlay'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { + FormControl, + FormGroup, + FormGroupDirective, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { DtFormFieldModule } from '@dynatrace/barista-components/form-field'; import { createComponent, @@ -49,10 +59,6 @@ describe('Combobox', () => { let overlayContainer: OverlayContainer; let overlayContainerElement: HTMLElement; - let fixture: ComponentFixture; - let input: HTMLInputElement; - let trigger: HTMLDivElement; - let combobox: DtCombobox; //let zone: MockNgZone; beforeEach(fakeAsync(() => { @@ -60,11 +66,18 @@ describe('Combobox', () => { imports: [ NoopAnimationsModule, DtComboboxModule, + DtFormFieldModule, CommonModule, + ReactiveFormsModule, + FormsModule, HttpClientTestingModule, DtIconModule.forRoot({ svgIconLocation: `{{name}}.svg` }), ], - declarations: [TestComponent, TestLoadingComponent], + declarations: [ + TestComponent, + TestLoadingComponent, + ComboboxInsideFormGroup, + ], providers: [ //{ provide: NgZone, useFactory: () => (zone = new MockNgZone()) }, ], @@ -83,6 +96,11 @@ describe('Combobox', () => { }); describe('Basic', () => { + let fixture: ComponentFixture; + let input: HTMLInputElement; + let trigger: HTMLDivElement; + let combobox: DtCombobox; + beforeEach(() => { fixture = createComponent(TestComponent); combobox = fixture.debugElement.query( @@ -141,6 +159,80 @@ describe('Combobox', () => { })); }); + describe('inside of a form group', () => { + let fixture: ComponentFixture; + let testComponent: ComboboxInsideFormGroup; + let combobox: HTMLElement; + + beforeEach(() => { + fixture = createComponent(ComboboxInsideFormGroup); + testComponent = fixture.componentInstance; + combobox = fixture.debugElement.query( + By.css('dt-combobox'), + ).nativeElement; + }); + + it('should not set the invalid class on a clean combobox', fakeAsync(() => { + expect(testComponent.formGroup.untouched).toBe(true); + expect(testComponent.formControl.invalid).toBe(true); + expect(combobox.classList).not.toContain('dt-combobox-invalid'); + expect(combobox.getAttribute('aria-invalid')).toBe('false'); + })); + + it('should appear as invalid if it becomes touched', fakeAsync(() => { + expect(combobox.classList).not.toContain('dt-combobox-invalid'); + expect(combobox.getAttribute('aria-invalid')).toBe('false'); + + testComponent.formControl.markAsDirty(); + fixture.detectChanges(); + + expect(combobox.classList).toContain('dt-combobox-invalid'); + expect(combobox.getAttribute('aria-invalid')).toBe('true'); + })); + + it('should not have the invalid class when the combobox becomes valid', fakeAsync(() => { + testComponent.formControl.markAsDirty(); + fixture.detectChanges(); + + expect(combobox.classList).toContain('dt-combobox-invalid'); + expect(combobox.getAttribute('aria-invalid')).toBe('true'); + + testComponent.formControl.setValue('value1'); + fixture.detectChanges(); + + expect(combobox.classList).not.toContain('dt-combobox-invalid'); + expect(combobox.getAttribute('aria-invalid')).toBe('false'); + })); + + it('should appear as invalid when the parent form group is submitted', fakeAsync(() => { + expect(combobox.classList).not.toContain('dt-combobox-invalid'); + expect(combobox.getAttribute('aria-invalid')).toBe('false'); + + dispatchFakeEvent( + fixture.debugElement.query(By.css('form')).nativeElement, + 'submit', + ); + fixture.detectChanges(); + + expect(combobox.classList).toContain('dt-combobox-invalid'); + expect(combobox.getAttribute('aria-invalid')).toBe('true'); + })); + + it('should render the error messages when the parent form is submitted', fakeAsync(() => { + const debugEl = fixture.debugElement.nativeElement; + + expect(debugEl.querySelectorAll('dt-error').length).toBe(0); + + dispatchFakeEvent( + fixture.debugElement.query(By.css('form')).nativeElement, + 'submit', + ); + fixture.detectChanges(); + + expect(debugEl.querySelectorAll('dt-error').length).toBe(1); + })); + }); + it('should not throw an error when loading is true initially', () => { let loadingFixture; try { @@ -161,7 +253,7 @@ describe('Combobox', () => { + + + + {{ option.name }} + + + This field is required + + + `, +}) +class ComboboxInsideFormGroup { + options: { name: string; value: string }[] = [ + { name: 'Value 1', value: 'value1' }, + { name: 'Value 2', value: 'value2' }, + { name: 'Value 3', value: 'value3' }, + ]; + initialValue = this.options[0]; + @ViewChild(FormGroupDirective) + formGroupDirective: FormGroupDirective; + @ViewChild(DtCombobox) combobox: DtCombobox; + formControl = new FormControl(null, Validators.required); + formGroup = new FormGroup({ + value: this.formControl, + }); +} diff --git a/libs/barista-components/experimental/combobox/src/combobox.ts b/libs/barista-components/experimental/combobox/src/combobox.ts index 383ebd74c7..8447cc7bd1 100644 --- a/libs/barista-components/experimental/combobox/src/combobox.ts +++ b/libs/barista-components/experimental/combobox/src/combobox.ts @@ -37,6 +37,9 @@ import { AfterContentInit, NgZone, Inject, + DoCheck, + OnChanges, + SimpleChanges, } from '@angular/core'; import { DtAutocomplete, @@ -66,9 +69,15 @@ import { DtLogger, DtLoggerFactory, DT_COMPARE_WITH_NON_FUNCTION_VALUE_ERROR_MSG, + CanUpdateErrorState, } from '@dynatrace/barista-components/core'; import { DtFormFieldControl } from '@dynatrace/barista-components/form-field'; -import { FormGroupDirective, NgControl, NgForm } from '@angular/forms'; +import { + ControlValueAccessor, + FormGroupDirective, + NgControl, + NgForm, +} from '@angular/forms'; import { TemplatePortal } from '@angular/cdk/portal'; import { coerceBooleanProperty, @@ -107,6 +116,7 @@ export class DtComboboxBase { public ngControl: NgControl, ) {} } + export const _DtComboboxMixinBase = mixinTabIndex( mixinDisabled(mixinErrorState(DtComboboxBase)), ); @@ -130,6 +140,7 @@ export const _DtComboboxMixinBase = mixinTabIndex( '[attr.aria-required]': 'required.toString()', '[attr.aria-disabled]': 'disabled.toString()', '[attr.aria-invalid]': 'errorState', + '[attr.aria-describedby]': '_ariaDescribedby || null', }, inputs: ['disabled', 'tabIndex'], providers: [{ provide: DtFormFieldControl, useExisting: DtCombobox }], @@ -143,20 +154,26 @@ export class DtCombobox AfterContentInit, AfterViewInit, OnDestroy, + OnChanges, + DoCheck, CanDisable, HasTabIndex, - DtFormFieldControl + ControlValueAccessor, + DtFormFieldControl, + CanUpdateErrorState { static ngAcceptInputType_disabled: BooleanInput; static ngAcceptInputType_tabIndex: NumberInput; /** The ID for the combobox. */ @Input() id: string; + /** The currently selected value in the combobox. */ @Input() get value(): T { return this._value; } + set value(newValue: T) { if (newValue !== this._value) { this._value = newValue; @@ -164,6 +181,7 @@ export class DtCombobox this._resetInputValue(); } } + private _value: T; /** When set to true, a loading indicator is shown to show to the user that data is currently being loaded/filtered. */ @@ -171,6 +189,7 @@ export class DtCombobox get loading(): boolean { return this._loading; } + set loading(value: boolean) { const coercedValue = coerceBooleanProperty(value); @@ -184,30 +203,47 @@ export class DtCombobox this._changeDetectorRef.markForCheck(); } } + _loading = false; static ngAcceptInputType_loading: BooleanInput; + /** Placeholder to be shown if no value has been selected. */ + @Input() + get placeholder(): string { + return this._placeholder; + } + + set placeholder(value: string) { + this._placeholder = value; + this.stateChanges.next(); + } + + private _placeholder: string; + /** Whether the control is required. */ @Input() get required(): boolean { return this._required; } + set required(value: boolean) { this._required = coerceBooleanProperty(value); + this.stateChanges.next(); } + private _required = false; static ngAcceptInputType_required: BooleanInput; /** An arbitrary class name that is added to the combobox dropdown. */ @Input() panelClass: string = ''; - /** A placeholder text for the input field. */ - @Input() placeholder: string | undefined; /** A function returning a display name for a given object that represents an option from the combobox. */ @Input() displayWith: (value: T) => string = (value: T) => `${value}`; /** Aria label of the combobox. */ @Input('aria-label') ariaLabel: string; /** Input that can be used to specify the `aria-labelledby` attribute. */ @Input('aria-labelledby') ariaLabelledBy: string; + /** Object used to control when error messages are shown. */ + @Input() errorStateMatcher: ErrorStateMatcher; /** * Function to compare the option values with the selected values. The first argument @@ -220,6 +256,7 @@ export class DtCombobox get compareWith(): (v1: T, v2: T) => boolean { return this._compareWith; } + set compareWith(fn: (v1: T, v2: T) => boolean) { // tslint:disable-next-line:strict-type-predicates if (typeof fn !== 'function') { @@ -233,6 +270,7 @@ export class DtCombobox this._initializeSelection(); } } + private _compareWith = (v1: T, v2: T) => v1 === v2; /** Event emitted when a new value has been selected. */ @@ -242,7 +280,7 @@ export class DtCombobox /** Event emitted when the combobox panel has been toggled. */ @Output() openedChange = new EventEmitter(); - /** Combined stream of all of the child options' change events. */ + /** Combined stream of all the child options' change events. */ readonly optionSelectionChanges: Observable> = defer(() => { if (this._options) { @@ -278,6 +316,9 @@ export class DtCombobox /** @internal `View -> model callback called when value changes` */ _onChange: (value: T) => void = () => {}; + /** @internal `View -> model callback called when combobox has been touched` */ + _onTouched = () => {}; + /** Whether the selection is currently empty. */ get empty(): boolean { return !this._selectionModel || this._selectionModel.isEmpty(); @@ -295,6 +336,9 @@ export class DtCombobox /** @internal whether the autocomplete panel is shown. Initially false. */ _panelOpen = false; + /** @internal The aria-describedby attribute on the combobox for improved a11y. */ + _ariaDescribedby: string; + /** @internal Deals with the selection logic. */ _selectionModel: SelectionModel>; @@ -326,6 +370,12 @@ export class DtCombobox this.tabIndex = parseInt(tabIndex, 10) || 0; + if (this.ngControl) { + // Note: we provide the value accessor through here, instead of + // the `providers` to avoid running into a circular import. + this.ngControl.valueAccessor = this; + } + // Force setter to be called in case id was not specified. this.id = this.id; } @@ -382,7 +432,7 @@ export class DtCombobox .subscribe(() => { this._autocomplete._additionalOptions = this._options.toArray(); // To prevent an expression has changed after checked - // error in the when setting the value. + // error when setting the value. Promise.resolve().then(() => { this._setSelectionByValue(this._value, this._initialOptionChange); }); @@ -404,19 +454,71 @@ export class DtCombobox this._resetInputValue(); } + ngDoCheck(): void { + if (this.ngControl) { + this.updateErrorState(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + // Updating the disabled state is handled by `mixinDisabled`, but we need to additionally let + // the parent form field know to run change detection when the disabled state changes. + if (changes.disabled) { + this.stateChanges.next(); + } + } + ngOnDestroy(): void { this._destroy.next(); this._destroy.complete(); + this.stateChanges.complete(); + } + + /** Sets the combobox's value. Part of the ControlValueAccessor. */ + writeValue(value: T): void { + if (this._options) { + this.value = value; + } + } + + /** + * Saves a callback function to be invoked when the combobox's value + * changes from user input. Part of the ControlValueAccessor. + */ + registerOnChange(fn: (value: T) => void): void { + this._onChange = fn; + } + + /** + * Saves a callback function to be invoked when the combobox is blurred + * by the user. Part of the ControlValueAccessor. + */ + registerOnTouched(fn: () => {}): void { + this._onTouched = fn; + } + + /** Disables the combobox. Part of the ControlValueAccessor. */ + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this._changeDetectorRef.markForCheck(); + this.stateChanges.next(); } - // /** Sets the list of element IDs that currently describe this control. Part of the FormFieldControl */ - setDescribedByIds(_: string[]): void { - // TODO ChMa: implement (what does this even do?) + /** + * Sets the list of element IDs that currently describe this control. + * Part of the FormFieldControl + */ + setDescribedByIds(ids: string[]): void { + this._ariaDescribedby = ids.join(' '); } - // /** Handles a click on the control's container.Part of the FormFieldControl */ + /** + * Handles a click on the control's container. + * Part of the FormFieldControl + */ onContainerClick(_: MouseEvent): void { - // TODO ChMa: implement (do we even need this handler?) + this._openPanel(); + this._changeDetectorRef.markForCheck(); } /** @internal called when the user selects a different option */ @@ -440,6 +542,7 @@ export class DtCombobox this._propagateChanges(); } + this.stateChanges.next(); this._changeDetectorRef.markForCheck(); } @@ -490,6 +593,7 @@ export class DtCombobox _closed(): void { this._panelOpen = false; this.openedChange.emit(false); + this._onTouched(); this._changeDetectorRef.markForCheck(); } @@ -498,6 +602,7 @@ export class DtCombobox const valueToEmit = this.selected ? this.selected.value : null; this._value = valueToEmit!; this.valueChange.emit(valueToEmit!); + this._onChange(valueToEmit!); this._changeDetectorRef.markForCheck(); } @@ -525,14 +630,6 @@ export class DtCombobox }); } - /** - * Saves a callback function to be invoked when the select's value - * changes from user input. Part of the ControlValueAccessor. - */ - registerOnChange(fn: (value: T) => void): void { - this._onChange = fn; - } - /** Updates the selection by value using selection model and keymanager to handle the active item */ private _setSelectionByValue(value: T, triggered: boolean = true): void { // If value is being reset programmatically diff --git a/libs/barista-components/form-field/README.md b/libs/barista-components/form-field/README.md index f1455a6407..2e824cdb56 100644 --- a/libs/barista-components/form-field/README.md +++ b/libs/barista-components/form-field/README.md @@ -28,11 +28,12 @@ class MyModule {} The following barista form-controls are compatible with the form-field. -- [Input](https://barista.dynatrace.com/components/input) -- [Select](https://barista.dynatrace.com/components/select) -- [Checkbox](https://barista.dynatrace.com/components/checkbox) -- [Switch](https://barista.dynatrace.com/components/switch) -- [Radio-group](https://barista.dynatrace.com/components/radio#radio-groups) +- [Input](/components/input) +- [Select](/components/select) +- [Checkbox](/components/checkbox) +- [Switch](/components/switch) +- [Radio-group](/components/radio#radio-groups) +- [Combobox](/components/combobox) ## Custom form-control diff --git a/libs/barista-components/form-field/src/form-field-control.ts b/libs/barista-components/form-field/src/form-field-control.ts index a3df86913a..517ab9a7ab 100644 --- a/libs/barista-components/form-field/src/form-field-control.ts +++ b/libs/barista-components/form-field/src/form-field-control.ts @@ -18,12 +18,12 @@ import { Directive } from '@angular/core'; import { NgControl } from '@angular/forms'; import { Observable } from 'rxjs'; -/** An interface which allows a control to work inside of a `DtFormField`. */ +/** An interface which allows a control to work inside a `DtFormField`. */ @Directive({ // The @Directive with selector is required here because we're still running a lot of things // against ViewEngine where directives without selectors are not allowed. // @breaking-change Will be removed with switch to ivy. - // convert to a selectorless Directive after we switch to Ivy. + // convert to a selector-less Directive after we switch to Ivy. selector: 'do-not-use-abstract-dt-form-field-control', }) export abstract class DtFormFieldControl { diff --git a/libs/barista-components/loading-distractor/src/loading-spinner.scss b/libs/barista-components/loading-distractor/src/loading-spinner.scss index b3d64d760a..c2ae63ade8 100644 --- a/libs/barista-components/loading-distractor/src/loading-spinner.scss +++ b/libs/barista-components/loading-distractor/src/loading-spinner.scss @@ -36,7 +36,8 @@ $dt-loading-spinner-ease: cubic-bezier(0.645, 0.045, 0.355, 1); infinite; } -:host-context(.dt-form-field) .dt-loading-spinner-svg, +:host-context(.dt-form-field) .dt-loading-spinner-svg[dtPrefix], +:host-context(.dt-form-field) .dt-loading-spinner-svg[dtSuffix], :host-context(.dt-filter-field) .dt-loading-spinner-svg { stroke: $gray-500; } diff --git a/libs/examples/src/combobox/combobox-examples.module.ts b/libs/examples/src/combobox/combobox-examples.module.ts index ff81a3a681..c9c724aef7 100644 --- a/libs/examples/src/combobox/combobox-examples.module.ts +++ b/libs/examples/src/combobox/combobox-examples.module.ts @@ -14,25 +14,26 @@ * limitations under the License. */ import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { DtComboboxModule } from '@dynatrace/barista-components/experimental/combobox'; +import { DtFormFieldModule } from '../../../barista-components/form-field'; import { DtExampleComboboxSimple } from './combobox-simple-example/combobox-simple-example'; -import { DtExampleComboboxFormControl } from './combobox-form-control-example/combobox-form-control-example'; -import { DtOptionModule } from '@dynatrace/barista-components/core'; -import { CommonModule } from '@angular/common'; import { DtExampleComboboxCustomOptionHeight } from './combobox-custom-option-height-example/combobox-custom-option-height-example'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - +import { DtExampleComboboxFormField } from './combobox-form-field-example/combobox-form-field-example'; +import { DtOptionModule } from '@dynatrace/barista-components/core'; @NgModule({ imports: [ DtComboboxModule, FormsModule, ReactiveFormsModule, DtOptionModule, + DtFormFieldModule, CommonModule, ], declarations: [ DtExampleComboboxSimple, - DtExampleComboboxFormControl, + DtExampleComboboxFormField, DtExampleComboboxCustomOptionHeight, ], }) diff --git a/libs/examples/src/combobox/combobox-form-control-example/combobox-form-control-example.html b/libs/examples/src/combobox/combobox-form-control-example/combobox-form-control-example.html deleted file mode 100644 index 86c52486e4..0000000000 --- a/libs/examples/src/combobox/combobox-form-control-example/combobox-form-control-example.html +++ /dev/null @@ -1,17 +0,0 @@ - - - {{ option.name }} - - diff --git a/libs/examples/src/combobox/combobox-form-field-example/combobox-form-field-example.html b/libs/examples/src/combobox/combobox-form-field-example/combobox-form-field-example.html new file mode 100644 index 0000000000..79cfdedf78 --- /dev/null +++ b/libs/examples/src/combobox/combobox-form-field-example/combobox-form-field-example.html @@ -0,0 +1,18 @@ + + Choose Coffee: + + + {{ option.name }} + + + Choose one of the coffees in the list + Please select a coffee + diff --git a/libs/examples/src/combobox/combobox-form-control-example/combobox-form-control-example.ts b/libs/examples/src/combobox/combobox-form-field-example/combobox-form-field-example.ts similarity index 60% rename from libs/examples/src/combobox/combobox-form-control-example/combobox-form-control-example.ts rename to libs/examples/src/combobox/combobox-form-field-example/combobox-form-field-example.ts index b3ef40f462..46ea75d749 100644 --- a/libs/examples/src/combobox/combobox-form-control-example/combobox-form-control-example.ts +++ b/libs/examples/src/combobox/combobox-form-field-example/combobox-form-field-example.ts @@ -1,4 +1,3 @@ -import { FormControl, Validators } from '@angular/forms'; /** * @license * Copyright 2021 Dynatrace LLC @@ -16,6 +15,7 @@ import { FormControl, Validators } from '@angular/forms'; */ import { ChangeDetectorRef, Component, ViewChild } from '@angular/core'; +import { FormControl, ValidatorFn } from '@angular/forms'; import { take } from 'rxjs/operators'; import { timer } from 'rxjs'; import { @@ -29,40 +29,36 @@ interface ExampleComboboxOption { } const allOptions: ExampleComboboxOption[] = [ - { name: 'Value 1', value: '[value: Value 1]' }, - { name: 'Value 2', value: '[value: Value 2]' }, - { name: 'Value 3', value: '[value: Value 3]' }, + { name: 'Espresso', value: 'espresso' }, + { name: 'Cappuccino', value: 'cappuccino' }, + { name: 'Americano', value: 'americano' }, + { name: 'No Coffee (Triggers an error)', value: 'noCoffee' }, ]; -/** - * Factory function for generating a filter function for a given filter string. - * - * @param filter Text to filter options for - */ -function optionFilter( - filter: string, -): (option: ExampleComboboxOption) => boolean { - return (option: ExampleComboboxOption): boolean => { - return option.value.toLowerCase().indexOf(filter.toLowerCase()) >= 0; +function coffeeValidator(): ValidatorFn { + return (control: FormControl): { [key: string]: boolean } | null => { + const isCoffee = control.value ? control.value.value !== 'noCoffee' : true; + return isCoffee ? null : { noCoffee: true }; }; } @Component({ - selector: 'dt-example-form-control-combobox', - templateUrl: './combobox-form-control-example.html', + selector: 'dt-example-form-field-combobox', + templateUrl: './combobox-form-field-example.html', }) -export class DtExampleComboboxFormControl { +export class DtExampleComboboxFormField { @ViewChild(DtCombobox) combobox: DtCombobox; - reporterCtrl: FormControl; + coffeeControl: FormControl; - _initialValue = allOptions[0]; _options = [...allOptions]; _loading = false; - _displayWith = (option: ExampleComboboxOption) => option.name; + + _displayCoffee = (value: ExampleComboboxOption) => + value.value === 'noCoffee' ? '' : value.name; constructor(private _changeDetectorRef: ChangeDetectorRef) { - this.reporterCtrl = new FormControl('Value 1', Validators.required); + this.coffeeControl = new FormControl(null, coffeeValidator()); } openedChanged(event: boolean): void { @@ -70,7 +66,9 @@ export class DtExampleComboboxFormControl { } valueChanged(event: ExampleComboboxOption): void { - this._options = [...allOptions].filter(optionFilter(event.value)); + this._options = [...allOptions].filter( + (option) => option.value === event.value, + ); } filterChanged(event: DtComboboxFilterChange): void { @@ -82,7 +80,9 @@ export class DtExampleComboboxFormControl { timer(1500) .pipe(take(1)) .subscribe(() => { - this._options = allOptions.filter(optionFilter(event.filter)); + this._options = allOptions.filter((option) => + option.name.toLowerCase().includes(event.filter.toLowerCase()), + ); this._loading = false; this._changeDetectorRef.markForCheck(); }); diff --git a/libs/examples/src/combobox/index.ts b/libs/examples/src/combobox/index.ts index 9728573227..6355426a18 100644 --- a/libs/examples/src/combobox/index.ts +++ b/libs/examples/src/combobox/index.ts @@ -15,6 +15,6 @@ */ export * from './combobox-simple-example/combobox-simple-example'; -export * from './combobox-form-control-example/combobox-form-control-example'; +export * from './combobox-form-field-example/combobox-form-field-example'; export * from './combobox-custom-option-height-example/combobox-custom-option-height-example'; export * from './combobox-examples.module'; diff --git a/libs/examples/src/index.ts b/libs/examples/src/index.ts index 7e5b9eb3eb..8a7c76352b 100644 --- a/libs/examples/src/index.ts +++ b/libs/examples/src/index.ts @@ -86,7 +86,7 @@ import { DtExampleCheckboxIndeterminate } from './checkbox/checkbox-indeterminat import { DtExampleCheckboxResponsive } from './checkbox/checkbox-responsive-example/checkbox-responsive-example'; import { DtExampleComboboxCustomOptionHeight } from './combobox/combobox-custom-option-height-example/combobox-custom-option-height-example'; import { DtExampleComboboxSimple } from './combobox/combobox-simple-example/combobox-simple-example'; -import { DtExampleComboboxFormControl } from './combobox/combobox-form-control-example/combobox-form-control-example'; +import { DtExampleComboboxFormField } from './combobox/combobox-form-field-example/combobox-form-field-example'; import { DtExampleConfirmationDialogDefault } from './confirmation-dialog/confirmation-dialog-default-example/confirmation-dialog-default-example'; import { DtExampleConfirmationDialogShowBackdrop } from './confirmation-dialog/confirmation-dialog-show-backdrop-example/confirmation-dialog-show-backdrop-example'; import { DtExampleConsumptionDefault } from './consumption/consumption-default-example/consumption-default-example'; @@ -476,8 +476,8 @@ export { DtExampleCheckboxIndeterminate, DtExampleCheckboxResponsive, DtExampleComboboxSimple, - DtExampleComboboxFormControl, DtExampleComboboxCustomOptionHeight, + DtExampleComboboxFormField, DtExampleConfirmationDialogDefault, DtExampleConfirmationDialogShowBackdrop, DtExampleConsumptionDefault, @@ -809,8 +809,8 @@ export const EXAMPLES_MAP = new Map>([ ['DtExampleCheckboxIndeterminate', DtExampleCheckboxIndeterminate], ['DtExampleCheckboxResponsive', DtExampleCheckboxResponsive], ['DtExampleComboboxSimple', DtExampleComboboxSimple], - ['DtExampleComboboxFormControl', DtExampleComboboxFormControl], ['DtExampleComboboxCustomOptionHeight', DtExampleComboboxCustomOptionHeight], + ['DtExampleComboboxFormField', DtExampleComboboxFormField], ['DtExampleConfirmationDialogDefault', DtExampleConfirmationDialogDefault], [ 'DtExampleConfirmationDialogShowBackdrop',