1
votes

I want to implement a custom route helper function for a Post model, which can either link to a thread root, or a thread root with a hash in the case that the Post is a reply to the thread (they share the Post model).

Where can I define this function to make it easily accessible from within controllers, views, and templates?

I've been able to get away with implementing the function as in the PostView, but now I would like to use it from the controller, and it seems like it would be appropriate to live in Router.Helpers like the other URL helpers.

I've tried implementing it in the Router, but that doesn't work as expected either.

  def post_path(conn, action, post) do
    if Post.thread?(post) do
      "#{board_thread_path(conn, action, post.board.path, post)}"
    else
      "#{board_thread_path(conn, action, post.board.path, post.thread)}#post-#{post.number}"
    end
  end
2

2 Answers

1
votes

There is absolutely no difference where you put a function. Functions in Elixir are fully stateless (and there's no class hierarchies), meaning they might be put in, literally, any module (as long as the calling point can access that module, which will be the norm unless you're using umbrellas).

Secondly, when you find yourself in a position of needing a function in both controller and view (and even template,) it’s a clear sign you are doing it wrong. A wild guess would be you want to use contexts.


I could have explained why it “does not work as expected either” and what induces this error if you had the error message or like posted. I expect the issue is with board_thread_path/4 that is injected by one of your use Blah in your model (?). I have no idea if it is injected publicly, or privately. If publicly, just call it using the fully-qualified name:

def post_path(conn, action, post) do
  path = MyApp.MyModule.board_thread_path(conn, action, post.board.path, post)
  if Post.thread?(post),
    do: path, else: path <> "#post-#{post.number}"
end

If privately, wrap in there into the public one (remember: that’s likely the wrong approach) and call the new wrapper function by the fully qualified name from everywhere.

To avoid FQ-names one might use Kernel.SpecialForms.import/2.

1
votes

I had a similar use case and if what you are after is also to automatically get your custom helper where other route helpers are available, I was able to achieve this by defining my own new module and importing it where a phoenix app already imports the router helpers: the controller and view functions in the MyAppWeb module, and (if you want to use it in your tests also) the using function in MyAppWeb.ConnCase.

So your new file could be lib/my_app_web/route_helpers.ex:

defmodule MyApp.RouteHelpers do
  import MyAppWeb.Router.Helpers, only: [board_thread_path: 4]

  alias MyApp.Post

  def post_path(conn, action, post) do
    if Post.thread?(post) do
      "#{board_thread_path(conn, action, post.board.path, post)}"
    else
      "#{board_thread_path(conn, action, post.board.path, post.thread)}#post-#{post.number}"
    end
  end
end

Then you would add import MyAppWeb.RouteHelpers to the aforementioned functions.

I'm a novice with Elixir/Phoenix so I'm not sure if this is the "right way" but it seems logical and it saves you from having to import the specific methods from the PostView module everywhere you might call the route.