Creating Your First Project

1. Project Setup

This guide will lead you through creating a small blog application. To set up the project, open a terminal and type:

ihp-new blog

Note for Windows: Make sure that you are located in the Linux part of the filesystem and not on the Linux-mounted Windows filesystem, i.e. not on a path starting with /mnt/. Otherwise PostgreSQL will complain about the accessibility of the project’s PostgreSQL database.

The Very First Time

The first time you set up IHP, this command might take 10 - 15 minutes to install. Any further projects after that will be a lot faster because all the packages are already cached on your computer.

If you don’t already use cachix, you will be prompted to install it. You don’t need it, but it is highly recommended, as it reduces build time dramatically. Learn more about cachix here.

While the build is running, take a look at “What Is Nix” by Shopify to get a general understanding of how Nix works.

In case some errors appear now or in later steps:

Directory Structure

The new blog directory now contains a couple of auto-generated files and directories that make up your app.

Here is a short overview of the whole structure:

File or DirectoryPurpose
Config/
Config/Config.hsConfiguration for the framework and your application
Config/nix/nixpkgs-config.nixConfiguration for the Nix package manager
Config/nix/haskell-packages/Custom Haskell dependencies can be placed here
Application/Your domain logic lives here
Application/Schema.sqlModels and database tables are defined here
Web/ControllerWeb application controllers
Web/View/Web application HTML template files
Web/Types.hsCentral place for all web application types
static/Images, CSS and JavaScript files
.ghciDefault config file for the Haskell interpreter
.gitignoreList of files to be ignored by git
App.cabal, Setup.hsConfig for the cabal package manager
flake.nixDeclares your app dependencies (like package.json for NPM or composer.json for PHP)
default.nixBackward compatibility for developers who don’t use Nix Flakes
MakefileDefault config file for the make build system

2. Hello, World!

Starting Your App

You now already have a working Haskell app ready to be started.

Switch to the blog directory before doing the next steps:

cd blog

Start the development server by running the following in the blog directory:

devenv up

Your application is starting now. The development server will automatically launch the built-in IDE. The server can be stopped by pressing CTRL+C.

By default, your app is available at http://localhost:8000 and your development tooling is at http://localhost:8001.

The development server automatically picks other ports when they are already in use by some other server. For example, it would pick http://localhost:8001 and http://localhost:8002 if port 8000 is used.

In the background, the built-in development server starts a PostgreSQL database connected to your application. Don’t worry about manually setting up the database. It also runs a WebSocket server to power live reloads on file saves inside your app.

The very first time you start this might take a while, and in rare cases may even require a restart (press CTRL+C and run devenv up again).

Hello Haskell World

Open http://localhost:8000 and you will see this:

It’s working screen

Let’s change this to a friendly hello world:

Open Web/View/Static/Welcome.hs in your text editor. Place your text editor and your web browser next to each other, to see the magic of live reloading:

IHP Hello World

Inside the HTML code replace It's working! with Hello World from Haskell!, like this:

IHP Hello World

You’ll see that the web browser magically refreshes once you save the file changes.

Did it work? Congratulations 🎉 You’ve officially built your first haskell web application :) That makes you a haskell programmer. Welcome to the Haskell community! :)

If you liked the live reloading, have some fun and play around with the welcome view before you continue with the next steps.

3. Data Structures & PostgreSQL

Schema Modeling

For our blog project, let’s first build a way to manage posts.

For working with posts, we first need to create a posts table inside our database. A single post has a title, a body and of course an id. IHP is using UUIDs instead of the typical numerical ids.

This is how our posts table might look like for our blog:

id :: UUIDtitle :: Textbody :: Text
8d040c2d-0199-4695-ac13-c301970cff1dMy Experience With Nix On OS XSome time ago, I’ve moved this jekyll-based blog …
b739bc7c-5ed1-43f4-944c-385aea80f182Deploying Private GitHub Repositories To NixOS ServersIn a previous post I’ve already shared a way to deploy private git repositories to a NixOS based server. While …

To work with posts in our application, we now have to define this data schema.

