151

I have got an array which I am looping through. Every time a condition is true, I want to append a copy of the HTML code below to a container element with some values.

Where can I put this HTML to re-use in a smart way?

<a href="#" class="list-group-item">
    <div class="image">
         <img src="" />
    </div>
    <p class="list-group-item-text"></p>
</a>

JQuery

$('.search').keyup(function() {
    $('.list-items').html(null);

    $.each(items, function(index) {
        // APPENDING CODE HERE
    });
});
Patrick Reck
  • 11,246
  • 11
  • 53
  • 86
  • 2
    If you're looking for a smart way, put all your info in a single DIV and style it. Don't create numerous tables with only two cells, don't wrap them into anchors. – Sergey Snegirev Sep 07 '13 at 13:39
  • 3
    OP is obviously interested in proper coding practices (kudos!), so I wanted to help. If I wanted to contribute to the actual problem, I'd post an answer. I'm sure anyone agrees that using a full-blown two-cell table to position an image and some text is barely justified except for some really exotic requirements (IE4?) – Sergey Snegirev Sep 07 '13 at 13:54
  • 1
    @BrianG. Posting a comment doesn't really imply you're going off on a tangent though. While I agree that comments are an appropriate venue for those (as long as they're still relevant), they're also appropriate for drive-by hints by people who don't have time to expand them into an answer yet. It's helpful to the OP to make it clear which you're doing. – millimoose Sep 07 '13 at 14:06
  • No-one answered your question, Patrick? – Sebastian Neira Sep 08 '13 at 11:46

8 Answers8

248

Old question, but since the question asks "using jQuery", I thought I'd provide an option that lets you do this without introducing any vendor dependency.

While there are a lot of templating engines out there, many of their features have fallen in to disfavour recently, with iteration (<% for), conditionals (<% if) and transforms (<%= myString | uppercase %>) seen as microlanguage at best, and anti-patterns at worst. Modern templating practices encourage simply mapping an object to its DOM (or other) representation, e.g. what we see with properties mapped to components in ReactJS (especially stateless components).

Templates Inside HTML

One property you can rely on for keeping the HTML for your template next to the rest of your HTML, is by using a non-executing <script> type, e.g. <script type="text/template">. For your case:

<script type="text/template" data-template="listitem">
    <a href="${url}" class="list-group-item">
        <table>
            <tr>
                <td><img src="${img}"></td>
                <td><p class="list-group-item-text">${title}</p></td>
            </tr>
        </table>
    </a>
</script>

On document load, read your template and tokenize it using a simple String#split

var itemTpl = $('script[data-template="listitem"]').text().split(/\$\{(.+?)\}/g);

Notice that with our token, you get it in the alternating [text, property, text, property] format. This lets us nicely map it using an Array#map, with a mapping function:

function render(props) {
  return function(tok, i) { return (i % 2) ? props[tok] : tok; };
}

Where props could look like { url: 'http://foo.com', img: '/images/bar.png', title: 'Lorem Ipsum' }.

Putting it all together assuming you've parsed and loaded your itemTpl as above, and you have an items array in-scope:

$('.search').keyup(function () {
  $('.list-items').append(items.map(function (item) {
    return itemTpl.map(render(item)).join('');
  }));
});

This approach is also only just barely jQuery - you should be able to take the same approach using vanilla javascript with document.querySelector and .innerHTML.

jsfiddle

Templates inside JS

A question to ask yourself is: do you really want/need to define templates as HTML files? You can always componentize + re-use a template the same way you'd re-use most things you want to repeat: with a function.

In es7-land, using destructuring, template strings, and arrow-functions, you can write downright pretty looking component functions that can be easily loaded using the $.fn.html method above.

const Item = ({ url, img, title }) => `
  <a href="${url}" class="list-group-item">
    <div class="image">
      <img src="${img}" />
    </div>
    <p class="list-group-item-text">${title}</p>
  </a>
`;

Then you could easily render it, even mapped from an array, like so:

$('.list-items').html([
  { url: '/foo', img: 'foo.png', title: 'Foo item' },
  { url: '/bar', img: 'bar.png', title: 'Bar item' },
].map(Item).join(''));

Oh and final note: don't forget to sanitize your properties passed to a template, if they're read from a DB, or someone could pass in HTML (and then run scripts, etc.) from your page.

Josh from Qaribou
  • 6,776
  • 2
  • 23
  • 21
  • 2
    Could you please add a demo/fiddle for "Templates Inside HTML" approach? – LCJ Mar 31 '17 at 17:47
  • ``, `

    ${title}

    `, in each and every part of your example it's clear that everything there needs escaping. My apologies if I'm coming off as overly feisty. There is a distinction between sanitizing and escaping, and most new people don't understand. Your answer doesn't help them understand. By calling your solution a templating solution, you further mislead folks into thinking concatenation/interpolation is appropriately solving it the problem.
    – Brad Jun 06 '17 at 23:34
  • I really am not seeing what you think is missing. I conclude by saying you should make sure people can't pass in those properties directly as HTML. If they're looking for an escaping function, they can look to [other answers](https://stackoverflow.com/a/35735254/2258817) – Josh from Qaribou Jun 06 '17 at 23:58
  • How can i use if else in Templates inside JS ? – ciprian2301 Mar 22 '18 at 23:48
  • 3
    Wow, this is really very nice. I'm adding a data-url attribute to each template. And fetching ajax data for each one. This is so nice and saved me so much time! Thank you. – Davey Apr 05 '18 at 16:09
  • @ciprian2301 you create conditionals when building the objects that you're going to render in your template. Often `Array.prototype` methods are your go-to for including this logic. e.g. `myThings.map(thing => (thing.type === 'foo') ? Foo(thing) : Bar(thing))` would render a Foo for every foo type, and a Bar for the other stuff. – Josh from Qaribou Dec 03 '18 at 18:12
  • How would you style the HTML elements, with JavScript also? since i need this to be embedded in other peoples browsers – ii iml0sto1 Aug 02 '19 at 06:16
  • @ii-iml0sto1 you can insert – Josh from Qaribou Feb 20 '20 at 17:07
  • I'm missing or don't understand how the substitution for ${url} is working. Can someone explain? – kstubs Jun 11 '20 at 19:30
  • 1
    @kstubs https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals – Josh from Qaribou Jun 12 '20 at 04:52
  • further read on ES6 and "Templating HTML with JS teamplate Strings"https://wesbos.com/template-strings-html – rob2universe Aug 24 '20 at 11:51
  • I think for naming variable on render function, it would better if we use "item" instead of "prop". Also "htmlOrKey" instead of "tok". it would more easier to read. – Samuel Ricky Saputro May 23 '21 at 10:03
  • Is there any way to add event handlers with either one of these implementations? – VIDesignz Feb 19 '22 at 18:55
  • Events can be handled once those templates are used to build their DOM representations. Then you can query into the element and attach an event listener. More advanced libs like vue are designed to make attaching listeners or sending updates easier. But if you only need a few simple onClicks, it's not so hard. I'd separate the arrow function from a string builder, into a string builder and a render function to execute on the built dom element. – Josh from Qaribou Aug 04 '22 at 19:26
176

You could decide to make use of a templating engine in your project, such as:

If you don't want to include another library, John Resig offers a jQuery solution, similar to the one below.


Browsers and screen readers ignore unrecognized script types:

<script id="hidden-template" type="text/x-custom-template">
    <tr>
        <td>Foo</td>
        <td>Bar</td>
    <tr>
</script>

Using jQuery, adding rows based on the template would resemble:

var template = $('#hidden-template').html();

$('button.addRow').click(function() {
    $('#targetTable').append(template);
});
Dave Jarvis
  • 30,436
  • 41
  • 178
  • 315
Mathijs Flietstra
  • 12,900
  • 3
  • 38
  • 67
  • @MaksimLuzik is right. Mustache (note the trademark spelling) is lighter-weight and will plug into the OP's code, but Handlebars has more features for handling arrays, and may provide a more sufficient solution in the end. Here's a nice comparison article: http://blog.cubettech.com/a-vital-comparison-handlebars-vs-mustache-vs-angularjs – Michael Scheper Jan 07 '15 at 20:55
  • 14
    Example of how to edit the template here: https://jsfiddle.net/meehanman/7vw8bc84/ – Dean Meehan Apr 13 '16 at 14:49
55

Use HTML template instead!

Since the accepted answer would represent overloading script method, I would like to suggest another which is, in my opinion, much cleaner and more secure due to XSS risks which come with overloading scripts.

I made a demo to show you how to use it in an action and how to inject one template into another, edit and then add to the document DOM.

example html

<template id="mytemplate">
  <style>
     .image{
        width: 100%;
        height: auto;
     }
  </style>
  <a href="#" class="list-group-item">
    <div class="image">
      <img src="" />
    </div>
    <p class="list-group-item-text"></p>
  </a>
</template>

example js

// select
var t = document.querySelector('#mytemplate');

// set
t.content.querySelector('img').src = 'demo.png';
t.content.querySelector('p').textContent= 'demo text';

// add to document DOM
var clone = document.importNode(t.content, true); // where true means deep copy
document.body.appendChild(clone);

HTML &lttemplate&gt

  • +Its content is effectively inert until activated. Essentially, your markup is hidden DOM and does not render.

  • +Any content within a template won't have side effects. Scripts don't run, images don't load, audio doesn't play ...until the template is used.

  • +Content is considered not to be in the document. Using document.getElementById() or querySelector() in the main page won't return child nodes of a template.

  • +Templates can be placed anywhere inside of <head>, <body>, or <frameset> and can contain any type of content which is allowed in those elements. Note that "anywhere" means that <template> can safely be used in places that the HTML parser disallows.

Fall back

Browser support should not be an issue but if you want to cover all possibilities you can make an easy check:

To feature detect <template>, create the DOM element and check that the .content property exists:

function supportsTemplate() {
  return 'content' in document.createElement('template');
}

if (supportsTemplate()) {
  // Good to go!
} else {
  // Use old templating techniques or libraries.
}

Some insights about Overloading script method

  • +Nothing is rendered - the browser doesn't render this block because the <script> tag has display:none by default.
  • +Inert - the browser doesn't parse the script content as JS because its type is set to something other than "text/javascript".
  • -Security issues - encourages the use of .innerHTML. Run-time string parsing of user-supplied data can easily lead to XSS vulnerabilities.

Full article: https://www.html5rocks.com/en/tutorials/webcomponents/template/#toc-old

Useful reference: https://developer.mozilla.org/en-US/docs/Web/API/Document/importNode http://caniuse.com/#feat=queryselector

CREATING WEB COMPONENTS Creating custom web components tutorial using HTML templates by Trawersy Media: https://youtu.be/PCWaFLy3VUo

DevWL
  • 17,345
  • 6
  • 90
  • 86
  • 4
    The `template` element is not supported by Internet Explorer (as of 2018, IE11). Tested with example 580 of http://w3c.github.io/html/single-page.html . – Roland Feb 16 '18 at 14:21
  • 1
  • 1
    The template approach worked well for me. One recommendation I have is to clone the template first (rather than at the end) and work with the cloned object. Otherwise, the changes you are making are happening to the template itself. If you had conditional logic where you set some properties/content only some of the time, those properties would be present in your next use of the template. By cloning before each use, you ensure that you always have a consistent base to work from. – jt. Aug 29 '19 at 13:55
13

Add somewhere in body

<div class="hide">
<a href="#" class="list-group-item">
    <table>
        <tr>
            <td><img src=""></td>
            <td><p class="list-group-item-text"></p></td>
        </tr>
    </table>
</a>
</div>

then create css

.hide { display: none; }

and add to your js

$('#output').append( $('.hide').html() );
Mateusz Nowak
  • 4,021
  • 2
  • 25
  • 37
  • 2
    The OP has a JSON and wants to render some html using that array as data source. How this answer is addressing the problem? Your answer takes a html node and clones/duplicates it and nothing related to data binding. – Adrian Iftode Sep 07 '13 at 14:03
  • So me is who is downvoting. – Adrian Iftode Sep 07 '13 at 14:04
  • I really think you're not helping into getting a better answer, as you could've started with reminding us that the point was wider that what was addressed by our answers. – Sebastian Neira Sep 07 '13 at 14:39
  • The answer is already given by Mathijs Flietstra, why should I add the same answer? Your answer is incomplete because it doesn't count the data part. – Adrian Iftode Sep 07 '13 at 14:43
  • 1
    This does work, but it is more efficient to do `.children().clone()`, rather than `.html()`. The speed is about 3:1 for a random `` I had on a page using this code `i=10000; time=performance.now(); while (--i) {$a.clone().append(0 ? $b.html() : $b.children().clone())} performance.now()-time`. The ratio is actually a bit more exaggerated because I'm using $a.clone(), but trying to empty it each iteration is more of a performance hit than cloning, so I'm not sure how to make it any more accurate because timing functions have their own cost. – Chinoto Vokro Jun 21 '16 at 00:39
  • Here's a version with the timer in the loop so we can use empty without including its penalty in the timing: `i=10000; total=0; while (--i) {$a.empty(); time=performance.now(); $a.append(0 ? $b.html() : $b.children().clone()); total+=performance.now()-time} total`. I was going to use jsperf, but it has been down for awhile... – Chinoto Vokro Jun 21 '16 at 00:41
  • 1
    This aproach is bad. mainly because you get template content download and parsed even if you did not use it. This is too big issue to consider this as a valid answer. Also as someone else menthon above, if you must to at least use clone instead of .html() – DevWL Oct 12 '17 at 05:02
4

Very good answer from DevWL about using the native HTML5 template element. To contribute to this good question from the OP I would like to add on how to use this template element using jQuery, for example:

$($('template').html()).insertAfter( $('#somewhere_else') );

The content of the template is not html, but just treated as data, so you need to wrap the content into a jQuery object to then access jQuery's methods.

Jacques
  • 991
  • 1
  • 12
  • 15
3

In order to solve this problem, I recognize two solutions:

  • The first one goes with AJAX, with which you'll have to load the template from another file and just add everytime you want with .clone().

    $.get('url/to/template', function(data) {
        temp = data
        $('.search').keyup(function() {
            $('.list-items').html(null);
    
            $.each(items, function(index) {
                 $(this).append(temp.clone())
            });
    
        });
    });
    

    Take into account that the event should be added once the ajax has completed to be sure the data is available!

  • The second one would be to directly add it anywhere in the original html, select it and hide it in jQuery:

    temp = $('.list_group_item').hide()
    

    You can after add a new instance of the template with

    $('.search').keyup(function() {
        $('.list-items').html(null);
    
        $.each(items, function(index) {
            $(this).append(temp.clone())
        });
    });
    
  • Same as the previous one, but if you don't want the template to remain there, but just in the javascript, I think you can use (have not tested it!) .detach() instead of hide.

    temp = $('.list_group_item').detach()
    

    .detach() removes elements from the DOM while keeping the data and events alive (.remove() does not!).

Sebastian Neira
  • 564
  • 2
  • 7
  • **-- No Problemo! --** – iConnor Sep 07 '13 at 13:48
  • The op has a JSON and wants to render some html using that array as data source. How this answer is addressing the problem? Your answer takes a html node and clones/duplicates it and nothing related to data binding. – Adrian Iftode Sep 07 '13 at 14:02
  • 1
    I quote from op: *I want to append a copy of the HTML code below to a container element with some values.* I believe this addresses his problem. – Sebastian Neira Sep 07 '13 at 14:14
  • And how is treated the "some values" part? – Adrian Iftode Sep 07 '13 at 14:32
  • Well, that does not mean does it not address the problem. I appreciate that you've told me that I was missing one of the points :) Anyway, how am I to know how to insert those values if I don't even know *what* values are we talking about? Just take a look at this part *Where can I put this HTML to re-use in a smart way?*. What is really being asked? About the JSON or the HTML inclusion? – Sebastian Neira Sep 07 '13 at 14:37
  • The question is about having an array, looping through its items, generating a the html for each item using the data. You solved the looping part, but you didn't solve the data part. The answer is incomplete. – Adrian Iftode Sep 07 '13 at 14:47
  • He's actually not asking for the data part, but in how to reuse the HTML. – Sebastian Neira Sep 07 '13 at 14:49
3

Other alternative: Pure

I use it and it has helped me a lot. An example shown on their website:

HTML

<div class="who">
</div>

JSON

{
  "who": "Hello Wrrrld"
}

Result

<div class="who">
  Hello Wrrrld
</div>
rnevius
  • 26,578
  • 10
  • 58
  • 86
alditis
  • 4,633
  • 3
  • 49
  • 76
0

Here's how to use the <template> element and jQuery a little more efficiently, since the other jQuery answers as of the time of writing use .html() which forces the HTML to be serialized and re-parsed.

First, the template to serve as an example:

<template id="example"><div class="item">
    <h1 class="title"></h1>
    <p class="description"></p>
</div></template>

Note that there is no space between the <template> tag and its content, nor between the end of the content and </template>. If you put space in there, it will be copied too.

Now, the JS:

// example data
const items = [
    {name: "Some item", description: "Some description"},
    // ...
];

const template = $("template#example").contents();
const templateTitle = template.find("h1.title");
const templateDescription = template.find("p.description");
const target = $("body");

for (const item of items) {
    // fill in the placeholders
    templateTitle.text(item.name);
    templateDescription.text(item.description);
    // copy the template and add it to the page
    target.append(template.clone());
}

This will add a copy of the template, with placeholders replaced, for every item in the items array. Note that several elements, like template and target, are only retrieved once.

kbolino
  • 1,441
  • 2
  • 18
  • 24
  • Regarding the use of `contents` from jQuery, one needs to keep in mind it is available only since jQuery v.3.2. See the [documentation](https://api.jquery.com/contents/) – Jacques Dec 19 '22 at 11:29