<!-- https://flowbite.com/docs/forms/input-field/ -->
<section class="m-8">
<div>
<label for="first_name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Typeahead</label>
<input type="text" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="use terms like qui, beat, eos, expl" required>
</div>
<ul class="max-w-md space-y-1 text-gray-500 list-disc list-inside dark:text-gray-400" id="results-container"></ul>
</section>
const {
MonoTypeOperatorFunction,
Observable,
debounceTime,
distinctUntilChanged,
filter,
from,
delay,
switchMap,
fromEvent,
map,
pipe,
timestamp,
pairwise,
timer,
OperatorFunction,
takeUntil,
shareReplay,
tap
} = rxjs;
interface ITypeaheadOperatorOptions<Out> {
/**
* The minimum length of the allowed search term.
*/
minLength: number;
/**
* The amount of time between key presses before making a request.
*/
debounceTime: number;
/**
* Whether to allow empty string to be treated as a valid search term.
* Useful for when you want to show defaul results when the user clears the search box
*
* @default true
*/
allowEmptyString?: boolean;
/**
* The function that will be called to load the results.
*/
loadFn: (searchTerm: string) => ObservableInput<Out>;
}
export function typeahead<Out>(
options: ITypeaheadOperatorOptions<Out>
): OperatorFunction<string, Out> {
const cache: Record<string, Observable<Out>> = {};
return (source) => {
return source.pipe(
debounceTime(options.debounceTime),
filter((value) => typeof value === "string"),
filter((value) => {
if (value === "") {
return options.allowEmptyString ?? true;
}
return value.length >= options.minLength;
}),
distinctUntilChanged(),
// switchMap((searchTerm) => options.loadFn(searchTerm))
switchMap((searchTerm) => {
if (!cache[searchTerm]) {
// Initialize Observable in cache if it doesn't exist
cache[searchTerm] = from(options.loadFn(searchTerm)).pipe(
takeUntil(timer(5000)),
shareReplay(),
);
}
// Return the cached observable
return cache[searchTerm];
})
);
};
}
const inputEl = document.getElementsByTagName("input").item(0);
const stream$ = fromEvent(inputEl, "input").pipe(map(() => inputEl.value));
const search$ = stream$.pipe(
typeahead({
debounceTime: 300,
minLength: 3,
loadFn: (searchTerm) =>
fetch(
`https://jsonplaceholder.typicode.com/posts?title_like=^${searchTerm}`
).then((response) => response.json())
})
);
const resultsContainerEl = document.getElementById("results-container");
search$.subscribe((results) => {
console.log(results);
resultsContainerEl.innerHTML = results
.map((result) => `<li>${result.title}</li>`)
.join("");
});
View Compiled