Today we’re going to implement the last of CRUD (Create-Read-Update-Delete) operations on our projects. We already know most of the process, so let’s dive into the code!

First, add DELETE method to /project/#Int route (it’ll look like this: /project/#Int ProjectR GET DELETE. Then, second step – handler. In Handler/Project.hs let’s implement:

deleteProjectR :: Int -> Handler Html
deleteProjectR projectId = do
  _ <- runDB $ deleteBy $ UID projectId
  redirect ProjectsR

and handling is done! Now, how about adding a trigger? In templates/project.hamlet we’ll add two buttons – one for project edition, and one for deletion.
That’s also fairly easy, just add:

  <div>
    <a href=@{ProjectEditR projectId} role="button" .btn .btn-primary>Edit
    <a href=@{ProjectsR} role="button" .btn .btn-danger #delete-project-#{projectId}>Delete

to the end of file. Now, while routing to edition works straight away, that’s not true in case of delete – we need to add a custom JS handler to it. In Yesod this is typically done by using Julius template language – which is simply JavaScript with some variable interpolation. Luckily for us, we don’t have to use pure JavaScript – Yesod in the scaffolding embeds jQuery on the page. While we could live without it, I doubt that many pages will be implemented without this library, so we’ll use it. Of course, it’s not as rich as React or Angular when it comes to user interfaces, but let’s face it – we just need a simple hook here. And it’s sufficient to write:

jQuery("#delete-project-#{rawJS $ show projectId}").on("click", function() {
  jQuery.ajax("@{ProjectR projectId}", { method: "DELETE" });
})

This is just your plain old JavaScript, with one detail – variable interpolation (#{rawJS $ show projectId}). As you can see, it’s a bit different than in Hamlet – the rawJS call is quite important here. If we didn’t use it, the default interpolation would come in – and it uses JSON encoding, so it wouldn’t work (in this exact setup – it’s of course possible to make it work with JSON!).

Note a trick we’ve done here – to keep every Handler a Handler Html, we’ve implemented redirection separately from deletion (deletion is sent on button click, while redirection happens on link). Note that this means bad design and code duplication – we did it purely to avoid API calls requiring JSON/XML responses. We’ll deal with them soon, but for now – let’s avoid them. There is one more problem with this code – it might occassionally display elements that were just deleted in projects list – this is because requests are not ordered, so redirection may be handled before deletion. In extreme case, deletion might not be sent properly. We’ll deal with all these problems in the next post, about API – for now, we can live with it.

For some reason integration with Julius is not as smooth as with Hamlet – several times stack didn’t catch my changes and I had to rebuild manually (reset stack command).

There are two more simple UI changes I want to make: add “back” button to edit page and “add new” button to main projects list.
Both these changes seem quite straightforward:

<a role="button" href=@{ProjectR projectId} .btn .btn-default>Back to view

to templates/project-edition.hamlet and

<a role="button" href=@{ProjectEditR newProjectId} .btn .btn-default>Add new project

to templates/projects.hamlet. But there’s a little problem here – we need to generate newProjectId. And it should be as unique as possible. Before we approach this problem, let’s understand why do we actually face it, and how to avoid such problems in the future.

UID field of Project is an artificial field, that doesn’t actually resemble any domain entity. It serves only for our internal purposes, and – in this sense – is a clone of ProjectId provided automatically by Yesod. We don’t have such trouble with ProjectId not only because we don’t use it – if we did, it would be automatically assigned to a unique identifier. So, to simplify our mental model, we actually shoot ourselves in the foot. Oh well, good that it’s such a simple application, it would be much worse if the app was a real server. We’ll fix this issue straight away, since it’ll be much easier that generating a unique identifier.

This adjustment requires quite some changes!

  • config/projectModels – removal of identifier field and UID uniqueness constraint
  • config/routes#Int to #ProjectId in route signatures
  • Handler/Project.hs – signatures, invocation of renderPage (projectId is totally internal now, so shouldn’t be displayed – but is still needed for routes), getBy404 to get404, deleteBy to delete and page selection – this one might be tricky, so here’s my solution:
    selectPage :: ProjectId -> Widget
    selectPage projectId = do
      project <- handlerToWidget $ runDB $ get404 projectId
      renderPage projectId project
    
  • Handler/Projects.hs – signatures and removal of mapping on fetched projects
  • Handler/ProjectEdit.hs – form (no identifier anymore!), signatures. And upserting – as you can see, upserting doesn’t take ProjectId as argument, therefore – by default – would simply add each project as a new one. To prevent this, we’ll have to implement two routes with two actions – insert for new ones and update for existing ones (using solution proposed on StackOverflow)
  • Database/Projects.hs – we’re actually going to remove this file altogether – we can insert projects via web interface, so for now we do not need hardcoded data. We’ll need it again when we get to testing, but it won’t be until next week, so we can wait. Removing this file will also cause us to remove the insertion hack from Application.hs.

Remember abour a runtime change – since we removed a field, automatic migration is not possible, so we need to perform it manually. The easiest way is to simply wipe out all the data and insert it later on – for now it’s good enough. It won’t be good enough when we get to testing, but we still have some time for fun before that happens.

These are mostly simple changes, but remember – if you have any trouble with implementing it, you can check out working code from GitHub. We’re focus on Handler/ProjectEdit.hs, since it changes quite vastly., and some of changes are not obvious.

First of all, we export four routes now: postProjectEditIdR, getProjectEditIdR, postProjectEditNoIdR, getProjectEditNoIdR. They are just a thin wrapper over postProjectEditR, which has a new signature, Maybe ProjectId -> Handler Html. Next change is in widget files – since we have one page, and two possible sources (new project/project edition), we need to support this in routes. To make it easier, we’ll introduce intermediate variables, defined as follows:

backRoute = maybe ProjectsR ProjectR projectId
postRoute = maybe ProjectEditNoIdR ProjectEditIdR projectId

Database fetch becomes quite tricky as well – the following form works:

postProjectEditR :: Maybe ProjectId -> Handler Html
postProjectEditR projectId = do
  project <- (runDB . get) `mapM` projectId
  renderForm projectId $ join project

`join` is used to flatten `Maybe`s (we have a `Maybe (Maybe Project)`, since we can have no id – first maybe – or database may contain no project – hence second).
Remember about modifying upsert call! Now we either update or insert, which boils down to:

updateCall = maybe insert (\id val -> repsert id val >> return id) projectId

this lambda expression doesn’t look nice, but repsert (replace, insert if doesn’t exist) doesn’t return anything by default, and we need the id here.

That’s it for today! We did a lot of good job – cleaned up several hacks, adjusted type signatures, added possibility to delete entities. The app is pretty much complete when it comes to basic functionality. In the next post we’ll clean up today’s DELETE implementation with the use of AJAX calls and HTTP API.

Stay tuned!