Photo by Fotis Fotopoulos on Unsplash
Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 9
Emitting Events in Vue 3
In the last article we handled errors in the response from our FastAPI backend.
We also added in a bit of extra code to process the data, and the postProcessData
method actually created data
and volData
elements, though we didn't do anything with it.
In this article we'll touch on emitting an event back to the parent component and passing data down to another component to use for charting purposes.
Passing data to App.vue
Finishing off processing data
We left off with data
and volData
elements defined in Selections.vue
, and we need to somehow pass them up into App.vue
and then down into another component that we'll create.
First, we need to actually return data from our postProcessData
method.
We could just emit the event directly from that method but it's better to keep each method focused on its own purpose.
Thus, the updated method will look like
const postProcessData = (res) => {
// Parse dates
const formatDate = (d) => new Date(d).toISOString().substr(0, 10);
const dates = res.map((e) => e.date).sort();
state.startDate = formatDate(dates[0]);
state.endDate = formatDate(dates[dates.length - 1]);
// Format the data
const data = res.map((e) => ({
x: new Date(e.date),
y: e.data,
}));
const volData = res.map((e) => ({
x: new Date(e.date),
y: e.volume ?? 0,
}));
return {
series: [{data}],
symbol: state.symbol,
interval: state.interval,
volume: [{name: 'volume', data: volData}]
}
};
We are returning the data in this manner because we have already set four data elements in App.vue
const series = ref([]);
const symbol = ref('');
const interval = ref('Daily');
const volume = ref([])
Thus we will want to set these values for use in the eventual chart component, so formatting the emitted data in this manner will make things easier in general.
The series
and volume
data are formatted the way they are because the charting platform we'll be using is ApexCharts and the data structure needs to be in the format set above.
Then we need to update handleSubmit
to get this data and emit it to the App.vue
component.
const handleSubmit = async () => {
v$.value.$validate();
if (!v$.value.$invalid) {
loading.value = true;
try {
const url = preProcessData();
const res = await fetchData(url);
const payload = postProcessData(res);
emit('setData', payload);
} catch (err) {
console.log('err', err);
dialog.value = true;
}
loading.value = false;
}
};
Emitting the event
In order to do this, we need to update our code in two other places.
First, we need to update the setup
method in Selections.vue
to include the emit
handler
export default defineComponent({
components: { JVPDialog, JVPInput, JVPSelect },
emits: ['setData'],
setup(_, { emit }) {
The _
in the setup
method is because we're not actually using props anywhere in this component.
You can pass in the props
argument but it won't do anything.
We then also need to listen for the event in App.vue
.
This will involve adding the event listener onto the Selections
element in the template
, but also adding a method to handle this data in the setup
function.
<template>
<div class="h-screen bg-gray-200">
<Selections class="px-4" @setData="setData" />
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
import Selections from './components/Selections.vue';
export default defineComponent({
components: { Selections },
setup() {
const series = ref([]);
const symbol = ref('');
const interval = ref('Daily');
const volume = ref([]);
const setData = (payload) => {
console.log('in setData');
console.log(payload);
};
return { series, symbol, interval, volume, setData };
},
});
</script>
Upon making these adjustments and submitting a request for data for $MSFT the resulting console looks like
Charting the data
Organizing data in App.vue
Now that we have the data in App.vue
we will need to store it instead of just logging it to the console.
This is fairly straightforward
const setData = (payload) => {
series.value = payload.series
symbol.value = payload.symbol
interval.value = payload.interval
volume.value = payload.volume
}
This seems fairly verbose, though not debilitatingly so.
Creating a Chart.vue component
We now need a component to hold this data and eventually render the chart.
<template>
<div>
<div>
{{ symbol }}
</div>
<div>
{{ interval }}
</div>
<div>
{{ series }}
</div>
<div>
{{ volume }}
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
symbol: {
type: String,
required: true,
},
interval: {
type: String,
required: true,
},
series: {
type: Array,
required: true,
},
volume: {
type: Array,
required: true,
},
},
setup(props) {
return {
...props,
};
},
});
</script>
We then update App.vue
to include this component
<template>
<div class="h-screen bg-gray-200">
<Chart
:series="series"
:symbol="symbol"
:interval="interval"
:volume="volume"
/>
<Selections class="px-4" @setData="setData" />
</div>
</template>
<script>
import { defineComponent, ref, reactive } from 'vue';
import Selections from './components/Selections.vue';
import Chart from './components/Chart.vue';
export default defineComponent({
components: { Selections, Chart },
setup() {
const series = ref([]);
const symbol = ref('');
const interval = ref('Daily');
const volume = ref([]);
const setData = (payload) => {
series.value = payload.series
symbol.value = payload.symbol
interval.value = payload.interval
volume.value = payload.volume
console.log('data fetched and set')
};
return { series, symbol, interval, volume, setData };
},
});
</script>
And we click the button to fetch the data and we get the following
which doesn't make any sense since the data has clearly been set in App.vue
.
What's actually happening is
- The
Chart.vue
component is mounted with data that exists inApp.vue
at first - Data is fetched from
Selections.vue
and passed up toApp.vue
- Data is then passed down to
Chart.vue
- Nothing is telling
Chart.vue
to check to see if props changed to trigger a re-render!!!
Watching for data changes
There are a few ways that we can make sure that Chart.vue
will watch the props and re-paint the DOM when data are updated.
The most straight-forward way is to make all four elements computed properties in Chart.vue
.
We will need to import computed
at the top of the script
tag, and then do the following
setup(props) {
const symbol = computed(() => props.symbol.toUpperCase());
const interval = computed(() => props.interval);
const series = computed(() => props.series);
const volume = computed(() => props.volume);
return {
symbol,
interval,
series,
volume,
};
},
Computed properties are like reactive elements in a Vue component. Any time the data inside the anonymous function for the computed property is updated then the resulting element will also update. We could utilize watchers but again, this is a fairly straightforward solution for what we need. Upon doing this and fetching the data again we see
and we have the updated data as anticipated.
In the next article we'll install and include Vue3 ApexCharts to render out the stock chart as well as the volume chart.