Hello everybody!
I’m using Phoenix Live View for a while and I like it so much!
I need your opinions and suggestions about something that I’m uncomfortable during my developing, because I’m not expert of Elixir and I don’t know if my mindset is correct.
Sorry for the long post
While coding I noticed that I wrote the same thing everytime: I need to load some data into a live view.
I would like to explain you how I use to manage my live views, what I have done to reduce boilerplate and what are your suggestion about that.
How I implement a live view
Typically my view state is in the url/query params (updated by the inputs), so I create a function that takes params and socket and returns the socket with the data assigned:
def load_resource_name(socket, params) do
resource = get_resource_name(params)
# optionally subscribe(PubSub, ....)
assign(socket, :resource_name, resource)
end
def get_resource_name(params) do
# parse/transform/validate params
# load the resource from DB or something else
# ...
end
In a view I always need more than one resource so the above code must be “duplicated” for each resource.
Then I need to load my resource on mount
, on handle_params
and on handle_info
(because I listen for data change via PubSub).
So I write:
def mount(params, _session, socket) do
{:ok,
socket
|> load_resource_a(params)
|> load_resource_b(params)
|> load_resource_c(params)}
end
def handle_params(params, _uri, socket) do
{:noreply,
socket
|> load_resource_a(params)
|> load_resource_b(params)
|> load_resource_c(params)}
end
def handle_info(message, socket) do
{:noreply,
socket
|> load_resource_a(message)
|> load_resource_b(message)
|> load_resource_c(message)}
end
Please ignore that handle_info has a message and not params as argument, it is not so important now.
First refactor is for sure to create a function that load everything and use it in each live view event:
def load_all_resources(socket, params) do
socket
|> load_resource_a(message)
|> load_resource_b(message)
|> load_resource_c(message)
end
def mount(params, _session, socket) do
{:ok, load_all_resources(socket, params)}
end
# the same for handle_params and handle_info ...
That is my pain, do you do the same?
My solution
I read many times the big disclaimer about macros , but I think that maybe this is a right case to use them.
I ended up with something like the following:
defmodule BurellixyzWeb.LiveViewData do
defmacro __using__(_env) do
quote do
import BurellixyzWeb.LiveViewData
@load_fns []
@reload_fns []
@before_compile BurellixyzWeb.LiveViewData
end
end
defmacro loader(resource_name, get_resource_opts \\ nil) do
resource_name_atom = String.to_atom(resource_name)
get_resource_fn = String.to_atom("get_" <> resource_name)
load_resource_fn = String.to_atom("load_" <> resource_name)
reload_resource_fn = String.to_atom("reload_" <> resource_name)
quote do
@load_fns [unquote(load_resource_fn) | @load_fns]
@reload_fns [unquote(reload_resource_fn) | @reload_fns]
def unquote(load_resource_fn)(socket, params) do
resource = unquote(get_resource_fn)(params, socket, unquote(get_resource_opts))
assign(socket, unquote(resource_name_atom), resource)
end
def unquote(reload_resource_fn)(socket, message \\ nil) do
resource = unquote(get_resource_fn)(message, socket, unquote(get_resource_opts))
assign(socket, unquote(resource_name_atom), resource)
end
end
end
defmacro __before_compile__(_env) do
quote do
def load_all_resources(socket, params) do
@load_fns
|> Enum.reverse()
|> Enum.reduce(socket, fn name, socket ->
apply(__MODULE__, name, [socket, params])
end)
|> assign(:params, params)
end
def reload_all_resources(socket, message) do
@reload_fns
|> Enum.reverse()
|> Enum.reduce(socket, fn name, socket ->
apply(__MODULE__, name, [socket, message])
end)
end
end
end
end
So the developer have to write only the function to get the resource, something like this:
use BurellixyzWeb.LiveViewData
def get_my_resource(params, socket) do
# load resource from DB or other source ...
end
loader("my_resource")
Macros will generate the others functions and the functions the group them all.
And, in simple cases, to avoid writing live view callbacks, developer can use the following macro:
defmacro default_callbacks do
quote do
def mount(params, _session, socket) do
{:ok, load_all_resources(socket, params)}
end
def handle_params(params, _uri, socket) do
{:noreply,
socket
|> load_all_resources(params)}
end
def handle_info(message, socket) do
{:noreply,
socket
|> reload_all_resources(message)}
end
defoverridable mount: 3, handle_params: 3, handle_info: 2
end
end
instead of live view callbacks, developer writes:
default_callbacks()
What do you think?
Many thanks!