fceruti

fceruti OP

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

And here’s a vim command to search and replace:

:%s/socket\.assigns\.\([a-zA-Z0-9_]*\)/\~a|\1|

First 10 of 13 Posts Switch mode

tfwright

tfwright

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

fceruti

fceruti OP

No reason, probably a bug :stuck_out_tongue:

ityonemo

ityonemo

  1. 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.
  2. 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
zachallaun

zachallaun

If you wanted to be really evil, you could overload the @ operator if the caller environment contains socket :smiling_imp:

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())

(don’t do this)

v0idpwn

v0idpwn

This would have a lot of potential if there were good overridable unary operators in Elixir.

Unfortunately, +, -, !, ^, not, @, & are too useful and meaningful to be overriden. There is ~~~ but its ugly and too hard to write :stuck_out_tongue: .

Makes me think Elixir maybe could support an unary $ operator.

ityonemo

ityonemo

I mean, it’s what happens in EEx templates, though!

cmo

cmo

$ is being used for the potential new type system

theodore

theodore

Pretty cool!

sodapopcan

sodapopcan

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 :see_no_evil_monkey: :sweat_smile:) , 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)

zachallaun

zachallaun

Glad it was informative!

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).

Where Next? Top

Trending in Guides/Tuts Top

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New

We're in Beta

About us Mission Statement