Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting value of <select> element with dynamic list of options #94

Open
raquo opened this issue Apr 13, 2021 · 1 comment
Open

Setting value of <select> element with dynamic list of options #94

raquo opened this issue Apr 13, 2021 · 1 comment

Comments

@raquo
Copy link
Owner

raquo commented Apr 13, 2021

Reported in gitter. Consider this:

select(
  value <-- $value,
  children <-- $children
)

When we set the value property, what the DOM "really" does is set selected := true attribute on the corresponding option element inside the select element. But, it is quite possible that such an option does not exist yet when the select element's value and children are coming from different observables – they might not sync naturally, so when a change happens it's possible that $value will emit before the change propagates to $children. This case should be fixable with the syncDelay operator and/or moving the children <-- $children modifier to before value <-- $value. However, what exactly needs to be done depends on whether the observables involved are signals or streams or a mix.

In the more general case, $children might be completely out of sync with $value, e.g. the former might have an async delay (yet the $value still knows what the value should be without the delay? Kinda sus...). In this case, $value should be something more like $value.combineWithFn($children)((v, _) => v) (but not exactly that).


Proper solution right now that is guaranteed to work in all cases is setting the selected prop on every option instead of setting the value prop on the parent select element. But that can get annoying.


We provide an API for controlled inputs, which fundamentally solves a similar problem. Maybe we could provide a similar helper here as well which would do the right thing. Something like this perhaps?

select(
  controlled(
    value <-- $value,
    children <-- $children
  )
)

It's not quite clear what "the right thing" should be in this case. Not sure how this would interact with setting the selected prop on child option elements, or with the controlled inputs feature (what if we want this select to be a traditional controlled input too).

@ivan-klass
Copy link

I end up with combining both options and value in a single case-class like below

case class SelectOptions[+T](options: Vector[T], tryChoose: Option[T]):
  private val withIndex = options.zipWithIndex

  val chosen: Option[T] = tryChoose.filter(options.contains).orElse(options.headOption)

  private val defaultAsIdx: Option[Int] = chosen.map(options.indexOf)

  def asSelected(value: String): Option[T] = value.toIntOption.flatMap(withIndex.lift).map(_._1)

  val defaultAsValue: String = if options.isEmpty then "" else defaultAsIdx.fold("")(_.toString)

  def asOptions(using RenderableText[T]): Seq[Node] = withIndex.map { case (t, idx) =>
    option(value := idx.toString, text(t).apply(true))
  }

case class Select[-T](selectTo: Observer[T], customize: HtmlMod)(using RenderableText[T]):
  def apply(params: SelectOptions[T]): Node = select(
    params.asOptions,
    value := params.defaultAsValue, // important: set value AFTER adding options
    onChange.mapToValue.map(params.asSelected).collect { case Some(v) => v } --> selectTo,
    onMountCallback(_ => params.chosen.foreach(selectTo.onNext)),
    customize
  )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants