CoreUI CSmartTable not playing nice with custom component in template

Hello! This is my first forum post as a new professional software developer so bear with me. I’m glad to be working with a framework I really enjoy that is intuitive and easy to understand, yet strong (Vue 3 app with django 3.1 and MySQL backend).

The app that I have been brought into uses CoreUI for Vue, which has a bunch of helpful and good looking components/styling (see Getting Started | Vue UI Components · CoreUI). One of them is CSmartTable, which can be used for displaying, sorting, filtering, and paginating data. Other CoreUI components also start with ‘C’, hence the CRow and CCol below.

Within the smart table, you can design templates to customize how each category/column is displayed, which I have done to include a custom component I made which allows the user to edit and save data in place. My component is called LiveEdit, and is included in the example below (template for the name column of a ‘sites’ smart table in the app).

<template #name="{ item }">
      <td class="py-2">
        <CRow class="mb-2">
          <CCol>
            <LiveEdit
              :initialContent="item.name"
              :endpoint="`/api/v1/sites/${item.id}/`"
              :attribute="'name'"
            />
          </CCol>
        </CRow>
      </td>
    </template>

Here’s where the problem starts.

This custom component works fine and renders nicely within the first, unsorted page of the table, but when I paginate, sort, or filter, the LiveEdits don’t change to reflect the true values.

For example: if there are two entries, one with name ‘test’ and the other with name ‘bug’, searching for ‘bug’ will eliminate the first and push the bug entry to the top of the list, but it’s ‘name’ section will still have the LiveEdit component that says ‘test’.

I know Vue rerenders components that are bound to changing variables, so I made a boolean flag that I “attached” to the template using v-if:

<template #name="{ item }" v-if="liveEditRefreshFlag">

Side note, let me know if this is bad practice because it feels like it…

Now the table refreshes the components based on the newly changed values properly, but only in my development instance. In production, it freezes the entire page. That’s because in dev I get this ominous warning:
Vue Infinite Recursion

I expect that the LiveEdit component is rendering the first time, and then putting the value in the ‘name’ cell into a prop of the text it is supposed to display. Then, when the table is remade and the v-ifs refresh the components, they are probably trying to put a LiveEdit component inside of another LiveEdit component creating some kind of dangerous loop.

Any workaround or advice on this would be greatly appreciated. I only imagined I’d turn to a forum when I have a complex problem that google couldn’t reasonably solve. If anyone wants to dive into the lab on how CoreUI and Vue are generating infinite recursion from templates, I think it would be very insightful if we figure it out.

See below for screenshots of the table, components, and pasted code to get a better idea of what I’m trying to deliver to the user.

Live edit component containing 1000

Live edit component containing 1000 AM

Live edit failing to update on search

Live edit failing to update on search AM

In Sites.vue

<CSmartTable
    :table-props="{
      striped: true,
      hover: true,
    }"
    :active-page="1"
    :items-per-page="10"
    header
    :items="
      !loading_sites ? (!show_removed ? non_deleted_items : deleted_items) : []
    "
    :columns="columns"
    @filtered-items-change="liveEditRefresh"
    @selected-items-change="liveEditRefresh"
  >
liveEditRefresh() {
      this.liveEditRefreshFlag = false
      this.liveEditRefreshFlag = true
},

In LiveEdit.vue

Treat this as an appendix. I hate posting entire files of code.
<template>
  <CButton
    class="p-1"
    v-if="!editing && $store.state.theme === 'dark'"
    @click="startEditing"
    color="light"
    style="width: 100%; min-height: 36px"
    variant="outline"
  >
    {{ content ? content : '' }}
  </CButton>
  <CButton
    class="p-1"
    v-else-if="!editing"
    @click="startEditing"
    color="dark"
    style="width: 100%; min-height: 36px"
    variant="outline"
  >
    {{ content ? content : '' }}
  </CButton>
  <div v-else style="width: 100%">
    <CFormTextarea
      id="focusTextArea"
      style="height: 36px; text-align: center"
      v-model="content"
      @keydown.enter="save"
      @keydown.esc="cancel"
      @focusout="
        ($event) => {
          save()
          // log($event)
        }
      "
    >
    </CFormTextarea>
    <div
      class="subtitle"
      style="height: 0px; font-size: 10px; text-align: center"
    >
      ([Enter] or click away to submit. [Esc] to cancel)
    </div>
  </div>
</template>

<script>
import { axios } from '@/common/api.service.js'

export default {
  name: 'LiveEdit',
  emits: ['refresh'],

  props: {
    // URL for API. Should include id when it gets passed: ex. "/api/v1/contacts/ + `${item.id}`"
    endpoint: String,
    // Name of attribute this LiveEdit contains: Ex. 'address'
    attribute: String,
    // Initial Value of this attribute: Ex. '1234 asdf lane'.
    initialContent: String,
  },

  data() {
    return {
      editing: false,
      content: this.initialContent,
    }
  },

  watch: {},

  methods: {
    startEditing() {
      this.editing = true
      // Magic to make the textarea focused every time. See https://stackoverflow.com/questions/58222526/how-to-set-autofocus-on-button-inside-modal-everytime-modal-pops-up
      // Don't need autoFocus in element either. That only works for first time
      // Also select all text for easy editing.
      this.$nextTick(() => {
        let ta = document.getElementById('focusTextArea')
        ta.focus()
        ta.select()
      })
    },

    save() {
      this.editing = false
      this.dynamicAPICall()
    },

    cancel() {
      this.editing = false
      this.content = this.initialContent
    },

    // Use this for logging in anonymous callbacks in components. Pretty dumb
    log(log) {
      console.log('Live edit component log', log)
    },

    async dynamicAPICall() {
      let outgoing_data = {}
      // Fancy hacky code to dynamically set LHS of outgoing_data: Ex. LHS can be anything. name, phone, email, etc...
      outgoing_data[`${this.attribute}`] = this.content
      // console.log(this.endpoint)
      // console.log(outgoing_data)
      try {
        // eslint-disable-next-line no-unused-vars
        const response = await axios({
          method: 'PATCH',
          url: this.endpoint,
          data: outgoing_data,
        })
        this.$emit('refresh')
      } catch (error) {
        this.$logAndAlertError(error)
      }
    },
  },
}
</script>

Why couldn’t my boss have started this project with typescript…