From 5842a2fe89a94c36702892f81383a208d208b297 Mon Sep 17 00:00:00 2001 From: Adrian Macuc Date: Tue, 3 Sep 2019 17:14:05 +0300 Subject: [PATCH] feat(suggest): support for custom item template and size --- .../ui-suggest/src/ui-suggest.component.html | 47 +++++--- .../src/ui-suggest.component.spec.ts | 114 +++++++++++++++++- .../ui-suggest/src/ui-suggest.component.ts | 24 +++- .../src/ui-suggest.mat-form-field.ts | 9 ++ 4 files changed, 172 insertions(+), 22 deletions(-) diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.html b/projects/angular/components/ui-suggest/src/ui-suggest.component.html index 94c32d76d..dc7c1d5e4 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.html +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.html @@ -129,33 +129,43 @@ [class.is-loading]="item.loading !== VirtualScrollItemStatus.loaded" [class.readonly]="item.loading !== VirtualScrollItemStatus.loaded" [class.selected]="!multiple && - isItemSelected(item)" + isItemSelected(item)" + [style.height.px]="itemSize" (click)="preventDefault($event); - updateValue(item, !multiple, true);" + updateValue(item, !multiple, true);" matRipple> -
- - -
- - {{item?.icon.matIcon}} - {{ item?.icon?.iconOnly ? null : item.text }} + + + + + +
+ + +
+ + {{item?.icon.matIcon}} + {{ item?.icon?.iconOnly ? null : item.text }} +
-
+ - + diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts b/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts index b1e4c4ffe..4f842e22b 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts @@ -62,6 +62,7 @@ class UiSuggestFixture { public clearable?: boolean; public searchable?: boolean; + public alwaysExpanded?: boolean; public disabled?: boolean; public multiple?: boolean; public readonly?: boolean; @@ -133,7 +134,7 @@ const sharedSpecifications = ( expect(displayValue.nativeElement.innerText.trim()).toEqual(component.defaultValue); }); - it('should remove NULL or Undefiend entries', () => { + it('should remove NULL or Undefined entries', () => { const item = generateSuggestionItem(); component.value = [undefined, null, null, undefined, item, null, undefined] as ISuggestValue[]; @@ -238,6 +239,32 @@ const sharedSpecifications = ( expect(itemListEntries.length).toEqual(0); }); + it('should render the list open and not close on selection if alwaysExpanded is true', (async () => { + const items = generateSuggetionItemList(10); + + component.alwaysExpanded = true; + component.items = items; + + fixture.detectChanges(); + await fixture.whenStable(); + + const itemListEntries = fixture.debugElement.queryAll(By.css('.mat-list-item')); + + expect(itemListEntries).not.toBeNull(); + expect(itemListEntries.length).toEqual(items.length); + + const itemIndex = Math.floor(Math.random() * items.length); + const currentListItem = fixture.debugElement.queryAll( + By.css('.mat-list-item'), + )[itemIndex]; + + currentListItem.nativeElement.dispatchEvent(EventGenerator.click); + fixture.detectChanges(); + + expect(itemListEntries).not.toBeNull(); + expect(itemListEntries.length).toEqual(items.length); + })); + it('should filter items if typed into', (done) => { let items = generateSuggetionItemList(40); const filteredItem: ISuggestValue = { @@ -1037,9 +1064,9 @@ const sharedSpecifications = ( const word = faker.random.word(); const wordWithWhitespace = `${ Array(6).fill(' ').join('') - }${word}${ + }${word}${ Array(6).fill(' ').join('') - }`; + }`; searchFor(wordWithWhitespace, fixture); await fixture.whenStable(); @@ -1597,6 +1624,7 @@ describe('Component: UiSuggest', () => { [clearable]="clearable" [searchable]="searchable" [enableCustomValue]="enableCustomValue" + [alwaysExpanded]="alwaysExpanded" [items]="items" [value]="value" [direction]="direction" @@ -1673,6 +1701,7 @@ describe('Component: UiSuggest', () => { [clearable]="clearable" [searchable]="searchable" [enableCustomValue]="enableCustomValue" + [alwaysExpanded]="alwaysExpanded" [items]="items" [value]="value" [direction]="direction" @@ -1837,4 +1866,83 @@ describe('Component: UiSuggest', () => { }); }); }); + + + @Component({ + template: ` + + +
{{ item.text }}
+
+
+ `, + }) + class UiSuggestCustomTemplateFixtureComponent extends UiSuggestFixture { } + + describe('Type: custom template', () => { + let fixture: ComponentFixture; + let component: UiSuggestCustomTemplateFixtureComponent; + + const beforeEachFn = () => { + TestBed.configureTestingModule({ + imports: [ + UiSuggestModule, + ReactiveFormsModule, + MatInputModule, + NoopAnimationsModule, + ], + declarations: [ + UiSuggestCustomTemplateFixtureComponent, + ], + }); + + const compFixture = TestBed.createComponent(UiSuggestCustomTemplateFixtureComponent); + + return { + fixture: compFixture, + component: compFixture.componentInstance, + uiSuggest: compFixture.componentInstance.uiSuggest, + }; + }; + + describe('Behavior: Specific', () => { + beforeEach(() => { + const setup = beforeEachFn(); + fixture = setup.fixture; + component = setup.component; + }); + + it('should render the list items using the provided custom template', (async () => { + const items = generateSuggetionItemList(5); + component.items = items; + + fixture.detectChanges(); + const display = fixture.debugElement.query(By.css('.display')); + display.nativeElement.dispatchEvent(EventGenerator.click); + + fixture.detectChanges(); + await fixture.whenStable(); + + const generatedItems = fixture.debugElement.queryAll(By.css('ui-suggest .item-template')); + + expect(items.length).toBe(generatedItems.length); + + items.forEach((item, index) => { + expect(item.text).toBe(generatedItems[index].nativeElement.innerText); + }); + })); + }); + }); }); diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.ts b/projects/angular/components/ui-suggest/src/ui-suggest.component.ts index 9b3d84ad5..51e194386 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.ts +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.ts @@ -6,6 +6,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + ContentChild, ElementRef, EventEmitter, HostBinding, @@ -16,6 +17,7 @@ import { Optional, Output, Self, + TemplateRef, ViewChild, ViewEncapsulation, } from '@angular/core'; @@ -144,6 +146,15 @@ export class UiSuggestComponent extends UiSuggestMatFormField this._cd.detectChanges(); } + + /** + * If true, the item list will render open and will not close on selection + * + * @ignore + */ + @Input() + public alwaysExpanded = false; + /** * Configure if the component allows multi-selection. * @@ -223,6 +234,13 @@ export class UiSuggestComponent extends UiSuggestMatFormField this.searchSourceFactory = (searchTerm = '') => inMemorySearch(searchTerm, this._lastSetItems); } + /** + * Reference for custom item template + * + */ + @ContentChild(TemplateRef, { static: true }) + public itemTemplate: TemplateRef | null = null; + /** * Computes the current tooltip value. * @@ -508,6 +526,10 @@ export class UiSuggestComponent extends UiSuggestMatFormField * @ignore */ ngOnInit() { + if (this.alwaysExpanded) { + this.open(); + } + merge( this._reset$.pipe( map(_ => ''), @@ -656,7 +678,7 @@ export class UiSuggestComponent extends UiSuggestMatFormField * @param [refocus=true] If the dropdown should be focused after closing. */ public close(refocus = true) { - if (!this.isOpen) { return; } + if (this.alwaysExpanded || !this.isOpen) { return; } if (this._isOnCustomValueIndex && !this.loading$.value) { if (!this.multiple) { diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts b/projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts index c88346503..d0c9654ec 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts +++ b/projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts @@ -101,11 +101,20 @@ export abstract class UiSuggestMatFormField implements this.stateChanges.next(); } + /** + * Set a custom size for the list items. + * + */ + @Input() + public customItemSize?: number; + /** * Computes the component item height depending on the current render mode. * */ public get itemSize() { + if (this.customItemSize) { return this.customItemSize; } + return this.isFormControl ? 32 : 40; }