Either way, Missus TeochewThunder and I review this spreadsheet together regularly. And her not being great with English, Ive had to resort to other ways to bring the message across. Color codes. Simplified columns. And at some point, I wondered if it wouldn't be simpler to just whip up something quick using VueJS. Plus, it would be way more fun.
Here's some HTML. In this, we set font properties. We have a div, id fpApp, in the body. And we include a remote link to the Vue library, along with a standard Vue declaration.
<!DOCTYPE html>
<html>
<head>
<title>Financial Projector</title>
<style>
body
{
font-family: Arial;
font-size: 14px;
}
</style>
</head>
<body>
<div id="fpApp">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
new Vue({
el: '#fpApp',
data: {
},
computed: {
},
methods: {
}
});
</script>
</body>
</html>
<html>
<head>
<title>Financial Projector</title>
<style>
body
{
font-family: Arial;
font-size: 14px;
}
</style>
</head>
<body>
<div id="fpApp">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
new Vue({
el: '#fpApp',
data: {
},
computed: {
},
methods: {
}
});
</script>
</body>
</html>
Add three divs inside fpApp - pnlFinancialProjection, pnlItem and pnlItems. Yes, the last one has a plural. Style all of them using the panel CSS class.
<!DOCTYPE html>
<html>
<head>
<title>Financial Projector</title>
<style>
body
{
font-family: Arial;
font-size: 14px;
}
</style>
</head>
<body>
<div id="fpApp">
<div id="pnlFinancialProjection" class="panel">
</div>
<div id="pnlItem" class="panel">
</div>
<div id="pnlItems" class="panel">
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
new Vue({
el: '#fpApp',
data: {
},
computed: {
},
methods: {
}
});
</script>
</body>
</html>
<html>
<head>
<title>Financial Projector</title>
<style>
body
{
font-family: Arial;
font-size: 14px;
}
</style>
</head>
<body>
<div id="fpApp">
<div id="pnlFinancialProjection" class="panel">
</div>
<div id="pnlItem" class="panel">
</div>
<div id="pnlItems" class="panel">
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
new Vue({
el: '#fpApp',
data: {
},
computed: {
},
methods: {
}
});
</script>
</body>
</html>
Let's style panel first. We have rounded corners and an orange outline. Each of them has 10 pixels spacing to the right and bottom, and 10 pixels padding. Most importantly, they are floated left so that they'll align nicely.
<style>
body
{
font-family: Arial;
font-size: 14px;
}
.panel
{
float: left;
padding: 10px;
border-radius: 20px;
outline: 1px solid rgba(255, 150, 0, 0.5);
margin-right: 10px;
margin-bottom: 10px;
}
</style>
body
{
font-family: Arial;
font-size: 14px;
}
.panel
{
float: left;
padding: 10px;
border-radius: 20px;
outline: 1px solid rgba(255, 150, 0, 0.5);
margin-right: 10px;
margin-bottom: 10px;
}
</style>
Then we specify fpApp to have 800 pixels width and be positioned in the middle of the screen via the margin property. pnlFinancialProjection and pnlItem will be 300 pixels tall, and have different widths that will fit inside fpApp, with the padding and spacings factored in. pnlItems has full width (plus padding and spacing) and no height specified. This is because pnlFinancialProjection and pnlItem will deal with finite data, while pnlItems will technically be unlimited.
<style>
body
{
font-family: Arial;
font-size: 14px;
}
#fpApp
{
width: 800px;
margin: 0 auto 0 auto;
}
#pnlFinancialProjection
{
width: 500px;
height: 300px;
}
#pnlItem
{
width: 240px;
height: 300px;
}
#pnlItems
{
width: 770px;
}
.panel
{
float: left;
padding: 10px;
border-radius: 20px;
outline: 1px solid rgba(255, 150, 0, 0.5);
margin-right: 10px;
margin-bottom: 10px;
}
</style>
body
{
font-family: Arial;
font-size: 14px;
}
#fpApp
{
width: 800px;
margin: 0 auto 0 auto;
}
#pnlFinancialProjection
{
width: 500px;
height: 300px;
}
#pnlItem
{
width: 240px;
height: 300px;
}
#pnlItems
{
width: 770px;
}
.panel
{
float: left;
padding: 10px;
border-radius: 20px;
outline: 1px solid rgba(255, 150, 0, 0.5);
margin-right: 10px;
margin-bottom: 10px;
}
</style>
Looks nice and neat so far. We'll be filling this in.
Before we continue, let's fill in some data. In data, we want the items array. The first element at index 0 is the default item that will be used for display when adding to this array, as a template. The three properties are name (an empty string) and month and amount (both integers with a value of 0).
data: {
items: [
{
name: "",
month: 0,
amount: 0
}
],
}
items: [
{
name: "",
month: 0,
amount: 0
}
],
}
Then we have currentIndex, default value 0.
data: {
currentIndex: 0,
items: [
{
name: "",
month: 0,
amount: 0
}
],
},
currentIndex: 0,
items: [
{
name: "",
month: 0,
amount: 0
}
],
},
With this, we can basically try making a form for adding new elements to items. We have a paragraph, and in it there's a label tag. Then we want the corresponding element, a select tag. Both id and ref attributes will be itemMonth.
<div id="pnlItem" class="panel">
<p>
<label for="itemMonth">Month</label>
<br />
<select ref="itemMonth" id="itemMOnth">
</select>
</p>
</div>
<p>
<label for="itemMonth">Month</label>
<br />
<select ref="itemMonth" id="itemMOnth">
</select>
</p>
</div>
Now this is a dropdown list of months, and we're going to populate this using data. For this, we create the months array in data. We then populate it using month names. The first element, at index 0, is "All".
data: {
currentIndex: 0,
items: [
{
name: "",
month: 0,
amount: 0
}
],
months: [
"All", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
]
},
currentIndex: 0,
items: [
{
name: "",
month: 0,
amount: 0
}
],
months: [
"All", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
]
},
In the HTML, we create a series of option tags by using the v-for attribute to iterate through months.
<select ref="itemMonth" id="itemMonth">
<option v-for="(month, index) in months">
</option>
</select>
<option v-for="(month, index) in months">
</option>
</select>
month, of course, will be used as the label for the option tags.
<select ref="itemMonth" id="itemMonth">
<option v-for="(month, index) in months">
{{ month }}
</option>
</select>
<option v-for="(month, index) in months">
{{ month }}
</option>
</select>
Here, we use the v-bind directive to ensure that every element has a unique key, and a value. In both cases, it is index. And also bind the selected property to whether index is equal to the month property of the current element of items pointed to by currentIndex.
<select ref="itemMonth" id="itemMonth">
<option v-for="(month, index) in months" v-bind:key="index" v-bind:value="index" v-bind:selected="items[currentIndex].month == index">
{{ month }}
</option>
</select>
<option v-for="(month, index) in months" v-bind:key="index" v-bind:value="index" v-bind:selected="items[currentIndex].month == index">
{{ month }}
</option>
</select>
There you go, there's your dropdown list.
The next element is a text box, itemName. Since currentIndex is 0, what you see is that the textbox's value is set to the value of the first element of items, which is an empty string.
<div id="pnlItem" class="panel">
<p>
<label for="itemMonth">Month</label>
<br />
<select ref="itemMonth" id="itemMonth">
<option v-for="(month, index) in months" v-bind:key="index" v-bind:value="index" v-bind:selected="items[currentIndex].month == index">
{{ month }}
</option>
</select>
</p>
<p>
<label for="itemName">Name</label>
<br />
<input ref="itemName" id="itemName" type="text" v-bind:value="items[currentIndex].name">
</p>
</div>
<p>
<label for="itemMonth">Month</label>
<br />
<select ref="itemMonth" id="itemMonth">
<option v-for="(month, index) in months" v-bind:key="index" v-bind:value="index" v-bind:selected="items[currentIndex].month == index">
{{ month }}
</option>
</select>
</p>
<p>
<label for="itemName">Name</label>
<br />
<input ref="itemName" id="itemName" type="text" v-bind:value="items[currentIndex].name">
</p>
</div>
The next elements are radio buttons which determine if the entry is income or expense - hence itemTypeIn or itemTypeOut. If the first element of items has an amount of 0 or more, itemTypeIn is checked; otherwise itemTypeOut is checked. We set the name attribute to type for both radio buttons so that only one can be selected.
<p>
<label for="itemName">Name</label>
<br />
<input ref="itemName" id="itemName" type="text" v-bind:value="items[currentIndex].name">
</p>
<p>
<label for="itemTypeIn">Type</label>
<br />
<input ref="itemTypeIn" id="itemTypeIn" name="type" type="radio" v-bind:checked="items[currentIndex].amount >= 0">In
<input ref="itemTypeOut" id="itemTypeOut" name="type" type="radio" v-bind:checked="items[currentIndex].amount < 0">Out
</p>
</div>
<label for="itemName">Name</label>
<br />
<input ref="itemName" id="itemName" type="text" v-bind:value="items[currentIndex].name">
</p>
<p>
<label for="itemTypeIn">Type</label>
<br />
<input ref="itemTypeIn" id="itemTypeIn" name="type" type="radio" v-bind:checked="items[currentIndex].amount >= 0">In
<input ref="itemTypeOut" id="itemTypeOut" name="type" type="radio" v-bind:checked="items[currentIndex].amount < 0">Out
</p>
</div>
Now we have the itemAmount textbox for the amount property, and this is straightforward... except that instead of just placing the value of amount in the textbox, we want the absolute value. Because, later on, depending on whether it is income or an expense, the value will be positive or negative.
<p>
<label for="itemTypeIn">Type</label>
<br />
<input ref="itemTypeIn" id="itemTypeIn" name="type" type="radio" v-bind:checked="items[currentIndex].amount >= 0">In
<input ref="itemTypeOut" id="itemTypeOut" name="type" type="radio" v-bind:checked="items[currentIndex].amount < 0">Out
</p>
<p>
<label for="itemAmount">Amount</label>
<br />
<input ref="itemAmount" id="itemAmount" type="number" v-bind:value="Math.abs(items[currentIndex].amount)">
</p>
</div>
<label for="itemTypeIn">Type</label>
<br />
<input ref="itemTypeIn" id="itemTypeIn" name="type" type="radio" v-bind:checked="items[currentIndex].amount >= 0">In
<input ref="itemTypeOut" id="itemTypeOut" name="type" type="radio" v-bind:checked="items[currentIndex].amount < 0">Out
</p>
<p>
<label for="itemAmount">Amount</label>
<br />
<input ref="itemAmount" id="itemAmount" type="number" v-bind:value="Math.abs(items[currentIndex].amount)">
</p>
</div>
Here's the rest of the input elements.
And then we add a button. It will say "ADD" or "UPDATE" based on the value of currentIndex. Since currentIndex is 0, it will say "ADD". As you can see, it also runs the addUpdateItem() method when clicked.
<p>
<label for="itemAmount">Amount</label>
<br />
<input ref="itemAmount" id="itemAmount" type="number" v-bind:value="Math.abs(items[currentIndex].amount)">
</p>
<input type="button" v-bind:value="currentIndex == 0 ? 'ADD' : 'UPDATE'" @click="addUpdateItem" />
</div>
<label for="itemAmount">Amount</label>
<br />
<input ref="itemAmount" id="itemAmount" type="number" v-bind:value="Math.abs(items[currentIndex].amount)">
</p>
<input type="button" v-bind:value="currentIndex == 0 ? 'ADD' : 'UPDATE'" @click="addUpdateItem" />
</div>
Here is the button. See? It says "ADD".
In the styles, here is some styling for what we've done so far. It's all cosmetic, but let's keep things pretty.
.panel
{
float: left;
padding: 10px;
border-radius: 20px;
outline: 1px solid rgba(255, 150, 0, 0.5);
margin-right: 10px;
margin-bottom: 10px;
}
label
{
width: 5em;
font-size: 0.5em;
float: left;
}
input[type=text], input[type=number], select
{
width: 10em;
padding: 0em;
}
input[type=button]
{
width: 5em;
height: 1.5em;
float: right;
margin-left: 0.5em;
border-radius: 5px;
background-color: rgba(255, 150, 0, 0.5);
color: rgb(255, 255, 255);
border: 0px solid rgb(0, 0, 0);
}
input[type=button]:hover
{
background-color: rgba(255, 150, 0, 1);
}
</style>
{
float: left;
padding: 10px;
border-radius: 20px;
outline: 1px solid rgba(255, 150, 0, 0.5);
margin-right: 10px;
margin-bottom: 10px;
}
label
{
width: 5em;
font-size: 0.5em;
float: left;
}
input[type=text], input[type=number], select
{
width: 10em;
padding: 0em;
}
input[type=button]
{
width: 5em;
height: 1.5em;
float: right;
margin-left: 0.5em;
border-radius: 5px;
background-color: rgba(255, 150, 0, 0.5);
color: rgb(255, 255, 255);
border: 0px solid rgb(0, 0, 0);
}
input[type=button]:hover
{
background-color: rgba(255, 150, 0, 1);
}
</style>
A nicely formatted form.
Let's handle adding stuff!
In data, cater for error handling with the object errors. There will be only two error types, so set two properties - name and amount. Both are set to an empty string by default.data: {
currentIndex: 0,
errors: {
name: "",
amount: "",
},
items:[
{
name: "",
month: 0,
amount: 0
}
],
months: [
"All", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
]
},
currentIndex: 0,
errors: {
name: "",
amount: "",
},
items:[
{
name: "",
month: 0,
amount: 0
}
],
months: [
"All", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
]
},
In methods, create the method addUpdateItem(). This is the method that will be called when the button is clicked, whether it's an add or update.
methods: {
addUpdateItem: function() {
}
}
addUpdateItem: function() {
}
}
We start by setting (or in some contexts, resetting) the properties of errors to empty strings.
methods: {
addUpdateItem: function() {
this.errors.name = "";
this.errors.amount = "";
}
}
addUpdateItem: function() {
this.errors.name = "";
this.errors.amount = "";
}
}
Then we grab the values of the various elements and assign them to variables. In particular, we want name, amount and month. We also declare errors as 0, to be incremented when there are errors.
methods: {
addUpdateItem: function() {
this.errors.name = "";
this.errors.amount = "";
let nameValue = this.$refs.itemName.value.trim();
let amountValue = parseFloat(this.$refs.itemAmount.value.trim());
let monthValue = parseInt(this.$refs.itemMonth.value);
let errors = 0;
}
}
addUpdateItem: function() {
this.errors.name = "";
this.errors.amount = "";
let nameValue = this.$refs.itemName.value.trim();
let amountValue = parseFloat(this.$refs.itemAmount.value.trim());
let monthValue = parseInt(this.$refs.itemMonth.value);
let errors = 0;
}
}
So next we check if nameValue is an empty string, and if amountValue is a positive number. These are the only two conditions we need. For either case, set the error in the appropriate errors object, and increment errors. After that, check if errors is greater than 0, and exit early if so.
let nameValue = this.$refs.itemName.value.trim();
let amountValue = parseFloat(this.$refs.itemAmount.value.trim());
let monthValue = parseInt(this.$refs.itemMonth.value);
let errors = 0;
if (nameValue == "") { this.errors.name = "Required"; errors++; }
if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
if (errors > 0) return;
let amountValue = parseFloat(this.$refs.itemAmount.value.trim());
let monthValue = parseInt(this.$refs.itemMonth.value);
let errors = 0;
if (nameValue == "") { this.errors.name = "Required"; errors++; }
if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
if (errors > 0) return;
Add these span tags to the HTML. In it, you should have binding for the appropriate properties in the errors object.
<p>
<label for="itemName">Name</label>
<br />
<input ref="itemName" id="itemName" type="text" v-bind:value="items[currentIndex].name">
<span class="error">{{ errors.name }}</span>
</p>
<p>
<label for="itemTypeIn">Type</label>
<br />
<input ref="itemTypeIn" id="itemTypeIn" name="type" type="radio" v-bind:checked="items[currentIndex].amount >= 0">In
<input ref="itemTypeOut" id="itemTypeOut" name="type" type="radio" v-bind:checked="items[currentIndex].amount < 0">Out
</p>
<p>
<label for="itemAmount">Amount</label>
<br />
<input ref="itemAmount" id="itemAmount" type="number" v-bind:value="Math.abs(items[currentIndex].amount)">
<span class="error">{{ errors.amount }}</span>
</p>
<label for="itemName">Name</label>
<br />
<input ref="itemName" id="itemName" type="text" v-bind:value="items[currentIndex].name">
<span class="error">{{ errors.name }}</span>
</p>
<p>
<label for="itemTypeIn">Type</label>
<br />
<input ref="itemTypeIn" id="itemTypeIn" name="type" type="radio" v-bind:checked="items[currentIndex].amount >= 0">In
<input ref="itemTypeOut" id="itemTypeOut" name="type" type="radio" v-bind:checked="items[currentIndex].amount < 0">Out
</p>
<p>
<label for="itemAmount">Amount</label>
<br />
<input ref="itemAmount" id="itemAmount" type="number" v-bind:value="Math.abs(items[currentIndex].amount)">
<span class="error">{{ errors.amount }}</span>
</p>
In styles, I'll have error be red, in a smaller font and bold.
input[type=button]:hover
{
background-color: rgba(255, 150, 0, 1);
}
.error
{
color: rgb(255, 0, 0);
font-size: 0.8em;
font-weight: bold;
}
</style>
{
background-color: rgba(255, 150, 0, 1);
}
.error
{
color: rgb(255, 0, 0);
font-size: 0.8em;
font-weight: bold;
}
</style>
Click the ADD button without changing any values. There, that's straightforward. See the errors?
Back to the method. We now set amountValue. If it's an expense, which means itemTypeOut is checked, then we set amountValue to a negative value.
if (nameValue == "") { this.errors.name = "Required"; errors++; }
if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
if (errors > 0) return;
if (this.$refs.itemTypeOut.checked) amountValue = amountValue * -1;
if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
if (errors > 0) return;
if (this.$refs.itemTypeOut.checked) amountValue = amountValue * -1;
Next, we check if currentIndex is 0. At this point, it will always be true. Later on, we'll cater for other scenarios.
if (nameValue == "") { this.errors.name = "Required"; errors++; }
if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
if (errors > 0) return;
if (this.$refs.itemTypeOut.checked) amountValue = amountValue * -1;
if (this.currentIndex == 0) {
}
if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
if (errors > 0) return;
if (this.$refs.itemTypeOut.checked) amountValue = amountValue * -1;
if (this.currentIndex == 0) {
}
So if currentIndex is 0, which means that this is an ADD operation, push the values into items.
if (this.currentIndex == 0) {
this.items.push({
name: nameValue,
month: monthValue,
amount: amountValue
});
}
this.items.push({
name: nameValue,
month: monthValue,
amount: amountValue
});
}
You won't be able to see anything now even if stuff gets added into items. But no worries, that's handled in the next part!



























