Well hello
So, I started to learn web programming with haskell and yesod. Yesod
book was too hard for me to grasp and I couldn't find a plausible entry-level tutorial, that would not be written 5 years ago and could compile. So I took an article by
yannesposito and fixed it.
I saw some effort to fix the tutorial on the school of haskell
here, but its formatting gave me an impression, that it is not maintained anymore.
Prerequisites: a basic understanding of haskell. If you lack it, I recommend you to read this.
1. Work environment setup
I use stack instead of cabal. I, together with yesod
manual, really recommend you to use it. Not to mention, that using yesod you don't really have a choice. The only way to create a new project is to use stack ¯\_(ツ)_/¯.
1.1 So, let's get us some stack!
It's as easy as running either one of the two.
curl -sSL https://get.haskellstack.org/ | sh
or
wget -qO- https://get.haskellstack.org/ | sh
I strongly recommend using the latest version of stack instead of just apt-getting it. Ubuntu repos often contain older and buggier versions of our favorite software.
[optional] Check what templates are available
$ stack templates
1.2 Generate a template project.
You can generate a yesod project only using stack. The init command has been removed from yesod. Use yesod-sqlite template to store you blog entries (see "Blog" chapter). Of course, if you don't intend to go
that far with this tutorial, you can use yesod-simple. So, let's create a new project called "yolo" with type yesod-sqlite.
stack new yolo yesod-sqlite
1.3 Install yesod
You should be able to run your project, for this you have to install yesod. This takes about 20 min.
stack install yesod-bin --install-ghc
1.4 Build and launch
Warning, first build will take a looong time
stack build && stack exec -- yesod devel
(3000 is the default port for yesod).
For more detailed reference about setting up yesod look
here.
My versions of stuff:
stack: Version 1.5.1, Git revision 600c1f01435a10d127938709556c1682ecfd694e
yesod-bin version: 1.5.2.6
The Glorious Glasgow Haskell Compilation System, version 8.0.2
2. Git setup
You know it's easier to live with a version control system.
git init .
git add .
git commit -m 'Initial commit'
3. Echo
Goal: going to localhost:3000/echo/word should generate a page with the same word.
Don't add the handler with `yesod add-handler`, instead, do it manually.
Add this to config/routes, thus adding a new page to the website.
/echo/#String EchoR GET
#String is the type of the input after slash and haskell's strict types prevent us from getting SQL injections, for example.
EchoR is the name of the GET request handler, GET is the type of supported requests.
And this is the handler, add it to src/Handler/Home.hs.
getEchoR :: String -> Handler Html
getEchoR theText = do
defaultLayout $ do
setTitle "My brilliant echo page!"
$(widgetFile "echo")
This tiny piece of code accomplishes a very simple task:
- theText is the argument, that we passed through /echo/<theText is here>
- for it we return a defaultLayout (that is specified in templates/defaultLayout.hamlet and is just a standart blank html page)
- set page's title "My brilliant echo page!"
- set main widget according to templates/echo.hamlet
Also, remember that RepHtml is deprecated.
So, let's add this echo.hamlet to the <projectroot>/templates! As you can see it's just a header with the text that we passed after slash of echo/<word here>.
<h1> #{theText}
Now run and check
localhost:3000/ :)
If you're getting an error like this
Illegal view pattern: fromPathPiece -> Just dyn_apb9
Use ViewPatterns to enable view patterns
or
Illegal view pattern: fromPathPiece -> Just dyn_aFon
then just open your package.yaml file, that stack has automatically created for you and add the following lines just after `dependencies:` section:
default-extensions: ViewPatterns
Else if you're getting something like this
yesod: devel port unavailable
CallStack (from HasCallStack):
error, called at ./Devel.hs:270:44 in main:Devel
that most probably you have another instance of site running and thus port 3000 is unavailable.
If you see this warning
Warning: Instead of 'ghc-options: -XViewPatterns -XViewPatterns' use
'extensions: ViewPatterns ViewPatterns'
It's okay, so far stack does not support 'extensions' section in .cabal file. Catch up with this topic in this thread.
If you see this warning
Foundation.hs:150:5: warning: [-Wincomplete-patterns]
Pattern match(es) are non-exhaustive
In an equation for ‘isAuthorized’:
Patterns not matched: (EchoR _) _
That means, that you need to add this line to Foundation.hs
isAuthorized (EchoR _) _ = return Authorized
All it does is grants permissions to access localhost/echo to everybody.
4. Mirror
Goal: create a page /mirror with an input field, which will post the actual word and its palindrome glued together, as in
book -> bookkoob or bo
-> boob.
Add the following to config/routes to create a new route (i. e. a page in our case).
/mirror MirrorR GET POST
Now we just need to add a handler to src/Handler/Mirror.hs
import Import
import qualified Data.Text as T
getMirrorR :: Handler RepHtml
getMirrorR = do
defaultLayout $ do
setTitle "You kek"
$(widgetFile "mirror")
postMirrorR :: Handler RepHtml
postMirrorR = do
postedText <- runInputPost $ ireq textField "content"
defaultLayout $ ($(widgetFile "posted"))
Don't be overwhelmed! It's quite easy to understand.
And add the handler import to src/Application.hs, you will see a section, where all other handlers are imported
import Handler.Mirror
Mirror.hs mentions two widget files: 'mirror' and 'posted', here's their contents
templates/mirror.hamlet
<h1> Enter your text
<form method=post action=@{MirrorR}>
<input type=text name=content>
<input type=submit>
templates/posted.hamlet
<h1>You've just posted
<p>#{postedText}#{T.reverse postedText}
<hr>
<p><a href=@{MirrorR}>Get back
There is no need to add anything to .cabal or .yaml files, because stack magically deducts everything on its own :)
Don't forget to add the new route to isAuthorized like in the previous example!
Now build, launch and check out your localhost:3000, you must see something similar to my pics
stack build && stack exec -- yesod devel
And after you entered some text in the form, you should get something like this
5. Blog
Again, add Handler.Article and Handler.Blog to Application.hs imports.
This is contents of Blog.hs
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
module Handler.Blog
( getBlogR
, postBlogR
, YesodNic
, nicHtmlField
)
where
import Import
import Data.Monoid
import Yesod.Form.Nic (YesodNic, nicHtmlField)
instance YesodNic App
entryForm :: Form Article
entryForm = renderDivs $ Article
<$> areq textField "Title" Nothing
<*> areq nicHtmlField "Content" Nothing
getBlogR :: Handler RepHtml
getBlogR = do
articles <- runDB $ selectList [] [Desc ArticleTitle]
(articleWidget, enctype) <- generateFormPost entryForm
defaultLayout $ do
setTitle "kek"
$(widgetFile "articles")
postBlogR :: Handler Html
postBlogR = do
((res, articleWidget), enctype) <- runFormPost entryForm
case res of
FormSuccess article -> do
articleId <- runDB $ insert article
setMessage $ toHtml $ (articleTitle article) Import.<> " created"
redirect $ ArticleR articleId
_ -> defaultLayout $ do
setTitle "You loose, sucker!"
$(widgetFile "articleAddError")
Article.hs contents
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
module Handler.Article
(
getArticleR
)
where
import Import
getArticleR :: ArticleId -> Handler Html
getArticleR articleId = do
article <- runDB $ get404 articleId
defaultLayout $ do
setTitle $ toHtml $ articleTitle article
$(widgetFile "article")
Add this to conf/models
Article
title Text
content Html
deriving
This to conf/routes
/blog BlogR GET POST
/blog/#ArticleId ArticleR GET
And add these lines to src/Foundation.hs. This is a hack, but you cannot view the contents unauthorized, right? :) Drawback: all users on the internets will be able to see your post.
isAuthorized BlogR _ = return Authorized
isAuthorized (ArticleR _) _ = return Authorized
All done! What will you see: