livebook-dev / kino

Client-driven interactive widgets for Livebook

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add Kino.LiveFrame

josevalim opened this issue · comments

It resembles LiveView. Here is a silly proof of concept:

import Kino.Control
import Kino.Shorts

defmodule Kino.LiveFrame do
  use GenServer, restart: :temporary

  def start_link({frame, arg}) do
    GenServer.start_link(__MODULE__, {frame, arg})
  end

  def init({frame, arg}) do
    {:ok, state} = c_init(arg)
    {:ok, {frame, state}, {:continue, :render}}
  end

  def handle_continue(:render, {frame, state}) do
    {:noreply, render({frame, state})}
  end

  def handle_info({{__MODULE__, fun}, data}, {frame, state}) when is_function(fun, 2) do
    state = fun.(data, state)
    {:noreply, render({frame, state})}
  end

  defp render({frame, state}) do
    Kino.Frame.render(frame, c_render(state))
    {frame, state}
  end

  defp control(from, fun) do
    Kino.Control.subscribe(from, {__MODULE__, fun})
    from
  end

  ## Callbacks (those can be callbacks in a new behaviour)

  def c_init(:ok) do
    {:ok, %{page: 0, name: nil, address: nil}}
  end

  defp step_zero(_, state) do
    %{state | page: 1}
  end
  
  defp step_one(%{data: %{name: name}}, state) do
    if name == "" do
      %{state | name: name}
    else
      %{state | name: name, page: 2}
    end
  end

  defp step_two(%{data: %{address: address}}, state) do
    case address do
      "BUMP" <> _ -> %{state | address: address <> "!"}
      "" -> %{state | address: ""}
      _ -> %{state | address: address, page: 3}
    end
  end

  defp go_back(_, state) do
    %{state | page: state.page - 1}
  end

  def c_render(%{page: 0}) do
    button("Start")
    |> control(&step_zero/2)
  end
  
  def c_render(%{page: 1} = state) do
    form(
      [name: Kino.Input.text("Name", default: state.name)],
      submit: "Step one"
    )
    |> control(&step_one/2)
  end

  def c_render(%{page: 2} = state) do
    Kino.Control.form(
      [address: Kino.Input.text("Address", default: state.address)],
      submit: "Step two"
    )
    |> control(&step_two/2)
    |> add_go_back()
  end

  def c_render(%{page: 3} = state) do
    "Well done, #{state.name}. You live in #{state.address}."
    |> add_go_back()
  end

  defp add_go_back(element) do
    button =
      button("Go back")
      |> control(&go_back/2)

    grid([element, button])
  end
end

frame = Kino.Frame.new() |> Kino.render()
Kino.start_child!({Kino.LiveFrame, {frame, :ok}})

We need Kino.monitor_origins before implementing it.

Closing in favor of #426.