defmodule Rephex.State do
@moduledoc """
Define Rephex state by `use`.
Defined state must be initialized by `init/1` in `Phoenix.LiveView.mount/3`.
## Example
defmodule ExampleWeb.State do
@type t :: %{
count: integer(),
add_twice_async: %AsyncResult{}
}
@initial_state %{
count: 0,
}
use Rephex.State, initial_state: @initial_state
def add_count(socket, %{amount: amount} = _payload) when is_integer(amount) do
# You can use `update_state`, `update_state_in` and `put_state_in` to update state
update_state_in(socket, [:count], &(&1 + amount))
end
end
defmodule ExampleWeb.AccountLive.Index do
use ExampleWeb, :live_view
use Rephex.LiveView
@impl true
def mount(_params, _session, socket) do
{:ok, socket |> ExampleWeb.State.init()}
end
end
"""
alias Phoenix.LiveView.Socket
@root Rephex.root()
@selector_keys_key :__selector_keys__
def init(%Socket{} = socket, %{} = initial_state) do
selector_keys = get_selector_keys_as_ordered(initial_state)
initial_state = Map.put(initial_state, @selector_keys_key, selector_keys)
socket
|> Phoenix.Component.assign(@root, initial_state)
|> update_selectors()
end
defp get_selector_keys_as_ordered(%{} = state) do
selector_module_to_key =
state
|> Enum.filter(fn {_k, v} -> is_struct(v, Rephex.Selector) end)
|> Enum.map(fn {k, v} -> {v.module, k} end)
|> Enum.into(%{})
selector_module_to_key
|> Map.keys()
|> Rephex.Selector.sort_selectors_by_update_order()
|> Enum.map(&Map.fetch!(selector_module_to_key, &1))
end
def update_selectors(%Socket{} = socket) do
selector_keys = Rephex.State.Assigns.get_state(socket)[@selector_keys_key]
if selector_keys == nil do
raise """
Rephex.State.Assigns.update_selectors/1:
Selector modules are not found in the state.
"""
end
if not is_list(selector_keys) do
raise """
Rephex.State.Assigns.update_selectors/1:
Selector modules must be a list.
"""
end
Enum.reduce(selector_keys, socket, fn key, socket ->
selector = Rephex.State.Assigns.get_state_in(socket, [key])
state = socket.assigns[@root]
Rephex.Selector.update(selector, state)
|> case do
nil -> socket
new_selector -> Rephex.State.Assigns.put_state_in(socket, [key], new_selector)
end
end)
end
defmacro __using__(opt) do
initial_state = Keyword.fetch!(opt, :initial_state)
quote do
import Rephex.State.Assigns
def init(%Socket{} = socket) do
socket |> Rephex.State.init(unquote(initial_state))
end
end
end
end
defmodule Rephex.State.Assigns do
alias Phoenix.LiveView.Socket
@root Rephex.root()
@selector_keys_key :__selector_keys__
@doc """
Update Rephex state.
## Example
def add_count(socket, %{amount: amount} = _payload) do
update_state(socket, fn state -> %{state | count: state.count + amount} end)
end
"""
def update_state(%Socket{parent_pid: parent_pid}, _fun) when parent_pid != nil,
do: raise("Use this function only in LiveView (root).")
def update_state(%Socket{} = socket, fun) when is_function(fun, 1) do
# I should keep meta data in the state
selector_keys = Rephex.State.Assigns.get_state(socket)[@selector_keys_key]
socket
|> Phoenix.Component.update(@root, fun)
|> Phoenix.Component.update(@root, &Map.put(&1, @selector_keys_key, selector_keys))
|> Rephex.State.update_selectors()
end
@doc """
Update Rephex state by `put_in/3`.
## Example
def put_value(socket, %{key: k, value: v} = _payload) do
put_state_in(socket, [:items, k], v)
end
"""
def put_state_in(%Socket{parent_pid: parent_pid}, _keys, _value) when parent_pid != nil,
do: raise("Use this function only in LiveView (root).")
def put_state_in(%Socket{} = socket, keys, value) when is_list(keys) do
update_state(socket, &put_in(&1, keys, value))
end
@doc """
Update Rephex state by `update_in/3`.
## Example
import Rephex.State.Assigns
def mlt_count(socket, %{mlt: mlt} = _payload) do
update_state_in(socket, [:count], &(&1 * mlt))
end
"""
def update_state_in(%Socket{parent_pid: parent_pid}, _keys, _fun) when parent_pid != nil,
do: raise("Use this function only in LiveView (root).")
def update_state_in(%Socket{} = socket, keys, fun) when is_list(keys) and is_function(fun, 1) do
update_state(socket, &update_in(&1, keys, fun))
end
def get_state(%Socket{parent_pid: parent_pid}) when parent_pid != nil,
do: raise("Use this function only in LiveView (root).")
def get_state(%Socket{} = socket) do
socket.assigns[@root]
end
def get_state_in(%Socket{parent_pid: parent_pid}, _keys) when parent_pid != nil,
do: raise("Use this function only in LiveView (root).")
def get_state_in(%Socket{} = socket, keys) when is_list(keys) do
get_state(socket) |> get_in(keys)
end
end