2
\$\begingroup\$

I have 11 data items and would like to print them as a group. Each group will have header and at the end of the group I need to print continue label.

For that, I use this Underscore template and multiple conditions. I'd like to make this more compact.

<script type="text/template" id="header">
<div id="recourseParameter">
    <h1> Parameter : <%= parameterCode %>  </h1>
    <table>
        <thead>
            <tr>
                <th colspan=5>header</th>
            </tr>
        </thead>
        <tbody>
</script>
<script id="closer" type="text/template">
        </tbody>
    </table>
</script>
<script id="footer" type="text/template">
    </tbody>
    </table>
        <span>All are done</span>
</script>

<script type="text/template" id="recourseParameterTemp">

    <% var limit = 3, dLength = recourseParameter.length %>
    <% var done = false %>
    <% _.each(recourseParameter, function (item, index) { %>

            <% if (index % limit == 0 )  { %>
                <%= _.template($('#header').html())({"parameterCode":parameterCode}) %>
            <% } %>

            <% if(item.description) { %>
                <tr>
                    <td colspan=5><%= item.description %></td>
                </tr>
            <% } %>

            <% if ((index+1) % limit == 0 )  { %>
                    <%= _.template($('#closer').html())() %>
                        <span>Continue ... </span> 
                    </div>
            <% }  %>

    <% }) %>

    <%= _.template($('#footer').html())() %>

</script>
\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$

If I understand your code correctly, it looks like you want to render tables with 3 records each, with the #header template for the top of each table and the #closer template for the bottom of each table, except for the last one, which should end with #footer instead of #closer.

First, the little things:

  1. I think you have some unused variables in your code: It looks like dLength and done are never used.
  2. limit is a magic number and seems out of place in your template. If I were you I would pass that number to the template function along with your other data.
  3. You render <div id="resourceParameter"> several times, but element IDs must be unique in a document. We shouldn't re-use an ID.
  4. For the last table, the </div> is never rendered, so the above div is never closed! Fixing this is discussed below.
  5. You're compiling templates inside of templates, which means that every time the outer template is rendered, you recompile the inner template, which is unnecessary and negates most of Underscore.js' rendering speed advantage. Fixing this is discussed toward the end.

Deal with groups of items, not index math

