Example App Blog with posts and comments demonstrating component composition.
This example demonstrates component composition. Before studying it, you should understand:
import Eleva from "eleva";
const app = new Eleva("BlogApp");
// Comment Component
app.component("Comment", {
setup({ props }) {
const { comments, commentIndex } = props;
const getComment = () => comments.value[commentIndex] || { author: "", date: "", text: "" };
return { getComment };
},
template: (ctx) => {
const comment = ctx.getComment();
return `
<div class="comment">
<div class="comment-header">
<strong>${comment.author}</strong>
<span class="date">${comment.date}</span>
</div>
<p>${comment.text}</p>
</div>
`;
}
});
// Post Component
app.component("BlogPost", {
setup({ signal, props }) {
const { posts, postId } = props;
const post = () =>
posts.value.find(p => p.id === postId) || {
title: "",
author: "",
date: "",
readTime: "",
excerpt: "",
comments: []
};
const showComments = signal(false);
const newComment = signal("");
const comments = signal([...(post().comments || [])]);
function toggleComments() {
showComments.value = !showComments.value;
}
function addComment() {
if (!newComment.value.trim()) return;
const nextComments = [...comments.value, {
id: Date.now(),
author: "Anonymous",
text: newComment.value,
date: new Date().toLocaleDateString()
}];
comments.value = nextComments;
posts.value = posts.value.map(p =>
p.id === postId ? { ...p, comments: nextComments } : p
);
newComment.value = "";
}
return { post, showComments, newComment, comments, toggleComments, addComment };
},
template: (ctx) => `
<article class="blog-post">
<header>
<h2>${ctx.post().title}</h2>
<div class="post-meta">
<span>By ${ctx.post().author}</span>
<span>${ctx.post().date}</span>
<span>${ctx.post().readTime} min read</span>
</div>
</header>
<div class="post-content">
<p>${ctx.post().excerpt}</p>
</div>
<footer>
<button @click="toggleComments">
${ctx.showComments.value ? 'Hide' : 'Show'} Comments (${ctx.comments.value.length})
</button>
</footer>
${ctx.showComments.value ? `
<div class="comments-section">
${ctx.comments.value.map((comment, commentIndex) => `
<div
key="${comment.id}"
class="comment-wrapper"
:comments="comments"
:commentIndex="${commentIndex}">
</div>
`).join('')}
<div class="add-comment">
<textarea
placeholder="Write a comment..."
@input="(e) => newComment.value = e.target.value"
>${ctx.newComment.value}</textarea>
<button @click="addComment">Post Comment</button>
</div>
</div>
` : ''}
</article>
`,
children: {
".comment-wrapper": "Comment"
}
});
// Main Blog Component
app.component("Blog", {
setup({ signal }) {
const posts = signal([
{
id: 1,
title: "Getting Started with Eleva",
author: "John Doe",
date: "January 3, 2026",
readTime: 5,
excerpt: "Learn the basics of Eleva, a minimalist JavaScript framework that emphasizes simplicity and performance. In this guide, we'll cover components, signals, and the template system.",
comments: [
{ id: 1, author: "Jane", text: "Great introduction!", date: "January 3, 2026" }
]
},
{
id: 2,
title: "Building Reactive UIs",
author: "Jane Smith",
date: "January 2, 2026",
readTime: 8,
excerpt: "Dive deep into Eleva's signal-based reactivity system. Understand how signals work under the hood and how to use them effectively in your applications.",
comments: []
},
{
id: 3,
title: "Plugin Development Guide",
author: "John Doe",
date: "January 1, 2026",
readTime: 10,
excerpt: "Learn how to extend Eleva with custom plugins. We'll build a complete plugin from scratch, covering the plugin lifecycle and best practices.",
comments: [
{ id: 1, author: "Dev", text: "Very helpful!", date: "January 1, 2026" },
{ id: 2, author: "Alex", text: "Can you cover testing?", date: "January 2, 2026" }
]
}
]);
return { posts };
},
template: (ctx) => `
<div class="blog">
<header class="blog-header">
<h1>Eleva Blog</h1>
<p>Tutorials, guides, and best practices</p>
</header>
<div class="posts">
${ctx.posts.value.map(post => `
<div
key="${post.id}"
class="post-wrapper"
:posts="posts"
:postId="${post.id}">
</div>
`).join('')}
</div>
</div>
`,
style: `
.blog { max-width: 800px; margin: 0 auto; padding: 20px; }
.blog-header { text-align: center; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 1px solid #eee; }
.blog-header h1 { margin: 0 0 10px 0; }
.blog-header p { color: #666; margin: 0; }
.blog-post { background: white; border: 1px solid #eee; border-radius: 8px; padding: 25px; margin-bottom: 20px; }
.blog-post h2 { margin: 0 0 10px 0; }
.post-meta { display: flex; gap: 15px; color: #666; font-size: 14px; margin-bottom: 15px; }
.post-content { color: #444; line-height: 1.6; }
.blog-post footer { margin-top: 20px; padding-top: 15px; border-top: 1px solid #eee; }
.blog-post footer button { background: none; border: 1px solid #ddd; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
.comments-section { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
.comment { background: #f8f9fa; padding: 15px; border-radius: 4px; margin-bottom: 10px; }
.comment-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
.comment-header .date { color: #999; font-size: 12px; }
.comment p { margin: 0; }
.add-comment textarea { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; min-height: 80px; margin-bottom: 10px; }
.add-comment button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
`,
children: {
".post-wrapper": "BlogPost"
}
});
app.mount(document.getElementById("app"), "Blog");
Blog (parent)
├── BlogPost (child, multiple)
│ └── Comment (grandchild, multiple)
This demonstrates Eleva’s component composition:
Blog manages the list of postsBlogPost handles individual post display and commentsComment renders a single comment| ← Back to Apps | Previous: Weather Dashboard | Next: Custom Plugin Guide → |