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

Dmitry Kurochkin dmitry.kurochkin at gmail.com
Sun Feb 13 16:28:42 CET 2011


On Sun, 13 Feb 2011 16:54:00 +0200, Michael Snoyman <michael at snoyman.com> wrote:
> On Sun, Feb 13, 2011 at 4:23 PM, Dmitry Kurochkin
> <dmitry.kurochkin at gmail.com> wrote:
> > 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.
> 
> I'm referring specifically to widgets called by defaultLayout: if the
> widget is not needed by defaultLayout, there's no cyclical dependency
> introduced.
> 

Still the argument remains. I want to have all menu-related code in a
separate module.

> [snip]
> 
> >> > 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?
> 
> Just combine the two widgets and call widgetToPageContent once:
> 
>     defaultLayout widget = do
>         pc <- widgetToPageContents $ do
>             menuWidget
>             widget
>         hamletToRepHtml ...
> 
> You can see an example of this in the Yesod docs site[1].
> 

I thought this would result in menuWidget placed directly before the
main widget body, right? In many cases simple concatenation is not
enough.

> [1] https://github.com/snoyberg/yesoddocs/blob/master/site/YesodDocs.hs#L89
> 
> > 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.
> 
> I think I didn't understand you the first time around, my apologies. I
> think the example from YesodDocs should clear things up.
> 
> >> [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.
> 
> Also something on my (ever growing) TODO list is to describe subsites
> in more detail.
> 
> >> > 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 :)
> 
> Just to confirm: are you talking for general widgets, or just widgets
> to be called from defaultLayout? As I mention above, the former can
> easily be put in separate modules, the latter would require more work.
> 

I am talking about defaultLayout widgets. In general, reasons for
putting widgets into separate modules does not depend on whether they
are used in defaultLayout. Though, I agree that the fact that only
defaultLayout widgets must be put into the Yesod instance module
somewhat improves the situation.

IMO from user point of view it does not matter much if widget is used in
handler or in defaultLayout. In most cases, at least. I just create a
menu widget and want be able to use it anywhere (even both in handlers
and defaultLayout). E.g. in my case the only difference between handler
and defaulLayout widget is the type signature, implementation does not
change.

Regards,
  Dmitry

> Michael



More information about the web-devel mailing list