As tempting as it might be, please refrain from starting to work on this (and future lecture instructions) before class starts. Half the fun is to explore the challenges together with your partner. Do watch the videos however before class starts!
In the videos you have learned some basics of the elixir programming language and Phoenix routes.
Today we are going to create a course page that lists the courses that you have taken in the last semesters. The idea is that we will end up with a page that displays the correct courses in form of a table depending on url:
My page is completely unstyled and ugly but functional! You will have an opportunity to style your page at the end but remember the golden rule: Functionality always comes first.
Let us start by revisiting the Router. We have learned how we can add basic routes. Lets look at how we can pass information into the url:
get "/courses", PageController, :courses
get "/courses/:slug", PageController, :courses
Notice that we created two routes and both point to the same action!
:slug
is a placeholder and allows us to put anything in the url such as:
localhost:4000/courses/spring_2025
localhost:4000/courses/1
We can fetch what was given in the controller action:
def courses(conn, %{"slug" => slug}) do
IO.inspect slug
render(conn, :courses, layout: false)
end
Figure out what 'IO.inspect' is doing. Also notice how the IO.inspect slug
line was written. This doesn't look like it but this is actually a function call. In Elixir parenthesis on function calls are optional if and only if there is no disambiguity:
IO.inspect slug # same as IO.inspect(slug)
do_this a, b, hello: 2 # same as do_this(a, b, [hello: 2])
How cool is that?
Also notice what happend to the first line of the function:
# instead of:
def courses(conn, params) do
slug = Map.get(params, "slug")
end
# we PATTERN-MATCHED to directly get the slug, discarding the rest of params
def courses(conn, %{"slug" => slug}) do
end
Pattern Matching is, in my opinion, one of the most amazing features of the language and reduces the code we write significiantly.
Lets move on by looking at the following handy function first:
defp slug_to_semester(slug) do
slug
|> String.capitalize()
|> String.replace("_", " ")
end
Together with your partner and AI figure out:
- what the difference between
def
anddefp
is - What the pipe
|>
operator is doing - Based on the url's in the images above, can you guess what this function is doing?
Once you can comfortably answer those questions go ahead and add it to your page_html.ex
file under the embed_templates
line. This will make the function available in all our *.html.heex
templates inside our page_html
folder.
Next we will look at a module attribute:
defmodule AppWeb.PageController do
use AppWeb, :controller
@person %{name: "Sam Adams", age: 30}
end
A module attribute is like a global constant for a module. They are prepared at compile time thus cannot be reassigned once your server is running. They are often used to set up configuration, settings, or in our case, simulate data.
We can forward the value of any variable / module attribute to the page via the controller action like this:
defmodule AppWeb.PageController do
use AppWeb, :controller
@person %{name: "Sam Adams", age: 30}
def home(conn, params) do
render(conn, :home, layout: false, person: @person)
end
end
In our home.html.heex
i could now use any of the following to display the person on the screen:
<p>{@person.name} is {@person.age} years old.</p>
<p><%= "%{@person.name} is %{@person.age} years old." %></p>
<p><%= @person.name %> is <%= @person.age %> years old.</p>
Objective: Forwarding Data
Now think about the course data we need for our table. Each course has a name and a "slug" that is used both to display the semester's name and filter the table for correct entries.
Create a module attribute @courses
inside your PageController
and fill it with appropriate data for at least the last two semester's worth of your courses.
Hint A list of maps should do the trick but if you want to challenge yourself try a list of two element tuples instead!
Don't forget to forward the @courses assign to your controller action like it was shown above!
We will now learn on how to display many elements at once.
Add this code to a new file courses.html.heex
under page_html
:
<table>
<thead>
<tr>
<th>Number</th>
</tr>
</thead>
<tbody>
<tr :for={n <- 1..10}>
<td>{n}</td>
</tr>
</tbody>
</table>
Objective: Courses are displayed in a table
Observe the output, then deduce and solve the mystery of how we can display our courses in form of a table!
Objective: Filtering courses
Going back to your controller action:
def courses(conn, ...) do
# @courses and slug provided
# TODO: filter courses by slug
render(...)
end
def courses(conn, ...) do
# only @courses provided
# TODO: do not filter
render(...)
end
In a video we have learned how we can use multiple clauses of a function so that we can handle parameters differently.
This is a perfect opportunity to try this out: One function variant can handle cases where we visited the /courses
while the other function can handle cases where we visited /courses/:slug
Also look up the Elixir documentation for a function that would allow you to filter the list of courses that you have.
If you did everything correctly your website should now produce similar results as I have shown in the screenshots at the beginning.
Objective: courses/2 is guarded
Data Protection is critical in any web application. Think about possible values of slug
. What if the user provided something invalid such as '1'?
We will want to guard against invalid values. We only want to allow and proceed with slug values that actually represent semester_strings we have data for!
Remember the video on guards and try to implement the logic. More information on guards can be found in the documentation.
You are only eligible for this objective if you actually guarded in the function header. Guarding logic within the function body (such as conditionals) is not acceptable.
You may simply let the server return an Error message when attempting to provide a slug that doesnt doesn't exist. This happens by default if attempting to call an action that doesn't exist.
If you feel adventurous you can attempt to instead display a custom error message such as "This appears to be an invalid semster."
Make sure that accessing /courses/invalid
doesn't display the full list of courses like /courses
Objective: Styling
Its time to improve the ugly look of our table. You have two options:
- Scan the Tailwind CSS Documentation for some basic classes with which we could apply some colors, padding, or borders and add them to your html.
For example:
<p>Unstyled</p>
<p class="text-lg font-semibold text-blue-800">Blue big, fat heading.</p>
- Alternatively you could already peek around in the Flowbite library, especially in the "Table" section and copy over a beautiful, professional looking template in which you can encase your own table.
For the objective I am looking into just some visible attempts in using Tailwind to style some parts of the website. Nothing fancy is expected at this point in the semester.