Monday, 25 May 2026

Web Tutorial: VueJS Financial Projector (Part 2/3)

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.

Friday, 22 May 2026

Web Tutorial: VueJS Financial Projector (Part 1/3)

It was late last year when I consulted my yearly spreadsheet as to whether or not I was on track for my financial goals for 2025. Why, what part surprises you? The fact that I have financial goals, or the fact that I use a spreadsheet? I'm a software developer, FFS.

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>


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>


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>


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>


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
      }
   ],
}


Then we have currentIndex, default value 0.
data: {
   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>


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"

  ]   
},


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>


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>


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>


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>


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>


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>


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>


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>


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"
  ]
},


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() {

   }

}


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 = "";

  }
}


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;

  }
}


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;


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>


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>


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;


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) {

}


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
   });
}


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!

Next

Displaying items added, updating and removing them.

Sunday, 17 May 2026

Keeping your users in the Dark (Mode)!

Media queries in CSS aren't new. I usually use them for mobile-friendly layouts or print views. However, I encountered a blind spot one day when i discovered that I was able to use style Dark Mode in CSS Media Queries!

Upon further research, this has been part of the standard since 2020. Which shows you how long I've had my eye off that particular ball. To illustrate, here's a sample HTML I wrote with a poem generated from ChatGPT.
<!DOCTYPE html>
<html>
  <head>
    <title>Dark Mode test</title>
    <style>
  
    </style>
  </head>

  <body>
    <h1>TESTING DARK MODE</h1>
    <p>
      When daylight fades from glass and screen,<br />
      and white gives way to something keen,<br />
      the world grows soft, the edges thin—<br />
      a quieter place to think within.
    </p>

    <p>
      The glare that once would bite and burn<br />
      now yields to night at every turn,<br />
      where pixels rest and shadows stay<br />
      like thoughts that choose a gentler way.
    </p>

    <p>
      No blinding page, no sterile light,<br />
      just constellations born of night—<br />
      a code of ink, a calmer tone,<br />
      a world that feels more like your own.
    </p>

    <p>
      And in that dark, so still, so wide,<br />
      the noise of day steps back aside—<br />
      you read, you write, you drift, you see<br />
      how brightness isn’t clarity.
    </p>
  </body>
</html>


Example

The majority of the time, the defaults for a site tend towards the Light Mode, like so.
<!DOCTYPE html>
<html>
  <head>
    <title>Dark Mode test</title>
    <style>
      body
      {
       background: rgb(255, 255, 255);
       color: rgb(100, 100, 100);
       font-family: Georgia;
       font-size: 15px;
      }

      h1
      {
        color: rgb(150, 50, 0);
      }

    </style>
  </head>

  <body>
    <h1>TESTING DARK MODE</h1>
    <p>
      When daylight fades from glass and screen,<br />
      and white gives way to something keen,<br />
      the world grows soft, the edges thin—<br />
      a quieter place to think within.
    </p>

    <p>
      The glare that once would bite and burn<br />
      now yields to night at every turn,<br />
      where pixels rest and shadows stay<br />
      like thoughts that choose a gentler way.
    </p>

    <p>
      No blinding page, no sterile light,<br />
      just constellations born of night—<br />
      a code of ink, a calmer tone,<br />
      a world that feels more like your own.
    </p>

    <p>
      And in that dark, so still, so wide,<br />
      the noise of day steps back aside—<br />
      you read, you write, you drift, you see<br />
      how brightness isn’t clarity.
    </p>
  </body>
</html>


This is what that site would look like...


Now in order to specify a Dark Mode, we first specify a CSS Media Query, with Dark.
<style>
  body
  {
    background: rgb(255, 255, 255);
    color: rgb(100, 100, 100);
    font-family: Georgia;
    font-size: 15px;
  }

  h1
  {
    color: rgb(150, 50, 0);
  }

  @media (prefers-color-scheme: dark)
  {

  }

</style>


And then we specify what to override in Dark Mode.
@media (prefers-color-scheme: dark)
{
  body
  {
    background: rgb(0, 0, 0);
    color: rgb(200, 200, 200);
  }

  h1
  {
    color: rgb(255, 200, 50);
  }

}


Emulating Dark Mode in your browser