Now, on to the meat of the question. I think the biggest code smell here is the modulo arithmetic, i.e. <% if(index % limit == 0 ) { %> et al. This works, but it's hard to read. Instead, we can make the code match our intent: We want tables with 3 records each, so let our code deal with the data in groups of 3. This saves us from having to do arithmetic on their indexes—or worring about their indexes at all. Changing only the #recourseParameterTemp template, we have this:

<script type="text/template" id="recourseParameterTemp">
    <% var group %>
    <% while(group = recourseParameter.splice(0, limit), group.length) { %>
        <%= _.template($('#header').html())({ "parameterCode": parameterCode }) %>

        <% _.each(group, function (item, index) { %>
            <% if(item.description) { %>
                <tr>
                    <td colspan=5><%= item.description %></td>
                </tr>
            <% } %>

            <% if(recourseParameter.length) { %>
                    <%= _.template($('#closer').html())() %>
                    <span>Continue ... </span> 
                </div>
            <% }  %>
        <% }) %>
    <% } %>
    <%= _.template($('#footer').html())() %>
</script>

Here's a fiddle of it in action.

I think further improvements are possible, however. For one thing, why is <div id="recourseParameter"> inside a sub-template, but </div> is inside the main template? Not only is this an odd inconsistency, it actually introduced the missing </div> bug mentioned above. We can fix this by moving <span>Continue...</span> and </div> into the #closer template, and also adding </div> to the #footer template (see this fiddle), but the bug points to a more general code smell: Templates with no closing tags, or no opening tags. If a template closes all of the tags it opens, it's much easier to notice problems like our missing </div>.

The single responsibility principle

We usually talk about the single responsibility principle as it relates to object-oriented programming, but it's very useful here, too: Our templates are meant to isolate blocks of code that are related, and a great template has a single responsibility. Our #recourseParameterTemp template has a bunch of responsibilities—it iterates through the data, it decides whether or not to render each <tr>, and it decides what kind of footer render. That's too much! Using this principle, a number of possible improvements reveal themselves.

In my changes, I introduced an additional loop, i.e. I put a while around the _.each. This makes it a little harder to read, but also reminds us that it's often good to extract the inside of a loop into a sub-template that has a single responsibility. Extracting the inside of the while loop into a template called #groupTmpl (along with the #closer/#footer changes mentioned above) gives us this:

<script type="text/template" id="groupTmpl">
    <%= _.template($('#header').html())({ parameterCode: parameterCode }) %>

    <% _.each(group, function (item, index) { %>
        <% if(item.description) { %>
            <tr><td colspan=5><%= item.description %></td></tr>
        <% } %>
    <% }) %>
    <% if(recourseParameter.length) { %>
        <%= _.template($('#closer').html())() %>
    <% } else { %>
        <%= _.template($('#footer').html())() %>
    <% } %>
</script>

<script type="text/template" id="recourseParameterTmpl">
    <% var group %>
    <% while(group = recourseParameter.splice(0, limit), group.length) { %>
        <%= _.template( $('#groupTmpl').html() )( {
                recourseParameter: recourseParameter,
                parameterCode: parameterCode,
                group: group } ) %>
    <% } %>
</script>

Our intent has become clearer. We see that #recourseParameterTmpl's single responsibility is to render a sub-template (#groupTmpl) once for each group, and that's all it does. This makes it easier to see the intent of the rest of the code, now in #groupTmpl. It also makes it easier to see that #groupTmpl still has multiple responsibilities: It renders the #header sub-template, it loops through each item in group, and it decides what kind of footer to render.

(You probably also noticed that I changed #recourseParamaterTemp to #recourseParameterTmpl; I did this just because "temporary" is frequently shortened to "temp" in programming; "tmpl" is less ambiguous.)

One footer template

We could extract the inside of the _.each loop into a sub-template, but I think we can agree that that's probably overkill. The main thing we can improve here is that #groupTmpl is deciding what kind of footer to render, but that shouldn't be its responsibility. Instead, we should just render one template, and let that template decide what the footer should look like. Looking at our existing #closer and #footer templates, we can see that they're almost identical, except for the contents of <span>. It makes sense to consolidate these into one template. Removing #closer entirely, #footer and #groupTmpl now look like this (here's the fiddle):

<script type="text/template" id="footer">
            </tbody>
        </table>
        <span>
            <%= recourseParameter.length ? "Continue..." : "All are done" %>
        </span>
    </div>
</script>

<script type="text/template" id="groupTmpl">
    <%= _.template($('#header').html())({ parameterCode: parameterCode }) %>

    <% _.each(group, function (item, index) { %>
        <% if(item.description) { %>
            <tr><td colspan=5><%= item.description %></td></tr>
        <% } %>
    <% }) %>

    <%= _.template($('#footer').html())({ recourseParameter: recourseParameter }) %>
</script>

Now #groupTmpl's intent is very clear, and we don't have any duplicated footer code.

Self-contained templates close their own tags

This is looking pretty good, but we still have the problem I mentioned above: #header opens tags that it doesn't close, and #footer closes tags that it doesn't open.

An alternative is to combine those templates into a single #wrapper template. Since Underscore.js templates don't have good support for "nested" templates, this gets a little hairy, unless we do one simple thing—which we ought to do anyway: Initialize the sub-template functions outside of the template itself. Instead of doing this:

<%= _.template($('#groupTmpl').html())( ... ) %>

We should do this, in our JavaScript:

var groupTmpl = _.template( $('#groupTmpl').html() );

...for each of our templates, and then pass the compiled template functions as locals when we use a template, which will allow us to do this in a template:

<%= groupTmpl( ... ) %>

In addition to looking cleaner, this avoids recompiling the sub-template every time the (outer) template is compiled, as mentioned above.

Along with this, instead of passing around a bunch of variables (recourseParameter, parameterCode, etc.) among our templates, it's probably a good idea to use the variable option to _.template. That is, when we call var tmpl = _.template(html, { variable: 'locals' }), then render it with tmpl({ foo: "bar" }), foo will be available as locals.foo inside the template, instead of just foo. While this seems less convenient, it lets us just pass locals to sub-templates instead of specifying the whole object. Instead of passing { variable: 'locals' } every time we compile a template, though, we can set it globally using _.templateSettings.variable = 'locals'.

Finally...

...we end up with templates that look like this:

<script type="text/template" id="wrapperTmpl">
    <div id="recourseParameter">
        <h1>Parameter : <%= locals.parameterCode %></h1>
        <table>
            <thead>
                <tr><th colspan=5>header</th></tr>
            </thead>
            <tbody>
                <%= locals.bodyTmpl(locals) %>
            </tbody>
        </table>
        <span>
            <%= locals.recourseParameter.length ? "Continue..." : "All are done" %>
        </span>
    </div>
</script>

<script type="text/template" id="groupTmpl">
    <% _.each(locals.group, function (item, index) { %>
        <% if(item.description) { %>
            <tr><td colspan=5><%= item.description %></td></tr>
        <% } %>
    <% }) %>
</script>

<script type="text/template" id="recourseParameterTmpl">
    <% locals = _.defaults({ bodyTmpl: locals.groupTmpl }, locals) %>
    <% while(locals.group = locals.recourseParameter.splice(0, locals.limit), locals.group.length) { %>
        <%= locals.wrapperTmpl( locals ) %>
    <% } %>
</script>

And JavaScript that looks like this:

_.templateSettings.variable = "locals";

var tmpl = _.template( $('#recourseParameterTmpl').html() ),
    locals = {
        limit: 3,
        parameterCode: "foo",
        recourseParameter: [
            { description: "Foo 1" },
            // ...
        ]
    },
    renderedTmpl
;

locals.wrapperTmpl = _.template( $('#wrapperTmpl').html() );
locals.groupTmpl   = _.template( $('#groupTmpl').html() );
// You could also automate the above with jQuery, e.g.:
//
//     $("[type='text/template']").each(function() {
//       var el = $(this), id = el.attr('id');
//       locals[id] = _.template( el.html() );
//     });

renderedTmpl = tmpl(locals)

$('body').append(renderedTmpl)

Here's the final fiddle. I hope that's helpful!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.