How to implement multiple typeahead search input in sveltekit with api request?

275 Views Asked by At

I'm trying to implement a feature where every time a user types into an input field, an API request is made, and the results are displayed directly below the input. It's similar to a typeahead or autocomplete functionality. While I could potentially make the request directly from the client and display the results, I need to handle the API request on the server side. This is because all requests must include a token in the header, and the cookie where the token is stored is set to httpOnly:true. Thus, I cannot access this cookie from the client side. Can someone guide me on how to achieve this? Any advice is welcome. Thank you

<script lang="ts">
    import { Button, Input, Label } from 'flowbite-svelte';

    export let data;
    let taskId = '';

    const onChange = async () => {};
</script>

<div class="max-w-screen-md mx-auto p-6 space-y-8">
    <div class="bg-white shadow-md rounded-lg p-6">
        <div class="text-center mb-8">
            <h2 class="text-2xl bold">Task Form</h2>
        </div>
        <div class="relative">
            <Label class="block mb-1">Task ID</Label>
            <Input
                label="Email"
                id="email"
                name="email"
                bind:value={taskId}
                on:change={onChange}
                required
                placeholder="Task id..."
            />

            <br />

            <Label class="block mb-1">유저 닉네임</Label>
            <Input label="nickname" id="nickname" name="nickname" required placeholder="nickname..." />
        </div>
    </div>

    <Button type="submit">Create</Button>
</div>


1

There are 1 best solutions below

0
Jay0813 On

Problem Statement: I aimed to implement a feature where every time a user inputs into a field, an API request is triggered, and the results are displayed right below the input. It's crucial to note that the API request needs to be handled server-side, not directly from the client. This is due to the necessity to include a token in the request header, which is stored in a cookie set to httpOnly: true. Thus, it's inaccessible from the client side.

Client-side Code:

To avoid making an immediate API request upon every keystroke, I employed a debounce functionality. This ensures the function only gets executed after the user has stopped typing for a specified duration. I manage the input values for both taskId and nickname using separate state variables. On each input, the debounce-wrapped API call functions, _handleTaskInput and _handleNicknameInput, are invoked. The results are stored in taskResults and nicknameResults and are subsequently displayed to the user.

Server-side Code:

This segment of the code handles the request sent from the client. It processes the POST request for the /api/task endpoint. The taskId value is extracted from the request body and based on it, a mock result data is generated and returned. In a real-world scenario, this section would typically query an external API or database to fetch the actual data.

Note: When making a fetch call to '/api/task' from the client, it corresponds to executing server-side code located at /routes/api/task/+server.ts.

Here's my code.

// client side
<script lang="ts">
    import debounce from '$lib/utils/debounce';
    import { Button, Input, Label } from 'flowbite-svelte';

    let taskId = '';
    let nickname = '';
    let nicknameResults: any[];
    let taskResults: any[];

    async function _getTaskList() {
        const response = await fetch('/api/task', {
            method: 'POST',
            body: JSON.stringify({ taskId }),
            headers: {
                'content-type': 'application/json'
            }
        });

        taskResults = await response.json();
    }

    async function _getNicknameList() {
        const response = await fetch('/api/user', {
            method: 'POST',
            body: JSON.stringify({ nickname }),
            headers: {
                'content-type': 'application/json'
            }
        });

        nicknameResults = await response.json();
    }

    function _selectTaskResult(result: { id: number; name: string }) {
        taskId = result.id.toString();
        taskResults = []; // 선택한 후 결과 목록을 숨깁니다.
    }

    function _selectNicknameResult(result: { id: number; name: string }) {
        nickname = result.id.toString();
        nicknameResults = []; // 선택한 후 결과 목록을 숨깁니다.
    }

    const _debouncedTaskList = debounce(_getTaskList, 300);
    const _debouncedNicknameList = debounce(_getNicknameList, 300);

    function _handleTaskInput(event: Event) {
        _debouncedTaskList();
    }

    function _handleNicknameInput(event: Event) {
        _debouncedNicknameList();
    }

    $: console.log({ nickname });
</script>

<div class="max-w-screen-md mx-auto p-6 space-y-8">
    <div class="bg-white shadow-md rounded-lg p-6 min-h-[800px]">
        <div class="text-center mb-8">
            <h2 class="text-2xl bold">Create Task</h2>
        </div>
        <div class="relative">
            <div class="relative">
                <Label class="block mb-1">taskId</Label>
                <Input
                    label="taskId"
                    id="taskId"
                    name="taskId"
                    bind:value={taskId}
                    on:input={_handleTaskInput}
                    required
                    placeholder="task id"
                />
                {#if taskResults && taskResults.length > 0}
                    <div class="absolute w-full top-16 mt-2 bg-white border border-gray-300 z-10">
                        {#each taskResults as taskResult}
                            <div
                                on:keydown={() => _selectTaskResult(taskResult)}
                                on:click={() => _selectTaskResult(taskResult)}
                                class="cursor-pointer hover:bg-gray-200 p-2"
                            >
                                {taskResult.id}
                            </div>
                        {/each}
                    </div>
                {/if}
            </div>
            <br />

            <div class="relative">
                <Label class="block mb-1">nickname</Label>
                <Input
                    label="nickname"
                    id="nickname"
                    name="nickname"
                    bind:value={nickname}
                    on:input={_handleNicknameInput}
                    required
                    placeholder="nickname"
                />

                {#if nicknameResults && nicknameResults.length > 0}
                    <div class="absolute w-full top-16 mt-2 bg-white border border-gray-300 z-10">
                        {#each nicknameResults as nicknameResult}
                            <div
                                on:keydown={() => _selectNicknameResult(nicknameResult)}
                                on:click={() => _selectNicknameResult(nicknameResult)}
                                class="cursor-pointer hover:bg-gray-200 p-2"
                            >
                                {nicknameResult.id}
                            </div>
                        {/each}
                    </div>
                {/if}
            </div>
        </div>
    </div>

    <Button type="submit">Create</Button>
</div>

// server side 
// path : /routes/task/+server.ts

import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ request }) => {
    const { taskId } = await request.json();

    if (!taskId) return json([]);

    // Api call here

    console.log({ taskId });
    const result = [
        { id: 1, name: taskId },
        { id: 2, name: taskId },
        { id: 3, name: taskId },
        { id: 4, name: taskId },
        { id: 5, name: taskId },
        { id: 6, name: taskId }
    ];
    return json(result);
};