For the curious: IHP has a built-in GUI-based schema designer. The schema designer will be used in the following sections of this tutorial. The schema designer helps to quickly build the DDL statements for your database schema without remembering all the PostgreSQL syntax and data types. But keep in mind: The schema designer is just a GUI tool to edit the Application/Schema.sql file. This file consists of DDL statements to build your database schema. The schema designer parses the Application/Schema.sql, applies changes to the syntax tree, and then writes it back into the Application/Schema.sql. If you love your VIM, you can always skip the GUI and go straight to the code at Application/Schema.sql. If you need to do something advanced which is not supported by the GUI, just manually do it with your code editor of choice. IHP is built by terminal hackers, so don’t worry, all operations can always be done from the terminal :-)

Open the IHP Schema Designer and add a new table with title and body as text columns. To do this right-click > Add Table to open the New Table dialog.

Schema Designer New Table

Enter the table name posts and click on Create Table.

In the right pane, you can see the columns of the newly created table. The id column has been automatically created for us.

Right-click into the Columns pane and select Add Column:

Schema Designer Add Column menu

Use this dialog to create the title and body columns.

After that, your schema should look like this:

Schema Designer First Table

Loading the Schema

Next, we need to make sure that our database schema with our posts table is imported into the local PostgreSQL database. Don’t worry, the local development PostgreSQL server is already running. The development server has conveniently already started it.

Open the Application/Schema.sql in your code editor to see the SQL queries which make up the database schema:

CREATE TABLE posts (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    title TEXT NOT NULL,
    body TEXT NOT NULL
);

To load the table into our local Postgres server, we need to click Migrate DB in the Schema Designer. Then click Run Migration.

The posts table has been created now. Let’s quickly connect to our database and see that everything is correct:

$ make psql

psql (9.6.12)
Type "help" for help.

-- Let's do a query to check that the table is there

app=# SELECT * FROM posts;

 id | title | body
----+-------+------
(0 rows)

-- Looks alright! :)



-- We can quit the PostgreSQL console by typing \q

app=# \q

Now our database is ready to be consumed by our app.

Record Types

By specifying the above schema, the framework automatically provides several types for us. Here is a short overview:

TypePost
Definitiondata Post = Post { id :: Id Post, title :: Text, body :: Text }
DescriptionA single record from the posts table
ExamplePost { id = cfefdc6c-c097-414c-91c0-cbc9a79fbe4f, title = "Hello World", body = "Some body text" } :: Post
TypeId Post
Definitionnewtype Id Post = Id UUID
DescriptionType for the id field
Example"5a8d1be2-33e3-4d3f-9812-b16576fe6a38" :: Id Post

For the curious: To dig deeper into the generated code, open the Schema Designer, right-click a table, and click Show Generated Haskell Code or look into build/Generated/Types.hs.

4. Apps, Controllers, Views

IHP follows the well-known MVC (Model-View-Controller) structure. Controllers and actions are used to deal with incoming requests.

A controller belongs to an application. The default application is called Web (that’s why all the controller and views are located there). Your whole project can consist of multiple sub-applications. Typically your production app will need e.g. an admin back-end application next to the default web application.

We can use the built-in code generators to generate a controller for our posts. A handy way to do it, is by right clicking on the table, and selecting Generate Controller.

Or, we can do it manually. Inside the development server, click on CODEGEN to open the Code Generator. There you can see everything that can be generated. Clicking on Controller:

You need to enter the controller name. Enter Posts and click Preview:

The preview will show you all the files which are going to be created or modified. Take a look and when you are ready, click on Generate.

After the files have been created as in the preview, your controller is ready to be used. Open your browser at http://localhost:8000/Posts to try out the new controller. The generator did all the initial work we need to get our usual CRUD (Create, Read, Update, Delete) actions going.

Here’s how the new /Posts page looks like:

Screenshot of the /Posts view

Next, we’re going to dig a bit deeper into all the changes made by the controller generator.

New Types

Let’s first take a closer look at the changes in Web/Types.hs. Here a new data structure was created:

data PostsController
    = PostsAction
    | NewPostAction
    | ShowPostAction { postId :: !(Id Post) }
    | CreatePostAction
    | EditPostAction { postId :: !(Id Post) }
    | UpdatePostAction { postId :: !(Id Post) }
    | DeletePostAction { postId :: !(Id Post) }
    deriving (Eq, Show, Data)

