Working With Genie Apps: Intermediate Topics


Handling forms

Now, the problem is that Bill Gates reads – a lot! It would be much easier if we would allow our users to add a few books themselves, to give us a hand. But since, obviously, we're not going to give them access to our Julia REPL, we should setup a web page with a form. Let's do it.

We'll start by adding the new routes:

# routes.jl
route("/bgbooks/create", BooksController.create, method = POST, named = :create_book)

The first route will be used to display the page with the new book form. The second will be the target page for submitting our form - this page will accept the form's payload. Please note that it's configured to match POST requests and that we gave it a name. We'll use the name in our form so that Genie will dynamically generate the correct links to the corresponding URL (to avoid hard coding URLs). This way we'll make sure that our form will always submit to the right URL, even if we change the route (as long as we don't change the name).

Now, to add the methods in BooksController. Add these definition under the billgatesbooks function (make sure you add them in BooksController, not in BooksController.API):

# BooksController.jl
function new()
  html(:books, :new)

function create()
  # code here

The new method should be clear: we'll just render a view file called new. As for create, for now it's just a placeholder.

Next, to add our view. Add a blank file called new.jl.html in app/resources/books/views. Using Julia:

julia> touch("app/resources/books/views/new.jl.html")

Make sure that it has this content:

<!-- app/resources/books/views/new.jl.html -->
<h2>Add a new book recommended by Bill Gates</h2>
  For inspiration you can visit <a href="" target="_blank">Bill Gates' website</a>
<form action="$(Genie.Router.linkto(:create_book))" method="POST">
  <input type="text" name="book_title" placeholder="Book title" /><br />
  <input type="text" name="book_author" placeholder="Book author" /><br />
  <input type="submit" value="Add book" />

Notice that the form's action calls the linkto method, passing in the name of the route to generate the URL, resulting in the following HTML: <form method="POST" action="/bgbooks/create">.

We should also update the BooksController.create method to do something useful with the form data. Let's make it create a new book, persist it to the database and redirect to the list of books. Here is the code:

# BooksController.jl
using Genie.Router, Genie.Renderer

function create()
  Book(title = params(:book_title), author = params(:book_author)) |> save && redirect(:get_bgbooks)

A few things are worth pointing out in this snippet:

  • again, we're accessing the params collection to extract the request data, in this case passing in the names of our

form's inputs as parameters.

We need to bring Genie.Router into scope in order to access params;

  • we're using the redirect method to perform a HTTP redirect. As the argument we're passing in the name of the route,

just like we did with the form's action. However, we didn't set any route to use this name. It turns out that Genie gives default names to all the routes.

We can use these – but a word of notice: these names are generated using the properties of the route, so if the route changes it's possible that the name will change too. So either make sure your route stays unchanged – or explicitly name your routes. The autogenerated name, get_bgbooks corresponds to the method (GET) and the route (bgbooks).

In order to get info about the defined routes you can use the Router.named_routes function:

julia> Router.named_routes()
OrderedCollections.OrderedDict{Symbol, Genie.Router.Route} with 6 entries:
  :get_bgbooks        => [GET] /bgbooks => billgatesbooks | :get_bgbooks
  :get_bgbooks_new    => [GET] /bgbooks/new => new | :get_bgbooks_new
  :get                => [GET] / => () | :get
  :get_api_v1_bgbooks => [GET] /api/v1/bgbooks | :get_api_v1_bgbooks
  :create_book        => [POST] /bgbooks/create | :create_book

Let's try it out. Input something and submit the form. If everything goes well a new book will be persisted to the database – and it will be added at the bottom of the list of books.

Uploading files

Our app looks great – but the list of books would be so much better if we'd display the covers as well. Let's do it!

Modify the database

The first thing we need to do is to modify our table to add a new column, for storing a reference to the name of the cover image. Per best practices, we'll use database migrations to modify the structure of our table:

julia> SearchLight.Generator.newmigration("add cover column")
[debug] New table migration created at db/migrations/2019030813344258_add_cover_column.jl

Now we need to edit the migration file - please make it look like this:

# db/migrations/*_add_cover_column.jl
module AddCoverColumn

import SearchLight.Migrations: add_column, add_index

# SQLite does not support column removal so the `remove_column` method is not implemented in the SearchLightSQLite adapter
# If using SQLite leave the next line commented -- otherwise uncomment it
# import SearchLight.Migrations: remove_column

function up()
  add_column(:books, :cover, :string)

function down()
  # if using the SQLite backend, leave the next line commented -- otherwise uncomment it
  # remove_column(:books, :cover)


Looking good - lets ask SearchLight to run it:

julia> SearchLight.Migration.lastup()
[debug] Executed migration AddCoverColumn up

If you want to double check, ask SearchLight for the migrations status:

julia> SearchLight.Migration.status()

|   |                  Module name & status  |
|   |                             File name  |
|   |                   CreateTableBooks: UP |
| 1 | 2018100120160530_create_table_books.jl |
|   |                     AddCoverColumn: UP |
| 2 |   2019030813344258_add_cover_column.jl |

Perfect! Now we need to add the new column as a field to the Books.Book model:

module Books

using SearchLight, SearchLight.Validation, BooksValidator

export Book

Base.@kwdef mutable struct Book <: AbstractModel
  id::DbId = DbId()
  title::String = ""
  author::String = ""
  cover::String = ""


As a quick test we can extend our JSON view and see that all goes well - make it look like this:

# app/resources/books/views/billgatesbooks.json.jl
"Bill's Gates list of recommended books" => [Dict("author" =>,
                                                  "title" => b.title,
                                                  "cover" => b.cover) for b in books]

If we navigate http://localhost:8000/api/v1/bgbooks you should see the newly added "cover" property (empty, but present).

Heads up!

Sometimes Julia/Genie/Revise fails to update structs on changes. If you get an error saying that Book does not have a cover field or that it can not be changed, you'll need to restart the Genie app.

File uploading

Next step, extending our form to upload images (book covers). Please edit the new.jl.html view file as follows:

<h3>Add a new book recommended by Bill Gates</h3>
  For inspiration you can visit <a href="" target="_blank">Bill Gates' website</a>
<form action="$(Genie.Router.linkto(:create_book))" method="POST" enctype="multipart/form-data">
  <input type="text" name="book_title" placeholder="Book title" /><br />
  <input type="text" name="book_author" placeholder="Book author" /><br />
  <input type="file" name="book_cover" /><br />
  <input type="submit" value="Add book" />

The new bits are:

  • we added a new attribute to our <form> tag: enctype="multipart/form-data". This is required in order to support files payloads.
  • there's a new input of type file: <input type="file" name="book_cover" />

You can see the updated form by visiting http://localhost:8000/bgbooks/new

Now, time to add a new book, with the cover! How about "Identity" by Francis Fukuyama? Sounds good. You can use whatever image you want for the cover, or maybe borrow the one from Bill Gates, I hope he won't mind Just download the file to your computer so you can upload it through our form.

Almost there - now to add the logic for handling the uploaded file server side. Please update the BooksController.create method to look like this:

# BooksController
function create()
  cover_path = if haskey(filespayload(), "book_cover")
      path = joinpath("img", "covers", filespayload("book_cover").name)
      write(joinpath("public", path), IOBuffer(filespayload("book_cover").data))


  Book( title = params(:book_title),
        author = params(:book_author),
        cover = cover_path) |> save && redirect(:get_bgbooks)

Also, very important, you need to make sure that BooksController is using Genie.Requests.

Regarding the code, there's nothing very fancy about it. First we check if the files payload contains an entry for our book_cover input. If yes, we compute the path where we want to store the file, write the file, and store the path in the database.

Please make sure that you create the folder covers/ within public/img/.

Great, now let's display the images. Let's start with the HTML view - please edit app/resources/books/views/billgatesbooks.jl.html and make sure it has the following content:

<!-- app/resources/books/views/billgatesbooks.jl.html -->
<h1>Bill's Gates top $( length(books) ) recommended books</h1>
for_each(books) do book
  <li><img src='$( isempty(book.cover) ? "img/docs.png" : book.cover )' width="100px" /> $(book.title) by $(</li>

Here we check if the cover property is not empty, and display the actual cover. Otherwise we show a placeholder image. You can check the result at http://localhost:8000/bgbooks

As for the JSON view, it already does what we want - you can check that the cover property is now outputted, as stored in the database: http://localhost:8000/api/v1/bgbooks

Success, we're done here!

Heads up!

In production you will have to make the upload code more robust - the big problem here is that we store the cover file as it comes from the user which can lead to name clashes and files being overwritten - not to mention security vulnerabilities. A more robust way would be to compute a hash based on author and title and rename the cover to that.

One more thing

So far so good, but what if we want to update the books we have already uploaded? It would be nice to add those missing covers. We need to add a bit of functionality to include editing features.

First things first - let's add the routes. Please add these two new route definitions to the routes.jl file:

route("/bgbooks/:id::Int/edit", BooksController.edit)
route("/bgbooks/:id::Int/update", BooksController.update, method = POST, named = :update_book)

We defined two new routes. The first will display the book object in the form, for editing. While the second will take care of actually updating the database, server side. For both routes we need to pass the id of the book that we want to edit - and we want to constrain it to an Int. We express this as the /:id::Int/ part of the route.

We also want to:

  • reuse the form which we have defined in app/resources/books/views/new.jl.html
  • make the form aware of whether it's used to create a new book, or for editing an existing one respond accordingly by setting the correct action
  • pre-fill the inputs with the book's info when editing a book.

OK, that's quite a list and this is where things become interesting. This is an important design pattern for CRUD web apps. In order to simplify the rendering of the form, we will always pass a book object into it. When editing a book it will be the book corresponding to the id passed into the route. And when creating a new book, it will be just an empty book object we'll create and then dispose of.

Using view partials

First, let's set up the views. In app/resources/books/views/ please create a new file called form.jl.html. Then, from app/resources/books/views/new.jl.html cut the <form> code. That is, everything between the opening and closing <form>...</form> tags. Paste it into the newly created form.jl.html file. Now, back to new.jl.html, instead of the previous <form>...</form> code add:

<% partial("app/resources/books/views/form.jl.html", context = @__MODULE__) %>

This line, as the partial function suggests, includes a view partial, which is a part of a view file, effectively including a view within another view. Notice that we're explicitly passing the context so Genie can set the correct variable scope when including the partial.

You can reload the new page to make sure that everything still works: http://localhost:8000/bgbooks/new

Now, let's add an Edit option to our list of books. Please go back to our list view file, billgatesbooks.jl.html. Here, for each iteration, within the for_each block we'll want to dynamically link to the edit page for the corresponding book.

for_each with view partials

However, this for_each which renders a Julia string is very ugly - and we now know how to refactor it, by using a view partial. Let's do it. First, replace the body of the for_each block:

<!-- app/resources/books/views/billgatesbooks.jl.html -->
"""<li><img src='$( isempty(book.cover) ? "img/docs.png" : book.cover )' width="100px" /> $(book.title) by $("""


partial("app/resources/books/views/book.jl.html", book = book, context = @__MODULE__)

Notice that we are using the partial function and we pass the book object into our view, under the name book (will be accessible in book inside the view partial). Again, we're passing the scope's context (our controller object).

Next, create the book.jl.html in app/resources/books/views/, for example with

julia> touch("app/resources/books/views/book.jl.html")

Add this content to it: TO BE CONTINUED