Correctly outputting data from two looped arrays in Vue.JS

Viewed 147

Im working on this basic chat app in Vue.JS that acts like a chat bot concept where the user’s input triggers a written response. It works but I only know how to output the input and the response such as below. The problem with this is that the {{ data.message }} and {{ data.response }} are looped under eachother, giving no order to the convo.

The user's input get stored in the 'message' array. The message gets looped

<div class="userMessageC">
  <div class="userMessage" v-for="(data, index) in messages" :key='index'>
    {{ data.message }}
  </div>
</div>

while the bot's responses get stored in the responses array.

<div class="botMessageC" v-for="(data, index) in responses" :key='index'>
  <span class="botAvatar">
    <img src="@/assets/pimAvatar.png" width="50"/> 
  </span>           
  <div class="botMessage">
    {{ data.response }} 
  </div>
</div>

What I want is that data.message is followed by data.response and then by data.message and then data.response and so on. How would I approach this? This is how it looks (out of order)

enter image description here.

A jsfiddle to give you better idea

4 Answers

Because your "bot" is pretty much mocked, your problem is greatly simplified: it has the same response time and the results come back in the same order as they were sent. Which is not always true for a real server.

What you want to do is create an utterance or pair entity which holds both message and response, call the server from a method returning a promise and, when fulfilled, add the response to the utterance. In template all you need to do is loop through utterances:

data: () => ({
  utterances: []
})
methods: {
  sendRequest(message) {
    const utterance = { message, response: '' };
    this.utterances.push(utterance);
    this.outputResponse(message).then(response => utterance.response = response);
  },
  outputResponse(message) {
    // do whatever you want to get response and return a resolved promise:
    return Promise.resolve(response);
  }
}

The template would look like this:

<div v-for="(u, key) in utterances" :key="key">
  <div v-text="u.message" />
  <div v-text="u.response" />
</div>

In a real life scenario, where the call to bot might actually be decoupled from your component, you'll likely want to give each utterance an uuid and map the response back to the utterance based on that unique identifier.
Also, recording the timestamp at which the message was input opens your door to storing and re-rendering them at a future time in the order they were sent.


The outputResponse method is a bit over-simplified above. Calling an actual server would look something like this:

outputResponse(message) {
  return axios.get('https://your.bot', params: { message })
    .then(response => response.data)
}

... assuming the bot responds with a simple string.


See it working.
To make it act more like a real life server, I made the "bot" response take a variable time, between 1 and 15 seconds:

Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
  el: '#app',
  data: () => ({
    utterances: [],
    message: ''
  }),
  methods: {
    send(message) {
      const utterance = {
        message,
        response: ''
      };
      this.utterances.push(utterance);
      this.getResponse(message).then(response => utterance.response = response);
      this.message = '';
    },
    getResponse: message => new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`<pre> => </pre>${message} back...`);
      }, (Math.floor(Math.random() * 15) + 1) * 1e3)
    })

  }
})
pre {
  margin: 2px 0 0;
  display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <div>
    <input v-model="message" @keyup.enter="send(message)">
    <button @click="send(message)">Send</button> (or press enter)
  </div>
  <div v-for="(u, k) in utterances" :key="k" style="display: flex">
    <div v-text="u.message"></div>
    <div v-html="u.response"></div>
  </div>
</div>

You could reference the index of the response like below. So it will show message on top and then response associated to that message below. You can use a ternary to do error handling as well in case there is a message but no response your code will not break

<div class="userMessageC">
  <div class="userMessage" v-for="(data, index) in messages" :key='index'>
    <p>{{ data.message }}</p>
   <p> {{responses[index].response ? responses[index].response : ""}}}</p>
  </div>
</div>

Well, I think you can use a computed property to update your messages and have the right order for both, questions and answers.

I don't understand well if is what you want to achieve, but is what I can understand.

Now, I suggest to identify every message with and Id (messageId) and attach the id to the response, then you can filter the response and put it in place once you get any response in any time.

I did some updates to your jsfiddle and you can see it here (I guess, I don't know very well how it works)

The template, if you want to have message then response, message then response, the better way is to have all in the same v-for statement:

<div class="messagesC"  v-for="(data, index) in rightOrder" :key='index'>
  <p class="userMessage">
    {{ data.message }}
  </p>
  <p class="botMessage">
    {{ data.response }}
  </p>            
</div>

Now in your methods you can add the Id

...
sendResponse() {
  const messageId = Math.random().toString(16).slice(2); // Generate uuid
  this.messages.push({
    message: this.message,
    messageId,
  }) // Push message with its id
  this.outputResponse(this.message, messageId);
},

outputResponse(input, messageId){ // receives message and messageId
  this.message = '';
  this.timer = setTimeout(() => {
    try {
        var output = input + "=" + eval(input);
    } catch (e) {
        var utterance = [
          ["hello"],
          ],intent = [
            ["hello back!"]
            ],fallback = ["this is a response to your input"];
        var text = (input.toLowerCase()).replace(/[^\w\s\d]/gi, "");    
        if (this.compare(utterance, intent, text)) {
            output = this.compare(utterance, intent, text);
        } else {
            output = fallback[Math.floor(Math.random() * fallback.length)];
        } 
    }
      this.responses.push({
       response: output,
       messageId
      }) // send back the response and the messageId
  }, 1000);
}
...

Now the computed property

computed: {
  // computed property that searches for an answer
  // taking into account the message order
  rightOrder() {
    return this.messages.map(message => {
      const response = {
      // Search a response with the messageId
      ...this.responses.find(
        response => response.messageId === message.messageId
      )}
      // return the message, messageId and its response
      return {
        message: message.message,
        ...response
      }
    })
  }
},

And finally the CSS

#window {
  float: left;
  width: 100%;
}
.userMessage {
  flex: flex-shrink;
  margin-right: 10px;
}
.botMessage {
  flex: flex-shrink;
}
.messagesC {
  display: flex;
  flex-wrap: wrap;
}
.click {
  float: left;
  background: black;
  color: white;
  padding: 0.5%;
}

You can send message without wait for the response, once the responses has a new response, it will be shown right after the message

I'd probably created a computed property that combined messages and responses and iterate over that

computed: {
  messagesAndResponses() {
    return this.messages.map((m, i) =>
      ({message: m.message, response: this.responses[i]})
    );
  }

then in the template

<div 
    v-for="(m,i) in messagesAndResponses"
    :key="i"
>
  <div class="userMessageC">
    <div class="userMessage">
      {{ m.message }}
    </div>
  </div>
  <div class="botMessageC">
    <span class="botAvatar">
      <img src="@/assets/pimAvatar.png" width="50"/> 
    </span>           
    <div class="botMessage">
      {{ m.response }} 
    </div>
  </div>
</div>
Related