Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 3
Custom <JVPInput /> and <JVPSelect /> components
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?
Custom JVPInput.vue
Component
We 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.
Custom JVPSelect.vue
Component
In 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.
Updating Selections.vue
Now 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.
Styling
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.vue
We'll add simple font styling to the label
:
- block
- text-sm
- font-medium
- text-gray-700
and slightly more complex styling to the actual <select>
tag:
- block
- w-full
- pl-3
- pr-10
- py-2
- mt-1
- text-base
- border-gray-300
- focus:outline-none
- focus:ring-indigo-500
- focus:border-indigo-500
- sm:text-sm
- rounded-md
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.vue
This 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:
- mt-1
- relative
- rounded-md
- shadow-sm
and then to the actual input
element we will add
- block
- w-full
- sm:text-sm
- rounded-md
- shadow-sm
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.