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',