Help with better design concerning components

Hey there! I am building an app that will render different charts based on a conditional data set. So first I have a route that fetches all the charts’ metadata:

//routes/charts.js
import Route from '@ember/routing/route';
import {
  inject as service
} from '@ember/service';

export default class ChartsRoute extends Route {
  model(params) {
    return this.store.findAll('chart-metadata');
  }
}

This will return infos about the charts that I will use to retrieve the actual data to build each chart.

For each object returned I do this:

//templates/charts.hbs
{{#each this.model as |chart|}}
    <ChartCard @chartInfo1={{chart.chartInfo1}} @chartInfo2={{chart.chartInfo2}} as |firstDataSet secondDataSet|>
       {{debugger}}
      <!-- Here I get empty chartInfo1 and empty chartInfo2 -->
       <LineChart @firstDataSet={{firstDataSet}} @secondDataSet={{secondDataSet}} />
    </ChartCard>
  {{/each}}

And here’s the component:

//components/chart-card.js
export default class ChartCardComponent extends Component {
  @service store;
  @tracked firstDataSet = A([]);
  @tracked secondDataSet = A([]);
  @tracked chartData = A([]);

  constructor(owner, args) {
    super(owner, args);
    this.fetchData();
  }

  async fetchData() {
    this.chartData = await this.store.query('chart-data', {chartInfo1: this.args.chartInfo1, chartInfo2: this.args.chartInfo2});

   //here I need to manipulate chartData to create the proper arrays to then finally render the charts.
   //that's what I have tried:
   
await Promise.all(this.chartData.map(async(rend) => {
  let objectKeys = Object.keys(rend.value);
  let objectValues = Object.values(rend.value);
  this.firstDataSet.pushObject({"date": rend.date, "value": objectValues[0]});
  this.secondDataSet.pushObject({"data": rend.date, "value": objectValues[1]});
}));
   
  }
}

And that’s the hbs file:

//components/chart-card.hbs
 {{yield this.carteiraDataSet this.benchmarkDataSet}}

Then I need to use the data that I’ve manipulated inside this component to generate the actual chart:

//components/line-chart.js
export default class LineChartComponent extends Component {
  @service store;

  @tracked scaleY;
  @tracked scaleX;
  @tracked margin;
  @tracked width;
  @tracked height;

  constructor(owner, args) {
    super(owner, args);
  }

...

  @action
  buildChart() {
    const dd = line().x(d => { debugger; return scaleX(d['data']); }).y(d => { debugger; return scaleY(d['valor']); });
    svg.append('path')
      .data([this.args.firstDataSet])
      .style('fill', 'none')
      .attr('id', 'priceChart')
      .attr('stroke', 'steelblue')
      .attr('stroke-this.width', '1.5')
      .attr('d', dd);
  }

}

I am not able to render the charts because the data manipulation that I am doing inside the ChartCard component is not synchronous and is not finished before the chart is rendered. I’m always getting empty arrays on firstDataSet and secondDataSet.

So my question is: what is the best way to deal with this case scenario? Am I building something with bad design (seems like it’s the case). What is the best approach to manipulate the data that I am fetching, pass it to the component that will build the charts and only then render the finished component on the screen? Calling the store inside the component is the best way?

Thanks in advance!

To do this you need to account for the asynchronicity. In your current design this would likely look like a loading state. Your <ChartCard> needs to expose a loading state that the inner template code can use to show a loading indicator. I typically do this using a hash. I also recommend ember-concurrency as it comes with built-in derived state like I’m talking about and also handles component clean up and canceling. But for this example I will approach it without.

{{yield (hash
  isLoading=this.isLoading
  firstDataSet=this.firstDataSet
  secondDataSet=this.secondDataSet
)}}
async fetchData() {
  let { chartInfo1, chartInfo2 } = this.args;
  this.isLoading = true;
  try {
    this.chartData = await this.store.query('chart-data', { chartInfo1, chartInfo2 });
  } finally {
    this.isLoading = false;
  }
  // There is no need for this logic to be async
  this.updateWithChartData();
}
updateWithChartData() {
  for (let rend of this.chartData) {
    let { date, value: [firstValue, secondValue] } = rend;
    this.firstDataSet.pushObject({ date, value: firstValue });
    this.secondDataSet.pushObject({ date, value: secondValue });
  }
}

Pretty cool. Thank you! It just worked. :wink: