
On Sun, 13 Feb 2011 16:54:00 +0200, Michael Snoyman
On Sun, Feb 13, 2011 at 4:23 PM, Dmitry Kurochkin
wrote: On Sun, 13 Feb 2011 15:35:48 +0200, Michael Snoyman
wrote: On Sun, Feb 13, 2011 at 12:51 PM, Dmitry Kurochkin
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/Yeso...
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