Beefy Boxes and Bandwidth Generously Provided by pair Networks
Welcome to the Monastery
 
PerlMonks  

Output should have multiple segments

by John M. Dlugosz (Monsignor)
on Jul 15, 2008 at 22:57 UTC ( [id://697821]=perlmeditation: print w/replies, xml ) Need Help??

I just got started with HTML::Mason, and upon factoring my template into smaller reusable components, I ran into something: how can the usage/existance of one aggregated component affect what went into the head section or attributes of the <body> tag? It generates output by expanding templates from top to bottom.

It reminds me of what I did a few weeks ago. I wrote some Perl code to generate SVG files for writing paper stationary (planners, note paper, etc.) and made individually coded components, much like Mason's concept. Adding a calendar to a page, for example, would generate a block of SVG as XML text to be appended to the "main" output; like the body of an HTML page. But it also adds its required stuff to a list of CSS styles and a list of defs, which go in the XML before the main content.

Those are hashes, and each component uses its own keys by prefixing the component name. So the calendar can add its styles, and if it already did so because I have more than one calendar on the page (e.g. I have 3 months along the bottom) that is redundant as it adds the same keys. Components can also inspect and modify what's already been added.

Likewise for the defs, which are individual <def> elements with everything in them that will be ref'ed by the main code. Adding the same one is redundant.

When I'm done, it generates the prelude stuff, and where the CSS goes, it combines all the items in the hash and generates one CSS style sheet embedded in the document. When it gets to the <defs> section, it lists all the collected <def> elements, in one place with no duplicates. Finally it appends the "main" code, and then the postlude.

Now with HTML::Mason, I want a component to add its CSS to that of the overall page, and likewise for script elements. The script can go in the middle of the body rather than having to be at the top, so that is less of an issue, but what if a component is used more than once on a page? I can't emit the script more than once! And putting the correct initialization in the body tag's onload attribute is a problem.

It's a different problem, but so much the same. So it occurs to me that in general as a design principle you may want to segment your output into multiple streams, and concatenate them at the end. Having a single stream them is just a special case.

Remember that when planning to generate text files, something you might do in Perl from time to time.

—John

Replies are listed 'Best First'.
Re: Output should have multiple segments
by moritz (Cardinal) on Jul 15, 2008 at 23:09 UTC
    IMHO the problems you describe are a symptom of wanting to do too much in the output layer (or template system, or "view" in the MVC model). Back in the old days I learned from a book that most programs are written in the "input - calculations - output" way, meaning first obtain all data, then do all the calculations, and only start to print them when you're all done. If you adhere to that, you don't have the problems you describe.

    Mason seems to encourage doing very much work at the template level (or at least the tutorial I once read did), which is why I don't use it.

    I also dislike it that in many catalyst tutorials some of the database queries are triggered in from the template.

    I can't (yet?) really put my finger on it why it seems wrong, but it just plain feels wrong, and is the reason why I still stick to HTML::Template::Compiled, although it's not as nifty as, for example, Template::Toolkit.

    Update: I thought more about why it feels wrong, and one thing that came to my mind is error handling. In Catalyst basically the controller does its work, creates a data structure (in the tutorials mostly DBIx::Class objects), and then hands off all the work to the controller.

    But if the template accesses methods from these objects, they can trigger a DB query, which in turn could very well throw an error. But at this point it's too late for the controller to catch it in any meaningful way, since it already made all decisions it has to make.

    The only remaining options are

    • Catch DB errors in the template system - you surely don't want that (duplicate code, separation of concerns)
    • Have an all-purpose error handler. That's probably not specific enough in many cases.
    • Catch the error, clear the stash (and flash), and re-run the controller with an error flag set

    All of them don't sound very convincing to me.

      I agree with moritz don't use Mason. I am working at a company now that uses it and being able to insert Perl code anywhere anytime just makes for a horrible environment. It becomes too easy to lazy with design and just stick something in the template because it takes 5 minutes and a well designed, reusable tool will take 20.

      I refer to it as duct tape programming. Think of it this way, "I can fix correctly now, it will take me a while but I will not have to deal with it again" vs "I can put it together with duct tape and hope the next person to deal with it will not be me!"

        Mason has a lot going for it. First of all, it uses Perl, not some new dinky language. Its architecture is sound, it is mature, and there are excellent documents on using it.

        When I first looked at ASP a good many years ago, that was my reaction too: mixing the code and data is bad, we should be separating it. It's awesome for just dropping in a value in the middle of a paragraph of HTML, and is really no different from how we interpolate in string literals.

        But I see that Mason has a few features so you are not forced to mix it to badly. If you decide to use "interpolation" in the same way as with Perl strings, and stick with pre-computed values in variables, and put real code elsewhere: The init section is solid Perl and computes all the needed values.

        For repeating lines, you can put a Perl loop around the HTML. In a specialized template notation that is all you could do, but here you could use any Perl any time. But don't. Stick to foreach loops on a % line.

        You can also call Mason components from normal Perl expressions, and are not forced to shift gears back and forth to do some computing, emit a component, and compute some more. You could even simply print ordinary Perl strings to the emitted output.

        —John

      I think a template model like Mason's could benefit from having multiple passes. That is, don't only have an output pass. First, just instantiate all the components that are being laid out or aggregated. Then do a compute pass on each one, where they all know about the complete page. Then do an output pass, which should gather and format results but not change anything.

        I think a template model like Mason's could benefit from having multiple passes.

        That's exactly where we differ. I think that a template system should only do the final pass - convert data structures to text.

        Try to think about it this way: if your application has two different views (for example a Template::Toolkit one that produces HTML, and a Mason one that produces XML), how much many of these passes would be duplicate in these two views?

        Let's say you have a web application that can produce both HTML pages and PDF files. In both cases you have a table of contents, and for each section you add to the output you also add a line to the table of contents.

        If you do the logic for adding that TOC line (which essentially requires either two passes or two output segments) in the template, you duplicate your effort in the two different template systems.

        OTOH if your perl code first assembles a data structure that represents the TOC, both of your templates just have to do their job: convert a data structure into text output. No more duplicate logic in there.

        You should probably bring that up on the mason devel list. It would help (me at least, fwiw) also if you elaborated on the instantiate and compute phases.

        You might be able to do what you're talking about already with Mason's so-called object-oriented techniques. For example, for altering the attributes of the <body> tag you might use a method or attribute, which can be overridden by subcomponents.

        That's how Mason works after all, but using a single output buffer. Ok, that's shallow, I hope the fellow masonites will forgive me for this one ;-)

        You should be able to quickly build yourself such a "multi-pass" system using your own conventions, following Mason's request cycle (briefly: gather output by running orderly each component in the inheritance chain)

        For the sake of example, here's a possible approach for such an autohandler:

        <%doc> # document everything (conventions, hacks etc) :P~ </%doc> <%once> # load modules at this step only if you're not able to load them "a +t startup" # (e.g. preloading them under a mod_perl enabled HTTPd) use My::App; </%once> <%init> # set up runtime/dynamic default configuration parameters $m->comp( '_conf', %ARGS ); # ^ these should all be "notes" ($m->notes) # a <%shared> replacement (yeah, that's me, # I always avoided it for not doing what I meant) ;-/ if ( $m->request_comp->method_exists('_init') ) { $m->request_comp->call_method('_init', %ARGS); } # fetch page content $m->notes( 'page_content' => $m->scomp( $m->fetch_next, %ARGS ) ); # ^ remember that you have to *handle* the "exceptions" # <%cleanup> replacement if ( $m->request_comp->method_exists('_cleanup') ) { $m->request_comp->call_method( '_cleanup', %ARGS ); } # render output return $m->comp( '_render', %ARGS ); </%init>
        Also notice that you can optimize this by moving out more app. logic into your My::App modules ;-)

        --
        altblue.

