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

Michael Snoyman michael at snoyman.com
Sun Feb 13 15:54:00 CET 2011


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.

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

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

Michael



More information about the web-devel mailing list