[added section on JMacro, not quite ready for publication yet. Jeremy Shaw **20110519165200 Ignore-this: e36960fb45260a0cc3fb34e1cf9981b3 ] addfile ./JMacro.markdown.lhs hunk ./JMacro.markdown.lhs 1 + +

Javascript via JMacro

+JMacro is a +library that makes it easy to generate javascript in Haskell. This is +useful even if are just trying to add a little bit of javascript code +to your HTML templates. + +To use JMacro with happstack and hsx, you should install the +hsx-jmacro and happstack-jmacro packages. + +The syntax used by JMacro is almost identical to javascript. So, you +do not have to learn some special DSL to use it. In fact, JMacro can +work with most javascript you find in the wild. + + * syntax checking ensures that your javascript is syntactically valid + at compile time. That eliminates many comman javascript errors and + reduces development time + + * hygienic names and scoping automatically and transparently ensures + that blocks of javascript code do not accidentally create variables + and functions with conflicting names. + + * Antiquotation, Marshalling, and Shared scope make it easy to pass + Haskell values into the javascript code. It also makes it easy to + programmatically generate javascript code. + +The happstack-jmacro library makes it easy to use JMacro with Happstack and HSP. + +The following should get you started. + +The JMacro library does not require any external +pre-processors. Instead it uses the magic of QuasiQuotation. + +So, we need to enabled the QuasiQuotes LANGUAGE extension: +
+ +> {-# LANGUAGE CPP, FlexibleInstances, GeneralizedNewtypeDeriving, + TypeSynonymInstances, QuasiQuotes #-} + +
+ +In this example we are also using HSX, which does require a +pre-processor. The following line will automatically run the +pre-processor for us (and also suppress warnings about orphan +instances): +
+ +> {-# OPTIONS_GHC -F -pgmFtrhsx -fno-warn-orphans #-} + +
+Next we have a boatload if imports: +
+ +> import Control.Applicative ((<$>), optional) +> import Control.Monad (msum) +> import Control.Monad.State (StateT, evalStateT) +> import Control.Monad.Trans (liftIO) +> import qualified Data.Map as Map +> import Data.Maybe (fromMaybe) +> import Data.Unique +> import Happstack.Server +> import Happstack.Server.HSP.HTML (defaultTemplate) -- ^ also imports 'ToMessage XML' +> import Happstack.Server.JMacro () -- ToMessage instance for JStat +> import HSP +> import HSP.ServerPartT () -- ^ instance 'XMLGenerator ServerPartT' +> import HSX.JMacro (IntegerSupply(..), nextInteger') -- ^ EmbedAsChild and EmbedAsAttr for JStat +> import Language.Javascript.JMacro +> import System.Random + +
+ +In order to ensure that each <script> tag generates unique variables +names, we need a source of unique prefixes. An easy way to do that is to +wrap the ServerPartT monad around a StateT monad that supplies +integers: +
+ +> type JMacroPart = ServerPartT (StateT Integer IO) + +> instance IntegerSupply JMacroPart where +> nextInteger = nextInteger' + +
+The nextInteger' helper function has the type: + +
+#ifdef HsColour +> nextInteger' :: (MonadState Integer m) => m Integer +#endif +
+ +To use JMacroPart with simpleHTTP, we just evaluate the StateT monad: +
+ +> main :: IO () +> main = simpleHTTP nullConf $ flatten handlers +> where +> flatten :: JMacroPart a -> ServerPartT IO a +> flatten = mapServerPartT (flip evalStateT 0) + +
+Now that we have the scene set, we can actually look at some JMacro usage. + +In this example we embed a single javascript block inside the page: +
+ +> helloJMacro :: JMacroPart Response +> helloJMacro = +> toResponse <$> defaultTemplate "Hello JMacro" () +>
+> <% [$jmacro| +> var helloNode = document.createElement('h1'); +> helloNode.appendChild(document.createTextNode("Hello, JMacro!")); +> document.body.appendChild(helloNode); +> |] %> +>
+ +
+ +We do not need to specify the <script> tag explicitly, it will automatically be created for us. + +We can also use jmacro inside html attributes, such as onclick. +
+ +> helloAttr :: JMacroPart Response +> helloAttr = +> toResponse <$> defaultTemplate "Hello JMacro" () +>

!") |]>Click me!

+ +
+Note that we do not have to worry about escaping the ", < or > in the +onclick handler. It is taken care of for us automatically! + +So far, using jmacro with HSP looks almost exactly like using HSP with +out jmacro. That's actually pretty exciting. It mean that the mental +tax for using jmacro over straight javascript is very low. + +Now let's look at an example of hygienic naming. Let's say we write +the following block of javascript code: +
+ +> clickMe :: JStat +> clickMe = +> [$jmacro| +> +> var clickNode = document.createElement('p'); +> clickNode.appendChild(document.createTextNode("Click me!")); +> document.body.appendChild(clickNode); +> var clickCnt = 0; +> clickNode.setAttribute('style', 'cursor: pointer'); +> clickNode.onclick = function () { clickCnt++; +> alert ('Been clicked ' + clickCnt + ' time(s).'); +> }; +> |] + +
+ +That block of code tracks how many times you have clicked on the +Click me! text. It uses a global variable to keep track of the +number of clicks. Normally that would spell trouble. If we tried to +use that code twice on the same page, the instances would overwrite +each other. + +But, jmacro automatically renames the variables for us so that the +names are unique. In the following code each Click me! tracks its +counts separately: +
+ +> clickPart :: JMacroPart Response +> clickPart = +> toResponse <$> defaultTemplate "Hygienic Naming" () +>
+>

A Demo of Happstack+HSP+JMacro

+> <% clickMe %> +> <% clickMe %> +>
+ +
+Of course, sometimes we want the code blocks to share a global +variable. We can easily do that by changing the line: +
+ +#ifdef HsColour +> var clickCnt = 0; +#endif + +
+to +
+ +#ifdef HsColour +> var !clickCnt = 0; +#endif + +
+Now all the copies of clickMe2 will share the same counter: + +
+ +> clickMe2 :: JStat +> clickMe2 = +> [$jmacro| +> +> var clickNode = document.createElement('p'); +> clickNode.appendChild(document.createTextNode("Click me!")); +> document.body.appendChild(clickNode); +> var !clickCnt = 0; +> clickNode.setAttribute("style", "cursor: pointer"); +> clickNode.onclick = function () { clickCnt++; +> alert ('Been clicked ' + clickCnt + ' time(s).'); +> }; +> |] +> +> clickPart2 :: JMacroPart Response +> clickPart2 = +> toResponse <$> defaultTemplate "Hygienic Naming" () +>
+>

A Demo of Happstack+HSP+JMacro

+> <% clickMe2 %> +> <% clickMe2 %> +>
+ +
+ +Often times we want to call some javascript code that contains some Haskell values spliced in. This can be done using `( )`. + +
+ +> fortunePart :: JMacroPart Response +> fortunePart = +> do let fortunes = ["Your will be cursed to write java for the rest of your days." +> , "Fortune smiles upon you, your future will be filled with lambdas" +> ] +> n <- liftIO $ randomRIO (0, (length fortunes) - 1) +> +> toResponse <$> defaultTemplate "Fortune" +> <% [$jmacro| +> function !revealFortune(fortune) +> { +> var b = document.getElementById("button"); +> b.setAttribute('disabled', 'disabled'); +> var p = document.getElementById("fortune"); +> p.appendChild(document.createTextNode(fortune)); +> } +> |] +> %> +>
+>

Your Fortune

+>

+> +>
+ +
+ +JMacro can embed 'primitives' such as `Int`, `Bool`, `Char`, `String`, etc, by default. But we can also embed other types by creating a `ToJExpr` instance for them. For example, let's say we create some types for reporting the weather: + +
+ +> data Skies = Cloudy | Clear +> deriving (Bounded, Enum, Eq, Ord, Read, Show) +> +> newtype Fahrenheit = Fahrenheit Double +> deriving (Num, Enum, Eq, Ord, Read, Show, ToJExpr, Random) +> +> data Weather = Weather +> { skies :: Skies +> , temp :: Fahrenheit +> } +> deriving (Eq, Ord, Read, Show) +> +> instance Random Skies where +> randomR (lo, hi) g = +> case randomR (fromEnum lo, fromEnum hi) g of +> (c, g') -> (toEnum c, g') +> random g = randomR (minBound, maxBound) g +> +> instance Random Weather where +> randomR (Weather skiesLo tempLo, Weather skiesHi tempHi) g = +> let (skies, g') = randomR (skiesLo, skiesHi) g +> (temp, g'') = randomR (tempLo, tempHi) g' +> in ((Weather skies temp), g'') +> random g = +> let (skies, g') = random g +> (temp, g'') = random g' +> in ((Weather skies temp), g'') + +
+ +To pass these values into the generated javascript, we simply create a `ToJExpr` instance: + +
+ +#ifdef HsColour +> class ToJExpr a where +> toJExpr :: a -> JExpr +#endif + +
+ +For `Fahrenheit`, we were actually able to derive the `ToJExpr` instance automatically (aka, `deriving (ToJExpr)`). + +For `Skies`, we can just convert the values into javascript strings: + +
+ +> instance ToJExpr Skies where +> toJExpr = toJExpr . show +> + +
+ +For the `Weather` type, we create a javascript object/hash/associative array: + +
+ +> instance ToJExpr Weather where +> toJExpr (Weather skies temp) = +> toJExpr (Map.fromList [ ("skies", toJExpr skies) +> , ("temp", toJExpr temp) +> ]) + +
+Now we can splice a random weather report into our javascript: +
+ +> weatherPart :: JMacroPart Response +> weatherPart = +> do weather <- liftIO $ randomRIO ((Weather minBound (-40)), (Weather maxBound 100)) +> toResponse <$> defaultTemplate "Weather Report" () +>
+> <% [$jmacro| +> var w = `(weather)`; +> var p = document.createElement('p'); +> p.appendChild(document.createTextNode("The skies will be " + w.skies + +> " and the temperature will be " + +> w.temp.toFixed(1) + "°F")); +> document.body.appendChild(p); +> |] %> +>
+ +
+`ToJExpr` has an instance for `JSValue` from the json library. So, if your type already has a `JSON` istance, you can trivially create a `ToJExpr` instance for it: +
+ +#ifdef HsColour +> instance ToJExpr Foo where +> toJExpr = toJExpr . showJSON +#endif + +
+So far we have use jmacro to generate javascript that is embedded in HTML. We can also use it to create standalone javascript files. + +First we have a script template that is parameterized by a greeting. +
+ +> externalJs :: String -> JStat +> externalJs greeting = +> [$jmacro| +> function !greet(noun) +> { +> alert(`(greeting)` + ' ' + noun); +> } +> |] + +
+Then we have a part with two sub-parts: +
+ +> externalPart :: JMacroPart Response +> externalPart = dir "external" $ msum [ + +
+If external/script.js is requested, then we check for a query string parameter 'greeting' and generate the script. `toResponse` will automatically convert the script to a `Response` and serve it with the content-type, text/javascript; charset=UTF-8: +
+ +> dir "script.js" $ +> do greeting <- optional $ look "greeting" +> ok $ toResponse $ externalJs (fromMaybe "hello" greeting) + +
+Next we have an html page that includes the external script, and calls jmacro; +
+ +> , toResponse <$> defaultTemplate "external" +>