nikokozak

nikokozak

Implementing Phoenix-like EEx functionality from scratch

Hello!

I’ve been working on a little news aggregator that uses EEx templates to render pages, and I’ve found myself reconstructing a lot of Phoenix’s functionality (I’m not actually using Phoenix, just Plug/Cowboy), but haven’t quite found an elegant way to deal with compiling my templates so that I don’t have to read from eex files in priv/ at runtime.

At the moment, I have a base Template module with a __using__ macro that injects the following render/3 function into my “views”. The function calls back out to the Template module, passing in information regarding the caller module, the file we want to render, etc:

In Template:

defmacro __using__(opts) do
    layout = Keyword.get(opts, :layout, "base.html.eex") |> (&(Path.join(@layout_dir, &1))).()
    quote do
        def render(conn, file, assigns \\ []), do: Template.render(__MODULE__, conn, file, assigns)

        def layout, do: unquote(layout)
    end
end

def render(module, %{ status: status } = conn, file, assigns) do
	rendered_template = render_template(module, file, assigns)
	rendered_layout = render_layout(module, [ assigns | [ inner_content: rendered_template ] ])

	Plug.Conn.send_resp(conn, (status || 200), rendered_layout)
end 

# Both render_layout and render_function are ultimately calls to this next function, render_file

defp render_file(file, assigns, eval_options \\ []) do
	quoted = EEx.compile_file(file)
	# Note that binding() will return, among others, the "assigns" parameter, allowing us to use the @key EEx convenience in templates.
	{result, _binding} = Code.eval_quoted(quoted, binding(), eval_options)
	result
end

Of course, this means that I’m constantly compiling and rendering files at runtime, when I could be precompiling them. In trying to figure out how Phoenix handles this, I found the following function in Phoenix.Template that seems to be at the heart of how templates are compiled in the framework:

defp compile(path, root, engines) do
	name   = template_path_to_name(path, root)
	defp   = String.to_atom(name)
	ext    = Path.extname(path) |> String.trim_leading(".") |> String.to_atom
	engine = Map.fetch!(engines, ext)
	quoted = engine.compile(path, name)

	{name, quote do
	  @file unquote(path)
	  @external_resource unquote(path)

	  defp unquote(defp)(var!(assigns)) do
	    _ = var!(assigns)
	    unquote(quoted)
	  end

	  defp render_template(unquote(name), assigns) do
	    unquote(defp)(assigns)
	  end
	end}
end

This gets called from a defmacro __before_compile__(env) declaration, which in turn unquotes the newly minted functions in whatever view we’re building. I want to clear up my understanding of how this works before running off and trying to re-implement this on my own:

  1. From what I gather, all .eex template files, regardless of whether or not they’re actually “used”, are compiled according to the namespace of their parent view? It’s my understanding that these get turned into named functions that correspond to their file names?

  2. When calling render(conn, <filename>) in a controller, we’re not actually saying “hey, find this file and render it”, rather what we’re doing is calling a function named according to <filename>? This function is the already-compiled EEx template injected into the corresponding view?

  3. Output from an EEx.compile_file/2 call will generate a “quoted” structure - to evaluate said structure, is it enough to simply unquote it within a function, and ensure that an assigns binding is present? At present I’ve been evaluating it with Code.eval_quoted, but it seems that the Phoenix implementation doesn’t use this?

  4. I’ve been incorporating the caller view-module’s “context” (i.e. functions declared in the “view” module) that I want available in the EEx template by passing in functions: [{<caller module>, <caller module>.__info__(:functions)}] into the Code.eval_quoted call within the parent Template module. Is this made redundant if I’m injecting a function into the view that contains the unquoted result of EEx.compile_file? In other words, will the now-unquoted structure generated by compile_file have access to the surrounding module’s functions?

It’s been hard developing an intuitive understanding of how this works behind the scenes - it’s so seamless when you’re using these constructs as they’re provided! Any help clarifying these points would be much appreciated.

Most Liked

stefanchrobot

stefanchrobot

Sorry if this sounds unhelpful, but can’t you just incorporate the Phoenix.View since it’s been pulled out of Phoenix?

I think that nimble_publisher might be interesting for your use case.

dmitriid

dmitriid

For my own little proof of concept with LiveView single-file components I do the following:

  • read file contents
  • generate module source. In this case I’m literally building a string of the source, but it can be done with creative uses of quote do (though I don’t have an example at the moment)
  • call Code.compile_quoted

This way you can compile everything on start (provided you start your compiler beforehand)


Another option (if you don’t want to monitor files and only compile them on startup) is to create a mix compiler task and add it to compiler list. Mix task, and config

nikokozak

nikokozak

I ended up doing a similar thing, pre-compiling everything via macros. It’s essentially a naive re-implementation of Phoenix’s way of doing things (in that it relies on “views” that hold the newly-compiled “templates” as functions), but as an exercise it was great in that now I feel I understand what’s going on. Here’s a gist of it, in case anyone finds it helpful.

Where Next?

Popular in Questions Top

vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
Kagamiiiii
Student &amp; New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

Other popular topics Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

We're in Beta

About us Mission Statement