Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
feat(combobox): Add form control support for combobox.
Browse files Browse the repository at this point in the history
  • Loading branch information
moterink authored and tomheller committed Jan 26, 2022
1 parent 080cb9e commit 1fe41cc
Show file tree
Hide file tree
Showing 15 changed files with 324 additions and 96 deletions.
14 changes: 7 additions & 7 deletions apps/demos/src/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ import {
DtExampleConsumptionError,
DtExampleConsumptionWarning,
DtExampleComboboxSimple,
DtExampleComboboxFormControl,
DtExampleContainerBreakpointObserverDefault,
DtExampleContainerBreakpointObserverIfElse,
DtExampleContainerBreakpointObserverIf,
Expand Down Expand Up @@ -333,6 +332,7 @@ import {
DtExampleTreeTableProblemIndicator,
DtExampleTreeTableSimple,
DtExampleComboboxCustomOptionHeight,
DtExampleComboboxFormField,
DtExampleSelectCustomValueTemplate,
DtExampleCalendarMinMax,
DtExampleTimepickerMinMax,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 },
Expand Down
4 changes: 4 additions & 0 deletions apps/demos/src/nav-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
],
},
{
Expand Down
9 changes: 5 additions & 4 deletions libs/barista-components/experimental/combobox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ automatically change the height of the options.

<ba-live-example name="DtExampleComboboxCustomOptionHeight"></ba-live-example>

## 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 `<dt-form-field>`. These include error
messages, hint text, prefix & suffix. For additional information about these
features, see the [form field documentation](/components/form-field).

<ba-live-example name="DtExampleComboboxFormControl"></ba-live-example>
<ba-live-example name="DtExampleComboboxFormField"></ba-live-example>
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -40,5 +38,7 @@ import { DtOptionModule } from '@dynatrace/barista-components/core';
DtLoadingDistractorModule,
PortalModule,
],
exports: [DtCombobox, DtOptionModule],
declarations: [DtCombobox],
})
export class DtComboboxModule {}
134 changes: 128 additions & 6 deletions libs/barista-components/experimental/combobox/src/combobox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -49,22 +59,25 @@ describe('Combobox', () => {
let overlayContainer: OverlayContainer;
let overlayContainerElement: HTMLElement;

let fixture: ComponentFixture<TestComponent>;
let input: HTMLInputElement;
let trigger: HTMLDivElement;
let combobox: DtCombobox<unknown>;
//let zone: MockNgZone;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
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()) },
],
Expand All @@ -83,6 +96,11 @@ describe('Combobox', () => {
});

describe('Basic', () => {
let fixture: ComponentFixture<TestComponent>;
let input: HTMLInputElement;
let trigger: HTMLDivElement;
let combobox: DtCombobox<unknown>;

beforeEach(() => {
fixture = createComponent(TestComponent);
combobox = fixture.debugElement.query(
Expand Down Expand Up @@ -141,6 +159,80 @@ describe('Combobox', () => {
}));
});

describe('inside of a form group', () => {
let fixture: ComponentFixture<ComboboxInsideFormGroup>;
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 {
Expand All @@ -161,7 +253,7 @@ describe('Combobox', () => {
<dt-combobox
(opened)="openedSpy()"
(closed)="closedSpy()"
[value]="value$ | async"
[value]="(value$ | async)!"
placeholder="My placeholder"
[compareWith]="compareFn"
[displayWith]="displayFn"
Expand Down Expand Up @@ -217,3 +309,33 @@ class TestLoadingComponent {
];
loading = true;
}

@Component({
template: `
<form [formGroup]="formGroup">
<dt-form-field>
<dt-combobox formControlName="value">
<dt-option *ngFor="let option of options" [value]="option.value">
{{ option.name }}
</dt-option>
</dt-combobox>
<dt-error>This field is required</dt-error>
</dt-form-field>
</form>
`,
})
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<any>;
formControl = new FormControl(null, Validators.required);
formGroup = new FormGroup({
value: this.formControl,
});
}
Loading

0 comments on commit 1fe41cc

Please sign in to comment.