How to do IO in a Widget/Hamlet referenced by defaultLayout?

189 Views Asked by At

I'm new to Yesod and seem to be completely lost with Widgets, Handlers, Hamlets, WHamlets, and what have you! Here's what I'm trying to do:

  • Every page on my site needs to have a navbar, which leads me to believe that the correct place for implementing this should be defaultLayout
  • Now, this navbar needs to display some information that is obtained from an IO action (it's an RPC call which gives this data, to be more specific).

Therefore, I tried writing the following function in Foundation.hs (the code layout is the basic yesod-sqlite scaffolding template):

nav = do
  globalStat <- handlerToWidget $ A2.getGlobalStat NWT.ariaRPCUrl
  $(whamletFile "templates/navbar.hamlet)

A2.getGlobalStat :: IO GlobalStatResponse

Here's what template/navbar.hamlet looks like:

<nav .navbar .navbar-default>
  <div .container-fluid>
    <p .navbar-right .navbar-text>
      <span>
        #{A2.glDownloadSpeed globalStat}
        <i .glyphicon .glyphicon-arrow-down>
      <span>
        #{A2.glUploadSpeed globalStat}
        <i .glyphicon .glyphicon-arrow-up>
      <span .label .label-success>
        On-the-watch

Here's what default-layout-wrapper.hamlet looks like:

<!-- SNIP -->
  <body>
    <div class="container">
      <header>
        ^{nav}
      <div id="main" role="main">
        ^{pageBody pc}
<!-- SNIP -->

Here's what defaultLayout looks like:

defaultLayout widget = do
    master <- getYesod
    mmsg <- getMessage
    pc <- widgetToPageContent $ do
        addStylesheet $ StaticR css_bootstrap_css
        $(widgetFile "default-layout")
    withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")

However, the code refuses to compile with one type-error after another. I've tried a lot of combinations of hametFile, whamletFile, handerToWidget, liftIO, even placing the nav function inside defaultLayout, but nothing seems to work. According to me my current code should compile, but I've obviously not understood how the Yesod-Core types are working.

How do I get this to work? And more importantly, what concept have I misunderstood?

Edit 1:

Have tried modifying the nav function to the following:

nav :: Handler Html
nav = do
  globalStat  <- liftIO $ A2.getGlobalStat NWT.ariaRPCUrl
  $(hamletFile "templates/navbar.hamlet")

But, it results in the following type mismatch in defaultLayout on the line with withUrlRenderer:

 Couldn't match type ‘HandlerT App IO Html’
                with ‘Text.Hamlet.Render (Route App) -> Html’
 Expected type: HtmlUrl (Route App)
   Actual type: Handler Html
 In the first argument of ‘Text.Hamlet.asHtmlUrl’, namely ‘nav’
 In a stmt of a 'do' block: Text.Hamlet.asHtmlUrl nav _render_a2ZY0 (intero)

Edit 2:

Tried changing the type signature of nav to:

nav :: Widget
nav = do
  globalStat  <- liftIO $ A2.getGlobalStat NWT.ariaRPCUrl
  $(hamletFile "templates/navbar.hamlet") 

But it results in a new type-mismatch, in the same line:

 Couldn't match type ‘WidgetT App IO ()’
                with ‘Text.Hamlet.Render (Route App) -> Html’
 Expected type: HtmlUrl (Route App)
   Actual type: Widget
 In the first argument of ‘Text.Hamlet.asHtmlUrl’, namely ‘nav’
 In a stmt of a 'do' block: Text.Hamlet.asHtmlUrl nav _render_a350l (intero)

Edit 3:

Here's a relevant snippet from -ddump-splices:

\ _render_a28TE
  -> do { asHtmlUrl (pageHead pc) _render_a28TE;
          id ((Text.Blaze.Internal.preEscapedText . Data.Text.pack) "\n");
          asHtmlUrl (pageBody pc) _render_a28TE;
          id ((Text.Blaze.Internal.preEscapedText . Data.Text.pack) "\n");
          asHtmlUrl testWidget2 _render_a28TE }

The type of (pageHead pc) and (pageBody pc) is HtmlUrl (Route App)

2

There are 2 best solutions below

0
On

Here's how I got this to work. There were actually two different problems I was facing:

  • Doing IO inside a Widget
  • Referencing a Widget in the default-layout-wrapper hamletFile.

Here's the solution for doing IO inside the widget:

nav :: Widget
nav = do
  globalStat <- liftIO $ A2.getGlobalStat NWT.ariaRPCUrl
  $(whamletFile "templates/navbar.hamlet")

Note: The type signature nav :: Widget seems to be necessary, else the type inference engine might get confused, and infer a very different type for the liftIO operation (which was originally happening with me).

For the second problem, I couldn't really find a solution for referencing a Widget within the default-layout-wrapper hamletFile. By the time this particular hamletFile is being rendered, the Widget monad has been converted to a PageContent type and now it needs an Html url type to be able to render it in conjunction with the withUrlRenderer function. Basically, I wasn't able to get Widget and PageContent to compose. However, the following approach gave me my desired result, in a different way:

default-layout.hamlet: Added the invocation to nav widget in this file. Moved some elements from default-layout-wrapper to this file:

<div .container>
  <header>
    ^{nav}
  <div #main role="main">
    $maybe msg <- mmsg
      <div #message>#{msg}
    ^{widget}

default-layout-wrapper.hamlet: Moved a few HTML element from this file to default-layout:

<!-- SNIP -->
  <body>
    <div class="container">
      ^{pageBody pc}
<!-- SNIP -->
6
On

Have a look at the answer to this SO question. Basically you can't perform IO in a template.

Also note that the type of defaultLayout is GHandler ... and GHandler is an instance of MonadIO, so you can perform IO in defaultLayout by using liftIO.

I would try:

defaultLayout = do
  ...
  globalStat <- liftIO $ handlerToWidget $ A2.getGlobalStat NWT.ariaRPCUrl
  uploadSpeed <- liftIO $ A2.glUploadSpeed globalStat
  downloadSpeed <- liftIO $ A2.glDownloadSpeed globalStat
  ...
  withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")

And in templates/default-layout-wrapper.hamlet:

...
^{nav uploadSpeed downloadSpeed}
...

And nav becomes something like:

nav uploadSpeed downloadSpeed =   $(whamletFile "templates/navbar.hamlet)

So the basic ideas are:

  • Do all of your IO in defaultLayout using liftIO
  • Pass data needed by sub-templates as function arguments

Update

To emulate this example in the Yesod book you need to write navbar like this:

navbar :: Widget
navbar = do
    globalStat <- liftIO A2.getGlobalStat NWT.ariaRPCUrl
    downloadSpeed <- liftIO A2.glDownloadSpeed globalStat
    uploadSpeed <- liftIO A.glUploadSpeed
    $(whamletFile "templates/navbar.hamlet)

And in navbar.whamlet refer to #{uploadSpeed} and #{downloadSpeed}.

You can't do IO in a whamlet file. Moreover, your A2 functions are IO-actions, but handlerToWidget requires a HandlerT action so you need to use liftIO to convert those calls.

Update 2

See http://lpaste.net/169497 for a working example which does IO in a Widget.