From 5eb60c207e19bbdf5fcb9dc04b9c5c2a22d15bed Mon Sep 17 00:00:00 2001 From: Cristine Guadelupe Date: Thu, 9 May 2024 15:45:40 +0800 Subject: [PATCH] DataTable update (#425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jonatan KÅ‚osko --- lib/kino/data_table.ex | 67 +++++++++++++++++++++++++++++++---- lib/kino/table.ex | 34 ++++++++++++++++-- test/kino/data_table_test.exs | 33 +++++++++++++++++ 3 files changed, 125 insertions(+), 9 deletions(-) diff --git a/lib/kino/data_table.ex b/lib/kino/data_table.ex index dd2ea2fc..8c87a8b4 100644 --- a/lib/kino/data_table.ex +++ b/lib/kino/data_table.ex @@ -46,10 +46,48 @@ defmodule Kino.DataTable do """ @spec new(Table.Reader.t(), keyword()) :: t() def new(tabular, opts \\ []) do - tabular = normalize_tabular(tabular) - name = Keyword.get(opts, :name, "Data") sorting_enabled = Keyword.get(opts, :sorting_enabled, true) + {data_rows, data_columns, count, inspected} = prepare_data(tabular, opts) + + Kino.Table.new(__MODULE__, {data_rows, data_columns, count, name, sorting_enabled, inspected}, + export: fn state -> {"text", state.inspected} end + ) + end + + @doc """ + Updates the table to display a new tabular data. + + ## Options + + * `:keys` - a list of keys to include in the table for each record. + The order is reflected in the rendered table. Optional + + ## Examples + + data = [ + %{id: 1, name: "Elixir", website: "https://elixir-lang.org"}, + %{id: 2, name: "Erlang", website: "https://www.erlang.org"} + ] + + kino = Kino.DataTable.new(data) + + Once created, you can update the table to display new data: + + new_data = [ + %{id: 1, name: "Elixir Lang", website: "https://elixir-lang.org"}, + %{id: 2, name: "Erlang Lang", website: "https://www.erlang.org"} + ] + + Kino.DataTable.update(kino, new_data) + """ + def update(kino, tabular, opts \\ []) do + {data_rows, data_columns, count, inspected} = prepare_data(tabular, opts) + Kino.Table.update(kino, {data_rows, data_columns, count, inspected}) + end + + defp prepare_data(tabular, opts) do + tabular = normalize_tabular(tabular) keys = opts[:keys] {_, meta, _} = reader = init_reader!(tabular) @@ -68,9 +106,7 @@ defmodule Kino.DataTable do inspected = inspect(tabular) - Kino.Table.new(__MODULE__, {data_rows, data_columns, count, name, sorting_enabled}, - export: fn _ -> {"text", inspected} end - ) + {data_rows, data_columns, count, inspected} end defp normalize_tabular([%struct{} | _] = tabular) do @@ -126,7 +162,7 @@ defmodule Kino.DataTable do end @impl true - def init({data_rows, data_columns, count, name, sorting_enabled}) do + def init({data_rows, data_columns, count, name, sorting_enabled, inspected}) do features = Kino.Utils.truthy_keys(pagination: true, sorting: sorting_enabled) info = %{name: name, features: features} @@ -138,7 +174,8 @@ defmodule Kino.DataTable do total_rows: count, slicing_fun: slicing_fun, slicing_cache: slicing_cache, - columns: Enum.map(data_columns, fn key -> %{key: key, label: value_to_string(key)} end) + columns: Enum.map(data_columns, fn key -> %{key: key, label: value_to_string(key)} end), + inspected: inspected }} end @@ -269,4 +306,20 @@ defmodule Kino.DataTable do inspect(value) end end + + @impl true + def on_update({data_rows, data_columns, count, inspected}, state) do + {count, slicing_fun, slicing_cache} = init_slicing(data_rows, count) + + {:ok, + %{ + state + | data_rows: data_rows, + total_rows: count, + slicing_fun: slicing_fun, + slicing_cache: slicing_cache, + columns: Enum.map(data_columns, fn key -> %{key: key, label: value_to_string(key)} end), + inspected: inspected + }} + end end diff --git a/lib/kino/table.ex b/lib/kino/table.ex index 1f3caec4..26aa3ac2 100644 --- a/lib/kino/table.ex +++ b/lib/kino/table.ex @@ -52,7 +52,14 @@ defmodule Kino.Table do @callback export_data(rows_spec(), state(), String.t()) :: {:ok, %{data: binary(), extension: String.t(), type: String.t()}} - @optional_callbacks export_data: 3 + @doc """ + Invoked to update state with new data. + + This callback is called in response to `update/2`. + """ + @callback on_update(update_arg :: term(), state :: state()) :: {:ok, state()} + + @optional_callbacks export_data: 3, on_update: 2 use Kino.JS, assets_path: "lib/assets/data_table/build" use Kino.JS.Live @@ -82,6 +89,17 @@ defmodule Kino.Table do Kino.JS.Live.new(__MODULE__, {module, init_arg}, export: export) end + @doc """ + Updates the table with new data. + + An arbitrary update event can be used and it is then handled by + the `c:on_update/2` callback. + """ + @spec update(t(), term()) :: :ok + def update(kino, update_arg) do + Kino.JS.Live.cast(kino, {:update, update_arg}) + end + @impl true def init({module, init_arg}, ctx) do {:ok, info, state} = module.init(init_arg) @@ -146,7 +164,7 @@ defmodule Kino.Table do end def handle_event("order_by", %{"key" => nil}, ctx) do - {:noreply, ctx |> assign(order: nil, page: 1) |> broadcast_update()} + {:noreply, ctx |> reset() |> broadcast_update()} end def handle_event("order_by", %{"key" => key_string, "direction" => direction}, ctx) do @@ -161,6 +179,18 @@ defmodule Kino.Table do {:noreply, ctx |> assign(relocates: relocates) |> broadcast_update()} end + @impl true + def handle_cast({:update, update_arg}, ctx) do + unless Kino.Utils.has_function?(ctx.assigns.module, :on_update, 2) do + raise ArgumentError, "module #{inspect(ctx.assigns.module)} does not define on_update/2" + end + + {:ok, state} = ctx.assigns.module.on_update(update_arg, ctx.assigns.state) + {:noreply, assign(ctx, state: state) |> reset() |> broadcast_update()} + end + + defp reset(ctx), do: assign(ctx, order: nil, page: 1) + defp broadcast_update(ctx) do {content, ctx} = get_content(ctx) broadcast_event(ctx, "update_content", content) diff --git a/test/kino/data_table_test.exs b/test/kino/data_table_test.exs index 7a0b8290..26111ea3 100644 --- a/test/kino/data_table_test.exs +++ b/test/kino/data_table_test.exs @@ -267,4 +267,37 @@ defmodule Kino.DataTableTest do data: [["1", "1"], ["2", "2"] | _] }) end + + test "supports data update" do + entries = [ + %User{id: 1, name: "Sherlock Holmes"}, + %User{id: 2, name: "John Watson"} + ] + + kino = Kino.DataTable.new(entries) + data = connect(kino) + + assert %{ + content: %{ + columns: [ + %{key: "0", label: ":id"}, + %{key: "1", label: ":name"} + ], + total_rows: 2 + } + } = data + + new_entries = [ + %User{id: 1, name: "Sherlock Holmes"}, + %User{id: 2, name: "John Watson"}, + %User{id: 3, name: "Tuka Tuka"} + ] + + Kino.DataTable.update(kino, new_entries) + + assert_broadcast_event(kino, "update_content", %{ + data: [["1", "Sherlock Holmes"], ["2", "John Watson"], ["3", "Tuka Tuka"]], + total_rows: 3 + }) + end end