The next part is to display items in a table, so you can see what items you added.
In the HTML, inside the
pnlItems div, add a table. The header will be styled using the CSS class
header, while the AMOUNT column will be styled using the CSS class
numeric.
<div id="pnlItems" class="panel">
<table>
<tr class="header">
<td width="20%">MONTH</td>
<td width="40%">NAME</td>
<td width="20%" class="numeric">AMOUNT</td>
<td width="10%"></td>
<td width="10%"></td>
</tr>
</table>
</div>
header will be in bold, and
numeric means that text is aligned right. That's really all there is to it.
.panel
{
float: left;
padding: 10px;
border-radius: 20px;
outline: 1px solid rgba(255, 150, 0, 0.5);
margin-right: 10px;
margin-bottom: 10px;
}
.numeric
{
text-align: right;
}
.header
{
font-weight: bold;
}
label
{
width: 5em;
font-size: 0.5em;
float: left;
}
So we've got the beginnings of a table.
Now let's add content. We want the rows to render for as many elements there are in
items. We also want to set
key because it's a repeated HTML element we're creating.
<div id="pnlItems" class="panel">
<table>
<tr class="header">
<td width="20%">MONTH</td>
<td width="40%">NAME</td>
<td width="20%" class="numeric">AMOUNT</td>
<td width="10%"></td>
<td width="10%"></td>
</tr>
<tr v-for="(i, index) in items" v-bind:key="index">
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
In here, we add the month, the name and amount. Note that the amount column is styled using the
numeric CSS class.
<tr v-for="(i, index) in items" v-bind:key="index">
<td>{{ months[i.month] }}</td>
<td>{{ i.name }}</td>
<td class="numeric">{{ i.amount }}</td>
<td></td>
<td></td>
</tr>
Now we can test this. When you refresh, the first item in items appears.
Now add something. Here I call it "Investment Dividend" and say it's incoming of SGD 1,500. Hey, a guy can dream.
Click the ADD button and you see it appears. So far so good.
Now add something else, an outgoing amount. Wifey's birthday isn't in March and I wish I spent only a thousand, but this is just an example.
See? Some things to correct.
- The amount appears negative. Would be nice if we could color code this.
- It appears in order of entry, which is fine until you have like 50 items.
- We also want to not show the first item.
We start off by adding these CSS classes,
inText and
outText. So incoming money is marked in
green, and outgoing money in
red.
label
{
width: 5em;
font-size: 0.5em;
float: left;
}
.inText
{
color: rgb(0, 200, 0);
}
.outText
{
color: rgb(200, 0, 0);
}
input[type=text], input[type=number], select
{
width: 10em;
padding: 0em;
}
Then we change the class. Instead of just styling using
numeric, we use a combination of
numeric and
inText or
outText, depending on whether the amount is positive.
<tr v-for="(i, index) in items" v-bind:key="index">
<td>{{ months[i.month] }}</td>
<td>{{ i.name }}</td>
<td v-bind:class="i.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ i.amount }}</td>
<td></td>
<td></td>
</tr>
Now in the HTML, we add a conditional. This means the HTML element renders only if
index is greater than 0.
<tr v-for="(i, index) in items" v-bind:key="index" v-if="index > 0">
If you retry everything, you should see that incoming and outgoing amounts are colored differently, and the first row no longer appears. But soon, we'll be doing something bigger.
In
computed, add the method
sortedItems(). This actually returns the sorted view of the
items array.
computed: {
sortedItems: function() {
return this.items
}
},
Here, we use the
map() method to iterate through
items and return the
index and the
element as a new object. This serves to preserve both the index,
index and the element,
item.
computed: {
sortedItems: function() {
return this.items
.map((item, index) => ({ item, index }))
}
},
And we continue by chaining on a
sort() method, sorting by the
month property of
item. This works because we need the index... but sorting items and adding or removing from it, might change
index.
computed: {
sortedItems: function() {
return this.items
.map((item, index) => ({ item, index }))
.sort((a, b) => a.item.month - b.item.month);
}
},
Now we'll need to change this. Instead of iterating through
items, we iterate through
sortedItems and we change all mentions of
i to
si. Since each element of
sortedItems is made of
index and
item, if we want to refer to the element's properties, we have to refer to it as
item.
<tr v-for="si in sortedItems" v-bind:key="si.index" v-if="si.index > 0">
<td>{{ months[si.item.month] }}</td>
<td>{{ si.item.name }}</td>
<td v-bind:class=" si.item.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ si.item.amount }}</td>
<td></td>
<td></td>
</tr>
We'll then add two buttons. One is an UPDATE button and will run the
setCurrentIndex() method, passing in
index as an argument. The other is a DELETE button that runs the
removeItem() method, also passing in
index as an argument.
<tr v-for="si in sortedItems" v-bind:key="si.index" v-if="si.index > 0">
<td>{{ months[si.item.month] }}</td>
<td>{{ si.item.name }}</td>
<td v-bind:class=" si.item.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ si.item.amount }}</td>
<td><input type="button" value="UPDATE" @click="setCurrentIndex(si.index)" /></td>
<td><input type="button" value="DELETE" @click="removeItem(si.index)" /></td>
</tr>
There be buttons! And you may notice, if you enter a June item first and then a February item, they are now sorted properly by month regardless of what order they were entered in. The first default element from items is no longer there, filtered out by the conditional.
Create these two methods.
removeItem() has a parameter,
index.
setCurrentIndex() has a parameter as well,
val.
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;
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) {
this.items.push({
name: nameValue,
month: monthValue,
amount: amountValue
});
}
},
removeItem: function(index) {
},
setCurrentIndex: function(val) {
}
}
setCurrentIndex() is straightforward - simply assign the value of
val to
currentIndex.
removeItem: function(index) {
},
setCurrentIndex: function(val) {
this.currentIndex = val;
}
removeItem() uses the
splice() method to remove the element at position
index in
items, then resets
currentIndex to 0 (just in case it was something else).
removeItem: function(index) {
this.items.splice(index, 1);
this.setCurrentIndex(0);
},
setCurrentIndex: function(val) {
this.currentIndex = val;
}
Now, let's test this. Add this item - "Bonus A" at SGD 5,000 in May. Then add "Bonus B" at SGD 15,000 in June. Click on UPDATE for "Bonus B".
setCurrentIndex() should ensure that Bonus B's details appear in the upper right! Also note that the button now says "UPDATE"! That's because
currentIndex is no longer 0.
Click on DELETE for "Bonus A". The item vanishes, and
setCurrentIndex() changes
currentIndex back to 0, so the upper right panel changes as well.
Update the
addUpdateItem() method. Before, we only handled the case for currentIndex being 0. Now if
currentIndex is
not 0, this means it's an update. And we update the values accordingly. The values, of course, have already been validated.
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;
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) {
this.items.push({
name: nameValue,
month: monthValue,
amount: amountValue
});
} else {
this.items[this.currentIndex].name = nameValue;
this.items[this.currentIndex].amount = amountValue;
this.items[this.currentIndex].month = monthValue;
}
this.setCurrentIndex(0);
},
In
pnlItem, we add another button. It says "NEW", and when you click on it, it sets
currentIndex back to 0. And it renders only if
currentIndex is greater than 0.
<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>
<input type="button" value="NEW" @click="setCurrentIndex(0)" v-if="currentIndex > 0" />
<input type="button" v-bind:value="currentIndex == 0 ? 'ADD' : 'UPDATE'" @click="addUpdateItem" />
Again, add these items - "Bonus A" at SGD 5,000 in May. Then add "Bonus B" at SGD 15,000 in June. Click UPDATE for Bonus A. The NEW button appears!
Ignore that button for now. Set the amount to 8000 and click UPDATE (the one in the top right corner). It should reflect the new value in the list of items below.
Now click the UPDATE button on "Bonus B". See the NEW button appear again? What happens when you click it? That's right - it should set
currentIndex to 0 and give you the "New Item" view.
Next
Showing the Financial Projection.The next part is to display items in a table, so you can see what items you added.
In the HTML, inside the
pnlItems div, add a table. The header will be styled using the CSS class
header, while the AMOUNT column will be styled using the CSS class
numeric.
<div id="pnlItems" class="panel">
<table>
<tr class="header">
<td width="20%">MONTH</td>
<td width="40%">NAME</td>
<td width="20%" class="numeric">AMOUNT</td>
<td width="10%"></td>
<td width="10%"></td>
</tr>
</table>
</div>
header will be in bold, and
numeric means that text is aligned right. That's really all there is to it.
.panel
{
float: left;
padding: 10px;
border-radius: 20px;
outline: 1px solid rgba(255, 150, 0, 0.5);
margin-right: 10px;
margin-bottom: 10px;
}
.numeric
{
text-align: right;
}
.header
{
font-weight: bold;
}
label
{
width: 5em;
font-size: 0.5em;
float: left;
}
So we've got the beginnings of a table.
Now let's add content. We want the rows to render for as many elements there are in
items. We also want to set
key because it's a repeated HTML element we're creating.
<div id="pnlItems" class="panel">
<table>
<tr class="header">
<td width="20%">MONTH</td>
<td width="40%">NAME</td>
<td width="20%" class="numeric">AMOUNT</td>
<td width="10%"></td>
<td width="10%"></td>
</tr>
<tr v-for="(i, index) in items" v-bind:key="index">
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
In here, we add the month, the name and amount. Note that the amount column is styled using the
numeric CSS class.
<tr v-for="(i, index) in items" v-bind:key="index">
<td>{{ months[i.month] }}</td>
<td>{{ i.name }}</td>
<td class="numeric">{{ i.amount }}</td>
<td></td>
<td></td>
</tr>
Now we can test this. When you refresh, the first item in items appears.
Now add something. Here I call it "Investment Dividend" and say it's incoming of SGD 1,500. Hey, a guy can dream.
Click the ADD button and you see it appears. So far so good.
Now add something else, an outgoing amount. Wifey's birthday isn't in March and I wish I spent only a thousand, but this is just an example.
See? Some things to correct.
- The amount appears negative. Would be nice if we could color code this.
- It appears in order of entry, which is fine until you have like 50 items.
- We also want to not show the first item.
We start off by adding these CSS classes,
inText and
outText. So incoming money is marked in
green, and outgoing money in
red.
label
{
width: 5em;
font-size: 0.5em;
float: left;
}
.inText
{
color: rgb(0, 200, 0);
}
.outText
{
color: rgb(200, 0, 0);
}
input[type=text], input[type=number], select
{
width: 10em;
padding: 0em;
}
Then we change the class. Instead of just styling using
numeric, we use a combination of
numeric and
inText or
outText, depending on whether the amount is positive.
<tr v-for="(i, index) in items" v-bind:key="index">
<td>{{ months[i.month] }}</td>
<td>{{ i.name }}</td>
<td v-bind:class="i.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ i.amount }}</td>
<td></td>
<td></td>
</tr>
Now in the HTML, we add a conditional. This means the HTML element renders only if
index is greater than 0.
<tr v-for="(i, index) in items" v-bind:key="index" v-if="index > 0">
If you retry everything, you should see that incoming and outgoing amounts are colored differently, and the first row no longer appears. But soon, we'll be doing something bigger.
In
computed, add the method
sortedItems(). This actually returns the sorted view of the
items array.
computed: {
sortedItems: function() {
return this.items
}
},
Here, we use the
map() method to iterate through
items and return the
index and the
element as a new object. This serves to preserve both the index,
index and the element,
item.
computed: {
sortedItems: function() {
return this.items
.map((item, index) => ({ item, index }))
}
},
And we continue by chaining on a
sort() method, sorting by the
month property of
item. This works because we need the index... but sorting items and adding or removing from it, might change
index.
computed: {
sortedItems: function() {
return this.items
.map((item, index) => ({ item, index }))
.sort((a, b) => a.item.month - b.item.month);
}
},
Now we'll need to change this. Instead of iterating through
items, we iterate through
sortedItems and we change all mentions of
i to
si. Since each element of
sortedItems is made of
index and
item, if we want to refer to the element's properties, we have to refer to it as
item.
<tr v-for="si in sortedItems" v-bind:key="si.index" v-if="si.index > 0">
<td>{{ months[si.item.month] }}</td>
<td>{{ si.item.name }}</td>
<td v-bind:class=" si.item.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ si.item.amount }}</td>
<td></td>
<td></td>
</tr>
We'll then add two buttons. One is an UPDATE button and will run the
setCurrentIndex() method, passing in
index as an argument. The other is a DELETE button that runs the
removeItem() method, also passing in
index as an argument.
<tr v-for="si in sortedItems" v-bind:key="si.index" v-if="si.index > 0">
<td>{{ months[si.item.month] }}</td>
<td>{{ si.item.name }}</td>
<td v-bind:class=" si.item.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ si.item.amount }}</td>
<td><input type="button" value="UPDATE" @click="setCurrentIndex(si.index)" /></td>
<td><input type="button" value="DELETE" @click="removeItem(si.index)" /></td>
</tr>
There be buttons! And you may notice, if you enter a June item first and then a February item, they are now sorted properly by month regardless of what order they were entered in. The first default element from items is no longer there, filtered out by the conditional.
Create these two methods.
removeItem() has a parameter,
index.
setCurrentIndex() has a parameter as well,
val.
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;
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) {
this.items.push({
name: nameValue,
month: monthValue,
amount: amountValue
});
}
},
removeItem: function(index) {
},
setCurrentIndex: function(val) {
}
}
setCurrentIndex() is straightforward - simply assign the value of
val to
currentIndex.
removeItem: function(index) {
},
setCurrentIndex: function(val) {
this.currentIndex = val;
}
removeItem() uses the
splice() method to remove the element at position
index in
items, then resets
currentIndex to 0 (just in case it was something else).
removeItem: function(index) {
this.items.splice(index, 1);
this.setCurrentIndex(0);
},
setCurrentIndex: function(val) {
this.currentIndex = val;
}
Now, let's test this. Add this item - "Bonus A" at SGD 5,000 in May. Then add "Bonus B" at SGD 15,000 in June. Click on UPDATE for "Bonus B".
setCurrentIndex() should ensure that Bonus B's details appear in the upper right! Also note that the button now says "UPDATE"! That's because
currentIndex is no longer 0.
Click on DELETE for "Bonus A". The item vanishes, and
setCurrentIndex() changes
currentIndex back to 0, so the upper right panel changes as well.
Update the
addUpdateItem() method. Before, we only handled the case for currentIndex being 0. Now if
currentIndex is
not 0, this means it's an update. And we update the values accordingly. The values, of course, have already been validated.
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;
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) {
this.items.push({
name: nameValue,
month: monthValue,
amount: amountValue
});
} else {
this.items[this.currentIndex].name = nameValue;
this.items[this.currentIndex].amount = amountValue;
this.items[this.currentIndex].month = monthValue;
}
this.setCurrentIndex(0);
},
In
pnlItem, we add another button. It says "NEW", and when you click on it, it sets
currentIndex back to 0. And it renders only if
currentIndex is greater than 0.
<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>
<input type="button" value="NEW" @click="setCurrentIndex(0)" v-if="currentIndex > 0" />
<input type="button" v-bind:value="currentIndex == 0 ? 'ADD' : 'UPDATE'" @click="addUpdateItem" />
Again, add these items - "Bonus A" at SGD 5,000 in May. Then add "Bonus B" at SGD 15,000 in June. Click UPDATE for Bonus A. The NEW button appears!
Ignore that button for now. Set the amount to 8000 and click UPDATE (the one in the top right corner). It should reflect the new value in the list of items below.
Now click the UPDATE button on "Bonus B". See the NEW button appear again? What happens when you click it? That's right - it should set
currentIndex to 0 and give you the "New Item" view.
Next
Showing the Financial Projection.