diff --git a/.changeset/young-waves-vanish.md b/.changeset/young-waves-vanish.md new file mode 100644 index 000000000..d79c2d091 --- /dev/null +++ b/.changeset/young-waves-vanish.md @@ -0,0 +1,6 @@ +--- +"@getodk/xforms-engine": patch +"@getodk/web-forms": patch +--- + +Fixed performance issues when rendering a select with options from a large entity list diff --git a/packages/web-forms/src/components/common/MultiselectDropdown.vue b/packages/web-forms/src/components/common/MultiselectDropdown.vue index ac731a9fb..178bceb05 100644 --- a/packages/web-forms/src/components/common/MultiselectDropdown.vue +++ b/packages/web-forms/src/components/common/MultiselectDropdown.vue @@ -14,15 +14,12 @@ interface MultiselectDropdownProps { const t: Translate = inject(TRANSLATE)!; const props = defineProps(); +const DEFAULT_PRIMEVUE_ITEM_HEIGHT = 38; + defineEmits(['update:modelValue', 'change']); const options = computed(() => { return props.question.currentState.valueOptions.map((option) => { - const label = props.question.getValueOption(option.value); - if (label == null) { - throw new Error(`Failed to find option for value: ${option.value}`); - } - return { value: option.value, label: option.label.formatted, @@ -42,9 +39,9 @@ if (props.question.appearances['no-buttons']) { const selectedLabels = computed(() => { const state = props.question.currentState; - return state.value.map((val) => { - const found = state.valueOptions.find((opt) => opt.value === val); - return found?.label.formatted; + return state.value.map((value) => { + const option = props.question.getValueOption(value); + return option?.label.formatted; }); }); @@ -68,6 +65,7 @@ const selectedLabels = computed(() => { option-label="search" :panel-class="panelClass" :model-value="question.currentState.value" + :virtual-scroller-options="{ itemSize: DEFAULT_PRIMEVUE_ITEM_HEIGHT }" @update:model-value="selectValues" @change="$emit('change')" > diff --git a/packages/web-forms/src/components/common/SearchableDropdown.vue b/packages/web-forms/src/components/common/SearchableDropdown.vue index df4935816..a29ff31c3 100644 --- a/packages/web-forms/src/components/common/SearchableDropdown.vue +++ b/packages/web-forms/src/components/common/SearchableDropdown.vue @@ -14,15 +14,12 @@ interface SearchableDropdownProps { const t: Translate = inject(TRANSLATE)!; const props = defineProps(); +const DEFAULT_PRIMEVUE_ITEM_HEIGHT = 38; + defineEmits(['update:modelValue', 'change']); const options = computed(() => { return props.question.currentState.valueOptions.map((option) => { - const label = props.question.getValueOption(option.value); - if (label == null) { - throw new Error(`Failed to find option for value: ${option.value}`); - } - return { value: option.value, label: option.label.formatted, @@ -36,9 +33,8 @@ const selectedLabel = computed(() => { if (!value) { return []; } - const valueOptions = props.question.currentState.valueOptions; - const found = valueOptions.find((opt) => opt.value === value); - return found?.label.formatted; + const option = props.question.getValueOption(value); + return option?.label.formatted; }); const selectValue = (value: string) => { @@ -58,6 +54,7 @@ const selectValue = (value: string) => { :options="options" option-label="search" option-value="value" + :virtual-scroller-options="{ itemSize: DEFAULT_PRIMEVUE_ITEM_HEIGHT }" @update:model-value="selectValue" @change="$emit('change')" > diff --git a/packages/xforms-engine/src/instance/SelectControl.ts b/packages/xforms-engine/src/instance/SelectControl.ts index 5b3a905c0..be4506334 100644 --- a/packages/xforms-engine/src/instance/SelectControl.ts +++ b/packages/xforms-engine/src/instance/SelectControl.ts @@ -212,12 +212,7 @@ export class SelectControl // SelectNode getValueOption(value: string): SelectItem | null { - // Note: this method is a client-facing convenience API for reading state, - // so it **MUST** read from client-reactive state! - const valueOption = this.currentState.valueOptions.find((item) => { - return item.value === value; - }); - + const valueOption = this.mapOptionsByValue().get(value); return valueOption ?? null; }