Search code examples
javascriptformssveltesveltekitsupabase

Sveltekit update reactive variable from form action


Hi!

So I'm working on a simple crud application with Sveltekit and Supabase. I'm trying to implement this sorting feature by fetching the table with the added .order() function when the user selects a sorting option. My problem is that I want to update the smoothies variable after the form action takes place with the new sorting option. Right now I'm just returning smoothies like I would in the load function but that doesn't seem to be right. So, how would I go about updating smoothies, and the dom, with the new data from the form action (and not reload the page as it would just get overriden by the default load function).

I am a Sveltekit and Supabase newbie so your help is greatly appreciated!

+page.svelte

<script>
    import { enhance } from '$app/forms';

    export let data;

    $: ({ smoothies } = data);

    const tabs = (e) => {
        e.target.classList.add('tab-active');

        const siblings = e.target.parentNode.children;

        for (let i = 0; i < siblings.length; i++) {
            if (siblings[i] !== e.target) {
                siblings[i].classList.remove('tab-active');
            }
        }
    };

    const handleSubmit = ({ form, data, action, cancel, submitter }) => {
        return async ({ result, update }) => {};
    };
</script>

<div class="flex flex-col items-center mx-auto gap-8">
    <a role="button" href="/create" class="btn btn-success mt-8">Create Smoothie</a>

    <form action="?/sortSmoothies" method="POST" use:enhance={handleSubmit}>
        <div class="tabs tabs-boxed">
            <button
                type="submit"
                name="created_at"
                value="true"
                class="tab tab-active"
                on:click={(e) => tabs(e)}>Created</button
            >
            <button type="submit" name="title" value="true" class="tab" on:click={(e) => tabs(e)}
                >Title</button
            >
            <button type="submit" name="rating" value="true" class="tab" on:click={(e) => tabs(e)}
                >Rating</button
            >
        </div>
    </form>

    <div class="grid 2xl:grid-cols-4 xl:grid-cols-3 lg:grid-cols-2 gap-6 mt-4">
        {#each smoothies as smoothie}
            <div class="card w-96 bg-base-100 shadow-xl">
                <div class="card-body">
                    <h2 class="card-title">{smoothie.title}</h2>
                    <p>{smoothie.method}</p>
                    <div class="rating absolute -right-5 -top-5">
                        <div
                            class="mask mask-star-2 bg-orange-400 w-12 h-12 flex items-center justify-center font-bold"
                        >
                            {smoothie.rating}
                        </div>
                    </div>

                    <div class="card-actions justify-end">
                        <a role="button" href="/{smoothie.id}" class="btn btn-warning btn-sm">Edit</a>
                        <form action="?/deleteSmoothie" method="POST">
                            <input type="hidden" name="smoothie" value={JSON.stringify(smoothie)} />
                            <button type="submit" class="btn btn-error btn-sm">Remove</button>
                        </form>
                    </div>
                </div>
            </div>
        {/each}
    </div>
</div>

+page.server.js

import { supabase } from '$lib/supabaseClient';
import { fail } from '@sveltejs/kit';

const fetchSmoothies = async (orderBy) => {
    const { data, error } = await supabase
        .from('smoothies')
        .select()
        .order(orderBy, { ascending: false });

    if (error) {
        throw new Error('Could not fetch smoothies');
    }

    return data ?? [];
};

export const load = async () => {
    console.log('load');
    const data = await fetchSmoothies('created_at');

    return {
        smoothies: data ?? []
    };
};

export const actions = {
    deleteSmoothie: async ({ request }) => {
        let { smoothie } = Object.fromEntries(await request.formData());
        smoothie = JSON.parse(smoothie);

        try {
            await supabase.from('smoothies').delete().eq('id', smoothie.id);
        } catch (err) {
            console.error(err);
            return fail(500, { message: 'Could not delete smoothie' });
        }
    },
    sortSmoothies: async ({ request }) => {
        console.log('action');
        const formData = Object.fromEntries(await request.formData());

        let orderBy;
        for (const prop in formData) {
            if (formData[prop] === 'true') {
                orderBy = prop;
                break;
            }
        }

        const data = await fetchSmoothies(orderBy);
        console.log(data);

        return {
            smoothies: data ?? []
        };
    }
};

Solution

  • You should generally not return page data from form actions. When using the enhance action, a successful action will automatically invalidate the page data causing load to run again which in turn should update your list.

    Though in this case, you might want to turn the sorting variable into a query parameter, that way the load function can access it and return the list in the correct order. You also would not need an action but instead use goto to update the URL with the new parameter.

    Simplified example:

    function sort(e) {
        const url = new URL($page.url);
        url.searchParams.set('sort', e.currentTarget.value);
        goto(url.href, { replaceState: true });
    }
    
    <select on:change={sort}>
        <option value="asc">Ascending</option>
        <option value="desc">Descending</option>
    </select>
    
    // +page.server.js
    export const load = e => {
        const sort = e.url.searchParams.get('sort');
        // ...
    };
    

    Of course you can use multiple parameters, use a form's submit event to trigger the sorting, etc.