Skip to content

Commit 2caa464

Browse files
authored
Merge pull request #55 from UiPath/feature/suggest_custom_template
feat(suggest): support for custom item template and size
2 parents 26bd17c + 5842a2f commit 2caa464

File tree

4 files changed

+172
-22
lines changed

4 files changed

+172
-22
lines changed

projects/angular/components/ui-suggest/src/ui-suggest.component.html

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -129,33 +129,43 @@
129129
[class.is-loading]="item.loading !== VirtualScrollItemStatus.loaded"
130130
[class.readonly]="item.loading !== VirtualScrollItemStatus.loaded"
131131
[class.selected]="!multiple &&
132-
isItemSelected(item)"
132+
isItemSelected(item)"
133+
[style.height.px]="itemSize"
133134
(click)="preventDefault($event);
134-
updateValue(item, !multiple, true);"
135+
updateValue(item, !multiple, true);"
135136
matRipple>
136-
<div [matTooltip]="disableTooltip ? null : item.text"
137-
matTooltipPosition="right"
138-
class="ui-suggest-item">
139-
<mat-checkbox *ngIf="multiple"
140-
[checked]="isItemSelected(item)"
141-
[indeterminate]="false">
142-
</mat-checkbox>
143-
<div [attr.aria-label]="item.text"
144-
class="text-label text-ellipsis"
145-
tabindex="-1">
146-
<mat-icon *ngIf="!!item?.icon?.svgIcon"
147-
[svgIcon]="item?.icon.svgIcon"></mat-icon>
148-
<mat-icon *ngIf="!!item?.icon?.matIcon">{{item?.icon.matIcon}}</mat-icon>
149-
{{ item?.icon?.iconOnly ? null : item.text }}
137+
<ng-container *ngIf="itemTemplate; else defaultItem">
138+
<ng-container *ngTemplateOutlet="itemTemplate; context: {
139+
$implicit: item
140+
}">
141+
</ng-container>
142+
</ng-container>
143+
<ng-template #defaultItem>
144+
<div [matTooltip]="disableTooltip ? null : item.text"
145+
matTooltipPosition="right"
146+
class="ui-suggest-item">
147+
<mat-checkbox *ngIf="multiple"
148+
[checked]="isItemSelected(item)"
149+
[indeterminate]="false">
150+
</mat-checkbox>
151+
<div [attr.aria-label]="item.text"
152+
class="text-label text-ellipsis"
153+
tabindex="-1">
154+
<mat-icon *ngIf="!!item?.icon?.svgIcon"
155+
[svgIcon]="item?.icon.svgIcon"></mat-icon>
156+
<mat-icon *ngIf="!!item?.icon?.matIcon">{{item?.icon.matIcon}}</mat-icon>
157+
{{ item?.icon?.iconOnly ? null : item.text }}
158+
</div>
150159
</div>
151-
</div>
160+
</ng-template>
152161
</mat-list-item>
153162
</ng-container>
154163

155164
<ng-container *ngTemplateOutlet="customValueTemplate; context: { $implicit: 'up' }">
156165
</ng-container>
157166

158-
<mat-list-item *ngIf="loading$ | async">
167+
<mat-list-item *ngIf="loading$ | async"
168+
[style.height.px]="itemSize">
159169
<mat-progress-spinner color="primary"
160170
mode="indeterminate"
161171
diameter="20"
@@ -180,6 +190,7 @@
180190
<mat-list-item *ngIf="isCustomValueVisible && direction === renderDirection"
181191
[class.active]="(renderDirection === 'down' ? -1 : items.length) === activeIndex"
182192
[matTooltip]="customValueLabelTranslator(inputControl.value)"
193+
[style.height.px]="itemSize"
183194
(click)="preventDefault($event); updateValue(inputControl.value, !multiple, true);"
184195
matTooltipPosition="right"
185196
class="text-ellipsis custom-item">

projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class UiSuggestFixture {
6262

6363
public clearable?: boolean;
6464
public searchable?: boolean;
65+
public alwaysExpanded?: boolean;
6566
public disabled?: boolean;
6667
public multiple?: boolean;
6768
public readonly?: boolean;
@@ -133,7 +134,7 @@ const sharedSpecifications = (
133134
expect(displayValue.nativeElement.innerText.trim()).toEqual(component.defaultValue);
134135
});
135136

