livebook-dev / kino

Client-driven interactive widgets for Livebook
Apache License 2.0
361 stars 60 forks source link

`Kino.Input.read` ignores updates #450

Closed 0x009922 closed 2 months ago

0x009922 commented 2 months ago

Example

# Kino bug: `Input.read()` doesn't return updated values

```elixir
Mix.install([
  {:kino, "~> 0.13.1"}
])

Section

text = Kino.Input.text("Type something")
button = Kino.Control.button("Reveal the bug")

Kino.listen(button, fn
  %{type: :click} ->
    text |> Kino.Input.read() |> IO.inspect(label: "current input value")

  _ ->
    nil
end)

Kino.Layout.grid([
  text,
  button
])


Demo: 

https://github.com/livebook-dev/kino/assets/43530070/a14d9067-76d7-4c3b-be64-7b57fb785763

### Expected behaviour

Expected on each button click to get the latest input value of the text field.

### Actual behaviour

The first read value is returned repeatedly on consecutive reads.

## Livebook version

| Livebook | Elixir |
| - | - |
| `v0.13.2` | `v1.17.1` | 
jonatanklosko commented 2 months ago

This is expected, Kino.Input.read should be used to read the input value in the executing cell, not in a background process. We make sure that Kino.Input.read returns the value from the time the cell started executing. In a background process the return value is basically undefined; for example if you change the input and evaluate another cell, then the background process would get the latest value on next read. The reason we do this is because we track dependency between inputs and cells, so when a cell calls Kino.Input.read, and the input value changes later, we mark that cell as stale.

For accessing the input value asynchronously you can either use Kino.Control.form with input and submit button (so that all form inputs are sent together). Or, you can subscribe to both the input and the button, and keep track of the current value in the state as it changes:

text = Kino.Input.text("Type something")
button = Kino.Control.button("Click")

Kino.Control.tagged_stream(button: button, text: text)
|> Kino.listen(%{text: ""}, fn
  {:text, event}, state ->
    {:cont, %{state | text: event.value}}

  {:button, _event}, state ->
    IO.inspect(state.text, label: "current input value")
    {:cont, state}
end)

Kino.Layout.grid([text, button])

We should add a note about this in the docs. @josevalim or perhaps we should change it such that Kino.Input.read raises when called from any process other than the cell? There could be a false positive, for example if you try to read inside Task.async_stream, but a simple workaround is to read the input into a variable upfront, which is guaranteed to be the same value anyway. wdyt?

josevalim commented 2 months ago

@jonatanklosko raising an error works for me!