Here i'm going to show you how to build a super simple web app using haskell and yesod. For the purposes of the tutorial, this is a single piece of code, however you would split the code among multiple filesif you were to build on this project.
Everyone knows that the internet peaked in the 90s and so today we're going to build somthing that no 90s website can be without, the Guest Book.
-- show Imports and Pragmas
{-# LANGUAGE OverloadedStrings, FlexibleContexts, ScopedTypeVariables,
GeneralizedNewtypeDeriving, TemplateHaskell, GADTs,
TypeFamilies, QuasiQuotes, MultiParamTypeClasses #-}
-- As usual, a generous helping of pragmas and imports.
-- These will need to be placed at the top of your source files.
-- Everything you need to work with Yesod, exported from one package
import Yesod
-- Standard platform datatypes & functions
import Data.Text (Text)
import Data.Time (UTCTime, getCurrentTime)
import Data.Time.Format (formatTime)
import System.Locale (defaultTimeLocale)
-- Using the `persistent` database layer
import Database.Persist
import Database.Persist.TH
import Database.Persist.Sqlite
-- Monad helpers
import Control.Applicative ((<$>),(<*>))
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runNoLoggingT)
import Control.Monad.Trans.Resource (runResourceT)
-- show Types.hs
-- Our datatype that represents our App
-- the fields of this type represent pieces of the
-- global environment in which it runs.
data GuestBook = GuestBook { _db :: Connection }
-- Declare that our App is a Yesod site
-- This will provide you with sensible defaults
-- and your website's defaultLayout
instance Yesod GuestBook where
defaultLayout contents = do
PageContent title headTags bodyTags <- widgetToPageContent $ do
addStylesheetRemote "//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css"
toWidget [cassius|
body
padding: 0px 10px
|]
contents
hamletToRepHtml [hamlet|
$doctype 5
<html>
<head>
<title>#{title}
^{headTags}
<body>^{bodyTags}
|]
-- Tell yesod to use the SqlPersist backend by default
instance YesodPersist GuestBook where
type YesodPersistBackend GuestBook = SqlPersist
-- We use runDB to more easily thread our database
-- connection into functions in the Handler monad.
runDB action = getYesod >>= runSqlConn action . _db
-- Provide a default pattern for the internationalisation machinery.
instance RenderMessage GuestBook FormMessage where
renderMessage _ _ = defaultFormMessage
-- show Models.hs
-- Define our data models & create a function to setup our tables.
-- (Uses a generous helping of Template Haskell)
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Signature
name Text
message Text
date UTCTime
deriving Show
|]
-- show Routes.hs
-- More TH, this time generating Type-Safe Routes
-- This will generate a sum datatype that describes
-- your app's routes and datatypes.
mkYesod "GuestBook" [parseRoutes|
/ HomeR GET
/sign SubmitR GET POST
|]
-- show Forms.hs
-- Simple form for creating Signatures
entryForm = renderBootstrap $ Signature
<$> areq textField "Name" Nothing
<*> areq textField "Message" Nothing
<*> lift (liftIO getCurrentTime)
-- show Views.hs
-- Our homepage view, querying and displaying signatures
-- from our all-important guestbook
getHomeR = do
-- Query from the db, annotating type so the compiler knows what we're querying
signatures :: [Entity Signature] <- runDB $ selectList [] [Asc SignatureDate]
-- Render out the template
defaultLayout [whamlet|
<h1>Welcome to the Guestbook!
<table .table.table-bordered>
<thead>
<tr>
<th> Name
<th> Message
<th> When
<tbody>
<!-- $forall loops over all the values -->
$forall Entity _ signature <- signatures
<tr>
<td> #{signatureName signature}
<td> #{signatureMessage signature}
<td> #{fTime $ signatureDate signature}
<!-- @{SubmitR} fills in the rendered route to our signing url -->
<p><a href=@{SubmitR}>Sign Guestbook.
|] where
-- A quick and dirty time formatter
fTime :: UTCTime -> String
fTime = formatTime defaultTimeLocale "%F %X %Z"
renderEntryForm widget enctype = [whamlet|
<form method=POST action=@{SubmitR} enctype=#{enctype}>
<table>^{widget}
<div .form-actions>
<button .btn.btn-primary type=submit> Sign Guestbook
|]
-- The GET handler for our signing page
getSubmitR = do
(formWidget, enctype) <- generateFormPost entryForm
defaultLayout [whamlet|
<h1> Sign The Guestbook!
^{renderEntryForm formWidget enctype}
<p><a href=@{HomeR}>Back Home.
|]
-- The POST handler for our signing page
postSubmitR = do
-- run the Posted form, and retrieve the values
((result, formWidget), enctype) <- runFormPost entryForm
case result of
FormSuccess signature@(Signature name message _) -> do
-- Insert our Signature object into the Database
runDB $ insert signature
-- Render out the success template
defaultLayout [whamlet|
<h2> Successfully Submitted!
<blockquote>
#{message}
<br>-- #{name}
<p><a href=@{HomeR}>Back to Guestbook.
|]
FormFailure _ -> defaultLayout [whamlet|
<h2> OOPS! Please, Try again.
^{renderEntryForm formWidget enctype}
|]
FormMissing -> redirect SubmitR
-- show Main.hs
-- We'll use a temporary in-memory database for this example
main = withSqliteConn ":memory:" $ \conn -> do
time <- liftIO getCurrentTime
runNoLoggingT $ runResourceT $ flip runSqlConn conn $ do
-- Create the db tables (running our auto-generated migration)
runMigration migrateAll
-- Create some example data
insert $ Signature "Johnny" "Hello, Haskeller" time
insert $ Signature "James" "Guestbook? What is this, Geocities?" time
insert $ Signature "Judy" "Wow, totally Retro" time
-- Initialise our webserver and begin serving content
-- Here we bind our database connection into our application state.
warpEnv $ GuestBook { _db = conn }
And there you go, A simple application to curate your very own piece of internet nostalgia.
You can find out more about yesod by reading the docs at YesodWeb