Home > database >  vue3 child component won't load with computed data as property
vue3 child component won't load with computed data as property

Time:02-09

Here's the problem I'm having. I have a Leads page, that is my Leads.vue template. It loads my leads and then passes the leads data to other components via props.

The LeadSources component receives a computed method as it's property. You can see that on the Leads.vue page, the LeadSources component calls the getSourceData() method for it's property data.

When I check the value of the props for LeadSources.vue in the setup() the value for chartData initially is an empty array. If the page hot-reloads then the LeadSources apexchart will populate with the :series data but otherwise I cannot get it to work.

Essentially it works like this.

Leads.vue passes getSourceData() to the LeadsSources.vue component which on the setup() sets it as the variable series and tries to load the apexchart with it.

It will not work if I refresh my page but if I save something in my IDE, the hot-reload will load the updated apexchart and the data appears. It seems like the prop values don't get set in the setup() function the first time around. How do I architecturally get around this? What's the proper way to set this up? I can't tell if the issue is on the leads.vue side of things or with the way that the LeadSources.vue component is being put together.

Any help would be appreciated, I've spent way too long trying to get this to work properly.

Leads.vue

<template>
  <!--begin::Leads-->
  <div >
    <div >
      <LeadTracker
        :lead-data="leadData"
        :key="componentKey"
      />
    </div>
  </div>

  <div >
    <div >
      <LeadSources
        chart-color="primary"
        chart-height="500"
        :chart-data="getSourceData"
        :chart-threshold="sourceThreshold"
        widget-classes="lead-sources"
      ></LeadSources>
    </div>
  </div>
  <!--end::Leads-->
</template>

<script lang="ts">
import { defineComponent, defineAsyncComponent, onMounted } from "vue";
import { setCurrentPageTitle } from "@/core/helpers/breadcrumb";
import LeadSources from "@/components/leads/sources/LeadSources.vue";
import LeadTracker from "@/components/leads/tracker/LeadTracker.vue";
import LeadService from "@/core/services/LeadService";
import ApiService from "@/core/services/ApiService";
import {Lead} from "@/core/interfaces/lead";

export default defineComponent({
  name: "leads",
  components: {
    LeadTracker,
    LeadSources
  },
  data() {
    return {
      leadData: [] as Lead[],
    }
  },
  beforeCreate: async function() {
    this.leadData = await new LeadService().getLeads()
  },
  setup() {
    onMounted(() => {
      setCurrentPageTitle("Lead Activity");
    });

    const sourceThreshold = 5;

    return {
      sourceThreshold,
      componentKey: 0
    };
  },
  computed: {
    getSourceData() {
      interface SingleSource {
        source: string;
        value: number;
      }
      const sourceData: Array<SingleSource> = [];

      // Make array for source names
      const sourceTypes = [];
      this.leadData.filter(lead => {
        if (!sourceTypes.includes(lead.source)) sourceTypes.push(lead.source);
      });

      // Create objects for each form by name, push to leadSourceData
      sourceTypes.filter(type => {
        let totalSourceLeads = 1;
        this.leadData.filter(form => {
          if (form.source == type) totalSourceLeads  ;
        });

        const leadSourceData = {
          source: type,
          value: totalSourceLeads
        };
        sourceData.push(leadSourceData);
      });

      // Sort by source popularity
      sourceData.sort(function(a, b) {
        return a.value - b.value;
      });

      return sourceData;
    }
  }
});
</script>

LeadSources.vue