Re: Output should have multiple segments
by Arunbear (Prior) on Jul 16, 2008 at 09:19 UTC
    Try inheritance, e.g. in your autohandler
    <html> <head> <style> /* default styling here */ <& SELF:.add_style &> </style> </head> <body> ... <% $m->call_next %> ... </body> </html> <%method .add_style> </%method>
    Then in another component (say mylist.html) that inherits from the autohandler
    ... <%method .add_style> /* custom styling here */ </%method>
    This way when mylist.html is requested, the custom css will get added within the html header.

    Alternatively you could build the a css file dynamically and let Mason handle it. E.g. in each component where you want to add CSS to that of the overall page, you could write the css to a database table, and then when Mason serves the css page, it could query the database for the css and generate an appropriate css page.

    I'll also highly recommend the Mason book if you haven't been reading it.

      The inheritance is not a complete solution, because the use of a contained component doesn't have any effect. You would have to manually override the method in the same component that contains components, and add the stuff to it.

      The database idea is interesting.

      You can avoid having to define dummy methods in your autohandlers (i.e. .add_style) and reduce output cluttering (i.e. empty style blocks) by testing if such "plugs" were defined:
      % if ( $m->request_comp->method_exists('.add_style') ) { <style><& SELF:.add_style &></style> % }

      --
      altblue.

