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:
to templates/project-edition.hamlet
and
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 ofidentifier
field andUID
uniqueness constraint -
config/routes
–#Int
to#ProjectId
in route signatures -
Handler/Project.hs
– signatures, invocation ofrenderPage
(projectId
is totally internal now, so shouldn’t be displayed – but is still needed for routes),getBy404
toget404
,deleteBy
todelete
and page selection – this one might be tricky, so here’s my solution: -
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 takeProjectId
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 fromApplication.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:
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!
code
more code
~~~~