Search code examples
javascriptvue.jsgoogle-cloud-firestoremomentjsvuex

Vue momentjs update relative time in real time from timestamp


I'm building a forum app and with v-for, I want to show when all the comments were posted with momentjs relative time, the problem is that the render never changes from 'A few seconds ago'. In this question, the response show to achieve what I want, basically using an interval when the component is created:

  created() {
    setInterval(() => {
      this.messages = this.messages.map(m => {
        m.ago = moment(m.time).fromNow();
        return m;
      });
    }, 1000);
  }

But due to that I fetch the data directly from vuex, I don't get how to integrate the solution with my code.

This is my store.js:

export default new Vuex.Store({
  state: {
    comments: [],
  },
  mutations: {
    loadComments: (state, comments) => {
      state.comments = comments;
    },
  },
  actions: {
    loadComments: async (context) => {
      let snapshot = await db
        .collection("comments")
        .orderBy("timestamp")
        .get();
      const comments = [];
      snapshot.forEach((doc) => {
        let appData = doc.data();
        appData.id = doc.id;
        appData.timestamp = moment()
          .startOf(doc.data().timestamp)
          .fromNow();
        comments.push(appData);
      });
      context.commit("loadComments", comments);
    },
  },
});

And this my script:

import { db, fb } from "../firebase";
import { mapState } from "vuex";

export default {
  name: "Forum",
  data() {
    return {
      commentText: null,
    };
  },
  mounted() {
    this.getComment();
  },
  methods: {
    getComment() {
      this.$store.dispatch("loadComments");
    },
    async sendComment() {
      await db
        .collection("comments")
        .add({
          content: this.commentText,
          author: this.name,
          timestamp: Date.now()
        });
      this.commentText = null;
      this.getComment();
    }
  },
  created() {
    let user = fb.auth().currentUser;
    this.name = user.displayName;
  },
  computed: mapState(["comments"])
};

Solution

  • It's best to abstract all of this logic into a single component for displaying dates, rather then needing to trigger updates across code boundaries. Keep all your date rendering code as minimal as possible so that it is easy to change your date rendering logic at any time.

    The date itself is data, but the visual representation of that date is not. So I wouldn't store the formatted date in the Vuex store.

    Create a component <from-now> which takes a date as a prop and renders the date using moment's fromNow. It also takes care of updating itself periodically (every minute). Since it is a component, you can display the full date as a tooltip so that users can know the exact time when they mouse over it.

    const components = new Set();
    
    // Force update every component at the same time periodically
    setInterval(() => {
      for (const comp of components) {
        comp.$forceUpdate();
      }
    }, 60 * 1000);
    
    Vue.component('FromNow', {
      template: '<span :title="title">{{ text }}</span>',
      props: ['date'],
      
      created() {
        components.add(this);
      },
      
      destroyed() {
        components.remove(this);
      },
      
      computed: {
        text() {
          return moment(this.date).fromNow();
        },
        
        title() {
          return moment(this.date).format('dddd, MMMM Do YYYY, h:mm:ss a');
        },
      },
    });
    
    new Vue({
      el: '#app',
      
      data: {
        // Test various seconds ago from now
        dates: [5, 60, 60 * 5, 60 * 60].map(sec => moment().subtract(sec, 'seconds')),
      },
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
    
    <div id="app">
      <p v-for="date of dates">
        <from-now :date="date"></from-now>
      </p>
    </div>

    For the sake of simplicity, <from-now> only displays relative dates. I use a similar component to this, but more general-purpose. It takes a format prop which allows me to choose how I want the date to be displayed (it can be a relative time, or a specific date format that moment accepts).