underscore.js nested templates

2020-05-14 01:51发布

问题:

Is it possible to somehow take a DOM element from a underscore template and use it as another template?

The idea is that my app needs to render a document that contains a loop with items and a summary. I need to occasionaly re-render only the summary or a few items, so I cannot just re-render the whole document.

However, I would like to keep it simple for the app users to create their own templates for the document and I think that keeping everything in one file for a document would make it easier.

I'm trying to use something like this:

<script type="text/template" id="document-template">
    <div id="document">
        <h1><%= name %></h1>
        <ul class="items">
            <% _.each(items, function(item) { %> 
                <li><%= item %></li>
            <% }); %>
        </ul>
        <div id="summary">
            <p>Total items: <%= totalitems %></p>
        </div>
    </div>
</script>

Now, I can easily do this var documentTemplate = _.template($('#document-template').html()); to turn this into a document template, but I would like to turn the summary part into a template and a list item into a template as well.

Can I do something like this:

var summaryTemplate = _.template($('#document-template #summary').html());
var itemTemplate = _.template($('#document-template .items li').html());

PS. Actually I am loading the template from an external file using jQuery's $.get. This way I will get the document-template in one big string. From there, I can do just documentTemplate = _.template(loadedString);.

Now, if I could just extract the #summary element from the string, it should work. But when I try to convert the string to DOM element ( var domElement = $(loadedString)) (so I could do this: summaryTemplate = _.template($('#summary',domElement).html());, it won't work, because underscore won't recognize the <%= %> tags anymore.

回答1:

You can pass the nested template as a variable in the template assignments to the main template, e.g.:

HTML:

<script type="text/template" id="sub_template">
  <article>
    <h1>id: <%= id %><h1>
  </article>
</script>

<script type="text/template" id="main_template">
  <% for (var i = 0; i < num; i++) { %>
    <%= renderSub({id:i}) %>
  <% } %>
</script>

JS:

 var renderSub = _.template( $('#sub_template').remove().text() ),
     renderMain = _.template( $('#main_template').remove().text() );

  renderMain({num:5, renderSub:renderSub});


回答2:

Playground

/////// TEMPLATES //////

    var mainTemplate = "<ul> \
                          <% _.each(items, function(item) { %> \
                               <%= listItem({item:item}) %> \
                          <% }); %> \
                        <ul>";

    var subTemplate = '<li><%=item %></li>';


/////// MODEL (our data) //////

    var model = {
        items : [1,2,3,4,5]
    }


/////// COMPILE //////

    // add the subTemplate to the model data, to be passed to the mainTemplate
    model.listItem = _.template(subTemplate);

    // Render main template to the DOM
    document.body.innerHTML = _.template(mainTemplate, model);


回答3:

Instead of loading it all at once as one template and then trying to load a portion of it again later, separate them as multiple templates.

Have your main document-template and a summary-template. The initial load pulls in the document-template and then pulls into the summary-template and inserts it into the compiled DOM from the first template. Then you can reload the second template at any time. See the following steps:

1) Load initial template, shown below:

<script type="text/template" id="document-template">
    <div id="document">
        <h1><%= name %></h1>
        <ul class="items">
            <% _.each(items, function(item) { %> 
                <li><%= item %></li>
            <% }); %>
        </ul>
    </div>
</script>

2) Once this is compiled into the DOM, load the second template via underscore, shown below:

<script type="text/template" id="summary-template">
    <div id="summary">
        <p>Total items: <%= totalitems %></p>
    </div>
</script>

3) Call $("#document").append(summaryTemplate), or whatever variable is holding the compiled template.

4) Repeat steps 2 and 3 any time you need to reload the summary, except first remove the existing $("#summary") before appending again

You can use this same strategy for the items as well, just make sure you use the correct jQuery selectors/methods so that you're overwriting the existing divs in the correct order, as append will only work for adding something to the end of an element.



回答4:

I know I'm 3 years late to the conversation, but none of these answers applied at all to a situation I had in my latest project. And since the way I was originally thinking of doing this ended up working, I figured I should post it in case anyone else wants to actually nest underscore.js templates, as the title of this question states.

My requirements were basically:

1) If there are dynamic pieces in a template, they should be able to re-render themselves individually--without requiring that the entire template be re-rendered with the updated data.

2) Dynamic pieces shouldn't need to be splintered off into their own templates on the same level as the parent template; rather, there should be a sort of template hierarchy--a template declaring all the sub-templates required for itself.

The concept of nested templates in Underscore.js is the same as in PHP (for example). The following code is an example of "echoing PHP":

<?php echo '<?php echo $var_unknown_until_runtime; ?>'; ?>

While this nested syntax can get super convoluted and confusing, the idea is simple:

Echo code that will echo values later.

This applies in Underscore.js templates as well.


Example

The code will look like this:

<script type="text/template" id="outer-tpl">
    <%= value %>

    <script type="text/template" id="inner-tpl">
        <%= '<%= value %\>' %> <!-- NOTE how the inner '>' is escaped ('\>') -->
    <%= "</script\>" %> <!-- NOTE same thing -->
</script>

Let's break this down a little bit. The line with the first NOTE is our key line here. Normally when Underscore compiles this template, you would expect the value of value to be printed in both the outer and inner templates. We did a few things to prevent this from happening:

1) Turn the nested underscore statement into a string.

2) Interpolate that string (<%= "string here" %>).

3) Escape the closing > in the nested underscore statement (\>). This prevents Underscore's regex from matching this with the opening tag from step 2 (<%=). When this string is echoed, the escape character will be removed, leaving %>, ready to be interpreted by Underscore on the next pass.

Doing this same thing in the second NOTE in our example prevents the browser from ending the first script tag. Nested script tags technically aren't allowed, so the browser looks for the first sequence of characters: </script> and then ends script execution. This stops the browser from recognizing this sequence.

For a more in-depth example, see this JSFiddle.


Disclaimers

I don't endorse this as anywhere near the best way to do this sort of thing. It is, after all, very hacky. While @John z's answer breaks my first requirement, which wasn't an option for me, the other option of splintering off dynamic pieces in a template into their own templates may be easier to deal with. It's just less organized.

The location of all these nested script tags in your DOM can get hard to keep track of (as the nested script tags will get injected dynamically into the DOM when and wherever you insert the outer template). The risk of duplication is high, and can be annoying to maintain.

The difficulty of nesting templates increases drastically for each level of nesting (to nest a template in a nested template, you'd have to escape everything again, etc...). The fiddle includes an example of this.