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

Dmitry Kurochkin dmitry.kurochkin at gmail.com
Sun Feb 13 15:23:37 CET 2011


On Sun, 13 Feb 2011 15:35:48 +0200, Michael Snoyman <michael at snoyman.com> wrote:
> 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.
> 

Glad to hear :)

> > 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?

Application may have many widgets. They can be pretty complex and big.
Each widget is not related to another. I like putting separate things to
separate modules, just like handlers for different resources.

> 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.
> 

I guess my Haskell and/or Yesod knowlege is not enough to solve this.

> > 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 will send you code later.

> > 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.
> 

I see. Thanks for the explanation.

> > 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.
> 

Yes, widgetToPageContent is used to convert the widget from handler and
produces a set of pieces for page generation (pc). If I use it for a
menu widget, I will get another PageContent (pc1). Now I need to take
body from pc1, and merge other pieces of pc1 with pc. E.g. menu widget
can produce javascript and CSS which needs to be merged with the main
PageContent. I did not find an existing function to do this. Did I miss
it?

Besides, even if there is such function, it would not allow for
convenient usage of widgets in default layout IMO. defaultLayout code
would look like:

    pc <- widgetToPageContent ... -- for handler widget
    pc1 <- widgetToPageContent ... -- for menu widget
    ... -- code to merge parts of pc1 with pc
    pc2 <- widgetToPageContent ... -- for another widget
    ... -- code to merge parts of pc2 with pc
    -- and so on for every widget

And in default layout you use ^{pageBody pc1}, ^{pageBody pc2}, etc.

The workaround described above is more convenient: Just use
^{menuWidget} and merging of CSS and javascript is done implicitly. But
this comes at a cost of layout splitting. I wonder if it can be made
transparent for users.

Sorry if I did not understand what you propose. Would appreciate an
example.

> [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 confess I did not look at subsites yet (AFAIK that part of the book is
not uptodate yet?). But I see your point: Since defaultLayout works with
subsites, widgets should be general. Makes sense.

> > 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 ;).
> 

See above why I think it is better to put widgets to distinct modules.
As for workarounds, I guess I am just not such a great haskeller :)

I think there should be a blessed way to put widgets in a separate
modules if you want to. Similar to handlers: There is Controller and you
can put everything there, but there are Handler.* modules as well for
those who likes it separate. This way poor haskellers like me will not
have invent workarounds :)

Regards,
  Dmitry

> Michael



More information about the web-devel mailing list