<template>
  <!--begin::Lead Sources Widget-->
  <div : >
    <!--begin::Body-->
    <div
      
    >
      <div >
        <!--begin::Text-->
        <div >
          <span >Lead Sources</span>
          <span >Where your leads are coming from.</span>

          <!--begin::Chart-->
          <div >
            <apexchart
              
              :options="chartOptions"
              :series="series"
              type="donut"
              :height="chartHeight"
              :threshold="chartThreshold"
            ></apexchart>
          </div>
          <!--end::Chart-->
        </div>

        <!--begin::Unused Data-->
        <div >
          <div >
            <div><span >Other Sources:</span></div>
            <div v-for="item in otherSources" :key="item.source">
              <span>{{ item.source }}</span>
              <span>{{ item.value }}%</span>
            </div>
          </div>
          <div >
            <div><span >Sources Not Utilized:</span></div>
            <div v-for="item in unusedSources" :key="item.source">
              <span>{{ item.source }}</span>
            </div>
          </div>
        </div>
        <!--end::Unused Data-->
      </div>
    </div>
  </div>
  <!--end::Lead Sources Widget-->
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  name: "LeadSource",
  props: {
    widgetClasses: String,
    chartColor: String,
    chartHeight: String,
    chartLabels: Array,
    chartData: Array,
    chartThreshold: Number
  },
  setup(props) {
    const sum = (data) => {
      let total = 0;
      data?.map(function(v) {
        total  = v;
      });
      return total;
    }

    const chartData = ref(props.chartData).value;

    const threshold = ref(props.chartThreshold).value;

    const usedSourcesLabel: string[] = [];
    const usedSourcesData: number[] = [];
    const otherSources: any = [];
    const unusedSources: any = [];

    const splitData = (data, max) => {
      // set used, other sources < 5%, unused sources
      data.filter((item) => {
        if (item.value > max) {
          usedSourcesLabel.push(item.source);
          usedSourcesData.push(item.value);
        } else if (item.value < max && item.value != 0 && item.value !== null) {
          otherSources.push(item);
        } else if (item.value == 0 || item.value === null) {
          unusedSources.push(item);
        }
      });
    };

    splitData(chartData, threshold);

    const chartOptions = {
      chart: {
        width: 380,
        type: "donut"
      },
      colors: [
        "#1C6767",
        "#CD2E3B",
        "#154D5D",
        "#F1D67E",
        "#4F9E82",
        "#EF8669",
        "#393939",
        "#30AEB4"
      ],
      plotOptions: {
        pie: {
          startAngle: -90,
          endAngle: 270
        }
      },
      dataLabels: {
        enabled: false
      },
      fill: {
        type: "gradient",
        gradient: {
          type: "horizontal",
          shadeIntensity: 0.5,
          opacityFrom: 1,
          opacityTo: 1,
          stops: [0, 100],
        }
      },
      legend: {
        show: true,
        position: "left",
        fontSize: "16px",
        height: 220,
        onItemClick: {
          toggleDataSeries: false
        },
        onItemHover: {
          highlightDataSeries: false
        },
        formatter: function (val, opts) {
          return val   " - "   opts.w.globals.series[opts.seriesIndex];
        }
      },
      title: {
        text: undefined
      },
      tooltip: {
        style: {
          fontSize: "14px"
        },
        marker: {
          show: false
        },
        y: {
          formatter: function(val) {
            return val   "%";
          },
          title: {
            formatter: (seriesName) => seriesName,
          },
        }
      },
      labels: usedSourcesLabel,
      annotations: {
        position: "front",
        yaxis: [{
          label: {
            text: "text annotation"
          }
        }],
        xaxis: [{
          label: {
            text: "text xaxis annotation"
          }
        }],
      },
      responsive: [{
        breakpoint: 480,
        options: {
          legend: {
            position: "bottom",
            horizontalAlign: "left"
          }
        }
      }]
    };
    const series = usedSourcesData;
    return {
      chartOptions,
      series,
      otherSources,
      unusedSources
    };
  }
});
</script>

Edited

I will attach the LeadService.ts class as well as the ApiService.ts class so you can see where the data is coming from

LeadService.ts

import ApiService from "@/core/services/ApiService";
import {Lead} from "@/core/interfaces/lead";

export default class LeadService {

  getLeads() {

    const accountInfo = JSON.parse(localStorage.getItem('accountInfo') || '{}');

    ApiService.setHeader();

    return ApiService.query("/leads", {params: {client_id : accountInfo.client_id}})
      .then(({ data }) => {
        let leadData: Lead[] = data['Items'];
        return leadData;
      })
      .catch(({ response }) => {
        return response;
      });

  }
}

ApiService.ts

import { App } from "vue";
import axios from "axios";
import VueAxios from "vue-axios";
import JwtService from "@/core/services/JwtService";
import { AxiosResponse, AxiosRequestConfig } from "axios";
import auth from "@/core/helpers/auth";

/**
 * @description service to call HTTP request via Axios
 */
class ApiService {
  /**
   * @description property to share vue instance
   */
  public static vueInstance: App;

  /**
   * @description initialize vue axios
   */
  public static init(app: App<Element>) {
    ApiService.vueInstance = app;
    ApiService.vueInstance.use(VueAxios, axios);
    ApiService.vueInstance.axios.defaults.baseURL = "https://api.domain.com/";
  }

  /**
   * @description set the default HTTP request headers
   */
  public static setHeader(): void {
    ApiService.vueInstance.axios.defaults.headers.common[
      "Authorization"
    ] = `Bearer ${auth.getSignInUserSession().getIdToken().jwtToken}`;
    ApiService.vueInstance.axios.defaults.headers.common[
      "Content-Type"
    ] = "application/json application/vnd.api json";
  } 

  /**
   * @description send the GET HTTP request
   * @param resource: string
   * @param params: AxiosRequestConfig
   * @returns Promise<AxiosResponse>
   */
  public static query(
    resource: string,
    params: AxiosRequestConfig
  ): Promise<AxiosResponse> {
    return ApiService.vueInstance.axios.get(resource, params).catch(error => {
      // @TODO log out and send home if response is 401 bad auth
      throw new Error(`[KT] ApiService ${error}`);
    });
  }
}

export default ApiService;

CodePudding user response:

I think the issue is caused when you're calling the data from the api. This code:

beforeCreate: async function() {
    this.leadData = await new LeadService().getLeads()
},

I'd refactor to

async created () {
    const service = new LeadService()
    const value = await service.getLeads()
}

Also it would be nice to be able to see how you're fetching your data. Sometimes this code: const value = await axios.get('/api/getStuff').data can be problematic because of paratheses issues. Which causes the same issue you described of, hot reloading working, but fresh not. I suspect the same sort of issue relies of the code executing like => (await new LeadService()).getLeads() Where you're probably awaiting the class, rather than the actual async code.

  •  Tags:  
  • Related