5

I am creating an app for tracking expenses.

Here is a codepen with all of the source code.

Expense Tracking App Screenshot


I have a global object called object that I render out to a Mustache template:

var object = {
    info : [
        {date: "14, July", day: "Saturday", item: "Lunch", price: 40}
        {date: "15, July", day: "Sunday", item: "Airport", price: 80}
        {date: "14, July", day: "Saturday", item: "Snacks", price: 25}
    ],
    withdrew: 0
};
{{#info}}
<tr>
    <td>{{date}}</td>
    <td>{{day}}</td>
    <td>{{item}}</td>
    <td>{{price}} /-</td>
    <td><a href="#" id="editInfo">Edit</a> | <a href="#" id="deleteInfo">Delete</a></td>
</tr>
{{/info}}
Mustache.render(template, object);

As the user enters details in the Add Item div (to the right of the screenshot), the info array in object gets filled (it currently has 3 entries).


The part I'm stuck on is editing a row when the user clicks either the "Edit" or "Delete" button in the "MODIFY" table column. I have the following click listener bound to the table:

var modifyBtn = document.querySelector('table');
modifyBtn.addEventListener('click', function(e){
    console.log(e.target);
});

With this, I am able to get both of the button nodes correctly with console.log(), but how do I make them point uniquely to the original array elements that each row is generated from?

Any help is highly appreciated.

Sam Holmes
  • 1,594
  • 13
  • 31
krupesh Anadkat
  • 1,932
  • 1
  • 20
  • 31

1 Answers1

2

There are a couple of ways to do this, but the way that immediately stands out to me is the following:

  1. Add the indexes to the array elements for the Mustache render function
  2. Render out the indexes as data- attributes in the HTML
  3. Retrieve this data in the click listener
  4. Modify the original info array using this data

Let's run through this.


Step 1: Add the indexes to the array elements for the Mustache render function

First things first, we want to retrieve the array indexes in the HTML. This data isn't available by default when rendering with Mustache, so we need to add it in ourselves. This can't be a permanent addition to the array objects. The actual indexes can change at any time (say, if an element is removed). Instead, we only want to render out the indexes for the HTML and nowhere else.

Create the following addIndexes() function:

function addIndexes(object) {
    object = JSON.parse(JSON.stringify(object));
    var info = object.info;

    for (var i = 0; i < info.length; i++) {
        info[i].index = i;
    }

    return object;
}
  • The first line clones the object. Remember, we don't want to modify the original object or array. This allows us to work on a copy that doesn't affect the original.
  • We run through the array and add the index of that array to a property called index.

var object = {
    info : [
        {date: "14, July", day: "Saturday", item: "Lunch", price: 40},
        {date: "15, July", day: "Sunday", item: "Airport", price: 80},
        {date: "14, July", day: "Saturday", item: "Snacks", price: 25}
    ],
    withdrew: 0
};

function addIndexes(object) {
    object = JSON.parse(JSON.stringify(object));
    var info = object.info;

    for (var i = 0; i < info.length; i++) {
        info[i].index = i;
    }

    return object;
}

console.log("--- The object AFTER addIndexes() ---");
console.log(addIndexes(object));

console.log("--- The object BEFORE addIndexes() ---");
console.log(object);

Now, simply modify the Mustache.render() method to use our new addIndexes() function:

function refreshDOM() {
    var outputBody = Mustache.render(template, addIndexes(object));
    ...
}

Step 2: Render out the indexes as data- attributes in the HTML

Now we need to attach those indexes in our HTML template. In your main index.html, go to the render function and change the button code to read this:

<td><a href="#" id="editInfo" data-index="{{index}}">Edit</a> | <a href="#" id="deleteInfo" data-index="{{index}}">Delete</a></td>

Now we are outputting the index to the data-index attribute, which we can retrieve later. This renders out each button element to look something like this:

<td><a href="#" id="editInfo" data-index="0">Edit</a> | <a href="#" id="deleteInfo" data-index="0">Delete</a></td>
<td><a href="#" id="editInfo" data-index="1">Edit</a> | <a href="#" id="deleteInfo" data-index="1">Delete</a></td>

And so on, for each row.

Step 3: Retrieve this data in the click listener

We can now actually handle this data. Change your modifyBtn click event listener to be the following:

var modifyBtn = document.querySelector('table');
modifyBtn.addEventListener('click', function(e){
    var el = e.target;
    var action = el.id;
    var index = parseInt(el.dataset.index);

    switch (action) {
        case "editInfo": editInfo(index); break;
        case "deleteInfo": deleteInfo(index); break;
    }
});
  • We're now getting the el (which is just the element) from event.target.
  • We can get a string with the action (like "editInfo") from your el.id.
  • The index value is data-index which can be retrieved with el.dataset.index. Note that this gets placed in HTML as a string (which we can't use), so we have to run parseInt() on it to get an integer from the string.
  • With a switch statement, we can now perform an action depending on the ID.

Neither of these handler functions (editInfo() nor deleteInfo()) are currently declared, so on to the final step!

Step 4: Modify the original info array using this data

I don't know what you want to do with the editInfo ID, so I'll leave that one up to you. I've created a simple deleteInfo function directly above the click listener:

function deleteInfo(index) {
    object.info.splice(index, 1);
    refreshDOM();
}

This will remove the specified index from the global object.info array and then call refreshDOM() to update.


We're now complete! Sorry this was such a long-winded answer, I wanted to break down each step as we went along. Let me know if this all made sense and if you have any questions!

Sam Holmes
  • 1,594
  • 13
  • 31
  • 1
    I have placed an "id: integer" key/value pair inside each object of Info array, just as you did before reading this answer (i thought no one will answer my questions and have to solve things by myself).. Now, reading your answer i just got some confidence that i kinda followed right approach. Also came to know, people like you are present to help :) By your elaborate answer, it helped me to learn few things, the way you defined "modifyBtn event listener" to get complete info of element is superb.. thanks. I want to ask about "Edit" functionality, i could add that feature with tiny bug. – krupesh Anadkat Jul 16 '18 at 09:49
  • The "id: *int*" is often the way to go if you're working with an actual database with primary keys! Definitely you were on the right track :) I'm happy I could help! And oh? What's the bug? – Sam Holmes Jul 16 '18 at 10:04
  • I have made "editInfo" event listener to edit row details, on clicking that editInfo button, all the row details is displayed in Input fields of "Add item Div" ) left sidebar in img, & i am using the same "Submit" button to confirm edit & then refreshDOM() (you must be famiiar with my functions a bit , as you answered my prev. question). The minor Bug is : the code inside Submit button event listener is executed mor than 1s onclick, & this adds new row entry in table (also confirming row edit) Defined Submit Event Listener at 2 places (outside -for new row & inside modifyBtn Event listener) – krupesh Anadkat Jul 16 '18 at 11:17
  • I'm having trouble understanding exactly what you mean. Please could create a codepen with this buggy code? – Sam Holmes Jul 16 '18 at 11:37
  • Sam, here is the link to App : https://krupeshanadkat.github.io/App/ when u click "edit" button , corresponding row Info will be shown in input fields. After making changes, when you hit Submit, (addbtn event listener is tied to it) Is executing at 2 places, i want it to execute only once, i.e The one which is present inside modifyBtn event listener. I donot want outer addBtn to execute during editing row entries. Hope you got my poor english. (You can check source code with dev tools of chrome.) – krupesh Anadkat Jul 16 '18 at 14:17
  • Sam, I have added a whole new question for above described problem , link : https://stackoverflow.com/questions/51374767/handling-2-event-listener-same-name-different-functions-run-only-required-ev – krupesh Anadkat Jul 17 '18 at 06:45
  • Awesome, will check it out now! – Sam Holmes Jul 17 '18 at 08:23