Phoenix Template: design pattern to check if exist or not

Hi guys.

I’m getting this error because of how I coded my template:

function nil.name/0 is undefined

The offending code:

 <%= @company.country.name %>

What’s elixir idiomatic way of writing this to check if exist display the country name else don’t display and don’t access country.name so I don’t trigger the error? I did an if else before but I feel like there may be something better out there.

Thanks

I just did:

  52       <%= if @company.country do %>
  53         <%= @company.country.name %>
  54       <% end %>

I think I’m just making it more complicated than it is.

You can add a country_name/1 function in your View module like so:

def country_name(%{country: %{name: name}}), do: name
def country_name(_), do: "" # or "No country"

Then just:

<%= country_name(company) %>
5 Likes

You can also use the bracket-based access syntax like this: @company.country[:name]

Furthermore, the bracket-based access syntax transparently ignores nil values. When trying to access anything on a nil value, nil is returned:

https://hexdocs.pm/elixir/Access.html

This would work just fine, but perhaps won’t transmit intent as good as an if/else or a separate view function would.

6 Likes

One thing to keep in mind is that the bracket based syntax will only help you on level deep. If you need to go deeper you may want to look into Kernel.get_in and friends.

2 Likes

For me it seems top work arbitrary deep:

iex(1)> nil[:i][:can][:go][:as][:many][:levels][:i][:want]
nil
4 Likes

Ah, I stand corrected.

Here’s the specific section in the docs:

https://hexdocs.pm/elixir/Access.html#module-bracket-based-access

Which states:

This syntax is very convenient as it can be nested arbitrarily:

iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"][:age], 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}

Furthermore, the bracket-based access syntax transparently ignores nil values. When trying to access anything on a nil value, nil is returned:

iex> keywords = [a: 1, b: 2]
iex> keywords[:c][:unknown]
nil

iex> nil[:a]
nil
1 Like

I also like using the view module for doing that kind of stuff, which allows my templates to need less amount of logic. They just display what’s given.

def render("myview.html", assigns) do
    country_name = assigns.company[:country][:name] || "Unavailable"
    render_template("myview.html", Map.merge(assigns, %{country_name: country_name}
end
4 Likes

Is there a more modern way of doing this? All of these solutions seem cumbersome to me, and the [:prop] syntax doesn’t work with objects with a schema. Looking for something lightweight, template-only.

I don’t think there is. Especially with LV you want to set defaults before the rendering happens rather than doing that stuff within the template.

2 Likes

I’m probably going to say something nothing to do with this as most of this forum is over my head :slight_smile: , but can’t you just check for nil in the template? I’m very new to Phoenix so this may be something added recently.

For example if I have at least 1 user in my users table the below works:

(for my mount function)
@default_user %User{id: 0, username: "Anonymous"}
...
<%= @current_user.id === 0 do %>
<%= live_patch "Register", to: Routes.user_registration_path(@socket, :new), class: "register-btn" %>
<%= live_patch "Log in", to: Routes.user_session_path(@socket, :new), class: "login-btn" %>

If I have no users in my table I will get an error similar to OP because it has no database values to compare against though. Can’t compare nothing with 0.

If I do the below it works whether I have no entires or the default user is set.

<%= if @current_user === nil || @current_user.id === 0 do %>
<%= live_patch "Register", to: Routes.user_registration_path(@socket, :new), class: "register-btn" %>
<%= live_patch "Log in", to: Routes.user_session_path(@socket, :new), class: "login-btn" %>

Should add it only works if you compare with nil before comparing with another value as it will go with whichever condition it met first.

Yeah, this is what I do, I was hoping there was something like to <%= default "No data" foo.bar.baz % >

I had the same issue as @Miserlou, and settled on the solution below:

I have an App.Helpers module that I’ve aliased in app_web.ex
(though for just this, it probably makes more sense as AppWeb.Helpers):

defp html_helpers do
  quote do
    ...
    alias App.Helpers
    ...
  end
...
end

In App.Helpers:

defmodule App.Helpers do
  @moduledoc """
  Global helper functions
  """

  @doc """
  Gets a field from a struct that can be deeply nested.
  Returns nil if key is not found.

  To be used in heex templates.
  """
  @spec get_field(struct(), [atom()]) :: any()
  def get_field(struct, keys) do
    access_keys = Enum.map(keys, &Access.key(&1))
    get_in(struct, access_keys)
  end
...
end

Then, I use it in heex templates like so:

<%= Helpers.get_field(@foo, [:bar, :baz]) %>

It’s the same thing as:

<%= get_in(foo, [Access.key(:bar), Access.key(:baz)]) %>

but I got tired of repeatedly typing Access.key and its verbosity when I needed to access a field in a struct returned by Ecto where the preloaded association(s) may be nil.

Edit:
Helpers.get_field/2 can be extended with a third parameter to return some other default value like so:

@spec get_field(struct(), [atom()], any()) :: any()
def get_field(struct, keys, default \\ nil) do
  access_keys = Enum.map(keys, &Access.key(&1))

  case get_in(struct, access_keys) do
    nil -> default
    other -> other
  end
end
<%= Helpers.get_field(@foo, [:bar, :baz], "No data") %>
1 Like