Cascading selects in Livebook with Kino?

I have a livebook cell that starts with a single input, then appends additional select inputs recursively based on the input of the previous select box. It’s a self-referencing table, so it’s using parent / child relations to populate the next box. I’m pretty surprised that this works as I thought the recursive Kino.listen/2 calls would break something, but pleasingly it works pretty well.

The obvious problem is that inputs are only appended, not removed. If an earlier selection in the list is changed, another box is simply appended on the end. I can’t find any obvious way to track inputs appended to a frame so I can remove them without clearing the whole form - is there any way to track these? I have also tried using a Kino form or just adding raw inputs but neither seems to allow an append like the frame does.

defmodule AreaInputs do
  import Ecto.Query

  defp parent_id_filter(queryable, parent_id) do
    case parent_id do
      nil -> queryable |> where([a], is_nil(a.parent_id))
      _ -> queryable |> where([a], a.parent_id == ^parent_id)
    end
  end

  def get_areas(parent_id) do
    Area
    |> parent_id_filter(parent_id)
    |> order_by([a], a.name)
    |> AreaRepo.all()
    |> Enum.map(fn area -> {area.id, area.name} end)
  end

  def append_select_and_listen(frame, event) do
    new_input = Kino.Input.select("", AreaInputs.get_areas(event.value))
    Kino.Frame.append(frame, new_input)

    Kino.listen(new_input, fn event_2 ->
      append_select_and_listen(frame, event_2)
    end)
  end
end

frame = Kino.Frame.new() |> Kino.render()
initial_input = Kino.Input.select("", AreaInputs.get_areas(nil))
Kino.Frame.render(frame, initial_input)

Kino.listen(initial_input, fn event ->
  AreaInputs.append_select_and_listen(frame, event)
end)

Hey @cd-slash : ) For this case I think you need a process to manage the frame and inputs, so you can re-render all the content. So more specifically, you could have a GenServer that has the frame and you would subscribe it to input events. Then, when an even is received, you re-render into the frame new set of inputs accordingly.

In the future we may have an abstraction to streamline this. Currently there is a proof of concept of a wizard in Kino Add Kino.Wizard POC by josevalim · Pull Request #426 · livebook-dev/kino · GitHub.

1 Like

Here’s one way to do it: playing with idea of dynamic forms · GitHub

And a quick video with the demo: CleanShot 2024-11-04 at 14.56.34 · CleanShot Cloud

1 Like