Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to access component methods or data #61

Closed
cedric5 opened this issue Nov 11, 2021 · 6 comments
Closed

Unable to access component methods or data #61

cedric5 opened this issue Nov 11, 2021 · 6 comments
Assignees
Labels
bug Something isn't working

Comments

@cedric5
Copy link

cedric5 commented Nov 11, 2021

Describe the bug
I'm trying to call a method within my component when ever I receive a websocket call.

However method or data calls within one of the channel methods (connected, received etc.) will throw an error.

How do I access my the data and methods of my component?

To Reproduce
Like the documentation I have declared my channel like this in the component.

    UpdateChannel: {
      received (data) {
        console.log('candidates:', this.candidates)
        this.fetchCandidates()
      }
    }
mounted () {
    this.$q.loading.show()
    this.$cable.subscribe({ channel: 'UpdateChannel', organization_id: this.currentOrg.id })
    this.fetchCandidates()
  }

Expected behaviour

  • Being able to read/write component data objects
  • Being able to call component methods

Screenshots
Whenever the the websocket update is triggerd
image

Plugin version (please complete the following information):
In my yarn.lock

actioncable-vue@^2.5.0:
  version "2.5.0"
  resolved "https://registry.yarnpkg.com/actioncable-vue/-/actioncable-vue-2.5.0.tgz#b72689b7d1bb4d3b52ad21d23b55fc70573e2067"
  integrity sha512-KIYvMZ60DWUjHESWUOTDXBptpUzdAX3OlgRc8A4NPOy+8sYTXUnioqd7EDQ/gPj/tFCH3WA/ipqPXfnfVZEJqA==
  dependencies:
    "@rails/actioncable" "^6.0.2"
    "@types/actioncable" "^5.2.3"

Additional context
I am using quasar

@cedric5 cedric5 added the bug Something isn't working label Nov 11, 2021
@cedric5
Copy link
Author

cedric5 commented Dec 22, 2021

Still experiencing this issue.
Is this project still maintained?

@x8BitRain
Copy link
Contributor

Still experiencing this issue. Is this project still maintained?

Where is this.fetchCandidates() located within the file? Can you please post a the full component or link a repo?

@cedric5
Copy link
Author

cedric5 commented Jan 12, 2022

A while ago I changed something that resolved this issue, I could not pinpoint what it was.
But today I fell upon the same issue again and cant figure it out what is wrong exactly

So now I have a component where everything is working as expected:

.row(style="height:100%")
  .col-md-9(style="width:100%; height:92%;")
    q-scroll-area(ref='scrollArea' style='height: 100%; max-width: 100%;')
      div(v-if="messages.length > 0"  v-for="message in messages" style='width: 100%; padding-left:24px; padding-right:24px;')
        q-chat-message(v-if="message.originatorRole === 'bot' || message.originatorRole === 'user'" name='Agent' avatar='https://recrubo.com/wp-content/themes/recrubo/dist/images/recrubo-logo.png' :text="[message.fallback]" sent :stamp='message.createdAt')
        q-chat-message(v-if="message.originatorRole === 'external'" :name='conversation.candidate.name' avatar='https://icon-library.com/images/avatar-icon-png/avatar-icon-png-11.jpg' :text="[message.fallback]" :stamp='message.createdAt')
      div(v-else)
        .text-center.absolute-center.text-body1 Please select a conversation
  .row(style="width: 100%;")
    q-input(@keyup.enter="sendMessage()" autogrow  style="padding-left:8px; padding-right:12px; width:100%;" bg-color="white" rounded outlined v-model='message' label='Type your message..')
      template(v-slot:append)
        q-btn(@click="sendMessage()" round dense flat icon='send')
      template(v-slot:after)
</template>
<script>
import { defineComponent } from 'vue'
import _ from 'lodash'
import backendApi from '@/api/backend'
import { mapGetters } from 'vuex'

