Skip to content

Commit c06ad98

Browse files
fix: add loading indicator in dropdown and tagbox in survey questions
* fix/AB#71072_add-a-loading-icon-when-opening-the-select-questions feat: add filter options to dropdown and tagbox widget for surveyjs. Dropdown list would be disabled until related data is fully loaded from the survey question instance to the widget * fix/AB#71072_add-a-loading-icon-when-opening-the-select-questions feat: clean up widgets from console.logs * fix/AB#71072_add-a-loading-icon-when-opening-the-select-questions feat: add DEFAULT_VISIBLE_OPTIONS property in common-list-filters.ts * fix/AB#71072_add-a-loading-icon-when-opening-the-select-questions feat: update choices loading state if new instance is created but question already has choices loaded * fix/AB#71072_add-a-loading-icon-when-opening-the-select-questions fix: update disabled state on visiblechoices are updated * remove limit of 100 when no search is used in tagbox / dropdown in survey * fix build --------- Co-authored-by: Antoine Hurard <[email protected]>
1 parent a2b9de5 commit c06ad98

File tree

5 files changed

+175
-42
lines changed

5 files changed

+175
-42
lines changed

libs/safe/src/lib/components/ui/core-grid/array-filter-menu/array-filter-menu.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ export class SafeArrayFilterMenuComponent implements OnInit {
8989
) {}
9090

