Problem
I have a collection of images with linked captions on a page. I want them each to have identical HTML.
Typically, i copy and paste the HTML over and over for each item. The problem is, if i want to tweak the HTML, i have to do it for all of them. It's time-consuming, and there's risk of mistakes.
Quick and Dirty Templating
I'd like to write just one copy of the HTML, list the content items as plain text, and on page-render the HTML would get automatically repeated for each content-item.
HTML
<p><img src=IMAGE-URL>
<br>
<a target='_blank' href=LINK-URL>CAPTION</a></p>
Content List
IMAGE-URL, LINK-URL, CAPTION
/data/khang.jpg, https://khangssite.com, Khang Le
/data/sam.jpg, https://samssite.com, Sam Smith
/data/joy.jpg, https://joyssite.com, Joy Jones
/data/sue.jpg, https://suessite.com, Sue Sneed
/data/dog.jpg, https://dogssite.com, Brown Dog
/data/cat.jpg, https://catssite.com, Black Cat
Single Item
Ideally, i could put the plain-text content for a single item anywhere on a page, with some kind of identifier to indicate which HTML template to use (similar to classes with CSS).
TEMPLATE=MyTemplate1, IMAGE-URL=khang.jpg, LINK-URL=https://khangssite.com, CAPTION=Khang Le
Implementation
I want to achieve this without a framework, library, etc. I'd like to put the HTML and content-list in the same .html file.
Definitely no database. It should be quick and simple to set it up within a page, without installing or configuring additional services.
Ideally, i'd like to do this without javascript, but that's not a strict requirement. If there's javascript, it should be ignorant of the fieldnames. Ideally, very short and simple. No jquery please.
CodePudding user response:
you mean Template literals (Template strings) ?
const arrData =
[ { img: '/data/khang.jpg', link: 'https://khangssite.com', txt: 'Khang Le' }
, { img: '/data/sam.jpg', link: 'https://samssite.com', txt: 'Sam Smith' }
, { img: '/data/joy.jpg', link: 'https://joyssite.com', txt: 'Joy Jones' }
, { img: '/data/sue.jpg', link: 'https://suessite.com', txt: 'Sue Sneed' }
, { img: '/data/dog.jpg', link: 'https://dogssite.com', txt: 'Brown Dog' }
, { img: '/data/cat.jpg', link: 'https://catssite.com', txt: 'Black Cat' }
]
const myObj = document.querySelector('#my-div')
arrData.forEach(({ img, link, txt }) =>
{
myObj.innerHTML = `
<p>
<img src="${img}">
<br>
<a target='_blank' href="${link}">${txt}</a>
</p>`
});
<div id="my-div"></div>
CodePudding user response:
This answer is a complete solution. It's exciting to edit the HTML template in codepen and watch the layout of each copy change in real time -- similar to the experience of editing a class CSS and watching the live changes.
Here's the code, followed by explanation.
HTML
<span id="template-container"></span>
<div hidden id="template-data">
IMG,, LINK,, CAPTION
https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive.jpg,, khangssite.com,, Khang Le
https://i.redd.it/jeuusd992wd41.jpg,, suessite.com,, Sue Sneed
https://picsum.photos/536/354,, catssite.com,, Black Cat
</div>
<template id="art-template">
<span >
<p>
<a href="${LINK}" target="_blank">
<img src="${IMG}" alt="" />
<br>
${CAPTION}
</a>
</p>
</span>
</template>
Javascript
// load header and data into arrays
let sRawData = document.querySelector("#template-data").innerHTML.trim();
const headersEnd = sRawData.indexOf("\n");
const headers = sRawData.slice(0, headersEnd).split(",,");
const aRows = sRawData.slice(headersEnd).trim().split("\n");
const data = aRows.map((element) => {
return element.split(",,");
});
// grab template and container
const templateHtml = document.querySelector("template").innerHTML;
const container = document.querySelector("#template-container");
// make html for each record
data.map((row) => {
let workingCopy = templateHtml;
// loop headers and load field from corresponding record into temlate
headers.forEach((header, column) => {
let value = row[column].trim();
let placeholder = `\$\{${header.trim()}\}`;
workingCopy = workingCopy.replaceAll(placeholder, value);
});
// append template to page, and loop to next record
container.innerHTML = workingCopy;
});
Edit the code on codepen: https://codepen.io/johnaweiss/pen/VwdzqpQ
Requirement
As specified in the question, this solution is intended to optimize the coding experience on the HTML side. That's the whole point of any web templating. Therefore, the JS has to work a little harder to make life easier for the HTML programmer.
The question seeks a reusable solution. Therefore, JS should be ignorant of the template, fields, and data-list. So unlike @MisterJojo's answer, the template and all data are in my HTML, not javascript. The JS code is generic.
Design
My solution is based on the <template>
tag. It's intended for precisely this usage. It has various advantages, like the template isn't displayed, processed, or validated by the browser, so it has less impact on performance. Programmer doesn't have to write an explicit display:none
style.
https://news.ycombinator.com/item?id=33089975
HTML
My HTML has three blocks:
- template: The HTML coder develops their desired display-structure of the output, in real HTML (not plain text). Uses
<template>
- data: The list of records each of which should be rendered using the same template. Uses
<span>
with a HIDDEN attribute. - container: The place to display all the output blocks. Uses
<span>
.
Template
My sample code includes 3 placeholders for data:
${LINK}
${IMG}
${CAPTION}
But of course you can use any placeholders, any number of them. I use string-literal delimiting-style (although i'm not actually using them as string-literals -- i just borrowed the delimiter style.)
Data Element
The question specifies data should be stored in HTML. It should require minimal keystrokes.
- I didn't want to redundantly retype the fieldnames on every row. I didn't use slotting, JSO, Jason, or XML syntax, because those are all verbose. https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots
- I just put the fieldname-headers on the first row.
- The headers are a visual aid for the HTML developer, and a key for Javascript to know the fieldnames and order.
- It's a simple delimited list. I eliminated all braces, brackets, equals, parens, etc.
- Record Delimiter: End-of-line
- Field Delimiter: Double-commas. Seems safe, and they're easy to type. I don't expect to see double-commas in any actual data. Beware, the developer must enter a space for any empty cells, to prevent unintended double-commas. The programmer can easily use a different delimiter if they prefer, as long as they update the Javascript. You can use single-commas if you're sure there will be no embedded commas within a cell.
- It's hidden using hidden attribute. No CSS needed.
- It's a span to ensure it takes up no room on the page.
JAVASCRIPT
Data
The data is processed by Javascript with two split statements, first on newline delimiter, then on the double-comma delimiter. That puts the whole thing into a 2D array.
Place-holder Substitution
Handling multiple entries requires plugging each entry into the template. This tool allows template variables in both tag attribute and content sections (eg to accommodate <img src
).
i went with simple string-replacement.
My JS uses trims to get rid of extra whitespace as needed.
Future
Inspired by @MisterJojo, an earlier version used template literals to do the substitution. However, that was a bit more complicated and verbose, and seemed to require use of eval
. But it seems a more appropriate method for templates, so maybe i'll revisit that.
A future version may adapt to whatever custom field-delimiter the HTML developer uses for the data block.
The dollar-curly string-literal-style delimiter for the placeholders is a bit awkward to type. So i'm interested in finding a less awkward non-alpha delimiter that won't conflict with HTML.
Maybe there are simpler ways to pull the data into JS, or do the substitutions. I've read components work well with <template>
, but i didn't go there.
Imo, the JS committee should develop a variable-placeholder feature for <template>
tags, and natively accommodate storing the data in HTML. It would be great if something like this solution was part of the rendering engine.
I'm working on a multi-template version. But it may be a while, as i don't have need for it at the moment.