Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 3
Custom <JVPInput /> and <JVPSelect /> components

Search for a command to run...
Custom <JVPInput /> and <JVPSelect /> components

No comments yet. Be the first to comment.
Set up a FastAPI backend to fetch stock data given a ticker, interval, and dates, create a Vue frontend to display the chart, and show how to create a Docker container for easy deployment.
Vuelidate for form validation
I'm working on a project in my new job and as part of the project I'm trying to register some components globally. As a company we're going to build out a component library and we want to be able to use some components without having to manually impo...

In the last blog post I built the functionality to handle form submission for the file upload itself, but we still didn't have a way to actually handle the files. In this post I'll set up the very basic markup as well as the accompanying script funct...

In the last blog post I touched on setting up a Google Drive folder to serve as the location of our uploads as well as creating the necessary endpoints for uploading and renaming the files. In this post I'll set up functions to send any necessary fil...

In the last blog post I set up a service account to authenticate for Google Drive's API, encrypted the resulting .json file, and set up functionality to decrypt said file to use for the purpose of actual authentication. In this post I will show you h...

Note: all credentials have already been destroyed so there is no need to let me know to not share credential information online. I just finished working on a project for a client/friend in which she wanted to have a page where she could have forms av...

This short article will continue along what was alluded to in part 2.
Within Selections.vue we have three input elements and one select element that are used to receive input regarding stock selection.
We would like to have uniform styling and will be implementing things like input validation so it make sense to compartmentalize these components.
Also, what the hell is the point of using Vue if we're not going to do something like this?
JVPInput.vue ComponentWe start by creating a simple, generic component
<template>
<div>
<label :for="id">{{ label }}</label>
<input
:value="modelValue"
:type="type"
:name="id"
:id="id"
:placeholder="placeholder"
class="mx-2 border"
@input="updateValue"
/>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import inputProps from '../utils/input-props';
export default defineComponent({
props: {
type: {
type: String,
required: false,
default: 'text',
},
placeholder: {
type: String,
required: false,
},
...inputProps,
},
emits: ['update:modelValue'],
setup(props, { emit }) {
// This is simply cleaner than putting emit code in the HTML
const updateValue = (e) => emit('update:modelValue', e.target.value);
return { updateValue };
},
});
</script>
The functionality of the component is fairly simple: split up the v-model directive into :value and @input functionality, and use passed-in props for the type, id, and name.
We can see that it looks like some information is missing, namely the modelValue, id, and label values that are referenced within the template tag.
These can be found in the inputProps object, which is defined in a separate file as
// input-props.js
export default {
id: {
type: String,
required: true,
},
modelValue: {
required: true,
},
label: {
type: String,
required: true,
},
hint: {
type: String,
required: false,
default: '',
},
};
The hint that is included in input-props.js is something that will be used later.
JVPSelect.vue ComponentIn a similar manner, but somewhat simpler, we create the JVPSelect.vue component
<template>
<div>
<label :for="id">{{ label }}</label>
<select :value="item" :id="id" class="mx-2 border" @input="updateItem">
<option
v-for="option in options"
:key="option"
:name="option"
:selected="modelValue"
>
{{ option }}
</option>
</select>
</div>
</template>
<script>
import { defineComponent, computed } from 'vue';
import inputProps from '../utils/input-props';
export default defineComponent({
props: {
options: {
type: Array,
required: true,
},
...inputProps,
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const item = computed(() => props.modelValue);
const updateItem = (e) => emit('update:modelValue', e.target.value);
return { item, updateItem };
},
});
</script>
Like the JVPInput.vue component, this is simply moving the functionality that existed in Selections.vue into its own component.
Selections.vueNow with these two components created we will update Selections.vue to utilize them.
We then update Selections.vue to use this component instead of the generic input component.
<template>
<form class="ml-5" @submit.prevent="handleSubmit">
<div class="my-1">
<JVPInput
v-model="state.symbol"
label="Symbol"
id="symbol"
name="symbol"
type="text"
placeholder="eg. MSFT"
/>
</div>
<div class="my-1">
<JVPInput
v-model="state.startDate"
label="Start Date"
id="start-date"
name="startDate"
type="text"
/>
</div>
<div class="my-1">
<JVPInput
v-model="state.endDate"
label="End Date"
id="end-date"
name="endDate"
type="text"
/>
</div>
<div class="my-1">
<JVPSelect v-model="state.interval" label="Interval" id="interval" :options="intervals" />
</div>
<button type="submit" class="border">Get Chart</button>
</form>
</template>
<script>
import { defineComponent, reactive } from 'vue';
import JVPInput from './JVPInput.vue';
import JVPSelect from './JVPSelect.vue';
export default defineComponent({
components: { JVPInput, JVPSelect },
setup() {
const intervals = ['Daily', 'Weekly', 'Monthly'];
const state = reactive({
symbol: '',
interval: 'Daily',
startDate: '',
endDate: '',
});
const handleSubmit = () => {
console.log('triggered handleSubmit');
};
return {
intervals,
state,
handleSubmit,
};
},
});
</script>
and the app will look no different than before, which is the goal, but it will make further customization much easier.
As we saw in part 2, the general app look is fairly ugly. We'll show it again here for reference.
We want to add some Tailwind styles to clean this up a little. In regard to the JVPSelect.vue component it won't make much difference than if we had left functionality in the Selections.vue component, but for organization it's better this way.
JVPSelect.vueWe'll add simple font styling to the label:
and slightly more complex styling to the actual <select> tag:
To see what each of these classes do to the element I encourage you to check out the Tailwind docs.
Full disclosure, though, I have purchased a license to Tailwind UI so a lot of this came from there because I'm terrible when it comes to design independently.
Now, the updated JVPSelect.vue component looks like this
<template>
<div>
<label :for="id" class="block text-sm font-medium text-gray-700">
{{ label }}
</label>
<select
:value="item"
:id="id"
class="
mt-1
block
w-full
pl-3
pr-10
py-2
text-base
border-gray-300
focus:outline-none focus:ring-indigo-500 focus:border-indigo-500
sm:text-sm
rounded-md
"
@input="updateItem"
>
<option
v-for="option in options"
:key="option"
:name="option"
:selected="modelValue"
>
{{ option }}
</option>
</select>
</div>
</template>
<script>
import { defineComponent, computed } from 'vue';
import inputProps from '../utils/input-props';
export default defineComponent({
props: {
options: {
type: Array,
required: true,
},
...inputProps,
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const item = computed(() => props.modelValue);
const updateItem = (e) => emit('update:modelValue', e.target.value);
return { item, updateItem };
},
});
</script>
JVPInput.vueThis component is going to be slightly more complicated.
One of the reasons for this is because we're going to be handling input validation in the JVPInput.vue component so we want to have <p> tag to display error messages or hints (remember the hint element in input-props.js???).
We want the labels to match what we already created for JVPSelect.vue, though, so that will be the same.
We will also wrap the <input> in a div element to make alignment a bit better as well as placement of any messages.
To this div we will add just a couple of classes:
and then to the actual input element we will add
Now the JVPInput.vue component looks like
<template>
<div>
<label :for="id" class="block text-sm font-medium text-gray-700">
{{ label }}
</label>
<div class="mt-1 relative rounded-md shadow-sm">
<input
:value="modelValue"
:type="type"
:name="id"
:id="id"
class="block w-full sm:text-sm rounded-md shadow-sm"
@input="updateValue"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import inputProps from '../utils/input-props';
export default defineComponent({
props: {
type: {
type: String,
required: false,
default: 'text',
},
placeholder: {
type: String,
required: false,
},
...inputProps,
},
emits: ['update:modelValue'],
setup(props, { emit }) {
// This is simply cleaner than putting emit code in the HTML
const updateValue = (e) => emit('update:modelValue', e.target.value);
return { updateValue };
},
});
</script>
And in order to make the actual app look half decent, we'll add a bg-gray-200 class to the main div in App.vue and a px-4 class to the <Selections /> element in App.vue
<template>
<div class="h-screen bg-gray-200">
<Selections class="px-4"/>
</div>
</template>
and the app now looks like
which is still objectively terrible, but we're making progress.
The benefit of what we've done in this step is that we will be able to compartmentalize the error display in the JVPInput.vue component and it will keep Selections.vue relatively clean.
In the next article we'll add input validation to the three JVPInput.vue elements, add error and hint message display, and add dynamic styling of the component itself for when it is in an error state.