Right now, I'm on a Mac and this is Chrome.

1. Get into your console. Open the Command Menu by pressing Cmd + Shift + P.

2. Type in "Show Rendering" and press select.

3. Toggle between the modes..

Here's a very simple look. Black background, light grey text.


Who are you in the Dark?

That was fun, I guess. So much we could do with this. I have ideas already. My mind is going... ahem... dark places.

Let there be Light!
T___T

Wednesday, 13 May 2026

Seriously, why try to cook Sam Altman?

It's no secret my opinion of Sam Altman isn't exactly high. To me, he's one of those snake oil salesmen that continually hypes up a product that, while interesting in limited scopes, is nowhere what it's purported to be. And he exudes the low-effort vibe of a con artist who flourishes not by being smarter than others, but by picking raving idiots as his target audience.

That said, I don't dislike the man, and bear him no ill will. And I certainly was not among those who cheered when one Daniel Moreno-Gama lobbed a Molotov cocktail at his home last April, then threatened to set fire to the OpenAI headquarters in San Francisco, USA. 

The threat.

What was his reason? Apparently, "AGI poses a threat to humanity and therefore it must be stopped." Come on, now. Really?

AGI

I don't know what this guy has been smoking, but that brand of AI that Sam Altman and his ilk have been forcing down our collective throats, LLMs, barely qualifies as AI, much less AGI. Before anyone jumps on my back, I'll concede that LLMs are incredibly useful. And when you compare them against their predecessor - autocomplete - a massive technological leap in terms of scale, pattern-matching and the like. However, calling them "intelligence" just tells me that your standards aren't particularly high.

How many times have we been promised that AGI is near? How many times has it not materialized? Countless, and countless. The simple fact of the matter is, this tech exists for a reason, and it's not to better the lives of humanity.

Smoke and mirrors
in service of the
almighty dollar.

No, like everything else, it's meant to make money.

AGI might make money once it actually exists in the real world rather than the fevered imagination of fanboys and science fiction enthusiasts, but it costs too much to get there. Meanwhile, faking AGI costs a lot less and satisfies the lower-level requirements. Who needs real creativity when you can simulate it? Who can even tell what actual creativity is?

So, if AGI is nowhere near, what's there to stop?

Humanity

Do human beings suck in general? Well yes, and I don't think that's ever been a controversial statement. But do we suck more just because of AI? We certainly suck more visibly. We have all manner of rubbish created by generative AI, churned out by people who have way too much time on their hands. Deepfakes. Nonsensical clips of Michael Jackson fighting Bruce Lee. AI porn.

AI generation of
Michael Jackson vs Bruce Lee

AI was the tool that made all this possible; but make no mistake, human beings are the ones who should be blamed for how it's used. If you're going to blame AI, why stop there? Blame Social Media. Blame the damn Internet.

I would argue that humanity's greatest threat is humanity. Not AI. If we're already our greatest threat, a pale imitation of ourselves isn't going to do a better job of ending us. It's just not.

Killing Sam Altman

Finally and most obviously, killing Sam Altman isn't going to do jackshit to stop the progress of AGI, assuming it's even progressing. For the obvious reason that Sam Altman does not represent the technological forefront of AI. (Though ChatGPT would probably disagree.)

Ergo, if AGI does indeed exist, it's not inside OpenAI.

Burn it down... and then what?

And all right, let's assume that this one douchebag techbro Sam Altman is behind AGI. You think you can destroy the code just by killing the CEO and firebombing the office? Son, it's 2026. Cloud services are a thing. They've been a thing for years.

Also, is Altman the only one far enough ahead in the race to matter? Think of all the other AI from big tech companies. Microsoft's Copilot. Google's Gemini AI. Meta's Meta AI - actually, I take that last one back. My point is, OpenAI is far from the only culpable one here. What exactly is killing Altman going to accomplish; scare all of them into not doing their usual thing? Good luck!

The fiery conclusion

All this is preposterous. Even overlooking the fact that killing a person just because you believe he heralds the inevitable doom of humanity, is morally questionable and definitely very illegal; the reasons for even going there make no damn sense. Daniel Moreno-Gama isn't the hero he imagines he is. But maybe he should stop watching so many Terminator movies.

Come with me if you want to live burn AI to the ground!
T___T