kitschpatrol / svelte-tweakpane-ui

A Svelte component library wrapping UI elements from Tweakpane, plus some additional functionality for convenience and flexibility.
https:///kitschpatrol.com/svelte-tweakpane-ui
MIT License
105 stars 3 forks source link

Default values #12

Closed cristianvogel closed 2 months ago

cristianvogel commented 2 months ago

Hi.

I tried to add this in to a local fork, but the codebase is a bit too specialised for me to do it. Its pretty cool though, and I'm gonna try to study it more... nevertheless

I'd like to be able to specify starting, default values on at least a slider. It seems like they do have some kind of default, but I don't see where that is set.

kitschpatrol commented 2 months ago

Hey, thanks for taking a look at the code... it's a bit dense / weird in places since component inheritance is not a super common pattern in Svelte.

The value prop that most higher-level components use is first defined here in GenericBinding.svelte, which is in charge of creating and managing an object containing the value that can be passed into Tweakpane.

Here value is defined as a required prop, with no default value, since nothing is assigned in its export let statement. This behavior is then inherited by all the more specialized components.

I'm probably misunderstanding your question, but currently I think of the default value as being whatever initial value is passed to the component, e.g. this trivial example of a slider with an initial value of 20:

<script>
    import { Slider } from 'svelte-tweakpane-ui';
    let value = 20;
</script>

<Slider bind:value min={0} max={100} />

Do you mind clarifying a bit what you're looking for beyond this existing behavior?

cristianvogel commented 2 months ago

Yeah it's an interesting architecture for me, hadn't seen anything quite like that in Svelte, and also noticed use of generics in the script tag , which I hadn't come across yet. Its impressive how complex stateful UI libraries can get.

I think I was stumbling a bit on understanding the two way binding.

I have a set of controls, that are declared in a manifest. They should get instantiated with a default value, but I was finding it complicated to instance the components in the Svelte markup using for each, whilst also setting the value to it's default on create. I just think that, although it might seem redundant, the default value is quite important , and it would be easier to manage I think as part of the constructor props. Like min or max or step even.

In my case, I am recalling defaults from persistent state, but also need a default for when there is no state persisting yet and things get a bit confusing

kitschpatrol commented 2 months ago

Yeah the generics attribute in the script tag is a little strange, my understanding is that the Svelte team went through a couple different ideas before landing on it — though you still sometimes see the alternative $$Generic syntax in the wild.

Thanks for clarifying the defaults situation, I think I'm following.

It might be somewhat similar to what I'm doing in tweakpane-css, which uses svelte-persistent-store to take care of both managing defaults and syncing to the browser's local storage. The Tweakpane CSS component derives the UI and certain options from the store, with default values used if nothing is yet available in the local store. In Svelte Tweakpane component instantiations, value is bound directly to the store object or to specific keys in the store.

It's not the cleanest component, but in this section you can see the defaultOptions object:

const defaultOptions: Options = {
    autoFolders: false,
    includeCalculated: false,
    prettyNames: true,
    showUnits: true,
    sortNames: false,
}

Which is assigned as the default value to a prop and then passed as the default value when the store is created, in case the local store values are not yet set:

export let options: Options = defaultOptions
...
let optionsStore: Writable<Options> = persisted('css-options', options)

(Assigning to the options prop is just an implementation detail which allows the user to further override the initial options, otherwise defaultOptions could be passed as persisted's second argument.)

Then passed to a Svelte Tweakpane UI <AutoObject> component in the markup:

<Folder bind:expanded={$expandedStateStore[optionsExpandedStateKey]} title="Options">
    <AutoObject bind:object={$optionsStore} />
    <Button on:click={resetOptions} title="Reset Options" />
</Folder>

Two-way binding of $optionsStore means that any changes from the Tweakpane are automatically persisted locally, overriding defaults on future loads after the first write to local storage.

This basically pushes the question of defaults out of the UI library and into the store instead. (This pattern is also used internally in Svelte Tweakpane UI to manage draggable pane position defaults and persistence.)

Adding a defaultValue prop to the Svelte Tweakpane UI components just seems tricky to me since value is already a required prop, and, at least in my mental model, value would immediately override anything set in a hypothetical defaultValue. It might make more sense if the value prop was optional, but that change would have some interesting and possibly more confusing consequences across the library.

Maybe that helps, or apologies if I'm still not completely getting it.

kitschpatrol commented 2 months ago

Closing this since I hope the example of managing defaults outside Svelte Tweakpane UI helps — but feel free to re-open if needed.