We have one constructor for each possible action. Here you can see a short description of all the constructors:

ActionRequestDescription
PostsActionGET /PostsLists all posts
NewPostActionGET /NewPostDisplays the form to create a post
ShowPostAction { postId = someId }GET /ShowPost?postId={someId}Shows the posts with id $someId
CreatePostActionPOST /CreatePostEndpoint to create a Post
EditPostAction { postId = someId }GET /EditPost?postId={someId}Displays the form to edit a post
UpdatePostAction { postId = someId }POST /UpdatePost?postId={someId}Endpoint to submit a post update
DeletePostAction { postId = someId }DELETE /DeletePost?postId={someId}Deletes the post

A request like “Show me the post with id e57cfb85-ad55-4d5c-b3b6-3affed9c662c“ can be represented like ShowPostAction { postId = e57cfb85-ad55-4d5c-b3b6-3affed9c662c }. Basically, the IHP router always maps an HTTP request to such an action data type. (By the way: The type Id Post is just a UUID, but wrapped within a newtype, newtype Id model = Id UUID).

Controller Implementation: Web/Controller/Posts.hs

The actual code that is run, when an action is executed, is defined in Web/Controller/Posts.hs. Let’s take a look, step by step.

Imports

module Web.Controller.Posts where

import Web.Controller.Prelude
import Web.View.Posts.Index
import Web.View.Posts.New
import Web.View.Posts.Edit
import Web.View.Posts.Show

In the header we just see some imports. Controllers always import a special Web.Controller.Prelude module. It provides e.g. controller helpers and also the framework-specific functions we will see below. The controller also imports all its views. Views are also just “normal” Haskell modules.

Controller Instance

instance Controller PostsController where

The controller logic is specified by implementing an instance of the Controller type-class.

Index Action

This is where the interesting part begins. As we will see below, the controller implementation is just an action function, pattern matching over our data PostsController structure we defined in Web/Types.hs.

    action PostsAction = do
        posts <- query @Post |> fetch
        render IndexView { .. }

This is the index action. It’s called when opening /Posts. First, it fetches all the posts from the database and then passes them along to the view. The IndexView { .. } is just shorthand for IndexView { posts = posts }.

New Action

    action NewPostAction = do
        let post = newRecord
        render NewView { .. }

This is our endpoint for /NewPost. It just creates an empty new post and then passes it to the NewView. The newRecord is giving us an empty Post model. It’s equivalent to manually writing Post { id = Default, title = "", body = "" }.

Show Action

    action ShowPostAction { postId } = do
        post <- fetch postId
        render ShowView { .. }

This is our show action at /ShowPost?postId=postId. Here we pattern match on the postId field of ShowPostAction to get the post id of the given request. Then we just call fetch on that postId which gives us the specific Post record. Finally, we just pass that post to the view.

Edit Action

    action EditPostAction { postId } = do
        post <- fetch postId
        render EditView { .. }

Our /EditPost?postId=postId action. It’s pretty much the same as in the action ShowPostAction, just with a different view.

Update Action

    action UpdatePostAction { postId } = do
        post <- fetch postId
        post
            |> buildPost
            |> ifValid \case
                Left post -> render EditView { .. }
                Right post -> do
                    post <- post |> updateRecord
                    setSuccessMessage "Post updated"
                    redirectTo EditPostAction { .. }

This action deals with update requests for a specific post. As usual, we pattern match on the postId and fetch it.

The interesting part is buildPost. It is a helper function defined later in the controller: buildPost = fill @["title", "body"]. The fill call inside buildPost reads the title and body attributes from the browser request and fills them into the post record. The buildPost is also the place for validation logic.

ifValid returns Either Post Post. Left post means that e.g. the title or body did not pass validation. Right post means that all parameters could be set on post without any errors.

In the error case (Left post ->) we just re-render the EditView. The EditView then tells the user about validation errors.

In the success case (Right post ->) we save the updated post to the database (with updateRecord). Then we set a success message and redirect the user back to the edit view.

Create Action

    action CreatePostAction = do
        let post = newRecord @Post
        post
            |> buildPost
            |> ifValid \case
                Left post -> render NewView { .. }
                Right post -> do
                    post <- post |> createRecord
                    setSuccessMessage "Post created"
                    redirectTo PostsAction