Re: Output should have multiple segments
by chakram88 (Pilgrim) on Jul 16, 2008 at 16:32 UTC
    Arunbear is on track with the approach that I use for getting specific items into the <head> section of pages generated from mason. And I use a similar approach for getting page-specific attributes into the <body> tag too:

    autohandler:

    <%perl> # does the requested comp have anything special to tell the body tag? % my $BodyAttr = $m->comp('REQUEST:body_attr') % if $m->request_comp->method_exists('body_attr'); </%perl> %# THE OUTPUT <html> <head> ... </head> </html> <& 'SELF:body_tag', attrs => $BodyAttr &> % $m->call next; </body> </html> <%method body_tag> <%args> $attrs => undef </%args> <%perl> my $att_str; if ($attrs && %$attrs) { for (keys %$attrs) { $att_str .= sprintf qq[ %s="%s"], $_, $attrs->{$_}; } } </%perl> <body<% $att_str %>> </%method>
    Other components provide the body tag attributes as a hash ref
    <%method body_attr> % my $bd_attr = { class => 'support', onload => q[myInit('foo','bar')] +}; % return $bd_attr; </%method>
    OUTPUTS:
    <html> <head> ... </head> </html> <body class="support" onload="myInit('foo','bar')"> </body> </html>

    Note: Quickly hacked together example of the approach, untested.

    Note2: OP asked about getting init code into the onload attribute of the body tag. I actually use a different approach, but that's a different discussion that is not perl at all.

Re: Output should have multiple segments
by autarch (Hermit) on Jul 17, 2008 at 11:51 UTC

    I've done projects where I've dealt with this issue, and came up with solutions similar to what others have proposed.

    However, ultimately I realized that this was basically wrong, at least for CSS and JS. For both of these, you're much better off serving a single cacheable file, rather than only including the files you need. This plays much better with typical web clients.

    As to the general problem, I haven't run into it too often, and usually when I do it's not too complicated. The most common example is wanting to generate the <title> tag in a top-level autohandler and getting the title from the requested component. This is easily done with Mason's method system.

Re: Output should have multiple segments
by Arunbear (Prior) on Jul 16, 2008 at 20:28 UTC

    Here is another way to put CSS into the main document from a component. This is based on the filter mechanism so will work even if the component isn't a top-level one

    In autohandler
    <html> <head> <style> /* default styling here */ __MYCSS__ </style> </head> <body> ... <% $m->call_next %> ... </body> </html> <%filter> s/__MYCSS__/$m->notes('mycss')/e </%filter>
    In the called component
    ... <%init> ... $m->notes(mycss => 'your css here'); </%init>
        $m->notes is very useful.

        That said, I dislike & mistrust using Mason's filter stuff, though there are times & ways in which I think it is the most expedient option.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://697821]
Approved by moritz
Front-paged by grinder
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others taking refuge in the Monastery: (3)
As of 2024-07-16 02:38 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?
    erzuuli‥ 🛈The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.