{-# Language OverloadedStrings , QuasiQuotes , RecordWildCards , LambdaCase , TemplateHaskell , BangPatterns , FlexibleContexts , FlexibleInstances , MultiParamTypeClasses , UndecidableInstances , ScopedTypeVariables , RankNTypes , TypeFamilies , ViewPatterns #-} module Yesod.AngularUI ( YesodAngular (..) , runAngularUI , addCommand , addCommandMaybe -- ^ mostly internal , addModules , addDirective , addConfig , addConfigRaw , addService , addRun , addFactoryStore -- ^ naive persistent store , addFactory , addController , addProvide , addFilter , addConstant , addValue -- some normal stuff , addSetup -- ^ before the code, maybe some imports? , addWhen , setDefaultRoute , addREST , addRESTRaw -- placeholders for some generic rest api/ maybe a subsite? , tcFile , utcFile , tcVFile , utcVFile , state , url , name , nameA , addData , GAngular ) where --- the chaos import Control.Applicative ((<$>)) import Control.Monad.Trans.Writer (WriterT, runWriterT, tell, execWriter) -- import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe, catMaybes) import Data.Monoid (First (..), Monoid (..), (<>)) import Data.Text (Text) import Text.Hamlet import Text.Blaze.Html import Text.Julius import Text.Lucius import Yesod.Core (Route, getUrlRenderParams, getMessageRender, getYesod, lift, lookupGetParam, newIdent, sendResponse, notFound) import Yesod.Core.Widget import qualified Text.Blaze.Html5 as H import Text.Jasmine (minify) import Yesod.Core.Types import Yesod.Core.Json import Language.Haskell.TH.Syntax (Q, Exp (..), Lit (..)) import Language.Haskell.TH (listE) import qualified Data.Text as T import Prelude hiding (head, init, last, readFile, tail, writeFile) import Control.Monad.Trans.Resource import Control.Monad.IO.Class import Text.Shakespeare.I18N import qualified Data.Text.Lazy.Encoding as E (encodeUtf8, decodeUtf8) import Yesod.AngularUI.Router import Yesod.AngularUI.Types as TS renSoMsg :: (SomeMessage master -> Text) -> SomeMessage master -> Html renSoMsg f = toHtml . f runAngularUI :: (YesodAngular master) => GAngular master IO () -- ^ angular app -> (Text -> WidgetT master IO () -> HandlerT master IO Html) -- ^ layout -> HandlerT master IO Html runAngularUI ga dl = do master <- getYesod mrender <- renSoMsg <$> getMessageRender urender <- getUrlRenderParams ((), AngularWriter{..}) <- runWriterT ga mc <- lookupGetParam "command" fromMaybe (return ()) $ mc >>= flip Map.lookup awCommands modname <- newIdent let defaultRoute = case (awDefaultRoute, awStateName) of (filter (`elem` awStateName) -> x:_, _) -> [julius|.otherwise("/#{rawJS x}")|] (_, x:_) -> [julius|.otherwise("/#{rawJS x}")|] (_,[]) -> mempty dl modname $ do mapM_ (\x -> addScriptEither $ x master) urlAngularJs angularUIEntry toWidgetHead $ Minify awLook toWidget (combined mrender urender) toWidgetBody $ Minify [julius| ^{awSetup} angular .module("#{rawJS modname}", #{rawJS $ show awModules }, function($provide) { // $provide.constant("menu",# {toJSON awMenu}); ^{awServices} }) ^{awDirectives} ^{awConfigs} .config(function($urlRouterProvider, $stateProvider) { $urlRouterProvider ^{awRoutes} ^{defaultRoute} ; $stateProvider ^{awStates}; }); ^{awControllers} |] newtype Minify a = Minify a instance render ~ RY site => ToWidgetHead site (Minify [CssUrl (Route site)]) where toWidgetHead (Minify j) = toWidgetHead $ \r -> H.style $ preEscapedLazyText $ mconcat $ map (renderCssUrl r) j instance render ~ RY site => ToWidgetBody site (Minify (render -> Javascript)) where toWidgetBody (Minify j) = toWidget $ \r -> H.script $ preEscapedLazyText $ E.decodeUtf8 $ minify $ E.encodeUtf8 $ renderJavascriptUrl r j addModules :: (Monad m) => [Text] -> GAngular master m () addModules x = tell mempty{ awModules = x } addConfig :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addConfig name' funcall = do let n = name' `mappend` "Provider" tell mempty { awConfigs = [julius|.config(["#{rawJS n}", function (#{rawJS n}){ #{rawJS n}.^{funcall}; }])|] } addRun :: (Monad m) => ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addRun funcall = tell mempty { awConfigs = [julius|.run(^{funcall})|] } addConfigRaw :: (Monad m) => ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addConfigRaw funcall = tell mempty { awConfigs = [julius|.config(^{funcall})|] } addDirective :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addDirective n funcall = tell mempty { awDirectives = [julius|.directive("#{rawJS n}", ^{funcall} )|] } addController :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addController n funcall = tell mempty { awDirectives = [julius|.controller("#{rawJS n}", ^{funcall} )|] } addFilter :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addFilter n funcall = tell mempty { awDirectives = [julius|.filter("#{rawJS n}", ^{funcall} )|] } addFactory :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addFactory n funcall = addProvide [js|factory("#{rawJS n}",^{funcall})|] addREST :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addREST n funcall = tell mempty { awModules = ["ngResource"] , awDirectives = [julius| .factory("#{rawJS n}",["$resource", function($resource){ return $resource(^{funcall}); }])|] } addRESTRaw :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addRESTRaw n funcall = tell mempty { awModules = ["ngResource"] , awDirectives = [julius| .factory("#{rawJS n}",["$resource", function($resource){ ^{funcall} }])|] } addService :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () -- ^ adds a service addService n funcall = addProvide [js|service("#{rawJS n}",^{funcall})|] addProvide :: (Monad m) => ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () addProvide funcall = tell mempty { awServices = [julius| $provide.^{funcall} ;|] } addConstant :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () -- ^ add constant, remember to quote if raw text addConstant n funcall = addProvide [julius|constant("#{rawJS n}",^{funcall})|] addValue :: (Monad m) => Text -> ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () -- ^ add constant, remember to quote if raw text addValue n funcall = addProvide [julius|value("#{rawJS n}",^{funcall})|] addSetup :: (Monad m) => ((Route master -> [(Text, Text)] -> Text) -> Javascript) -> GAngular master m () -- ^ inject this code befor calling angular addSetup funcall = tell mempty { awSetup = [julius|^{funcall};|] } addCommand :: (FromJSON input, ToJSON output) => (input -> HandlerT master IO output) -> GAngular master IO Text -- ^ add a command (which is always printed) addCommand f = addCommandMaybe (fmap (Just <$>) f) addCommandMaybe :: (FromJSON input, ToJSON output) => (input -> HandlerT master IO (Maybe output)) -> GAngular master IO Text addCommandMaybe f = do n <- lift newIdent tell (mempty :: AngularWriter master IO) { awCommands = Map.singleton n handler } return $ "?command=" `mappend` n where handler = requireJsonBody >>= f >>= \case Just output -> do repjson <- returnJson output sendResponse repjson Nothing -> notFound addWhen :: ( Monad m , MonadThrow m , MonadBaseControl IO m , MonadIO m ) => Text -- ^ one route -> Text -- ^ the other one -> GAngular master m () addWhen fro to = tell mempty {awRoutes = [julius|.when("#{rawJS fro}","#{rawJS to}")|] } setDefaultRoute :: (Monad m) => Text -> GAngular master m() setDefaultRoute x = tell mempty { awDefaultRoute = [x] } addFactoryStore :: (Monad m) => Text -> GAngular master m () addFactoryStore n = addFactory (n <> "Store") [julius| function(){ var lc = {}; return { update: function (s){ _.extend(lc,s)} , full: function (s){ return lc; } , set: function (e,v) { lc[e] = v; } , get: function (e){ return lc[e]; } , has: function (e){ return _.has(lc,e); } , getD : function (d, e){ return _.has(lc,e) ? lc[e] : d; } , isEmpty : function (){ return _.isEmpty(lc) } , clear : function(){lc = {}} } } |] url :: Monad m => Text -> WriterT (UiState master) m () url u = tell mempty {uisUrl = Just u} name :: Monad m => Text -> WriterT (UiState master) m () name n = tell mempty {uisName = First (Just n)} nameA :: Monad m => Text -> WriterT (UiState master) m () nameA n = tell mempty { uisName = First (Just n) , uiTC = mempty { tcTempl = TmplInl "" } , uiAbstract = True } addData :: Monad m => JavascriptUrl (Route master) -> WriterT (UiState master) m () addData d = tell mempty {uiData = [d]} emptyFunction :: JavascriptUrl url emptyFunction = [julius| function(){} |] liftT :: Text -> Q Exp liftT t = do p <- [|T.pack|] return $ AppE p $ LitE $ StringL $ T.unpack t utcFile :: Text -> Text -> Q Exp utcFile u st = [|tell mempty { uisName = First (Just $(liftT st)) , uisUrl = Just $(liftT u) , uiTC = UiTC (TmplExt $(autoHamlet st "")) (CtrlExt $(fromMaybe [| emptyFunction |] $ autoJulius st "")) $(listE $ catMaybes [autoLucius st "", autoCassius st ""]) }|] utcVFile :: Text -> Text -> Text -> Q Exp utcVFile u st view = [|tell mempty { uisName = First (Just $(liftT st)) , uisUrl = Just $(liftT u) , uiV = [ ( $(liftT view) , UiTC (TmplExt $(autoHamlet st view)) (CtrlExt $(fromMaybe [| emptyFunction |] $ autoJulius st view)) $(listE $ catMaybes [autoLucius st view, autoCassius st view]) ) ] }|] tcFile :: Text -> Q Exp tcFile st = [|tell mempty { uisName = First (Just $(liftT st)) , uiTC = UiTC (TmplExt $(autoHamlet st "")) (CtrlExt $(fromMaybe [| emptyFunction |] $ autoJulius st "")) $(listE $ catMaybes [autoLucius st "", autoCassius st ""]) }|] tcVFile :: Text -> Text -> Q Exp tcVFile st view = [|tell mempty { uisName = First (Just $(liftT st)) , uiV = [ ( $(liftT view) , UiTC (TmplExt $(autoHamlet st view)) (CtrlExt $(fromMaybe [| emptyFunction |] $ autoJulius st view)) $(listE $ catMaybes [autoLucius st view, autoCassius st view]) ) ] }|] state :: ( Monad m , MonadThrow m , MonadBaseControl IO m , MonadIO m ) => GUiState master () -> GAngular master m () state sa = do let a = execWriter sa tell mempty {awUiState = [a]} addUIState a renderTemplate :: ( Monad m , MonadThrow m , MonadBaseControl IO m , MonadIO m ) => StateTemplate master -> GAngular master m (Maybe (JavascriptUrl (Route master))) renderTemplate = \case TmplExt t -> do n <- lift newIdent tell mempty { combined = [ihamlet|