Our create action, dealing with POST /CreatePost requests.

It’s pretty much like the update action. When the validation succeeded, it saves the record to the database using createRecord.

Delete Action

    action DeletePostAction { postId } = do
        post <- fetch postId
        deleteRecord post
        setSuccessMessage "Post deleted"
        redirectTo PostsAction

The last action is dealing with DELETE /DeletePost?postId=postId requests. It’s pretty much like the other actions, we just call deleteRecord here.

Routes

The router is configured in Web/Routes.hs. The generator just places a single line there:

instance AutoRoute PostsController

This empty instance magically sets up the routing for all the actions. Later you will learn how you can customize the URLs according to your needs (e.g. “beautiful URLs” for SEO).

Note that the word ‘Post’ here still refers to a post on our blog and is unrelated to the HTTP-POST request method.

Views

We should also quickly take a look at our views.

Let’s first look at the show view in Web/View/Posts/Show.hs:

module Web.View.Posts.Show where
import Web.View.Prelude

data ShowView = ShowView { post :: Post }

instance View ShowView where
    html ShowView { .. } = [hsx|
        {breadcrumb}
        <h1>Show Post</h1>
        <p>{post}</p>

    |]
        where
            breadcrumb = renderBreadcrumb
                            [ breadcrumbLink "Posts" PostsAction
                            , breadcrumbText "Show Post"
                            ]

We can see that ShowView is just a data definition. There is also a View ShowView instance. The HTML-like syntax inside the html function is hsx code. It’s similar to React’s JSX. You can write HTML code as usual there. Everything inside the [hsx|...|] block is also type-checked and converted to Haskell code at compile-time.

Now that we have a rough overview of all the parts belonging to our Post, it’s time to do some coding ourselves.

5. Extending the Blog

The generated controller already feels close to a super simple blog. Now it’s time to make it more beautiful.

Creating a Post

First, we quickly need to create a new blog post. Open http://localhost:8000/Posts and click on + New. Then enter Hello World! into the “Title” field and Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam into the “Body”.

Click Create Post. You should now see the new post listed on the index view.

Index View

Displaying a Post

Let’s first improve the show view. Right now the headline is “Show Post”, and the actual post body is just a dump of the Post Data definition.

Open the Web/View/Posts/Show.hs and replace <h1>Show Post</h1> with <h1>{post.title}</h1>. Also add a <div>{post.body}</div> below the <h1>.

The Web/View/Posts/Show.hs file should look like this:

module Web.View.Posts.Show where
import Web.View.Prelude

data ShowView = ShowView { post :: Post }

instance View ShowView where
    html ShowView { .. } = [hsx|
        {breadcrumb}
        <h1>{post.title}</h1>
        <p>{post.body}</p>

    |]
        where
            breadcrumb = renderBreadcrumb
                            [ breadcrumbLink "Posts" PostsAction
                            , breadcrumbText "Show Post"
                            ]

After you saved the changes, you should see that the changes have been reflected in the browser already. In the background, the page has been refreshed automatically. This refresh is using a diff based approach by using morphdom.

Display all posts

After creating your post, you should have already seen that the posts list is now displaying all the post fields. Let’s change it to only display the post’s title.

Open the Web/View/Posts/Index.hs and replace <td>{post}</td> with <td>{post.title}</td>.

Let’s also make it clickable by wrapping it in a link. We can just put a <a href={ShowPostAction post.id}> around it. The line should now look like:

<td><a href={ShowPostAction post.id}>{post.title}</a></td>

Now we can also remove the “Show” link. We can do that by removing the next line <td><a href={ShowPostAction post.id}>Show</a></td>.

Also remove the corresponding <th></th> from thead so that there are only three th tags:

<thead>
    <tr>
        <th>Post</th>
        <th></th>
        <th></th>
    </tr>
</thead>

Adding Validation

Let’s make sure that every post has at least a title. Validations can be defined inside our controller Web/Controller/Posts.hs.

Right now at the bottom of the file, we have this:

buildPost post = post
    |> fill @["title","body"]

Replace the implementation with this:

buildPost post = post
    |> fill @["title","body"]
    |> validateField #title nonEmpty

