#20: Intro to Module Plugs
Let’s pick up where we left off in episode 19 by converting our function plug to a module plug.
Back in our MovieController
, we have our function plug movie_total
that assigns the total number of our movies to our connection.
However, we don’t need it to be set for every action in our resource. Let’s update it to only be set for the index action.
after plug :movie_total
let’s add the guard clause: when action in [:index]
lib/teacher_web/controllers/movie_controller.exdefmodule TeacherWeb.MovieController do
plug :movie_total when action in [:index]
…
end
Then let’s the open our app.html.eex
and remove the line where we render the movie total.
Now in our movie index.html.eex
template we can add our movie total like we had before
Template path: lib/teacher_web/templates/movie/index.html.eex
...
<p>Movie total: <%= @conn.assigns[:movie_total] %></p>
...
Another way we can access our movie_total
is with at movie total. And since we always expect our movie total to always available for this action, let’s use it instead.
Template path: lib/teacher_web/templates/movie/index.html.eex
...
<p>Movie total: <%= @movie_total %></p>
...
Let’s jump to the browser and ensure we only see the movie total on our index action - and great it’s still working as expected.
Now let’s open our MovieData
module and convert it to a module plug.
A module plug has two functions: init
and call
.
init
initializes a set of options.
We’ll just return our options to start.
call
takes a Plug.Conn
struct and the options returned by our init function. This is where we would modify our struct. It also needs to return our connection struct.
Let’s use the same logic from our movie_total
function for now.
lib/teacher/movie_data.exdefmodule Teacher.MovieData do
…
def init(opts) do
opts
end
def call(conn, _opts) do
movie_total = Repo.one(from m in Movie, select: count("*"))
assign(conn, :movie_total, movie_total)
end
end
Now that we have our module plug, let’s go back to our MovieController
and update it to use our new plug.
First we’ll want alias our new module, so let’s change our import
to alias
.
Then we’ll replace our function plug name with the our module plug, MovieData
.
lib/teacher_web/controllers/movie_controller.exdefmodule TeacherWeb.MovieController do
use Teacher.Web, :controller
alias Teacher.MovieData
alias TeacherWeb.Movie
plug MovieData when action in [:index]
…
end
Back in our browser we see that our module plug is working - our movie total is being loaded.
Now let’s go back to our module plug, and in the init
function we’re just returning any options that are passed in. We’re then ignoring them in the call
function. Let’s change that.
Let’s pass in a message as an option that we’ll render along with our movie total.
The options passed into our init function are a keyword list, so let’s use Keyword.fetch/2
to return our message.
Now we could use Keyword.fetch!/2
, which will return our message or raise an exception, whereas the Keyword.fetch/2
function, will return an :ok tuple with our message or :error if they key doesn’t exist.
Let’s use this (and have some fun) using pattern matching to match against our two cases.
We’ll update our call
function accept our message as the second argument.
Then we’ll get our movie_total
at the top of our function.
Now we can use a case statement with our message.
First we’ll pattern match against our success case, which will return an ok tuple with our message.
Next we’ll pattern match against our error case.
Great now if we have a message, we’ll build a custom message that contains our fetched message along with our movie_total
.
Then let’s assign it to our connection as movie_total_msg
.
Then in our :error case, let’s create a default message, which will include our movie_total
.
Again we’ll assign it to our connection as movie_total_msg
lib/teacher/movie_data.exdefmodule Teacher.MovieData do
…
def init(opts) do
Keyword.fetch(opts, :msg)
end
def call(conn, msg) do
movie_total = Repo.one(from m in Movie, select: count("*"))
case msg do
{:ok, msg} ->
custom_msg = "#{msg} #{movie_total}"
assign(conn, :movie_total_msg, custom_msg)
:error ->
default_msg = "We found #{movie_total} movies."
assign(conn, :movie_total_msg, default_msg)
end
end
end
Then let’s open our movie_controller
and we’ll include a message as an option for our plug.
lib/teacher_web/controllers/movie_controller.exdefmodule TeacherWeb.MovieController do
…
plug MovieData, [msg: "Your total number of movies:"] when action in [:index]
…
end
With that we can then open our movie index.html.eex
and update our message to just render @movie_total_msg
Template path: lib/teacher_web/templates/movie/index.html.eex
...
<p><%= @movie_total_msg %></p>
...
And if we refresh our browser we see our custom message.
Now let’s go back to our plug and change our init
function to fetch a key that doesn’t exist. Keyword.fetch(opts, :foo)
lib/teacher/movie_data.exdefmodule Teacher.MovieData do
…
def init(opts) do
Keyword.fetch(opts, :foo)
end
def call(conn, msg) do
movie_total = Repo.one(from m in Movie, select: count("*"))
case msg do
{:ok, msg} ->
custom_msg = "#{msg} #{movie_total}"
assign(conn, :movie_total_msg, custom_msg)
:error ->
default_msg = "We found #{movie_total} movies."
assign(conn, :movie_total_msg, default_msg)
end
end
end
Then back in our browser, we see our default message being displayed.