cocopon / tweakpane

:control_knobs: Compact GUI for fine-tuning parameters and monitoring value changes
https://tweakpane.github.io/docs/
MIT License
3.69k stars 92 forks source link

Type problem when calling addBinding dynamically #608

Closed lucafaggianelli closed 8 months ago

lucafaggianelli commented 8 months ago

Hey there,

I stumbled upon a minor typing issue, I need to populate a Pane dynamically / programmatically from a series of entries, just for context I'm working on a UI builder so based on the selected UI template there are certain set of theme options, here's my code:

const themeOptions: ThemeOptions = {
  asideBackground: { default: '#47394e0f' },
  asideBorderWidth: { default: 1, options: { min: 1, max: 10 } },
  asideBorderColor: { default: '#eae0e7' },
  colorPrimary: { default: '#891797' },
  colorTextSecondary: { default: '#918c8c' },
  imageMarginLeft: { default: 48, options: { min: 0 } },
  imageMarginRight: { default: 24, options: { min: 0 } },
  imageRadius: { default: 50, options: { min: 0, max: 100 } },
  imageSize: { default: 52.8, options: { min: 0 } },
}

const data = {hey: ''}

// just for reference to be sure typing is working
pane.addBinding(data, 'hey')

Object.entries(themeOptions).forEach(([key, option]) => {
  // type error
  pane.addBinding(data, key, option.options)
})

image

I think that the key argument of addBinding is a bit too strict as it doesn't allow a generic string, but only the keys of the data object passed as first argument.

Any idea? or hint on how to implement my usecase?

davelsan commented 8 months ago

That error makes sense to me. You are trying to bind theme keys to the data object. But even if you fix that, there is stil the limitation of Object.entries, which does not produce a fully typed object.

Assuming theme is of type ThemeOptions, you could do the following.

const TypedObject = {
  entries: <T extends {}>(obj: T) => {
    return Object.entries(obj) as [keyof T, T[keyof T]][];
  }
}

TypedObject.entries(theme).forEach(([key, option]) => {
  pane.addBinding(theme, key, option.options); // changed `data` for `theme`
})

Edit: Actually, Object.entries will work just fine in this case too, if you use theme instead of data. Maybe I'm missing something about what you are trying to achieve?

lucafaggianelli commented 8 months ago

mmm my bad, indeed I finalized my code and it's working with Object.entries:

import type { BindingParams } from 'tweakpane'

export interface ThemeOption {
  default: string | number
  options?: BindingParams
}

export type ThemeOptions = Record<string, ThemeOption>

const initDesigner = (
  container: HTMLElement,
  theme: ThemeOptions,
  onChange: (key: string, value: string | number) => void,
) => {
  const pane = new Pane({
    title: 'Resume Designer',
    expanded: true,
    container,
  })

  const initialData: Record<string, string | number> = {}

  Object.entries(theme).forEach(([key, option]) => {
    initialData[key] = option.default
    pane
      .addBinding(initialData, key, option.options)
      .on('change', (event) => {
        onChange(key, event.value)
      })
  })

  pane.addButton({ title: 'Reset' }).on('click', () => {
    // pane.importState({})
  })

  return {...initialData}
}

Thanks for your help!

lucafaggianelli commented 8 months ago

Closing as this is not a bug

davelsan commented 8 months ago

Happy to hear you got it working.