export default defineComponent({
  channels: {
    ChatChannel: {
      received (data) {
        const message = data.attributes
        message.id = data.id
        this.messages.push(message)
        this.messages = _.orderBy(this.messages, ['createdAt'], ['ASC'])
        this.scrollToBottom()
        const conversation = _.cloneDeep(this.conversation)
        conversation.unreadMessages = false
        this.$emit('updateConversation', conversation)
        backendApi.update('conversation', this.conversation).then(() => {}) // Updating unread status in backend
      }
    }
  },
  props: {
    conversation: Object
  },
  data () {
    return {
      messages: [],
      message: ''
    }
  },
  computed: {
    ...mapGetters('user', ['currentOrg'])
  },
  methods: {
    fetchMessages () {
      this.$q.loading.show()
      backendApi.find('flow-thread', this.conversation.flowThread.id, { include: ['messages'] })
        .then((data) => {
          this.messages = []
          this.messages.push(data.data.messages)
          this.messages = _.flatten(this.messages)
          this.messages = _.orderBy(this.messages, ['createdAt'], ['ASC'])
        }).finally(() => {
          this.scrollToBottom()
        })
    },
    sendMessage () {
      backendApi.request(process.env.API_URL + '/messaging/send-message/' + this.conversation.flowThread.id, 'POST', {}, { message: this.message })
        .then((data) => {
          this.message = ''
          this.messages.push(data.data)
          this.messages = _.flatten(this.messages)
          this.messages = _.orderBy(this.messages, ['createdAt'], ['ASC'])
        }).finally(() => {
          this.scrollToBottom()
        })
    },
    scrollToBottom () {
      setTimeout(() => {
        this.$refs.scrollArea.setScrollPercentage('vertical', 1)
        this.$q.loading.hide()
      }, 300)
    }
  },

  mounted () {
    this.$cable.subscribe({
      channel: 'ChatChannel',
      thread_id: this.conversation.flowThread.id
    })
  },
  watch: {
    conversation: {
      immediate: true,
      handler () {
        this.fetchMessages()
      }
    }
  }

})
</script>

And the other component that I was working on today where I am experiencing the same issue as described before.

.row
  .col-md-12(style="height: 50%;")
    q-infinite-scroll(@load='onload' :offset='500')
      div(v-if="conversations.length > 0" v-for='conversation in conversations' :key='conversation.id')
        q-intersection.example-item(transition='scale')
          q-item(clickable v-ripple @click="setSelectedConversation(conversation)")
            q-item-section(avatar)
              q-avatar(color='primary' text-color='white')
                | {{ avatarInitials(conversation.candidate) }}
            q-item-section
              q-item-label {{ conversation.candidate.fullName }}
              q-item-label(caption lines='1') {{ conversation.flowThread.channel }}
            q-item-section(side v-if="conversation.unreadMessages")
              q-icon(name='fiber_manual_record' size='xs' color='green')
      div(v-if="allConversationsLoaded")
        q-separator(style="margin-top:12px" inset)
        .text-subtitle1.text-center(style="margin-top:12px; color: #909396; padding-bottom:24px;") All conversations loaded
      template(v-if="!allConversationsLoaded" v-slot:loading)
        .row.justify-center.q-my-md
          q-spinner-dots(color='primary' size='40px')
</template>
<script>
import { defineComponent } from 'vue'
import _ from 'lodash'
import backendApi from '@/api/backend'
import { mapGetters } from 'vuex'