Now open http://localhost:8000/NewPost and click Save Post without filling the text fields. You will get a “This field cannot be empty” error message next to the empty title field.

Schema Designer Title non empty

You can find a list of all available validator functions in the API Documentation.

Timestamps

It would be nice to always show the latest post on the index view. Let’s add a timestamp to do exactly that.

Take a look at Application/Fixtures.sql. The file should look like this:

-- .......
-- .......

INSERT INTO public.posts VALUES ('fcbd2232-cdc2-4d0c-9312-1fd94448d90a', 'Hello World!', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam');

(If you don’t see an entry for your test post in Application/Fixtures.sql, then click Migrate DB -> Update DB in the Schema Designer (or use make dumpdb from the command line).)

All our existing posts are saved here. You should also commit this file to git to share your fixtures with your teammates. We will need these saved fixtures in a moment when we want to update the database schema.

Let’s add a new created_at column. Open http://localhost:8001/Tables, right-click on posts, select Add Column to Table, enter created_at for the name, select Timestamp for the type, set the default value to NOW(), and press Create Column.

Schema Designer Timestamp column

Now open the /Posts again inside your browser. You will see this error:

Database looks outdated. The database result does not match the expected type.

This happens because we only added the created_at column to the Application/Schema.sql file by using the Schema Designer. But the actual running Postgres server still uses the older database schema.

To update the local database, open the Schema Designer, and click the Migrate DB button. Then click Run Migration. This button will then bring the Schema and the actual database sync again by adding the missing column.

In general, the workflow for making database schema changes locally is: Make changes to the Schema.sql and update Database with Migrate DB.

You can open http://localhost:8000/Posts again. The error is gone now.

Now we can order the posts by our new created_at field. Open Web/Controller/Posts.hs and add orderByDesc #createdAt like this inside the action PostsAction:

action PostsAction = do
    posts <- query @Post
        |> orderByDesc #createdAt
        |> fetch
    render IndexView { .. }

Let’s also show the creation time in the ShowView in Web/View/Posts/Show.hs. There we add <p>{post.createdAt |> timeAgo}</p> below the title:

    html ShowView { .. } = [hsx|
        {breadcrumb}
        <h1>{post.title}</h1>
        <p>{post.createdAt |> timeAgo}</p>
        <div>{post.body}</div>
    |]

Open the view to check that it’s working. If everything is fine, you will see something like 5 minutes ago below the title. The timeAgo helper uses a bit of JavaScript to automatically display the given timestamp in the current time zone and in a relative format. In case you want to show the absolute time (like 10.6.2019, 15:58), just use dateTime instead of timeAgo.

Schema Designer created at view

Markdown

Right now our posts can only be plain text. Let’s make it more powerful by adding support for Markdown.

Adding a Markdown Library

To deal with Markdown, instead of implementing a custom Markdown parser, let’s just use an existing package. There’s the excellent mmark package we can use.

To install this package, open the flake.nix file and append mmark to the haskellPackages list. The file will now look like this:

{
    inputs = {
        ihp.url = "github:digitallyinduced/ihp/v1.1";
        nixpkgs.follows = "ihp/nixpkgs";
        flake-parts.follows = "ihp/flake-parts";
        devenv.follows = "ihp/devenv";
        systems.follows = "ihp/systems";
    };

    outputs = inputs@{ ihp, flake-parts, systems, ... }:
        flake-parts.lib.mkFlake { inherit inputs; } {

            systems = import systems;
            imports = [ ihp.flakeModules.default ];

            perSystem = { pkgs, ... }: {
                ihp = {
                    enable = true;
                    projectPath = ./.;
                    packages = with pkgs; [
                        # Native dependencies, e.g. imagemagick
                    ];
                    haskellPackages = p: with p; [
                        # Haskell dependencies go here
                        p.ihp
                        cabal-install
                        base
                        wai
                        text
                        hlint
                        mmark # <--------- OUR NEW PACKAGE ADDED HERE
                    ];
                };
            };

        };
}

Stop the development server by pressing CTRL+C. Then update the local development environment by running devenv up. This will download and install the mmark package.

Markdown Rendering

Now that we have mmark installed, we need to integrate it into our ShowView. First, we need to import it: add the following line to the top of Web/View/Posts/Show.hs:

import qualified Text.MMark as MMark

Next change {post.body} to {post.body |> renderMarkdown}. This pipes the body field through a function renderMarkdown. Of course, we also have to define the function now.

Add the following to the bottom of the show view:

renderMarkdown text = text

This function now does nothing except return its input text. Our Markdown package provides two functions, MMark.parse and MMark.render to deal with the Markdown. Let’s first deal with parsing:

renderMarkdown text = text |> MMark.parse ""

The empty string we pass to MMark.parse is usually the file name of the .markdown file. As we don’t have any Markdown file, we just pass an empty string.

Now open the web app and take a look at a blog post. You will see something like this:

Right MMark {..}

This is the parsed representation of the Markdown. Of course, that’s not very helpful. We also have to connect it with MMark.render to get HTML code for our Markdown. Replace the renderMarkdown with the following code:

renderMarkdown text =
    case text |> MMark.parse "" of
        Left error -> "Something went wrong"
        Right markdown -> MMark.render markdown |> tshow |> preEscapedToHtml

The show view will now show real formatted text, as we would have expected.

Forms & Validation

Let’s also quickly update our form. Right now we have a one-line text field there. We can replace it with a text area to support multi-line text.

Open Web/View/Posts/Edit.hs and change {(textField #body)} to {(textareaField #body)}. We can also add a short hint that the text area supports Markdown: Replace {(textareaField #body)} with {(textareaField #body) { helpText = "You can use Markdown here"} }.

renderForm :: Post -> Html
renderForm post = formFor post [hsx|
    {(textField #title)}
    {(textareaField #body) { helpText = "You can use Markdown here"} }
    {submitButton}
|]

After that, do the same in Web/View/Posts/New.hs.

We can also add an error message when the user tries to save invalid Markdown. We can quickly write a custom validator for that:

Open Web/Controller/Posts.hs and import MMark at the top:

import qualified Text.MMark as MMark

Then add this custom validator to the bottom of the file:

isMarkdown :: Text -> ValidatorResult
isMarkdown text =
    case MMark.parse "" text of
        Left _ -> Failure "Please provide valid Markdown"
        Right _ -> Success

We can use the validator by adding a new validateField #body isMarkdown line to the buildPost function:

buildPost post = post
    |> fill @["title","body"]
    |> validateField #title nonEmpty
    |> validateField #body nonEmpty
    |> validateField #body isMarkdown

Create a new post with just # (a headline without any text) as the content to see our new error message.

6. Adding Comments

It’s time to add comments to our blog. For that open the Schema Designer and add a new table comments with the fields id, post_id, author and body:

Schema Designer Comments

When adding the post_id column, it will automatically set the type to UUID. Unless you unselect the checkbox References posts it will also automatically create a foreign key constraint for this column:

By default the foreign key constraint has set its ON DELETE behavior to NO ACTION. To change the ON DELETE, click on the FOREIGN KEY: posts next to the post_id field.

Press the Migrate DB to add our new comments table to the dev database.

The Controller

Let’s add a controller for our comments. We can use the visual code generator for this:

Use Comments as the controller name:

Click Preview > Generate:

The controller is generated now. But we need to do some adjustments to better integrate the comments into the posts.

“Add comment”

First we need to make it possible to create a new comment for a post. Open Web/View/Posts/Show.hs and append <a href={NewCommentAction}>Add Comment</a> to the HSX code:

instance View ShowView where
    html ShowView { .. } = [hsx|
        {breadcrumb}
        <h1>{post.title}</h1>
        <p>{post.createdAt |> timeAgo}</p>
        <div>{post.body |> renderMarkdown}</div>

        <a href={NewCommentAction}>Add Comment</a>
    |]
        where
            breadcrumb = renderBreadcrumb
                            [ breadcrumbLink "Posts" PostsAction
                            , breadcrumbText "Show Post"
                            ]

This creates an Add Comment link, which links to the New Comment Form we just generated. After clicking the Add Comment link, we can see this:

Pretty empty

We can see, there is a post id field which is a field with a lot of 0s. When we try to submit this form, it will fail because there is no post with this id. Let’s first make it possible, that the post id is automatically set to the post where we originally clicked on “New Comment”.

For that, open Web/Types.hs. We can see the definition of CommentsController:

data CommentsController
    = CommentsAction
    | NewCommentAction
    | ShowCommentAction { commentId :: !(Id Comment) }
    | CreateCommentAction
    | EditCommentAction { commentId :: !(Id Comment) }
    | UpdateCommentAction { commentId :: !(Id Comment) }
    | DeleteCommentAction { commentId :: !(Id Comment) }
    deriving (Eq, Show, Data)

Let’s add an argument postId :: !(Id Post) to NewCommentAction:

data CommentsController
    -- ...
    | NewCommentAction { postId :: !(Id Post) }
    -- ...

After making this change, we can see some of the type errors in the browser. This is because all references to NewCommentAction now need to be passed the postId value. Think of these type errors as a TODO list of changes to be made, reported to us by the compiler.

Open Web/View/Posts/Show.hs and change <a href={NewCommentAction}>Add Comment</a> to:

<a href={NewCommentAction post.id}>Add Comment</a>

After that, another type error can be found in Web/View/Comments/Index.hs. In this auto-generated view we have an Index button at the top:

<h1>Index <a href={pathTo NewCommentAction} class="btn btn-primary ml-4">+ New</a></h1>

Let’s just remove this button by changing this line to:

<h1>Comments</h1>

Now we see another type error:

Web/Controller/Comments.hs:14:12: error:
The constructor ‘NewCommentAction’ should have 1 argument, but has been given none
In the pattern: NewCommentAction
      In an equation for ‘action’:
          action NewCommentAction
            = do let comment = ...
                 render NewView {..}
      In the instance declaration for ‘Controller CommentsController
   |
14 |     action NewCommentAction = do
   |            ^^^^^^^^^^^^^^^^

Open Web/Controller/Comments.hs and add the missing { postId } in the pattern match at line 14:

    action NewCommentAction { postId } = do
        let comment = newRecord
        render NewView { .. }

Now all type errors should be fixed.

Open http://localhost:8000/Posts and open the Show View of a post by clicking its title. Now Click Add Comment. Take a look at the URL, it will look something like:

http://localhost:8000/NewComment?postId=7f37115f-c850-4fcb-838f-1971cea0544e

You can see that the postId has been passed as a query parameter. In the form, the post id field is still filled with 0s. Let’s fix this. Open Web/Controller/Comments.hs and change the NewCommentAction to this:

    action NewCommentAction { postId } = do
        let comment = newRecord
                |> set #postId postId
        render NewView { .. }

This will set the postId of our new comment record to the postId given to the action.

Now take a look at your form. The postId will be pre-filled now: Pretty empty

Of course, seeing the UUID is not very human-friendly. It would be better to just show the post title to our users. For that, we have to fetch and pass the post to our form and then make the postId a hidden field.

Append post <- fetch postId to fetch the post to the NewCommentAction:

    action NewCommentAction { postId } = do
        let comment = newRecord
                |> set #postId postId
        post <- fetch postId
        render NewView { .. }

Because the error view is rendering our NewView in an error case, we also have to update the CreateCommentAction:

    action CreateCommentAction = do
        -- ...
                Left comment -> do
                    post <- fetch comment.postId -- <---- NEW
                    render NewView { .. }
                Right comment -> - ....

Inside the Web/View/Comments/New.hs retrieve the post variable from the action by updating the NewView:

data NewView = NewView
    { comment :: Comment
    , post :: Post
    }

This way the post is passed from the action to our view.

Now we can use the post variable to show the post title. Change <h1>New Comment</h1> to:

<h1>New Comment for <q>{post.title}</q></h1>

Let’s also make the text field for postId a hidden field:

renderForm :: Comment -> Html
renderForm comment = formFor comment [hsx|
    {(hiddenField #postId)}
    {(textField #author)}
    {(textField #body)}
    {submitButton}
|]

Our form is complete now :-) Time to take a look:

New Comment View after our changes

Great, let’s add our first comment:

Creating Our New Comment

It works. We’re redirected to the CommentsAction. If you look at the table, we can see that our postId has been set successfully.

Let’s change our CreateCommentAction to make it redirect back to our Post again after commenting. Open Web/Controller/Comments.hs and take a look at the CreateCommentAction.

In the success case (Right comment -> ...) we see:

redirectTo CommentsAction

Change this to:

redirectTo ShowPostAction { postId = comment.postId }

Open the browser and create a new comment to verify that this redirect is working:

Redirect is working

Show Comments of a Post

Next we’re going to display our comments below the post. Open Web/View/Posts/Show.hs and append the following code to the HSX block:

<div>{post.comments}</div>

It will display something like this:

Redirect is working

What is shown is the technical representation of a query like query @Comment |> filterWhere (#id, "'7f37115f-c850-4fcb-838f-1971cea0544e") (this representation changes a bit between versions still, so don’t worry if yours does not look exactly like this). But we don’t want just the query, we want the actual comments. We cannot do this from our view, because views should be pure functions without IO. So we need to tell the action to actually fetch them for us.

Inside the Show.hs we need to update the type signature to tell our action what we want. Right now we have:

data ShowView = ShowView { post :: Post }

Add an Include "comments" like this:

data ShowView = ShowView { post :: Include "comments" Post }

This specifies that our view requires a post and should also include its comments. This will trigger a type error to be shown in the browser because our ShowPostAction is not passing the comments yet.

To fix this, open Web/Controller/Posts.hs and take a look at the ShowPostAction. Right now we have a fetch call:

post <- fetch postId

We need to extend our fetch to also include comments. We can use fetchRelated for this:

post <- fetch postId
    >>= fetchRelated #comments

The type of post has changed from Post to Include "comments" Post. In general, when you’re dealing with has-many relationships, use Include "relatedRecords" and fetchRelated to specify and fetch data according to your needs.

The type error is fixed now. When opening the Show View of a post, you will see that the comments are displayed. When you take a look at the Logs in the Dev tools you can see, that when opening a Post, two SQL queries will be fired:

("SELECT posts.* FROM posts WHERE id = ?  LIMIT 1",[Plain "'7f37115f-c850-4fcb-838f-1971cea0544e'"])
("SELECT comments.* FROM comments WHERE post_id = ?  ",[Plain "'7f37115f-c850-4fcb-838f-1971cea0544e'"])

Right now the view is displaying the comments as a string. Let’s make it more beautiful. Open Web/View/Posts/Show.hs.

Let’s first change the {post.comments} to make a <div> for each comment:

<div>{forEach post.comments renderComment}</div>

We also need to define the renderComment at the end of the file:

renderComment comment = [hsx|<div>{comment}</div>|]

Let’s also add some more structure for displaying the comments:

renderComment comment = [hsx|
        <div class="mt-4">
            <h5>{comment.author}</h5>
            <p>{comment.body}</p>
        </div>
    |]

This is how it looks now:

Post with comments

Ordering Comments

Right now comments are displayed in the order they’re stored in the database. So updating a comment will change the order. Let’s change this so that the newest comment is always displayed first.

Open the Schema Designer. Select the comments Table. Right-click in the Columns Pane, Click Add Column. Enter created_at. The column type will be auto-selected, and the default value automatically sets to NOW(). Click Create Column.

Now click Migrate DB to add the new column to the local dev database.

Now we have a created_at timestamp we can use for ordering. Open Web/Controller/Posts.hs and change the action from:

    action ShowPostAction { postId } = do
        post <- fetch postId
            >>= fetchRelated #comments
        render ShowView { .. }

To the following:

    action ShowPostAction { postId } = do
        post <- fetch postId
            >>= pure . modify #comments (orderByDesc #createdAt)
            >>= fetchRelated #comments
        render ShowView { .. }

The modify #comments (orderByDesc #createdAt) basically just does a |> orderByDesc #createdAt to the query builder inside the #comments field. Then it just writes it back to the field. The fetchRelated #comments will then use the query builder stored inside #comments to fetch the comments, thus using the ORDER BY we added to the query.

That’s it already. Taking a look at our post, we can see that the newest comment is shown first now.

Post with ordered comments

We hope you enjoyed the journey so far! Tell us on twitter or celebrate your first haskell app in our slack community! :)

Have Fun!

You now understand enough of IHP and Haskell to be dangerous. The best way to continue your journey is to start building things. Take a look at the The Basics Section to learn more about all the provided modules.

Good to know for playing with IHP: When you want to delete a project just delete the project directory and it’s gone.