Wednesday, August 14, 2013

A few tips on yesod + angularjs

Running a website in a language as obscure as Haskell can at times be challenging. Here are some code snippets I have found that help make Yesod, and Javascript easier to work with together.

Setting up a non-minified Scaffolded, development enviornment.



-- | Adding the (\bs -> Right bs) will fix up your dev enviornment to produce readable Javascript
addStaticContent 
        | development = addStaticContentExternal (\bs->Right bs) genFileName Settings.staticDir (StaticR . flip StaticRoute [])    
        | otherwise   = addStaticContentExternal minifym genFileName Settings.staticDir (StaticR . flip StaticRoute [])                         

Make a Tests.hs file and put javascript tests in it

  • I am still looking for a way to encorporate the lovely unit testing found in so many js envioros, into Yesod but not much luck yet.
  • You can use coffee and typescript with yesod but I haven't done this.
  • fay doesn't seem to play nice with angular but at least you get compiled haskelly errors
  • Make a wrapper for your getparams method to get data into haskell in less steps

    {-# LANGUAGE TupleSections, OverloadedStrings, QuasiQuotes, TemplateHaskell, TypeFamilies, RecordWildCards,DeriveGeneric, MultiParamTypeClasses, FlexibleInstances  #-}
    module ContentCfgTypes
           ( module ContentCfgTypes
           ) where
    import Prelude hiding (head, init, last
                          ,readFile, tail, writeFile)
    import GHC.Generics
    import Control.Applicative ((<$>), (<*>))
    import Yesod 
    --import Text.Julius
    import qualified Data.Aeson as A
    
    
    import Data.Text
    
    
    --import Yesod
    
    {-| Content Types are the configuration object and helpers for them 
        for all the different content widgets possible in onping2.0.
    
        Because they are defined for use in a database context they have to be strictly 
        defined.  
    |-} 
    
    
    -- | Every ContentConfig type has to have a constructor here 
    -- This is what is put into the content array
    
    type MaybeConfig a = Maybe (A.Result a)
    
    data ContentConfig = TestConfig !TestConfigObj
    
                         deriving (Read,Show,Eq,Generic) 
    
                                  
    
    instance FromJSON ContentConfig 
    instance ToJSON ContentConfig
    
    
                  
    lookupContent ::(A.FromJSON a , MonadHandler m) => ( (Text,Text)  -> (Text,Value) ) -> m (Maybe (A.Result a))
    lookupContent objTransformer = do 
      req <- getRequest
      case reqGetParams req of 
        [] -> return Nothing
        lst -> return $ Just (A.fromJSON.object $ objTransformer <$> lst )
    
    
    {-| Lookup content will take a list of values and parse them
        with a transformer into the right values.  Then will map them
        into a JSON Object.
        To do this the final Data type must be an Instance of FromJSON and
        the transormer provided must convert (Text,Text) into a (Text,Val) type
        according to whatever Transformation rule you want |-}
    
    --Example stuff
     
    data TestObj =  TestObj { testWidth :: Int
                              ,testTitle :: Text 
                              ,testStep :: Int 
                            }
                 deriving (Read, Show,Eq)
    
    instance FromJSON TestObj where 
        parseJSON (Object tObj) = TestObj <$> 
                              tObj .: "width" <*> 
                              tObj .: "title" <*> 
                              tObj .: "step" 
    
        parseJSON _ = fail "Rule: Expecting Test Object Received, Other"
    
    instance ToJSON TestObj where 
        toJSON (TestObj {..}) = object 
                            [ 
                             "width" .= testWidth 
                             ,"step" .= testSte
                             ,"title" .= testTitle 
                             ]
    
    
    
    -- | An example transformer on a get parameter string
    
    exTransformObj :: (Text,Text) -> (Text, Value)
    exTransformObj (t,v)
      | t == "width" = (t .= intVal v)
      | t == "title" = (t .= textVal v)
      | otherwise = (t .= toJSON v)
    
    

    As you can see I make use of Data.Aeson in mine. Mostly because I wanted this to feel like the Yesod native: parseJsonBody

    Another thing I would suggest is coming up with a way to duplicate the route ideas found in Yesod on the client route information contained in Angular. The way I did it was to define a type called ClientRoute then anywhere I am using it I mirror the Yesod convention for routes (i.e. @{MyRouteR}) with @{MyRouteCR}.

    The combination of widgets, client routes, JSON parsed GET, JSON parsed POST and extensible directives (a la AngularJS) are very effective for speedy web development with high assurance.

    a few other ideas

  • Custom default layouts
      make a layout with no css or anything else for creating angularJS directives
  • JavascriptUrl for directives
      you can separate your directives into composable (serverside) pieces using the JavascriptUrl type
       #{toJSON foo} 
      can be used to create javascript objects statically based on the parameters you pulled out of your get string, this is killer!
  • Thoughts on Haskell and Javascript mixing

    So obviously, I chose Haskell and am stuck with Javascript. However I don't mind Javascript all that much (except when I do). The beautiful thing about working with the two together is how well anchored Yesod can help you keep your client code. use Angular, use Directives, use Widgets and compose compose compose...

    If anyone already has a great install of Karma or something like it that works with Yesod, please let me know!

    No comments:

    Post a Comment