🎬 Movie Carousel Example
​
**1. Handlebars Helpers **​
// --- Register All Helpers BEFORE Rendering Templates ---
window.Handlebars.registerHelper("firstImage", function (images) {
if (Array.isArray(images) && images.length > 0) return images[0]
if (typeof images === "string" && images) return images
return "https://placehold.co/200x300?text=No+Image"
})
window.Handlebars.registerHelper("truncateText", function (text, maxLen) {
if (!text) return ""
if (text.length <= maxLen) return text
var truncated = text.slice(0, maxLen)
var lastSpace = truncated.lastIndexOf(" ")
if (lastSpace > 0) truncated = truncated.slice(0, lastSpace)
return truncated + "..."
})
window.Handlebars.registerHelper("formatDate", function (dateStr) {
if (!dateStr) return ""
var d = new Date(dateStr)
if (isNaN(d.getTime())) return dateStr
var options = { year: "numeric", month: "long", day: "numeric" }
return d.toLocaleDateString(undefined, options)
})
window.Handlebars.registerHelper("json", function (context) {
// DO NOT apply any extra escaping or quote replacement!
return JSON.stringify(context)
})
2. Handlebars List Panel Template​
Best Practices:
- Use double quotes around
data-item
attribute.- Use
{{json item}}
directly (no extra escaping).- After rendering, assign items to
window.carouselDataItems
.
{{#each items as |item index|}}
<!-- Note: Use 'carousel-card' its needed -->
<div class="carousel-card" data-index="{{index}}" data-hide-image-on-more="{{item.hideImageOnMore}}">
<img class="card-image" src="{{firstImage item.images}}" alt="Image for {{item.title}}" />
<div class="card-right">
<div class="event-title"><b>{{item.title}}</b></div>
{{#if item.subTitle1}}
<div class="event-subtitle"><b>Release:</b> {{formatDate item.subTitle1}}</div>
{{/if}}
<div class="short-description">{{truncateText item.shortDescription 220}}</div>
<div class="event-buttons">
<!-- Use carousel.selectByName and class="card-button" and data-type="panel" when changing to a panel -->
<button class="card-button" data-type="panel" onclick='askRoomAndPurchase(this); carousel.selectByName("Purchase")'>Purchase</button>
</div>
</div>
</div>
{{/each}}
<div class="pagination-controls">
<button class="prev-btn list-btn" {{#unless hasPrev}}disabled{{/unless}}>Prev</button>
<span> Page {{getcurrentPage}} of {{getPageCount}} </span>
<button class="next-btn list-btn" {{#unless hasNext}}disabled{{/unless}}>Next</button>
</div>
<script>
// Make all item data available to JS after rendering
window.carouselDataItems = {{{json items}}};
</script>
3. Handlebars Purchase Panel Template​
- Single purchase panel (render one panel, populate with JS on open).
- Use double quotes on
data-item
.
<div class="purchase-panel" data-index="" data-item="">
<div class="purchase-title">Purchase Movie</div>
<div class="purchase-label">Movie:</div>
<!-- Movie title will be filled in by JS -->
<div
class="purchase-movie-title"
style="font-weight:700; color:#2563eb; margin-bottom:0.7rem;"
>
</div>
<form class="purchase-form" data-index="" data-title="">
<label class="purchase-label" for="purchase-name">Your Name:</label>
<input
type="text"
id="purchase-name"
name="name"
placeholder="Enter your name"
autocomplete="off"
required
/>
<label class="purchase-label" for="purchase-room">Room Number:</label>
<input
type="text"
id="purchase-room"
name="room"
placeholder="Enter your room number"
autocomplete="off"
required
/>
<div style="display:flex; gap:0.7rem; margin-top:0.7rem;">
<button
type="button"
class="purchase-submit-btn"
onclick="submitPurchaseForm(this)"
>
Submit
</button>
<button
type="button"
class="panel-button list-btn"
data-type="panel"
onclick="carousel.selectByName('List')"
>
Back
</button>
</div>
</form>
</div>
4. JavaScript: Purchase Panel Logic​
- Find the card and use its
data-item
for the selected movie.- Always parse valid JSON, never double-escaped.
function askRoomAndPurchase(btn) {
var card = btn.closest(".carousel-card")
var idx = card ? card.getAttribute("data-index") : null
if (typeof carousel !== "undefined" && carousel.selectByName) {
window.selectedPurchaseIndex = idx
carousel.selectByName("Purchase")
setTimeout(function () {
// Find the purchase panel (it may be outside the card)
var panel = (carousel.shadowRoot || document).querySelector(
".purchase-panel"
)
var itemData = card ? card.getAttribute("data-item") : null
var item = itemData ? JSON.parse(itemData) : null
if (panel && item) {
// Fill in purchase panel with selected item data
panel.setAttribute("data-item", itemData) // store the JSON for later
panel.setAttribute("data-index", idx)
var titleField = panel.querySelector(".purchase-movie-title")
if (titleField) titleField.textContent = item.title
var form = panel.querySelector(".purchase-form")
if (form) form.setAttribute("data-title", item.title)
}
}, 200) // Allow for DOM update after panel switch
}
}
function submitPurchaseForm(btn) {
var form = btn.closest(".purchase-form")
var title = form ? form.getAttribute("data-title") : null
var name = form ? form.querySelector('input[name="name"]').value.trim() : ""
var room = form ? form.querySelector('input[name="room"]').value.trim() : ""
if (!name) {
alert("Please enter your name.")
return
}
if (!room) {
alert("Please enter your room number.")
return
}
if (title) {
alert(
"Purchase submitted!\nMovie: " +
title +
"\nName: " +
name +
"\nRoom: " +
room
)
form.reset()
if (typeof carousel !== "undefined" && carousel.selectByName) {
carousel.selectByName("List")
}
} else {
alert("Movie information not found.")
}
}
5. CSS (Responsive, Accessible, Copy-Paste Ready)​
.carousel-card {
display: flex;
flex-direction: row;
align-items: stretch;
background: #fff;
border-radius: 1.5rem;
box-shadow:
0 8px 32px 0 rgba(52, 78, 134, 0.1),
0 1.5px 8px 0 rgba(40, 30, 70, 0.04);
margin-bottom: 2.2rem;
padding: 0;
border: 1.5px solid #e5e7eb;
min-width: 0;
width: 100%;
overflow: visible;
transition:
box-shadow 0.18s,
transform 0.12s;
}
.card-image {
flex: 1 1 18%;
min-width: 110px;
max-width: 140px;
height: 180px;
object-fit: cover;
border-top-left-radius: 1.5rem;
border-bottom-left-radius: 1.5rem;
background: #f3f4f6;
margin: 0;
display: block;
}
.card-right {
flex: 1 1 70%;
padding: 1.5rem 1.2rem 1.5rem 1.2rem;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-width: 0;
min-height: 0;
}
.event-title {
font-size: 1.2rem;
font-weight: 800;
color: #1e293b;
margin-bottom: 0.5rem;
font-family: "Inter", Arial, sans-serif;
font-weight: bold;
}
.event-subtitle {
color: #64748b;
font-size: 1.08rem;
margin-bottom: 0.7rem;
font-weight: 700;
font-family: "Inter", Arial, sans-serif;
font-weight: bold;
}
.short-description {
color: #334155;
font-size: 1.13rem;
margin-bottom: 1.1rem;
line-height: 1.55;
font-family: "Inter", Arial, sans-serif;
word-break: break-word;
}
.event-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
margin-top: 1.2rem;
justify-content: flex-start;
}
.card-button {
background: linear-gradient(135deg, #2d7be5 0%, #3b82f6 100%);
color: #fff;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
font-size: 1.01rem;
padding: 0.7em 1.5em;
border-radius: 1.2em;
border: none;
box-shadow: 0 2px 8px rgba(52, 123, 229, 0.1);
cursor: pointer;
transition:
background 0.18s,
box-shadow 0.14s,
transform 0.1s;
min-width: 98px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-family: "Inter", Arial, sans-serif;
line-height: 1.15;
}
.card-button:hover,
.card-button:focus {
background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
box-shadow: 0 4px 16px rgba(52, 123, 229, 0.13);
transform: scale(1.04);
}
.list-btn {
background: #f3f4f6;
color: #2d7be5;
border: none;
border-radius: 1.2em;
padding: 0.6em 1.2em;
font-weight: 600;
font-size: 1.01rem;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
}
.list-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1.2rem;
margin: 1.2rem 0 0 0;
}
.prev-btn,
.next-btn {
background: #f3f4f6;
color: #2d7be5;
border: none;
border-radius: 1.2em;
padding: 0.6em 1.2em;
font-weight: 600;
font-size: 1.01rem;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
}
@media (max-width: 700px) {
.carousel-card {
flex-direction: column;
border-radius: 1.1rem;
max-width: 99vw;
min-width: 0;
align-items: stretch;
}
.card-image {
border-radius: 1.1rem 1.1rem 0 0;
min-width: 0;
max-width: 100%;
width: 100%;
height: 180px;
object-fit: cover;
margin: 0;
display: block;
}
.card-right {
padding: 1.1rem 1.1rem 1.1rem 1.1rem;
width: 100%;
}
.event-buttons {
flex-direction: column;
gap: 0.7rem;
margin-top: 1.1rem;
}
.card-button {
width: 100%;
min-width: 0;
min-height: 54px;
font-size: 1.1rem;
padding: 1.2em 0;
border-radius: 1.2em;
margin-left: 0;
}
}
.purchase-panel {
background: #fff;
border-radius: 1.3rem;
box-shadow: 0 4px 18px 0 rgba(52, 78, 134, 0.1);
padding: 2rem 1.5rem 1.5rem 1.5rem;
max-width: 400px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.purchase-title {
font-size: 1.3rem;
font-weight: 800;
color: #1e293b;
margin-bottom: 0.7rem;
font-family: "Inter", Arial, sans-serif;
}
.purchase-label {
font-weight: 700;
color: #1e293b;
margin-bottom: 0.3rem;
font-size: 1.08rem;
}
.purchase-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.1rem;
}
.purchase-form input[type="text"] {
font-size: 1.1rem;
padding: 0.5em 1em;
border-radius: 1.2rem;
border: 1.4px solid #e2e8f0;
background: #f8fafc;
color: #22223b;
outline: none;
box-shadow: 0 1.5px 7px rgba(100, 100, 150, 0.04);
margin-bottom: 0.2em;
}
.purchase-form input[type="text"]:focus {
border-color: #2d7be5;
box-shadow: 0 0 0 2px #e0e7ff80;
}
.purchase-form .purchase-submit-btn {
background: linear-gradient(135deg, #2d7be5 0%, #3b82f6 100%);
color: #fff;
font-weight: 700;
border-radius: 1.2em;
padding: 0.7em 1.5em;
border: none;
font-size: 1.01rem;
cursor: pointer;
margin-top: 0.7rem;
min-width: 120px;
}
.purchase-form .purchase-submit-btn:hover,
.purchase-form .purchase-submit-btn:focus {
background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
}
.purchase-form .panel-button {
margin-top: 0.7rem;
background: #f3f4f6;
color: #2d7be5;
border: none;
border-radius: 1.2em;
padding: 0.6em 1.2em;
font-weight: 600;
font-size: 1.01rem;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
}
@media (max-width: 700px) {
.purchase-panel {
padding: 1.1rem 0.7rem 1rem 0.7rem;
border-radius: 1.1rem;
max-width: 99vw;
}
}
6. Sample Movie Data​
{
"items": [
{
"description": "On the rugged isle of Berk, where Vikings and dragons have been bitter enemies for generations, Hiccup stands apart, defying centuries of tradition when he befriends Toothless, a feared Night Fury dragon. Their unlikely bond reveals the true nature of dragons, challenging the very foundations of Viking society.",
"hideImageOnMore": true,
"images": [
"https://image.tmdb.org/t/p/w200/q5pXRYTycaeW6dEgsCrd4mYPmxM.jpg"
],
"panels": [],
"shortDescription": "On the rugged isle of Berk, where Vikings and dragons have been bitter enemies for generations, Hiccup stands apart, defying centuries of tradition when he befriends Toothless, a feared Night Fury dragon. Their unlikely bond reveals the true nature of dragons, challenging the very foundations of Viking society.",
"subTitle1": "2025-06-06",
"subTitle2": false,
"title": "How to Train Your Dragon"
},
{
"description": "After the underlying tech for M3GAN is stolen and misused by a powerful defense contractor to create a military-grade weapon known as Amelia, M3GAN's creator Gemma realizes that the only option is to resurrect M3GAN and give her a few upgrades, making her faster, stronger, and more lethal.",
"hideImageOnMore": true,
"images": [
"https://image.tmdb.org/t/p/w200/4a63rQqIDTrYNdcnTXdPsQyxVLo.jpg"
],
"panels": [],
"shortDescription": "After the underlying tech for M3GAN is stolen and misused by a powerful defense contractor to create a military-grade weapon known as Amelia, M3GAN's creator Gemma realizes that the only option is to resurrect M3GAN and give her a few upgrades, making her
faster, stronger, and more lethal.", "subTitle1": "2025-06-25", "subTitle2": false, "title": "M3GAN 2.0" } ] }
---
## **7. Final Notes and Checklist**
- ✅ **`data-item` uses double quotes, with plain JSON from `JSON.stringify()`**
- ✅ **No double-escaping or HTML escaping in `json` helper**
- ✅ **All helpers registered before rendering**
- ✅ **JS and CSS are production-ready and mobile-friendly**
---