svecosystem / runed

Magical utilities for your Svelte applications (WIP)
https://runed.dev
MIT License
461 stars 23 forks source link

useToggle #45

Closed Tyson910 closed 3 months ago

Tyson910 commented 4 months ago

Describe the feature in detail (code, mocks, or screenshots encouraged)

Describe the feature in detail (code, mocks, or screenshots encouraged)

useToggle rune to implement a common state pattern – it switches state between given values e.g. ['light mode', 'dark mode'], ['grid view', 'list view' , 'map view'], ['online', 'idle', 'offline']

The rune accepts an array as single argument, the first option will be used as the default value. useToggle returns an object with a read/write value and toggle function.

Here's how I imagine it would look like in action:

import { describe, expect, it } from "vitest";
import { useToggle } from "runed";

describe("useToggle", () => {
    it("Toggle loops back to first element when last element is reached", () => {
        const { value, toggle } = useToggle(["light", "dark"] as const);
        expect(value).toBe("light");
        toggle();
        expect(value).toBe("dark");
                toggle();
        expect(value).toBe("light");
    });
});

What type of pull request would this be?

New Feature

Provide relevant links or additional information.

No response

What type of pull request would this be?

New Feature

Provide relevant links or additional information.

No response

Tyson910 commented 4 months ago

Realizing now that if I lose reactivity when I destructure a flattened box (using box.flatten()), not sure if this is a feature or bug. Rough draft for an implementation would be

export function useToggle<T = boolean>(options: readonly T[] = [false, true] as const) {
    let selectedIndex = box(0);
    return box.flatten({
        value: box.with(() => options[selectedIndex.value]),
        // maybe next()/prev() instead?
        toggle() {
            if (selectedIndex.value < options.length - 1) {
                selectedIndex.value += 1;
            } else {
                selectedIndex.value = 0;
            }
        },
    });
}
import { describe, expect, it } from "vitest";
import { useToggle } from "runed";

describe("useToggle", () => {
    it("Toggle loops back to first element when last element is reached", () => {
        const themeToggle = useToggle(["light", "dark"] as const);
        expect(themeToggle.value).toBe("light");
        themeToggle.toggle();
        expect(themeToggle.value).toBe("dark");
                themeToggle.toggle();  
        expect(themeToggle.value).toBe("light");
    });
});
abdel-17 commented 4 months ago

I think a a toggle with more than two states is a bit strange. I'd just make this a boolean.

Tyson910 commented 4 months ago

I could see that name causing some confusion & I’m open to changing the name of the util function, maybe to a useEnum, useSwitch, useStateSwitch, or useMultiToggle instead? The functionality of switch state between these given values still feels like it could be of value here

abdel-17 commented 4 months ago

a switch also implies two states. useEnum is a lot more descriptive.

TGlide commented 3 months ago

TBH this seems like a good use case for a vanilla JS function:

function toggle<T>(curr: T, items: T[]): T {
    return items[items.indexOf(curr) + 1] ?? items[0];
}

const themes = ['dark', 'light', 'auto'] as const
type Theme = typeof themes[number]
let theme = $state<Theme>(themes[0])

const toggleTheme = () => theme = toggle(theme, themes)

// vs

import { useToggle } from 'runed'

const themes = ['dark', 'light', 'auto'] as const
type Theme = typeof themes[number]
let theme = $state<Theme>(themes[0])

const toggleTheme = useToggle(themes, (v) => theme = v)

Not sure it's a huge benefit to add this util