Sup ppl, I just wanted to share a little macro I created to simplify LiveView code. It allows you to convert
socket.assings.variable
into
~a|variable|
For instance here’s a function from Chris McCord’s live_trek
def handle_event("delete", %{"id" => id}, socket) do
todo = Todos.get_todo!(socket.assigns.scope, id)
{:ok, _} = Todos.delete_todo(socket.assigns.scope, todo)
{:noreply, socket}
end
Would be
def handle_event("delete", %{"id" => id}, socket) do
todo = Todos.get_todo!(~a|scope|, id)
{:ok, _} = Todos.delete_todo(~a|scope|, todo)
{:noreply, socket}
end
Necessary? Not really.
But I thought it looks much better, specially when you have lot’s of socket.assigns and want to reduce verbosit, avoid unpacking assigns and reduce formatter line breaks.
Here’s v0.1
defmacro sigil_a(expr, _modifiers) do
socket = Macro.var(:socket, nil)
quote do
Map.get(unquote(socket).assigns, String.to_existing_atom(unquote(expr)))
end
end
Nice idea. Curious, why Map.get rather than fetch? I think the behavior being replaced raises if the expected key is missing, whereas this will return nil I think
You can also move String.to_existing_atom to just String.to_atom outside of the quoted block, this saves you a function call at runtime.
might want to try var!(socket) inside the quote instead of Macro.var outside the quote. I think they are equivalent but var!(…) Is slightly more legible and idiomatic IMO
If you wanted to be really evil, you could overload the @ operator if the caller environment contains socket
defmodule Evil do
defmacro __using__(_) do
quote do
import Kernel, except: [@: 1]
import Evil
end
end
import Kernel, except: [@: 1]
defmacro @name do
var = Macro.var(:socket, nil)
if assigns_call?(__CALLER__, name) do
{name, _, _} = name
quote(do: Map.fetch!(unquote(var).assigns, unquote(name)))
else
quote(do: Kernel.@(unquote(name)))
end
end
defp assigns_call?(env, {name, _, nil}) when is_atom(name) do
Macro.Env.has_var?(env, {:socket, nil})
end
defp assigns_call?(_, _), do: false
end
defmodule Hmm do
use Evil
@okay :foo
def maybe(socket) do
@okay
end
def wow do
@okay
end
end
dbg(Hmm.maybe(%{assigns: %{okay: :NEAT!}}))
dbg(Hmm.wow())
Innnnnteresting. I was trying to figure out how to do something like this and couldn’t figure it out. I was trying to use var!/2 for this to no avail. I never understood that you can check the caller env.
I was looking to create a single guard that would work on both socket and assigns. I have a lot of single module CRUD LiveViews so I wanted something like:
def handle_event("update", %{"user" => user_params}, socket) when in_action([:edit]) do
#...
end
def render(assigns) when in_action([:edit]) do
~H"""
"""
end
This would check whichever existed of socket.assigns.live_action or assigns.live_action was in [:edit].
Even though I don’t think that is particularly evil (that is, what I wanted to do—what you’re suggesting is absolutely pure evil ) , I ultimately decided I’d rather not do anything out the ordinary like this so I never bothered to seek help figuring it out, but it’s nice to know how it could be done, so thanks!
(that is, if that would even solve the problem… I haven’t actually tried, just responded immediately, lol, but still informative for me)
It’s definitely worth digging into the environment info you have available in macros. Even in the OP’s example, you could check the caller env for a socket var and raise an informative error if it doesn’t exist (as opposed to the underlying Map.fetch! error or whatever you end up using).