136-
it('should remove NULL or Undefiend entries', () => {
137+
it('should remove NULL or Undefined entries', () => {
137138
const item = generateSuggestionItem();
138139
component.value = [undefined, null, null, undefined, item, null, undefined] as ISuggestValue[];
139140

@@ -238,6 +239,32 @@ const sharedSpecifications = (
238239
expect(itemListEntries.length).toEqual(0);
239240
});
240241

242+
it('should render the list open and not close on selection if alwaysExpanded is true', (async () => {
243+
const items = generateSuggetionItemList(10);
244+
245+
component.alwaysExpanded = true;
246+
component.items = items;
247+
248+
fixture.detectChanges();
249+
await fixture.whenStable();
250+
251+
const itemListEntries = fixture.debugElement.queryAll(By.css('.mat-list-item'));
252+
253+
expect(itemListEntries).not.toBeNull();
254+
expect(itemListEntries.length).toEqual(items.length);
255+
256+
const itemIndex = Math.floor(Math.random() * items.length);
257+
const currentListItem = fixture.debugElement.queryAll(
258+
By.css('.mat-list-item'),
259+
)[itemIndex];
260+
261+
currentListItem.nativeElement.dispatchEvent(EventGenerator.click);
262+
fixture.detectChanges();
263+
264+
expect(itemListEntries).not.toBeNull();
265+
expect(itemListEntries.length).toEqual(items.length);
266+
}));
267+
241268
it('should filter items if typed into', (done) => {
242269
let items = generateSuggetionItemList(40);
243270
const filteredItem: ISuggestValue = {
@@ -1037,9 +1064,9 @@ const sharedSpecifications = (
10371064
const word = faker.random.word();
10381065
const wordWithWhitespace = `${
10391066
Array(6).fill(' ').join('')
1040-
}${word}${
1067+
}${word}${
10411068
Array(6).fill(' ').join('')
1042-
}`;
1069+
}`;
10431070

10441071
searchFor(wordWithWhitespace, fixture);
10451072
await fixture.whenStable();
@@ -1597,6 +1624,7 @@ describe('Component: UiSuggest', () => {
15971624
[clearable]="clearable"
15981625
[searchable]="searchable"
15991626
[enableCustomValue]="enableCustomValue"
1627+
[alwaysExpanded]="alwaysExpanded"
16001628
[items]="items"
16011629
[value]="value"
16021630
[direction]="direction"
@@ -1673,6 +1701,7 @@ describe('Component: UiSuggest', () => {
16731701
[clearable]="clearable"
16741702
[searchable]="searchable"
16751703
[enableCustomValue]="enableCustomValue"
1704+
[alwaysExpanded]="alwaysExpanded"
16761705
[items]="items"
16771706
[value]="value"
16781707
[direction]="direction"
@@ -1837,4 +1866,83 @@ describe('Component: UiSuggest', () => {
18371866
});
18381867
});
18391868
});
1869+
1870+
1871+
@Component({
1872+
template: `
1873+
<ui-suggest [placeholder]="placeholder"
1874+
[defaultValue]="defaultValue"
1875+
[clearable]="clearable"
1876+
[searchable]="searchable"
1877+
[enableCustomValue]="enableCustomValue"
1878+
[alwaysExpanded]="alwaysExpanded"
1879+
[items]="items"
1880+
[value]="value"
1881+
[direction]="direction"
1882+
[displayPriority]="displayPriority"
1883+
[disabled]="disabled"
1884+
[multiple]="multiple"
1885+
[readonly]="readonly">
1886+
<ng-template let-item >
1887+
<div class="item-template">{{ item.text }}</div>
1888+
</ng-template>
1889+
</ui-suggest>
1890+
`,
1891+
})
1892+
class UiSuggestCustomTemplateFixtureComponent extends UiSuggestFixture { }
1893+
1894+
describe('Type: custom template', () => {
1895+
let fixture: ComponentFixture<UiSuggestCustomTemplateFixtureComponent>;
1896+
let component: UiSuggestCustomTemplateFixtureComponent;
1897+
1898+
const beforeEachFn = () => {
1899+
TestBed.configureTestingModule({
1900+
imports: [
1901+
UiSuggestModule,
1902+
ReactiveFormsModule,
1903+
MatInputModule,
1904+
NoopAnimationsModule,
1905+
],
1906+
declarations: [
1907+
UiSuggestCustomTemplateFixtureComponent,
1908+
],
1909+
});
1910+
1911+
const compFixture = TestBed.createComponent(UiSuggestCustomTemplateFixtureComponent);
1912+
1913+
return {
1914+
fixture: compFixture,
1915+
component: compFixture.componentInstance,
1916+
uiSuggest: compFixture.componentInstance.uiSuggest,
1917+
};
1918+
};
1919+
1920+
describe('Behavior: Specific', () => {
1921+
beforeEach(() => {
1922+
const setup = beforeEachFn();
1923+
fixture = setup.fixture;
1924+
component = setup.component;
1925+
});
1926+
1927+
it('should render the list items using the provided custom template', (async () => {
1928+
const items = generateSuggetionItemList(5);
1929+
component.items = items;
1930+
1931+
fixture.detectChanges();
1932+
const display = fixture.debugElement.query(By.css('.display'));
1933+
display.nativeElement.dispatchEvent(EventGenerator.click);
1934+
1935+
fixture.detectChanges();
1936+
await fixture.whenStable();
1937+
1938+
const generatedItems = fixture.debugElement.queryAll(By.css('ui-suggest .item-template'));
1939+
1940+
expect(items.length).toBe(generatedItems.length);
1941+
1942+
items.forEach((item, index) => {
1943+
expect(item.text).toBe(generatedItems[index].nativeElement.innerText);
1944+
});
1945+
}));
1946+
});
1947+
});
18401948
});

projects/angular/components/ui-suggest/src/ui-suggest.component.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ChangeDetectionStrategy,
77
ChangeDetectorRef,
88
Component,
9+
ContentChild,
910
ElementRef,
1011
EventEmitter,
1112
HostBinding,
@@ -16,6 +17,7 @@ import {
1617
Optional,
1718
Output,
1819
Self,
20+
TemplateRef,
1921
ViewChild,
2022
ViewEncapsulation,
2123
} from '@angular/core';
@@ -144,6 +146,15 @@ export class UiSuggestComponent extends UiSuggestMatFormField
144146
this._cd.detectChanges();
145147
}
146148

149+
150+
/**
151+
* If true, the item list will render open and will not close on selection
152+
*
153+
* @ignore
154+
*/
155+
@Input()
156+
public alwaysExpanded = false;
157+
147158
/**
148159
* Configure if the component allows multi-selection.
149160
*
@@ -223,6 +234,13 @@ export class UiSuggestComponent extends UiSuggestMatFormField
223234
this.searchSourceFactory = (searchTerm = '') => inMemorySearch(searchTerm, this._lastSetItems);
224235
}
225236

237+
/**
238+
* Reference for custom item template
239+
*
240+
*/
241+
@ContentChild(TemplateRef, { static: true })
242+
public itemTemplate: TemplateRef<any> | null = null;
243+
226244
/**
227245
* Computes the current tooltip value.
228246
*
@@ -508,6 +526,10 @@ export class UiSuggestComponent extends UiSuggestMatFormField
508526
* @ignore
509527
*/
510528
ngOnInit() {
529+
if (this.alwaysExpanded) {
530+
this.open();
531+
}
532+
511533
merge(
512534
this._reset$.pipe(
513535
map(_ => ''),
@@ -656,7 +678,7 @@ export class UiSuggestComponent extends UiSuggestMatFormField
656678
* @param [refocus=true] If the dropdown should be focused after closing.
657679
*/
658680
public close(refocus = true) {
659-
if (!this.isOpen) { return; }
681+
if (this.alwaysExpanded || !this.isOpen) { return; }
660682

661683
if (this._isOnCustomValueIndex && !this.loading$.value) {
662684
if (!this.multiple) {

projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,20 @@ export abstract class UiSuggestMatFormField implements
101101
this.stateChanges.next();
102102
}
103103

104+
/**
105+
* Set a custom size for the list items.
106+
*
107+
*/
108+
@Input()
109+
public customItemSize?: number;
110+
104111
/**
105112
* Computes the component item height depending on the current render mode.
106113
*
107114
*/
108115
public get itemSize() {
116+
if (this.customItemSize) { return this.customItemSize; }
117+
109118
return this.isFormControl ? 32 : 40;
110119
}
111120

0 commit comments

Comments
 (0)