9191
ngOnInit(): void {
92-
this.choices1 = this.data.slice();
93-
this.choices2 = this.data.slice();
92+
this.choices1 = (this.data || []).slice();
93+
this.choices2 = (this.data || []).slice();
9494
this.form = this.fb.group({
9595
logic: this.filter.logic,
9696
filters: this.fb.array([

libs/safe/src/lib/components/ui/core-grid/dropdown-filter-menu/dropdown-filter-menu.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ export class SafeDropdownFilterMenuComponent implements OnInit {
7171
) {}
7272

7373
ngOnInit(): void {
74-
this.choices1 = this.data.slice();
75-
this.choices2 = this.data.slice();
74+
this.choices1 = (this.data || []).slice();
75+
this.choices2 = (this.data || []).slice();
7676
this.form = this.fb.group({
7777
logic: this.filter.logic,
7878
filters: this.fb.array([

libs/safe/src/lib/survey/widgets/dropdown-widget.ts

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { DomService } from '../../services/dom/dom.service';
33
import { Question } from '../types';
44
import { QuestionDropdown } from 'survey-knockout';
55
import { isArray, isObject } from 'lodash';
6+
import { Subscription, debounceTime, map, tap } from 'rxjs';
7+
import updateChoices from './utils/common-list-filters';
68

79
/**
810
* Init dropdown widget
@@ -11,6 +13,8 @@ import { isArray, isObject } from 'lodash';
1113
* @param domService Shared dom service
1214
*/
1315
export const init = (Survey: any, domService: DomService): void => {
16+
let currentSearchValue = '';
17+
const componentSubscriptions = new Subscription();
1418
const widget = {
1519
name: 'dropdown-widget',
1620
widgetIsLoaded: (): boolean => true,
@@ -29,29 +33,33 @@ export const init = (Survey: any, domService: DomService): void => {
2933
}
3034
dropdownInstance.placeholder = question.placeholder;
3135
dropdownInstance.readonly = question.isReadOnly;
32-
dropdownInstance.disabled = question.isReadOnly;
3336
dropdownInstance.registerOnChange((value: any) => {
3437
if (!isObject(value) && !isArray(value)) {
3538
question.value = value;
3639
}
3740
});
38-
const updateChoices = () => {
39-
if (question.visibleChoices && Array.isArray(question.visibleChoices)) {
40-
dropdownInstance.data = question.visibleChoices.map((choice: any) =>
41-
typeof choice === 'string'
42-
? {
43-
text: choice,
44-
value: choice,
45-
}
46-
: {
47-
text: choice.text,
48-
value: choice.value,
49-
}
50-
);
51-
}
52-
};
41+
42+
// We subscribe to whatever you write on the field so we can filter the data accordingly
43+
componentSubscriptions.add(
44+
dropdownInstance.filterChange
45+
.pipe(
46+
debounceTime(500), // Debounce time to limit quantity of updates
47+
tap(() => (dropdownInstance.loading = true)),
48+
map((searchValue: string) => searchValue?.toLowerCase()) // Make the filter non-case sensitive
49+
)
50+
.subscribe((searchValue: string) => {
51+
currentSearchValue = searchValue;
52+
updateChoices(dropdownInstance, question, searchValue);
53+
})
54+
);
55+
5356
question._propertyValueChangedVirtual = () => {
54-
updateChoices();
57+
updateChoices(
58+
dropdownInstance,
59+
question,
60+
currentSearchValue,
61+
Boolean(question.value)
62+
);
5563
};
5664
question.registerFunctionOnPropertyValueChanged(
5765
'visibleChoices',
@@ -67,7 +75,14 @@ export const init = (Survey: any, domService: DomService): void => {
6775
dropdownInstance.disabled = value;
6876
}
6977
);
70-
updateChoices();
78+
if (question.visibleChoices.length) {
79+
updateChoices(
80+
dropdownInstance,
81+
question,
82+
currentSearchValue,
83+
Boolean(question.value)
84+
);
85+
}
7186
el.parentElement?.appendChild(dropdownDiv);
7287
},
7388
willUnmount: (question: any): void => {
@@ -79,6 +94,7 @@ export const init = (Survey: any, domService: DomService): void => {
7994
question._propertyValueChangedVirtual
8095
);
8196
question._propertyValueChangedVirtual = undefined;
97+
componentSubscriptions.unsubscribe();
8298
},
8399
};
84100

@@ -98,6 +114,9 @@ export const init = (Survey: any, domService: DomService): void => {
98114
itemHeight: 28,
99115
};
100116
dropdownInstance.valuePrimitive = true;
117+
dropdownInstance.filterable = true;
118+
dropdownInstance.loading = true;
119+
dropdownInstance.disabled = true;
101120
dropdownInstance.textField = 'text';
102121
dropdownInstance.valueField = 'value';
103122
return dropdownInstance;

libs/safe/src/lib/survey/widgets/tagbox-widget.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { MultiSelectComponent } from '@progress/kendo-angular-dropdowns';
22
import { DomService } from '../../services/dom/dom.service';
33
import { Question } from '../types';
4+
import { Subscription, debounceTime, map, tap } from 'rxjs';
5+
import updateChoices from './utils/common-list-filters';
46

57
/**
68
* Init tagbox question
@@ -16,7 +18,8 @@ export const init = (Survey: any, domService: DomService): void => {
1618
'tagbox',
1719
'<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g><path d="M15,11H0V5h15V11z M1,10h13V6H1V10z"/></g><rect x="2" y="7" width="4" height="2"/><rect x="7" y="7" width="4" height="2"/></svg>'
1820
);
19-
21+
let currentSearchValue = '';
22+
const componentSubscriptions = new Subscription();
2023
const componentName = 'tagbox';
2124
const widget = {
2225
name: 'tagbox',
@@ -28,7 +31,6 @@ export const init = (Survey: any, domService: DomService): void => {
2831
},
2932
widgetIsLoaded: (): boolean => true,
3033
isFit: (question: Question): boolean => {
31-
console.log(question);
3234
return question.getType() === componentName;
3335
},
3436
init: () => {
@@ -69,27 +71,31 @@ export const init = (Survey: any, domService: DomService): void => {
6971
tagboxInstance.value = question.value;
7072
tagboxInstance.placeholder = question.placeholder;
7173
tagboxInstance.readonly = question.isReadOnly;
72-
tagboxInstance.disabled = question.isReadOnly;
7374
tagboxInstance.registerOnChange((value: any) => {
7475
question.value = value;
7576
});
76-
const updateChoices = () => {
77-
if (question.visibleChoices && Array.isArray(question.visibleChoices)) {
78-
tagboxInstance.data = question.visibleChoices.map((choice: any) =>
79-
typeof choice === 'string'
80-
? {
81-
text: choice,
82-
value: choice,
83-
}
84-
: {
85-
text: choice.text,
86-
value: choice.value,
87-
}
88-
);
89-
}
90-
};
77+
78+
// We subscribe to whatever you write on the field so we can filter the data accordingly
79+
componentSubscriptions.add(
80+
tagboxInstance.filterChange
81+
.pipe(
82+
debounceTime(500), // Debounce time to limit quantity of updates
83+
tap(() => (tagboxInstance.loading = true)),
84+
map((searchValue: string) => searchValue?.toLowerCase()) // Make the filter non-case sensitive
85+
)
86+
.subscribe((searchValue: string) => {
87+
currentSearchValue = searchValue;
88+
updateChoices(tagboxInstance, question, searchValue);
89+
})
90+
);
91+
9192
question._propertyValueChangedVirtual = () => {
92-
updateChoices();
93+
updateChoices(
94+
tagboxInstance,
95+
question,
96+
currentSearchValue,
97+
Boolean(question.value)
98+
);
9399
};
94100
question.registerFunctionOnPropertyValueChanged(
95101
'visibleChoices',
@@ -102,7 +108,14 @@ export const init = (Survey: any, domService: DomService): void => {
102108
tagboxInstance.disabled = value;
103109
}
104110
);
105-
updateChoices();
111+
if (question.visibleChoices.length) {
112+
updateChoices(
113+
tagboxInstance,
114+
question,
115+
currentSearchValue,
116+
Boolean(question.value)
117+
);
118+
}
106119
el.parentElement?.appendChild(tagboxDiv);
107120
},
108121
willUnmount: (question: any): void => {
@@ -114,6 +127,7 @@ export const init = (Survey: any, domService: DomService): void => {
114127
question._propertyValueChangedVirtual
115128
);
116129
question._propertyValueChangedVirtual = undefined;
130+
componentSubscriptions.unsubscribe();
117131
},
118132
};
119133

@@ -133,6 +147,9 @@ export const init = (Survey: any, domService: DomService): void => {
133147
itemHeight: 28,
134148
};
135149
tagboxInstance.valuePrimitive = true;
150+
tagboxInstance.filterable = true;
151+
tagboxInstance.loading = true;
152+
tagboxInstance.disabled = true;
136153
tagboxInstance.textField = 'text';
137154
tagboxInstance.valueField = 'value';
138155
return tagboxInstance;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
ComboBoxComponent,
3+
MultiSelectComponent,
4+
} from '@progress/kendo-angular-dropdowns';
5+
6+
/**
7+
* Default options length displayed in the widgets list
8+
*/
9+
const DEFAULT_VISIBLE_OPTIONS = 100;
10+
11+
/**
12+
* Update visible choices of the given widget and attached question choices with the given search value
13+
*
14+
* @param widget Widget shown in the surveyjs question
15+
* @param surveyQuestion surveyjs question instance for the given widget
16+
* @param searchValue search value to filter item list
17+
* @param hasDefaultValue if question contains a value set
18+
*/
19+
function updateChoices(
20+
widget: ComboBoxComponent | MultiSelectComponent,
21+
surveyQuestion: any,
22+
searchValue: string = '',
23+
hasDefaultValue: boolean = false
24+
) {
25+
if (searchValue === '') {
26+
// Without search value uses virtualization
27+
widget.data = surveyQuestion.visibleChoices.map((choice: any) =>
28+
typeof choice === 'string'
29+
? {
30+
text: choice,
31+
value: choice,
32+
}
33+
: {
34+
text: choice.text,
35+
value: choice.value,
36+
}
37+
);
38+
} else {
39+
// Filters the data to those that include the search value and sets the choices to the first 100
40+
if (
41+
surveyQuestion.visibleChoices &&
42+
Array.isArray(surveyQuestion.visibleChoices)
43+
) {
44+
const filterData = surveyQuestion.visibleChoices.filter((choice: any) =>
45+
typeof choice === 'string'
46+
? choice.toLowerCase().includes(searchValue)
47+
: choice.text.toLowerCase().includes(searchValue)
48+
);
49+
const dataToShow = filterData
50+
.map((choice: any) =>
51+
typeof choice === 'string'
52+
? {
53+
text: choice,
54+
value: choice,
55+
}
56+
: {
57+
text: choice.text,
58+
value: choice.value,
59+
}
60+
)
61+
.slice(0, DEFAULT_VISIBLE_OPTIONS);
62+
widget.data = dataToShow;
63+
}
64+
}
65+
66+
if (hasDefaultValue) {
67+
const choicesAux = surveyQuestion.visibleChoices.filter((ch: any) => {
68+
const searchValue = typeof ch === 'string' ? ch : ch.value;
69+
return widget instanceof ComboBoxComponent
70+
? searchValue === surveyQuestion.value
71+
: surveyQuestion.value.includes(searchValue);
72+
});
73+
74+
// Set the default values selected at the start of the list
75+
if (choicesAux.length) {
76+
widget.data = [
77+
...choicesAux.map((choice: any) =>
78+
typeof choice === 'string'
79+
? {
80+
text: choice,
81+
value: choice,
82+
}
83+
: {
84+
text: choice.text,
85+
value: choice.value,
86+
}
87+
),
88+
...widget.data,
89+
];
90+
}
91+
}
92+
93+
widget.loading = false;
94+
widget.disabled = surveyQuestion.isReadOnly;
95+
widget.wrapper.nativeElement.click();
96+
}
97+
export default updateChoices;

0 commit comments

Comments
 (0)