[web-devel] [Yesod] widgets in default layout

Michael Snoyman michael at snoyman.com
Sun Feb 13 14:35:48 CET 2011


On Sun, Feb 13, 2011 at 12:51 PM, Dmitry Kurochkin
<dmitry.kurochkin at gmail.com> wrote:
> Hello.
>
> I am trying to make a menu widget for a site. It would render list of
> menu items and mark the active one. I started with a new Menu module
> that exports (mainMenu :: Widget ()) function. The function gets the
> current route, iterates over a list of (item title, item route) list and
> constructs the menu. Now I can import Menu in a handler module and use
> ^{mainMenu} in hamlet template.
>
> Next I tried to put the menu widget to default layout - menu should be
> on every page so default layout is where it belongs. Rest of the email
> describes problems I got.

For the record, I think this is a very good approach. Let's address
specific issues below.

> 1. Module import loop.
>
> I need to import the widget module in the module which defines
> defaultLayout, i.e. the main application module. But the widget module
> uses the main application module to have routes and probably other
> staff, hence import loop. I tried to separate foundation and route
> declaration from Yesod instance declaration, so that the widget module
> could import just routes declaration and Yesod instance could import the
> widget module. Turns out that does not work:

The simplest solution is to just define the mainMenu widget in the
same file as the Yesod instance. Is there a reason to avoid this? It's
definitely possible to split up the Yesod instance and the call to
mkYesodData, but doing so requires an orphan instance. It's not a
particularly *dangerous* orphan instance, but even so I think avoiding
orphans is a good goal.

> Attempting to interpret your app...
> Compile failed:
>
> Menu.hs:20:23:
>    Couldn't match expected type `Route m'
>           against inferred type `TestAppRoute'
>      NB: `Route' is a type function, and may not be injective
>    In the first argument of `\ u[a7Kv]
>                                  -> hamlet-0.7.0.2:Text.Hamlet.Quasi.urlToHamletMonad
>                                       u[a7Kv] []', namely
>        `AboutR'
>    In a stmt of a 'do' expression:
>        \ u[a7Kv]
>            -> hamlet-0.7.0.2:Text.Hamlet.Quasi.urlToHamletMonad u[a7Kv] []
>          AboutR
>    In the first argument of `hamlet-0.7.0.2:Text.Hamlet.Quasi.toHamletValue', namely
>        `do { (hamlet-0.7.0.2:Text.Hamlet.Quasi.htmlToHamletMonad
>             . preEscapedString)
>                "<div id=\"menu\"><div class=\"left\"><div class=\"right\"></div><div class=\"container\"><a href=\"";
>              \ u[a7Kv]
>                  -> hamlet-0.7.0.2:Text.Hamlet.Quasi.urlToHamletMonad u[a7Kv] []
>                AboutR;
>              (hamlet-0.7.0.2:Text.Hamlet.Quasi.htmlToHamletMonad
>             . preEscapedString)
>                "\">itemTitle item</a></div></div></div>" }'
>
> I do not understand details, but it is clear that the widget needs the
> Yesod instance. So I have to put the widget in the main application
> module. This does not look good considering that application may have
> many widgets.

Without seeing your code, I can't be certain what's going on. However,
that doesn't look like a problem about missing a Yesod instance. It
actually looks like the kind of problem that could be solved with more
explicit type signatures.

> I have also stumbled upon a bug in devel server: It tries to recompile
> the source in a loop, without changing source of course. Annoying but
> not a critical issue.

It's not a bug, it's the intended behavior. Let's say that you write
module A that does not depend on any modules and you start up
devel-server. Everything compiles and runs fine, and devel-server
begins monitoring A.hs for file changes. Meanwhile, you write module B
(which contains a bug), and then add "import B" to the import list for
A.

Now, devel-server is going to try and recompile module A, but will
fail since B is invalid. At this point, devel-server will not know
that A depends on B. If devel-server simply waits for there to be a
change to its monitored files, it will never notice that you've
corrected the bug in module B: it will simply idle until someone makes
a change to module A. That's why it continuously retries to compile
the whole thing once there's an error.

> 2. Widget does not work in default layout.
>
> I guess this is a known and expected behavior. My feeling is that
> hamletToRepHtml can not embed widgets because it may be too late to add
> cassius and julius. As a workaround I split default layout into outer
> and inner layout. Outer layout renders just HTML <head> and <body>.
> While outer layout is rendered as a widget that embeds the actual page
> contents. Since outer layout is rendered as a widget, it may embed other
> widgets like menu.
>
> I imagine that hamletToRepHtml could render all embedded widgets before
> the main body. Though, it may be difficult to implement, have
> performance or other issues. Anyway, I think it is not uncommon to
> include a widget in default layout. So Yesod should provide an easy way
> to do it.

You should try looking at the scaffolded site: the function you want
to use is widgetToPageContent[1]. It converts a complete Widget into
the individual pieces that you need.

[1] http://hackage.haskell.org/packages/archive/yesod-core/0.7.0.1/doc/html/Yesod-Core.html#v:widgetToPageContent

> The last issue is that (mainMenu :: Widget ()) does not work, I had to
> change it to (GWidget sub TestApp ()). Again I do not know details, but
> this was unexpected to me. Perhaps the Widget type synonim should be
> changed?

OK, now I might have a better understanding of the error message you
were referencing above. Take a look at the type signatures for the
functions you are calling: defaultLayout is a function that can be
called from either a master site handler or a subsite handler. For
example, if I wrote a blog subsite, that subsite should be able to use
the same styles as the master site. Now:

    type Widget = GWidget TestApp TestApp

in your application, which means that it only works for a situation
for where the subsite is the same as the master site. This is the case
with most of your handler functions, which is why the scaffolded site
provides this convenience synonym. However, when you want to write a
function which is generic enough to work for arbitrary subsites, you
can't use this convenience synonym.

> I would appreciate advices on how to solve the above problems. Perhaps I
> am just missing something and there is a proper way to do what I want.
> For now I will avoid using widgets in default layout and move part of
> layout to individual handler templates, primarily because I find it too
> ugly to define widgets in the main application module.

I suppose that's a matter of personal preference, but to me it makes
perfect sense to declare a mainMenu function in the same module that
defines defaultLayout. You can find plenty of ways around this.
Introducing orphans instances is one. A particularly ugly approach
could be to include the mainMenu widget as part of your foundation
datatype and have defaultLayout refer to that, though I in no way
recommend such a course of action. I'm just saying it's available for
masochists ;).

Michael



More information about the web-devel mailing list