Skip to content

Commit

Permalink
feat(post 003): add keep open / loading
Browse files Browse the repository at this point in the history
  • Loading branch information
pdanpdan committed Nov 25, 2023
1 parent ca19bc1 commit 854934d
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 100 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"dependencies": {
"@vueuse/core": "^10.6.1",
"vue": "^3.3.8"
"vue": "^3.3.9"
},
"devDependencies": {
"@vue/eslint-config-airbnb": "^7.0.0",
Expand All @@ -20,7 +20,7 @@
"postcss": "^8.4.31",
"sass": "^1.69.5",
"vite-plugin-vitepress-demo": "^2.2.0",
"vitepress": "1.0.0-rc.30"
"vitepress": "1.0.0-rc.31"
},
"pnpm": {
"peerDependencyRules": {
Expand Down
95 changes: 78 additions & 17 deletions pages/posts/post_003.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ In order to make a Quasar QSelect filterable you need a `@filter` function that

Most of the time this requires an original (complete) list of options, a filtering function and a filtered list of options.

Also you may want to keep the options list opened while filtering (when you are using async filtering).

## Solution

You can create a composable `useFilteredSelect` that encapsulates and hides all the complexity.
Expand All @@ -34,6 +36,17 @@ This composable:
- the optional `props`
- an `options` key with the filtered list of options
- an `onFilter` event handler for QSelect
- a `loading` key with the loading state of the filter (see below)

::: info Keeping the list of options open while filtering
If you want to keep the list of options open while filtering then pass `loading: true` in the `props`.

In that case the returned reactive object will also include a `loading` key that will control the loading indicator on QSelect.
:::

::: warning
The filtered `options` and `loading` status are shared by all QSelects using the same returned reactive object.
:::

This composable will be combined with a filtering function. There are three pre-defined functions:
- one that filters the options as string, case insensitive, match anywhere
Expand All @@ -48,7 +61,7 @@ The search function generator `createMappedFilterFn` gets a configuration object

::: code-group
```ts [useFilteredSelect.ts]
import { shallowRef, unref, reactive, type MaybeRef, type ShallowRef } from 'vue';
import { shallowRef, ref, unref, reactive, type MaybeRef, type ShallowRef, type Ref } from 'vue';
import type { QSelect, QSelectProps } from 'quasar';

type MaybePromise<T> = T | Promise<T> | PromiseLike<T>;
Expand Down Expand Up @@ -90,28 +103,41 @@ export function useFilteredSelect<T, K extends keyof QSelectProps>(
) {
const getOptions = typeof optionsOrFn === 'function' ? optionsOrFn : () => optionsOrFn;
const filteredOpts = shallowRef(typeof optionsOrFn === 'function' ? [] : optionsOrFn);
const loading = ref(false);
const keepOpen = props && props['loading'] === true;

const useProps: Pick<QSelectProps, K> | { options: ShallowRef<QSelectProps[ 'options' ]>, onFilter: QSelectProps[ 'onFilter' ]; } = {
const useProps: Pick<QSelectProps, K> & { options: ShallowRef<QSelectProps[ 'options' ]>, onFilter: QSelectProps[ 'onFilter' ], loading?: Ref<boolean>; } = {
...props,
options: filteredOpts,
onFilter(
search: string,
doneFn: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
abortFn: () => void,
) {
if (keepOpen === true) {
doneFn(() => {});
}
loading.value = true;

Promise.resolve(getOptions(search))
.then((options) => {
const newOpts = filterFn(search, unref(options));
doneFn(() => {
filteredOpts.value = newOpts;
loading.value = false;
}, afterFn);
})
.catch(() => {
abortFn();
loading.value = false;
});
},
};

if (keepOpen === true) {
useProps.loading = loading;
}

return reactive(useProps);
}
```
Expand All @@ -125,7 +151,7 @@ export function useFilteredSelect<T, K extends keyof QSelectProps>(
label="String filter"
v-bind="filteredSelectProps1"
>
<template v-slot:no-option>
<template #no-option>
<q-item>
<q-item-section class="text-grey"> No results </q-item-section>
</q-item>
Expand All @@ -137,7 +163,7 @@ export function useFilteredSelect<T, K extends keyof QSelectProps>(
label="Obj filter - starts with"
v-bind="filteredSelectProps2"
>
<template v-slot:no-option>
<template #no-option>
<q-item>
<q-item-section class="text-grey"> No results </q-item-section>
</q-item>
Expand All @@ -149,7 +175,7 @@ export function useFilteredSelect<T, K extends keyof QSelectProps>(
label="Obj filter - mapped"
v-bind="filteredSelectProps3"
>
<template v-slot:no-option>
<template #no-option>
<q-item>
<q-item-section class="text-grey"> No results </q-item-section>
</q-item>
Expand All @@ -158,10 +184,11 @@ export function useFilteredSelect<T, K extends keyof QSelectProps>(
<q-select
v-model="model4"
label="String filter - async full list"
label="String filter - async get full list"
hint="Keep the list open on filtering"
v-bind="filteredSelectProps4"
>
<template v-slot:no-option>
<template #no-option>
<q-item>
<q-item-section class="text-grey"> No results </q-item-section>
</q-item>
Expand All @@ -170,10 +197,10 @@ export function useFilteredSelect<T, K extends keyof QSelectProps>(
<q-select
v-model="model5"
label="String filter - async filtered list"
label="String filter - async get filtered list"
v-bind="filteredSelectProps5"
>
<template v-slot:no-option>
<template #no-option>
<q-item>
<q-item-section class="text-grey"> No results </q-item-section>
</q-item>
Expand All @@ -184,15 +211,31 @@ export function useFilteredSelect<T, K extends keyof QSelectProps>(
</template>
<script lang="ts">
const stringOptions = ['Google', 'Facebook', 'Twitter', 'Apple', 'Oracle'];
const stringOptions = [
'Google',
'Facebook',
'Twitter',
'Apple',
'Oracle',
].reduce((acc, name) => {
for (let i = 100; i <= 300; i += 1) {
acc.push(`${name} - ${i}`);
}
return acc;
}, []);
const objOptions = [
{ label: 'Google', value: 1 },
{ label: 'Facebook', value: 2 },
{ label: 'Twitter', value: 3 },
{ label: 'Apple', value: 4 },
{ label: 'Oracle', value: 5 },
];
].reduce((acc, { label, value }) => {
for (let i = 100; i <= 300; i += 1) {
acc.push({ label: `${label} - ${i}`, value: `${value}#${i}` });
}
return acc;
}, []);
</script>
<script setup lang="ts">
Expand All @@ -207,17 +250,29 @@ import {
const filteredSelectProps1 = useFilteredSelect(
stringOptions,
undefined /* same as stringFunctionFn */,
{ outlined: true, useInput: true, inputDebounce: 0 },
{ outlined: true, useInput: true, inputDebounce: 0, behavior: 'menu' },
);
const filteredSelectProps2 = useFilteredSelect(
objOptions,
createMappedFilterFn({ key: 'label', compareType: 'startsWith' }),
{ filled: true, useInput: true, inputDebounce: 0, color: 'red' },
{
filled: true,
useInput: true,
inputDebounce: 0,
color: 'red',
behavior: 'menu',
},
);
const filteredSelectProps3 = useFilteredSelect(
objOptions,
createMappedFilterFn({ key: 'label' }),
{ useInput: true, mapOptions: true, emitValue: true, inputDebounce: 0 },
{
useInput: true,
mapOptions: true,
emitValue: true,
inputDebounce: 0,
behavior: 'menu',
},
);
const filteredSelectProps4 = useFilteredSelect(
() =>
Expand All @@ -227,7 +282,13 @@ const filteredSelectProps4 = useFilteredSelect(
}, 500);
}),
stringFilterFn /* same as undefined */,
{ outlined: true, useInput: true, inputDebounce: 0 },
{
outlined: true,
useInput: true,
inputDebounce: 0,
loading: true,
behavior: 'menu',
},
);
const filteredSelectProps5 = useFilteredSelect(
(search) =>
Expand All @@ -246,7 +307,7 @@ const filteredSelectProps5 = useFilteredSelect(
}, 500);
}),
noFilterFn,
{ outlined: true, useInput: true, inputDebounce: 0 },
{ outlined: true, useInput: true, inputDebounce: 0, behavior: 'menu' },
);
const model1 = ref(null);
Expand All @@ -262,5 +323,5 @@ const model5 = ref(null);

<code-frame
:title="$frontmatter.title"
src="https://pdanpdan.github.io/quasar-play/?file=src%2FuseFilteredSelect.ts&preview=t&previewMode=preview&editor=codemirror#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"
src="https://pdanpdan.github.io/quasar-play/?file=src%2FuseFilteredSelect.ts&preview=t&previewMode=preview&editor=codemirror#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"
/>
Loading

0 comments on commit 854934d

Please sign in to comment.