2011-12-28 16:15:55 +00:00
-----
isHidden: false
menupriority: 1
kind: article
created_at: 2011-12-28T15:14:40+02:00
2012-01-11 14:06:31 +00:00
en: title: Haskell web programming
en: subtitle: A Yesod tutorial
fr: title: Site en Haskell
fr: subtitle: Un tutoriel Yesod
2011-12-28 16:15:55 +00:00
author_name: Yann Esposito
author_uri: yannesposito.com
tags:
- yesod
- haskell
- programming
- web
2012-01-06 14:04:34 +00:00
macros:
html5: '< span class = "sc" > html5< / span > '
html: '< span class = "sc" > html< / span > '
2012-01-17 15:26:32 +00:00
http: '< span class = "sc" > http< / span > '
url: '< span class = "sc" > url< / span > '
css: '< span class = "sc" > css< / span > '
2011-12-28 16:15:55 +00:00
-----
2012-01-05 14:50:50 +00:00
< %= blogimage("flying_neo.jpg","Neo Flying at warp speed") %>
2011-12-28 16:15:55 +00:00
begindiv(intro)
2012-01-18 11:31:00 +00:00
en: < %= tldr %> A simple yesod tutorial.
en: Yesod is an Haskell web framework.
en: You shouldn't need to know Haskell.
2011-12-28 16:15:55 +00:00
2012-01-18 11:31:00 +00:00
fr: < %= tlal %> Un tutoriel pour yesod, un framework web Haskell.
fr: Vous ne devriez pas avoir besoin de savoir programmer en Haskell.
fr: Par contre je suis désolé pour les francophones,
fr: mais je n'ai pas eu le courage de traduire cet article en Français.
2011-12-28 16:15:55 +00:00
2012-01-06 15:38:52 +00:00
> <center><sc><b>Table of content</b></sc></center>
>
2011-12-30 22:03:41 +00:00
> * Table of Content (generated)
2011-12-29 16:05:05 +00:00
> {:toc}
2011-12-28 16:15:55 +00:00
2011-12-29 16:05:05 +00:00
enddiv
2011-12-28 16:15:55 +00:00
2012-01-18 11:31:00 +00:00
Why Haskell?
2011-12-28 16:15:55 +00:00
2012-01-06 14:04:34 +00:00
< %= blogimage("haskell-benchmark.png","Impressive Haskell Benchmark") %>
2012-01-05 14:50:50 +00:00
2012-01-18 11:31:00 +00:00
Its efficiency (see [Snap Benchmark][snapbench] _&_
[Warp Benchmark][warpbench][^benchmarkdigression]).
Haskell is an order of magnitude faster than interpreted languages
like [Ruby][haskellvsruby] and [Python][haskellvspython][^speeddigression].
2012-01-05 14:50:50 +00:00
2012-01-18 11:31:00 +00:00
Haskell is a high level language and make it harder to shoot you in the foot
than `C` or `C++` for example.
2012-01-16 15:57:26 +00:00
One of the best property of Haskell being:
2012-01-04 15:43:59 +00:00
2012-01-18 11:31:00 +00:00
> "If your program compile it will be
> very close to what the programmer intended".
2012-01-04 15:43:59 +00:00
2012-01-18 13:07:36 +00:00
Haskell web frameworks handle parallel tasks perfectly.
For example even better than node.js[^nodejstroll].
2012-01-18 11:31:00 +00:00
From the pure technical "point of vue",
Haskell seems to be the perfect web development tool.
2012-01-04 15:43:59 +00:00
2012-01-06 15:38:52 +00:00
Weaknesses of Haskell certainly won't be technical but human:
2012-01-04 15:43:59 +00:00
- Hard to grasp Haskell
2012-01-05 15:30:24 +00:00
- Hard to find a Haskell programmer
- The Haskell community is smaller than the community for X
- There is no [heroku ](http://heroku.com ) for Haskell (even if [Greg Weber did it ](http://www.yesodweb.com/blog/2011/07/haskell-on-heroku ), it was more a workaround).
< %= leftblogimage("thousands_smiths.jpg","Thousands of Agent Smith") %>
2012-01-04 15:43:59 +00:00
I don't say these are not important drawbacks.
2012-01-18 11:31:00 +00:00
But Haskell is certainly the best technical choice.
It makes it possible to handle an impressive number of connexions.
I believe Haskell is the best choice if you efficiency,
security and ability to adapt.
Furthermore, the Haskell community is great and learning Haskell
is fun and will make you a better programmer.
Focusing on Web Application, instead of reinventing the wheel, we should
choose an existing web framework.
2012-01-06 15:38:52 +00:00
Actually there are three main choices:
2011-12-28 16:15:55 +00:00
1. [Happstack ](http://happstack.com )
2. [Snap ](http://snapframework.com )
3. [Yesod ](http://yesodweb.com )
2011-12-30 22:03:41 +00:00
I don't think there is a real winner between these three framework.
The choice I made for yesod is highly subjective.
2012-01-18 11:31:00 +00:00
I just lurked a bit and tried some tutorials.
2012-01-06 15:38:52 +00:00
I had the feeling yesod make a better job at helping newcomers.
2012-01-18 11:31:00 +00:00
Furthermore, apparently the yesod team seems the most active.
Of course I might be wrong since it is a matter of feeling.
2011-12-29 16:05:05 +00:00
2012-01-05 14:50:50 +00:00
< %= blogimage("owl_draw.png","1. Draw some circles. 2. Draw the rest of the fucking owl") %>
2012-01-06 15:38:52 +00:00
Why did I write this article?
2012-01-18 11:31:00 +00:00
The yesod documentation and particularly the book are excellent.
2012-01-06 15:38:52 +00:00
But I missed an intermediate tutorial.
2012-01-17 15:26:32 +00:00
This tutorial won't explain all details.
2012-01-18 11:31:00 +00:00
I tried to give a step by step of how to start from a five minute tutorial
to an almost production ready architecture.
Furthermore explaining something to others is a great way to learn.
2012-01-17 15:26:32 +00:00
If you are used to Haskell and Yesod, this tutorial won't learn you much.
If you are completely new to Haskell and Yesod it might hopefully helps you.
2011-12-28 16:15:55 +00:00
2012-01-06 15:38:52 +00:00
During this tutorial you'll install, initialize and configure your first yesod project.
2012-01-18 11:31:00 +00:00
Then there is a very minimal 5 minutes yesod tutorial to heat up and verify the awesomeness of yesod.
2012-01-17 09:55:50 +00:00
Then we will clean up the 5 minutes tutorial to use some "best practices".
2012-01-17 15:26:32 +00:00
Finally there will be a more standard real world example; a minimal blog system.
2011-12-29 16:05:05 +00:00
[warpbench]: http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks
2012-01-06 14:04:34 +00:00
[snapbench]: http://snapframework.com/blog/2010/11/17/snap-0.3-benchmarks
2012-01-11 14:06:31 +00:00
[^benchmarkdigression]: One can argue these benchmark contains many problems. But benchmark are just here to give an order of idea. Mainly Haskell is very fast.
[^speeddigression]: Generally _high level_ Haskell is slower than C, but _low level_ Haskell is equivalent to C speed. It means that even if you can easily link C code with Haskell, this is not needed to reach the same speed. Furthermore writing a web service in C/C++ seems to be a very bad idea. You can take a look at a [discussion on HN about this ](http://news.ycombinator.com/item?id=3449388 ).
2012-01-06 14:04:34 +00:00
[^nodejstroll]: If you are curious, you can search about [the Fibonacci node.js troll ](http://www.unlimitednovelty.com/2011/10/nodejs-has-jumped-shark.html ). Without any tweaking, [Haskell handled this problem perfectly ](http://mathias-biilmann.net/posts/2011/10/is-haskell-the-cure ). I tested it myself using yesod instead of Snap.
[haskellvsruby]: http://shootout.alioth.debian.org/u64q/benchmark.php?test=all& lang=ghc& lang2=yarv
[haskellvspython]: http://shootout.alioth.debian.org/u64q/benchmark.php?test=all& lang=ghc& lang2=python3
2011-12-28 16:15:55 +00:00
2012-01-02 14:39:00 +00:00
## Before the real start
### Install
2011-12-28 16:15:55 +00:00
2012-01-18 11:31:00 +00:00
The recommended way to install [Haskell][haskell]
is to download the [Haskell Platform][haskellplatform].
2011-12-28 16:15:55 +00:00
[haskell]: http://www.haskell.org
[haskellplatform]: http://www.haskell.org/platform
2012-01-18 11:31:00 +00:00
Once done, you need to install yesod.
Open a terminal session and do:
2011-12-28 16:15:55 +00:00
< code class = "zsh" >
2012-01-11 14:06:31 +00:00
~ cabal update
~ cabal install yesod cabal-dev
2011-12-28 16:15:55 +00:00
< / code >
2012-01-17 15:26:32 +00:00
There are few steps but it should take some time to finish.
2011-12-28 16:15:55 +00:00
2012-01-02 14:39:00 +00:00
### Initialize
2011-12-28 16:15:55 +00:00
2012-01-17 15:26:32 +00:00
You are now ready to initialize your first yesod project.
2011-12-30 22:03:41 +00:00
Open a terminal and type:
2011-12-28 16:15:55 +00:00
< code class = "zsh" >
2012-01-11 14:06:31 +00:00
~ yesod init
2011-12-28 16:15:55 +00:00
< / code >
2012-01-18 11:31:00 +00:00
Enter your name, choose `yosog` for the project name and enter `Yosog` for the name of the Foundation.
Finally choose `sqlite` .
2012-01-17 15:26:32 +00:00
Now, start the development cycle:
2011-12-28 16:15:55 +00:00
< code class = "zsh" >
2012-01-11 14:06:31 +00:00
~ cd yosog
2012-01-17 09:55:50 +00:00
~ cabal-dev install & & yesod --dev devel
2011-12-28 16:15:55 +00:00
< / code >
2012-01-17 15:26:32 +00:00
This will compile the entire project. Be patient it could take a while the first time.
2011-12-30 22:03:41 +00:00
Once finished a server is launched and you could visit it by clicking this link:
2011-12-28 16:15:55 +00:00
[`http://localhost:3000` ](http://localhost:3000 )
2012-01-05 15:49:30 +00:00
Congratulation! Yesod works!
2011-12-30 22:03:41 +00:00
2012-01-05 15:49:30 +00:00
< blockquote >
2012-01-06 15:38:52 +00:00
Note: if something is messed up use the following command line inside the project directory.
2012-01-05 15:49:30 +00:00
< code class = "zsh" >
\rm -rf dist/* ; cabal-dev install && yesod --dev devel
< / code >
< / blockquote >
2012-01-02 14:39:00 +00:00
2012-01-18 11:31:00 +00:00
Until the end of the tutorial, use another terminal and let this one open
in a corner to see what occurs.
2011-12-28 16:15:55 +00:00
2012-01-02 14:39:00 +00:00
### Configure git
2011-12-28 16:15:55 +00:00
2012-01-18 11:31:00 +00:00
> Of course this step is not mandatory for the tutorial
> but it is a good practice.
2011-12-28 16:15:55 +00:00
2011-12-30 22:03:41 +00:00
Copy this `.gitignore` file into the `yosog` folder.
2011-12-28 16:15:55 +00:00
< code class = "zsh" file = ".gitignore" >
cabal-dev
dist
.static-cache
static/tmp
*.sqlite3
< / code >
Then initialize your git repository:
< code class = "zsh" >
2012-01-11 14:06:31 +00:00
~ git init .
~ git add .
~ git commit -a -m "Initial yesod commit"
2011-12-28 16:15:55 +00:00
< / code >
2012-01-18 13:07:36 +00:00
We are almost ready to start.
2011-12-28 16:15:55 +00:00
2012-01-17 15:26:32 +00:00
### Some last minute words
2011-12-28 16:15:55 +00:00
2012-01-18 11:31:00 +00:00
Up until here, we have a directory containing a bunch of files
and a local web server listening the port 3000.
2011-12-28 16:15:55 +00:00
If we modify a file inside this directory, yesod should try
2011-12-30 22:03:41 +00:00
to recompile as fast as possible the site.
2011-12-28 16:15:55 +00:00
Instead of explaining the role of every file,
2012-01-06 15:38:52 +00:00
let's focus only on the important files/directories for this tutorial:
2011-12-28 16:15:55 +00:00
1. `config/routes`
2. `Handler/`
3. `templates/`
2011-12-30 22:03:41 +00:00
4. `config/models`
2011-12-28 16:15:55 +00:00
Obviously:
2012-01-17 15:26:32 +00:00
| `config/routes` | is where you'll configure the map %url → Code. |
| `Handler/` | contains the files that will contain the code called when a %url is accessed. |
2012-01-17 16:37:34 +00:00
| `templates/` | contains %html, js and %css templates. |
2012-01-05 16:05:56 +00:00
| `config/models` | is where you'll configure the persistent objects (database tables). |
2011-12-28 16:15:55 +00:00
2012-01-05 15:49:30 +00:00
During this tutorial we'll modify other files as well,
2012-01-06 14:04:34 +00:00
but we won't explore them in detail.
2012-01-16 15:57:26 +00:00
2012-01-17 15:26:32 +00:00
Also note, shell command are executed in the root directory of your project instead specified otherwise.
2012-01-17 09:55:50 +00:00
2012-01-17 15:26:32 +00:00
We are now ready to start!
2011-12-28 16:15:55 +00:00
2012-01-02 14:39:00 +00:00
## Echo
2011-12-29 16:05:05 +00:00
2012-01-18 11:31:00 +00:00
To verify the quality of the security of the yesod framework,
let's make a minimal echo application.
2011-12-29 16:05:05 +00:00
2012-01-17 15:26:32 +00:00
> Goal:
>
> Make a server that when accessed `/echo/[some text]` should return a web page containing "some text" inside an `h1` bloc.
2011-12-28 16:15:55 +00:00
2012-01-17 15:26:32 +00:00
In a first time, we must declare the %url of the form `/echo/...` are meaningful.
2011-12-29 16:05:05 +00:00
Let's take a look at the file `config/routes` :
2011-12-28 16:15:55 +00:00
2012-01-16 22:06:20 +00:00
< pre >
2011-12-29 16:05:05 +00:00
/static StaticR Static getStatic
/auth AuthR Auth getAuth
/favicon.ico FaviconR GET
/robots.txt RobotsR GET
/ RootR GET
2012-01-16 22:06:20 +00:00
< / pre >
2011-12-29 16:05:05 +00:00
We want to add a route of the form `/echo/[anything]` somehow and do some action with this.
2012-01-17 15:26:32 +00:00
Add the following:
2011-12-29 16:05:05 +00:00
< pre >
/echo/#String EchoR GET
< / pre >
2012-01-17 15:26:32 +00:00
This line contains three elements: the %url pattern, a handler name, an %http method.
2012-01-16 15:57:26 +00:00
I am not particularly fan of the big R notation but this is the standard convention.
2011-12-29 16:05:05 +00:00
2011-12-30 22:03:41 +00:00
If you save `config/routes` , you should see your terminal in which you launched `yesod devel` activate and certainly displaying an error message.
2011-12-29 16:05:05 +00:00
< pre >
Application.hs:31:1: Not in scope: `getEchoR'
< / pre >
Why? Simply because we didn't written the code for the handler `EchoR` .
2012-01-18 13:07:36 +00:00
Edit the file `Handler/Root.hs` and append this:
2011-12-29 16:05:05 +00:00
< code class = "haskell" >
getEchoR :: String -> Handler RepHtml
getEchoR theText = do
defaultLayout $ do
[whamlet|< h1 > #{theText}|]
2011-12-28 16:15:55 +00:00
< / code >
2011-12-30 22:03:41 +00:00
Don't worry if you find all of this a bit cryptic.
2012-01-16 15:57:26 +00:00
In short it just declare a function named `getEchoR` with one argument (`theText`) of type String.
When this function is called, it return a `Handler RepHtml` whatever it is.
2012-01-17 16:37:34 +00:00
But mainly this will encapsulate our expected result inside an %html text.
2011-12-30 22:03:41 +00:00
2011-12-29 16:05:05 +00:00
After saving the file, you should see yesod recompile the application.
When the compilation is finished you'll see the message: `Starting devel application` .
2011-12-30 22:03:41 +00:00
Now you can visit: [`http://localhost:3000/echo/Yesod%20rocks!` ](http://localhost:3000/echo/Yesod%20rocks! )
2011-12-29 16:05:05 +00:00
2012-01-17 15:26:32 +00:00
TADA! It works!
2011-12-29 16:05:05 +00:00
2012-01-06 16:19:39 +00:00
### Bulletproof?
2011-12-30 22:03:41 +00:00
2012-01-05 14:50:50 +00:00
< %= blogimage("neo_bullet_proof.jpg","Neo stops a myriad of bullets") %>
2012-01-17 15:26:32 +00:00
Even this extremely minimal web application has some impressive properties.
For exemple, imagine an attacker entering this %url:
2011-12-29 16:05:05 +00:00
2011-12-30 00:18:14 +00:00
[`http://localhost:3000/echo/<a>I'm <script>alert("Bad!");` ](http://localhost:3000/echo/<a>I'm <script>alert("Bad!" );)< % "</ script > " %>
2011-12-29 16:05:05 +00:00
The special characters are protected for us.
2012-01-17 15:26:32 +00:00
A malicious user could not hide some bad script inside.
2011-12-29 16:05:05 +00:00
2012-01-17 15:26:32 +00:00
This behavior is a direct consequence of _type safety_ .
The %url string is put inside a %url type.
2012-01-18 11:31:00 +00:00
Then the interesting part in the %url is put inside a String type.
To pass from %url type to String type some transformation are made.
For example, replace all "`%20`" by space characters.
Then to show the String inside an %html document, the string is put inside an %html type.
Some transformations occurs like replace "< code > < < / code > " by "`< `".
2012-01-17 15:26:32 +00:00
Thanks to yesod, this tedious job is done for us.
2011-12-29 16:05:05 +00:00
< code class = "zsh" >
"http://localhost:3000/echo/some%20text< a > " :: URL
↓
"some text< a > " :: String
↓
2012-01-17 16:37:34 +00:00
"some text < a> " :: Html
2011-12-29 16:05:05 +00:00
< / code >
2012-01-16 15:57:26 +00:00
Yesod is not only fast, it helps us to remain secure.
2012-01-17 15:26:32 +00:00
It protects us from many common errors in other paradigms.
Yes, I am looking at you PHP!
2011-12-29 16:05:05 +00:00
2012-01-02 14:39:00 +00:00
### Cleaning up
2011-12-29 16:05:05 +00:00
2012-01-17 15:26:32 +00:00
Even this very minimal example should be enhanced.
We will clean up many details:
- Use %html5 boilerplate
- Use a general %css (cleaner than the empty by default)
- Dispatch handler code into different files
- Use `Data.Text` instead of `String`
- Put our "views"[^explainviewwidget] inside the `template` directory
2011-12-29 16:05:05 +00:00
2012-01-17 15:26:32 +00:00
[^explainviewwidget]: By view I mean yesod widget's hamlet, lucius and julius files.
2011-12-29 16:05:05 +00:00
2012-01-17 15:26:32 +00:00
#### %html5 boilerplate and a better %css
2012-01-17 09:55:50 +00:00
Copy the boilerplate template to the default template.
If you take a look at them, the format is not %html but hamlet.
2012-01-16 15:57:26 +00:00
2012-01-17 09:55:50 +00:00
< code class = "zsh" >
~ cp templates/boilerplate-wrapper.hamlet templates/default-layout-wrapper.hamlet
< / code >
2012-01-17 15:26:32 +00:00
It is time to change the default %css.
2012-01-16 15:57:26 +00:00
Add a file named `default-layout.lucius` inside the `template/` directory containing:
2012-01-17 09:55:50 +00:00
< code class = "css" file = "default-layout.lucius" >
2012-01-16 15:57:26 +00:00
body {
2012-01-16 22:06:20 +00:00
font-family: Helvetica, sans-serif;
font-size: 18px; }
2012-01-16 15:57:26 +00:00
#content {
padding: 1em;
border: #CCC solid 2px;
border-radius: 5px;
margin: 1em;
width: 37em;
margin: 1em auto;
background: #F2F2F2 ;
line-height: 1.5em;
color: #333 ; }
.required { margin: 1em 0; }
2012-01-16 22:06:20 +00:00
.optional { margin: 1em 0; }
2012-01-16 15:57:26 +00:00
label { width: 8em; display: inline-block; }
2012-01-16 22:06:20 +00:00
input, textarea { background: #FAFAFA }
textarea { width: 27em; height: 9em;}
2012-01-16 15:57:26 +00:00
ul { list-style: square; }
a { color: #A56 ; }
a:hover { color: #C58 ; }
a:active { color: #C58 ; }
a:visited { color: #943 ; }
< / code >
2012-01-18 11:31:00 +00:00
Personally I would prefer if such a minimal %css was put with the scaffolding tool.
I am sure somebody already made such a minimal %css which give the impression
the browser handle correctly %html without any style applied to it.
But I digress.
2012-01-17 09:55:50 +00:00
2012-01-17 15:26:32 +00:00
#### Separate Handlers
2011-12-30 16:14:57 +00:00
2012-01-17 15:26:32 +00:00
Generally you don't want to have all your code inside a unique file.
This is why we will separate our handlers.
2011-12-30 16:14:57 +00:00
In a first time create a new file `Handler/Echo.hs` containing:
< code class = "haskell" >
module Handler.Echo where
import Import
getEchoR :: String -> Handler RepHtml
getEchoR theText = do
defaultLayout $ do
[whamlet|< h1 > #{theText}|]
< / code >
2012-01-17 15:26:32 +00:00
Do not forget to remove the getEchoR function inside `Handler/Root.hs` .
2011-12-30 16:14:57 +00:00
2012-01-17 15:26:32 +00:00
We must declare this new file into`yosog.cabal`.
Just after `Handler.Root` , add:
2011-12-30 16:14:57 +00:00
< pre >
Handler.Echo
< / pre >
2012-01-17 15:26:32 +00:00
We must also declare this new Handler module inside `Application.hs` .
2011-12-30 16:14:57 +00:00
Just after the "`import Handler.Root`", add:
< code class = "haskell" >
import Handler.Echo
< / code >
2012-01-17 15:26:32 +00:00
This is it.
2011-12-30 16:14:57 +00:00
2012-01-18 11:31:00 +00:00
< small > < em > ps:< / em > I am sure not so far in the future we could simply write
`yesod add-handler Echo` to declare it and create a new handler file.</ small >
2012-01-17 15:26:32 +00:00
#### `Data.Text`
2011-12-30 16:14:57 +00:00
2012-01-02 14:39:00 +00:00
It is a good practice to use `Data.Text` instead of `String` .
2011-12-30 16:14:57 +00:00
2012-01-17 15:26:32 +00:00
To declare it, add this import directive to `Foundation.hs` (just after the last one):
2011-12-30 16:14:57 +00:00
< code class = "diff" >
import Data.Text
< / code >
2012-01-17 15:26:32 +00:00
We have to modify `config/routes` and our handler accordingly.
Replace `#String` by `#Text` in `config/routes` :
2011-12-30 16:14:57 +00:00
< pre >
/echo/#Text EchoR GET
< / pre >
And do the same in `Handler/Echo.hs` :
< code class = "haskell" file = "Echo.hs" >
module Handler.Echo where
import Import
getEchoR :: Text -> Handler RepHtml
getEchoR theText = do
defaultLayout $ do
[whamlet|< h1 > #{theText}|]
< / code >
2012-01-17 15:26:32 +00:00
#### Use templates
2011-12-30 16:14:57 +00:00
2012-01-17 15:26:32 +00:00
Some %html (more precisely hamlet) is written directly inside our handler.
We should put this part inside another file.
Create the new file `template/echo.hamlet` containing:
2011-12-30 16:14:57 +00:00
< code class = "haskell" file = "echo.hamlet" >
< h1 > #{theText}
< / code >
and modify the handler `Handler/Echo.hs` :
< code class = "haskell" >
getEchoR :: Text -> Handler RepHtml
getEchoR theText = do
defaultLayout $ do
$(widgetFile "echo")
< / code >
2011-12-29 16:05:05 +00:00
2012-01-17 15:26:32 +00:00
At this point, our web application is structured between different files.
2012-01-02 14:39:00 +00:00
Handler are grouped, we use `Data.Text` and our views are in templates.
2012-01-18 13:07:36 +00:00
It is the time to make a slightly more complex example.
2011-12-30 22:03:41 +00:00
2012-01-17 15:26:32 +00:00
## Mirror
< %= leftblogimage("mirror.jpg","Neo touching a mirror") %>
2012-01-02 14:39:00 +00:00
Let's make another minimal application.
You should see a form containing a text field and a validation button.
2012-01-18 11:31:00 +00:00
When you enter some text (for example "Jormungad") and validate,
the next page present you the content and its reverse appended to it.
2012-01-17 15:26:32 +00:00
In our example it should return "JormungaddagnumroJ".
2012-01-02 14:39:00 +00:00
First, add a new route:
2012-01-16 22:06:20 +00:00
< pre >
2012-01-17 15:26:32 +00:00
/mirror MirrorR GET POST
2012-01-16 22:06:20 +00:00
< / pre >
2012-01-02 14:39:00 +00:00
2012-01-17 15:26:32 +00:00
This time the path `/mirror` will accept GET and POST requests.
Add the corresponding new Handler file:
2012-01-02 14:39:00 +00:00
2012-01-17 15:26:32 +00:00
< code class = "haskell" file = "Mirror.hs" >
module Handler.Mirror where
2012-01-02 14:39:00 +00:00
import Import
2012-01-17 15:26:32 +00:00
import qualified Data.Text as T
2012-01-02 14:39:00 +00:00
2012-01-17 15:26:32 +00:00
getMirrorR :: Handler RepHtml
getMirrorR = do
2012-01-02 14:39:00 +00:00
defaultLayout $ do
2012-01-17 15:26:32 +00:00
$(widgetFile "mirror")
2012-01-02 14:39:00 +00:00
2012-01-17 15:26:32 +00:00
postMirrorR :: Handler RepHtml
postMirrorR = do
postedText < - runInputPost $ ireq textField " content "
defaultLayout $ do
$(widgetFile "posted")
2012-01-02 14:39:00 +00:00
< / code >
Don't forget to declare it inside `yosog.cabal` and `Application.hs` .
2012-01-18 11:31:00 +00:00
We will need to use the `reverse` function provided by `Data.Text`
which explain the additional import.
2012-01-17 15:26:32 +00:00
2012-01-02 14:39:00 +00:00
The only new thing here is the line that get the POST parameter named "content".
2012-01-18 11:31:00 +00:00
If you want to know more detail about it and form in general you can take
look at [the yesod book ](http://www.yesodweb.com/book/forms ).
2012-01-02 14:39:00 +00:00
Create the two corresponding templates:
2012-01-17 15:26:32 +00:00
< code class = "html" file = "mirror.hamlet" >
2012-01-02 14:39:00 +00:00
< h1 > Enter your text
2012-01-17 15:26:32 +00:00
< form method = post action = @{MirrorR} >
2012-01-02 14:39:00 +00:00
< input type = text name = content >
< input type = submit >
< / code >
< code class = "html" file = "posted.hamlet" >
< h1 > You've just posted
2012-01-17 15:26:32 +00:00
< p > #{postedText}#{T.reverse postedText}
2012-01-02 14:39:00 +00:00
< hr >
2012-01-17 15:26:32 +00:00
< p > < a href = @{MirrorR} > Get back
2012-01-02 14:39:00 +00:00
< / code >
And that is all.
2012-01-17 15:26:32 +00:00
This time, we won't need to clean up.
2012-01-02 14:39:00 +00:00
We may have used another way to generate the form
2012-01-16 22:06:20 +00:00
but we'll see this in the next section.
2012-01-02 14:39:00 +00:00
2012-01-17 15:26:32 +00:00
Just try it by [clicking here ](http://localhost:3000/mirror ).
2012-01-02 14:39:00 +00:00
2012-01-17 15:26:32 +00:00
Also you can try to enter strange values.
Like before, your application is quite secure.
2012-01-02 14:39:00 +00:00
2012-01-16 15:57:26 +00:00
## A Blog
2012-01-17 15:26:32 +00:00
We saw how to retrieve %http parameters.
2012-01-18 13:07:36 +00:00
It is the time to save things into a database.
2012-01-16 15:57:26 +00:00
2012-01-18 13:07:36 +00:00
As before add some routes inside `config/routes` :
2012-01-16 15:57:26 +00:00
2012-01-16 22:06:20 +00:00
< pre >
2012-01-16 15:57:26 +00:00
/blog BlogR GET POST
/blog/#ArticleId ArticleR GET
2012-01-16 22:06:20 +00:00
< / pre >
2012-01-16 15:57:26 +00:00
This example will be very minimal:
2012-01-18 13:07:36 +00:00
- `GET` on `/blog` should display the list of articles.
- `POST` on `/blog` should create a new article
- `GET` on `/blog/<article id>` should display the content of the article.
2012-01-16 15:57:26 +00:00
2012-01-18 13:07:36 +00:00
First we declare another model object.
Append the following content to `config/models` :
2012-01-16 15:57:26 +00:00
2012-01-16 22:06:20 +00:00
< pre >
2012-01-16 15:57:26 +00:00
Article
title Text
content Html
deriving
2012-01-16 22:06:20 +00:00
< / pre >
2012-01-16 15:57:26 +00:00
2012-01-18 13:07:36 +00:00
As `Html` is not an instance of `Read` , `Show` and `Eq` ,
we had to add the `deriving` line.
2012-01-16 15:57:26 +00:00
If you forget it, there will be an error.
2012-01-17 15:26:32 +00:00
After the route and the model, we write the handler.
First, declare a new Handler module.
2012-01-16 15:57:26 +00:00
Add `import Handler.Blog` inside `Application.hs` and add it into `yosog.cabal` .
2012-01-18 13:07:36 +00:00
Let's write the content of `Handler/Blog.hs` .
2012-01-18 11:31:00 +00:00
We start by declaring the module and by importing some block necessary to
handle Html in forms.
2012-01-02 14:39:00 +00:00
2012-01-16 15:57:26 +00:00
< code class = "haskell" >
module Handler.Blog
( getBlogR
, postBlogR
, getArticleR
)
where
import Import
2012-01-16 22:06:20 +00:00
2012-01-17 16:37:34 +00:00
-- to use Html into forms
2012-01-16 15:57:26 +00:00
import Yesod.Form.Nic (YesodNic, nicHtmlField)
instance YesodNic Yosog
< / code >
< code class = "haskell" >
entryForm :: Form Article
entryForm = renderDivs $ Article
< $> areq textField "Title" Nothing
< *> areq nicHtmlField "Content" Nothing
2012-01-16 22:06:20 +00:00
< / code >
2012-01-16 15:57:26 +00:00
2012-01-17 16:37:34 +00:00
This function defines a form for adding a new article.
Don't pay attention to all the syntax.
If you are curious you can take a look at Applicative Functor.
You just have to remember `areq` is for required form input.
Its arguments being: `areq type label default_value` .
2012-01-16 22:06:20 +00:00
< code class = "haskell" >
-- The view showing the list of articles
2012-01-16 15:57:26 +00:00
getBlogR :: Handler RepHtml
getBlogR = do
2012-01-16 22:06:20 +00:00
-- Get the list of articles inside the database.
articles < - runDB $ selectList [ ] [ Desc ArticleTitle ]
2012-01-17 16:37:34 +00:00
-- We'll need the two "objects": articleWidget and enctype
2012-01-16 22:06:20 +00:00
-- to construct the form (see template/articles.hamlet).
2012-01-17 16:37:34 +00:00
((_,articleWidget), enctype) < - generateFormPost entryForm
2012-01-16 15:57:26 +00:00
defaultLayout $ do
$(widgetFile "articles")
2012-01-16 22:06:20 +00:00
< / code >
2012-01-16 15:57:26 +00:00
2012-01-17 16:37:34 +00:00
This handler should display a list of articles.
We get the list from the DB and we construct the form.
Just take a look at the corresponding template:
2012-01-16 22:06:20 +00:00
< code class = "html" file = "articles.hamlet" >
< h1 > Articles
$if null articles
2012-01-17 16:37:34 +00:00
-- Show a standard message if there is no article
< p > _{MsgNoEntries}
2012-01-16 22:06:20 +00:00
$else
2012-01-17 16:37:34 +00:00
-- Show the list of articles
2012-01-16 22:06:20 +00:00
< ul >
$forall article < - articles
< li >
< a href = @{ArticleR ( fst article ) } > #{articleTitle (snd article)}
2012-01-17 16:37:34 +00:00
< hr / >
< form method = post enctype = #{enctype} >
^{articleWidget}
< div >
2012-01-18 11:31:00 +00:00
< input type = submit value = "Post New Article" >
2012-01-16 22:06:20 +00:00
< / code >
2012-01-17 15:26:32 +00:00
You should remark we added some logic inside the template.
2012-01-17 16:37:34 +00:00
There is a test and a "loop".
Another very interesting part is the creation of the form.
The `articleWidget` was created by yesod.
2012-01-18 11:31:00 +00:00
We have given him the right parameters
(input required or optional, labels, default values).
2012-01-17 16:37:34 +00:00
And now we have a protected form made for us.
But we have to create the submit button.
Get back to `Handler/Blog.hs` .
2012-01-16 22:06:20 +00:00
< code class = "haskell" >
2012-01-17 16:37:34 +00:00
-- we continue Handler/Blog.hs
2012-01-16 15:57:26 +00:00
postBlogR :: Handler RepHtml
postBlogR = do
2012-01-17 16:37:34 +00:00
((res,articleWidget),enctype) < - runFormPost entryForm
2012-01-16 15:57:26 +00:00
case res of
FormSuccess article -> do
articleId < - runDB $ insert article
2012-01-17 16:37:34 +00:00
setMessage $ (articleTitle article) ++. " created"
2012-01-16 15:57:26 +00:00
redirect RedirectPermanent $ ArticleR articleId
_ -> defaultLayout $ do
2012-01-17 16:37:34 +00:00
setTitle "Please correct your entry form"
2012-01-16 15:57:26 +00:00
$(widgetFile "articleAddError")
2012-01-17 16:37:34 +00:00
where
(++.) t s = toHtml $ T.append t (T.pack s)
2012-01-17 15:26:32 +00:00
< / code >
2012-01-16 15:57:26 +00:00
2012-01-17 16:37:34 +00:00
This function should be used to create a new article.
2012-01-18 11:31:00 +00:00
The `(++.)` local function is just a trick to make an append occurs within the right type.
2012-01-17 16:37:34 +00:00
We handle the form response.
2012-01-18 11:31:00 +00:00
If there is an error we display an error page.
2012-01-17 16:37:34 +00:00
For example if we left some required value blank.
2012-01-18 11:31:00 +00:00
If things goes right:
- we add the new article inside the DB (`runDB $ insert article`)
- we add a message to be displayed (`setMessage $ ...`)
- we are redirected to the article web page.
Here is the content of the error Page:
< code class = "haskell" >
< form method = post enctype = #{enctype} >
^{articleWidget}
< div >
< input type = submit value = "Post New Article" >
< / code >
2012-01-17 16:37:34 +00:00
2012-01-18 11:31:00 +00:00
Finally we need to display an article:
2012-01-17 16:37:34 +00:00
2012-01-17 15:26:32 +00:00
< code class = "haskell" >
2012-01-16 15:57:26 +00:00
getArticleR :: ArticleId -> Handler RepHtml
getArticleR articleId = do
article < - runDB $ get404 articleId
defaultLayout $ do
2012-01-17 16:37:34 +00:00
setTitle $ toHtml $ articleTitle article
2012-01-16 15:57:26 +00:00
$(widgetFile "article")
< / code >
2012-01-11 14:06:31 +00:00
2012-01-18 11:31:00 +00:00
The `get404` function try to do a get on the DB.
If it fails it return a 404 page.
The rest should be clear.
Here is the content of `template/article.hamlet` :
2011-12-29 16:05:05 +00:00
2012-01-18 11:31:00 +00:00
< code class = "html" file = "article.hamlet" >
< h1 > #{articleTitle article}
< article > #{articleContent article}
< / code >
2011-12-29 16:05:05 +00:00
2012-01-18 11:31:00 +00:00
This is the end of this tutorial.
I made it very minimal.
If you want to go further, you should take a look at the
recent [i18n blog tutorial ](http://yesodweb.com/blog/2012/01/blog-example ).
It will be obvious I inspired my own tutorial on it.
You'll learn in a very straightforward way how easy it is to use authorizations,
Time and internationalization.
2012-01-18 13:07:36 +00:00
Just for fun, try to enter an article with the following content:
2012-01-18 11:31:00 +00:00
< code class = "html" >
2012-01-18 13:07:36 +00:00
< p > A last try to < em > cross script< / em >
and < em > SQL injection< / em > < / p >
< p > Here is the first try:
< script > alert ( "You loose" ) ; < / script > < / p >
2012-01-18 11:31:00 +00:00
< p > And Here is the last < / p >
"); DROP TABLE ARTICLE;;
< / code >