export default defineComponent({
  channels: {
    ConversationChannel: {
      received (data) {
        const conversation = data.attributes
        conversation.id = data.id
        const index = _.findIndex(this.conversations, { id: conversation.id })
        this.conversations[index] = conversation
      }
    }
  },
  data () {
    return {
      conversations: [],
      conversationPage: 1,
      maxConversationPage: 0
    }
  },
  computed: {
    ...mapGetters('user', ['currentUser']),
    allConversationsLoaded () {
      return this.conversationPage > this.maxConversationPage
    }
  },
  methods: {
    avatarInitials (candidate) {
      return candidate.name.substr(0, 1).toUpperCase()
    },
    onload (index, done) {
      if (!this.allConversationsLoaded) {
        setTimeout(() => {
          this.conversationPage += 1
          backendApi.findAll('conversations', { page: this.conversationPage, size: 30, include: ['user', 'candidate', 'flowThread'] })
            .then((data) => {
              this.maxConversationPage = data.meta.pageCount
              this.conversations = _.concat(this.conversations, data.data)
            })
            .finally(() => {
              done()
            })
        }, 2000)
      } else {
        done()
      }
    },
    setSelectedConversation (conversation) {
      conversation.unreadMessages = false
      backendApi.update('conversation', conversation).then(() => {}) // Updating unread status in backend
      this.$emit('selectedConversation', conversation)
    },
    fetchConversations () {
      backendApi.findAll('conversations', { page: this.conversationPage, size: 30, include: ['user', 'candidate', 'flowThread'] })
        .then((data) => {
          this.maxConversationPage = data.meta.pageCount
          this.conversations = _.concat(this.conversations, data.data)
        })
        .finally(() => {
          this.fetching = false
        })
    }
  },
  mounted () {
    this.fetchConversations()
    this.$cable.subscribe({
      channel: 'ConversationChannel',
      organization_id: this.currentUser.organization.id
    })
  }
})
</script>

Which results in a Uncaught TypeError: this.conversations is undefined
Neither calling a method as in the issue description is working

I have tried recompiling multiple times.

@cedric5
Copy link
Author

cedric5 commented Jan 12, 2022

After some hours of debugging I found the following:

I made a simple test component named Test without all the clutter.

<template lang="pug">
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
  channels: {
    ConversationChannel: {
      connected () {
        console.log('connected')
      },
      received (data) {
        this.conversations.push(data)
        console.log(this.conversations)
      }
    }
  },
  data () {
    return {
      conversations: []
    }
  },
  mounted () {
    this.$cable.subscribe({
      channel: 'ConversationChannel',
      organization_id: '2f406421-ca98-43f7-9c6a-d6183b935afa'
    })
  }

})
</script>

I have a page where I initialize this component

<template lang="pug">
div
  q-splitter(v-model='splitterModel' style='height: 92.5vh;')
    template(v-slot:before)
      conversations(@selectedConversation="setSelectedConversation($event)")
    template(v-slot:after)
      q-splitter(v-model='splitterModel2')
        template(v-slot:before)
          test(v-if="selectedConversation")
        template(v-slot:after)
          .col-md-3
            candidate-card(v-if="selectedConversation" :basicMode="true" :candidate="selectedConversation.candidate")
</template>

Whener I use a v-for to decide to render the component it all works as expected.
How ever if I remove the v-for the above mentioned errors appear.

test(v-if="selectedConversation") works
test() does not work

edit:

Using test(v-if="selectedConversation") works but if you afterwards subscribe to another channel in another component but within the same parent component the errors recur again. It looks like the context of the channels {} block seems to be changing and can not handle multiple subscription within the same (parent) component.

I will try to create a demo project one of these days.

@phlegx
Copy link

phlegx commented Feb 14, 2024

" It looks like the context of the channels {} block seems to be changing and can not handle multiple subscription within the same (parent) component."

Can confirm! If the subscription is in a parent component, the child components with subscriptions have the context of the parent.

@phlegx
Copy link

phlegx commented Feb 14, 2024

The problem is, that actioncable-vue uses context._uid in some parts of the code (e.g. src/cable.js#L231).

Vue 3 don't uses this._uid anymore. For the options API it can be retrieved with this.$.uid. For composition API the method call getCurrentInstance().uid should be used.

Here two workarounds for options API and composition API.

Options API

/* cable.js: Small constant export to use it as mixin. */
export const ActionCable = {
  beforeCreate() {
    this._uid = this.$.uid
  },
}

/* In the component include cable.js as mixin. */
import { ActionCable } from 'cable.js'
export default {
  ...
  mixins: [ActionCable],
  ...
}

Composition API

import { getCurrentInstance } from 'vue'
...
app.mixin({
  beforeCreate() {
    this._uid = getCurrentInstance().uid
  },
})

Related to #49: created hook want work, so we need to use beforeCreate hook.
@mclintprojects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants