Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing a Blog Comment Interface with Vue.js

Tech May 16 1

Comment Input Form

Capture user metadata using a reactive object bound to standard input elements. The form collects a display name, contact email, and a optional personal website.

<template>
  <section class="comment-draft">
    <div class="input-group">
      <label for="author">Display Name</label>
      <input id="author" v-model="draft.author" placeholder="How should we call you?" required />
    </div>
    <div class="input-group">
      <label for="contact">Email Address</label>
      <input id="contact" v-model="draft.contact" type="email" placeholder="For notifications (optional)" />
    </div>
    <div class="input-group">
      <label for="site">Personal Website</label>
      <input id="site" v-model="draft.site" placeholder="https://example.com (optional)" />
    </div>
    <button class="publish-btn" @click="handlePublish">Post Comment</button>
  </section>
</template>

Rendering the Thread

Iterate over the fetched dataset using a dedicated component. Pass each record as a prop to isolate rendering logic.

<div class="thread-container">
  <CommentNode
    v-for="item in threadData"
    :key="item.identifier"
    :node="item"
  />
</div>

State Management and Data Fetching

Maintain form state and comment lists within the parent component. When refreshing the list after submission, capture the current vertical scroll position and restore it after the DOM updates to prevent jarring viewport jumps.

data() {
  return {
    postDetails: {},
    isFetching: false,
    pageSize: 10,
    draft: {
      author: '',
      contact: '',
      site: '',
      body: '',
      postId: null,
      parentId: null
    },
    threadData: []
  };
},
methods: {
  async loadThread() {
    this.isFetching = true;
    const scrollPos = window.scrollY;
    try {
      const response = await api.fetchComments(this.postDetails.id, this.pageSize);
      if (response.status === 'success') {
        this.threadData = response.payload;
      }
    } catch (error) {
      console.error('Failed to load comments:', error);
    } finally {
      this.isFetching = false;
      this.$nextTick(() => window.scrollTo(0, scrollPos));
    }
  },
  handlePublish() {
    // Validate and submit draft
    this.loadThread();
  }
}

Comment Node Component

Encapsulate individual comment rendering, timestamp formatting, and reply togggling. Use a local identifier to control the visibility of the reply interface without affecting sibling nodes.

<template>
  <article class="node-wrapper">
    <div class="avatar-section">
      <a v-if="node.site && node.contact" :href="node.site" target="_blank" rel="noopener">
        <img :src="generateAvatar(node.contact)" alt="User Avatar" loading="lazy" />
      </a>
      <img v-else src="@/assets/default-avatar.svg" alt="Default" loading="lazy" />
    </div>
    <div class="content-section">
      <header class="meta-header">
        <a class="author-link" :href="node.site" target="_blank" rel="noopener">{{ node.author }}</a>
        <span class="timestamp">{{ formatTimestamp(node.createdAt) }}</span>
      </header>
      <p class="message-body">{{ node.body }}</p>
      <footer class="action-bar">
        <button class="action-btn" @click="toggleReply(node.identifier)">Reply</button>
        <span class="vote-count"><i class="icon-up"></i> {{ node.upvotes || 0 }}</span>
      </footer>

      <div v-if="activeReplyId === node.identifier" class="reply-interface">
        <div class="reply-input-area">
          <textarea :placeholder="`Responding to @${node.author}...`" rows="3"></textarea>
        </div>
        <div class="reply-actions">
          <button class="submit-reply">Send</button>
        </div>
      </div>
    </div>
  </article>
</template>

<script>
import { formatRelativeTime } from '@/utils/dateHelpers';

export default {
  name: 'CommentNode',
  props: {
    node: { type: Object, required: true }
  },
  data() {
    return {
      activeReplyId: null
    };
  },
  methods: {
    formatTimestamp(ts) {
      return formatRelativeTime(ts);
    },
    generateAvatar(email) {
      const hash = email.split('@')[0];
      return `https://avatar.service.com/u/${hash}?size=80`;
    },
    toggleReply(id) {
      this.activeReplyId = this.activeReplyId === id ? null : id;
    }
  }
};
</script>

Interface Styling

Apply scoped styles to structure the layout, handle avatar scaling, and format the repply input area. Flexbox ensures consistent alignment across varying content lengths.

<style scoped>
.node-wrapper {
  display: flex;
  gap: 1rem;
  padding: 1.5rem 0;
  border-bottom: 1px solid #e0e0e0;
  background: #fafafa;
}
.avatar-section img {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid #ccc;
  transition: transform 0.2s ease;
}
.avatar-section img:hover {
  transform: scale(1.1);
}
.content-section {
  flex: 1;
  min-width: 0;
}
.meta-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}
.author-link {
  font-weight: 600;
  color: #333;
  text-decoration: none;
}
.author-link:hover { color: #0056b3; }
.timestamp { font-size: 0.85rem; color: #888; }
.message-body {
  margin: 0.75rem 0;
  line-height: 1.5;
  word-break: break-word;
  color: #222;
}
.action-bar {
  display: flex;
  align-items: center;
  gap: 1rem;
  font-size: 0.9rem;
  color: #666;
}
.action-btn {
  background: none;
  border: none;
  cursor: pointer;
  color: #666;
  padding: 0;
  font-size: inherit;
}
.action-btn:hover { color: #0056b3; }
.reply-interface {
  margin-top: 1rem;
  padding: 1rem;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.reply-input-area textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 6px;
  resize: vertical;
  font-family: inherit;
}
.reply-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.75rem;
}
.submit-reply {
  padding: 0.5rem 1.25rem;
  background: #0056b3;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.submit-reply:hover { background: #004494; }
</style>
Tags: Vue.js

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.