Changing multiple elements values with just 2 buttons

Hi Kirupa & team!

I’ve been surfing and learning so much in here. I have to say it’s one of the best places to learn javascript - and I search a lot.
Way back to Flash era I really learned so much with you :slight_smile:
(Thanks!)
I love VanillaJS and the power its getting. I’m a designer who also makes static websites and loves programming but I’m a complete novice. I’m very curious and love to research but not at all a real developer.
I see a lot of tutorials and some courses but I don’t really have time and projects to practice and really evolve.
I’m now in a new one which I think it’s way over my head. But I love challenges, so… :slight_smile:

About this post.

I’m making a (very) simple App but for me it has enormous challenges. The first, and biggest one (I hope), is to control the several players points with just two buttons. The idea is to have a kind of table with the different points for each player e.g. Badges, Tickets, etc., and select a specific point (badges of player 1, for instance) and Add or Remove points from it with one of the 2 buttons available. Then click in another value and do the same

After some totally failed attempts, I’ve come up with this “almost there” one (inspired by your “Handling Events for Multiple Elements” tutorial) but my solution seems very dirty. And I’m stuck with these problems:

the button keeps adding to all selected elements

It’s not updating the value in the table. Eventually I will store this on an Object but I suppose it will be more or less the same

It’s not styling only the selected element. I can’t test to remove the style if clicked outside.

I’ve made this Codepen https://codepen.io/Desirat/pen/qBpKbor?editors=0011
The idea (and only functional part) is to click only the values and then the + button.
Thanks for listening

Hi @ridesirat - glad the content here has been helping you…ever since the Flash days haha! :stuck_out_tongue:

For your example, I am unable to even get the functional part to work. Clicking on the values and hitting the + button doesn’t do anything.

If I understood your problem, is the plan to eventually be able to select Player 1 and then use the + or - buttons to move the score up or down. Similarly, selecting Player 2 and then using the + or - buttons would increase or decrease the scores for that player.

Do I have it right?

Thanks,
Kirupa :slight_smile:

Hey mate,

TBH I know what you are trying to I just couldn’t follow what you were doing with all your attempts, so I found it quicker to just do a re-factor.

Some personal preferences:

  • If you find yourself using a <div> in your HTML consider using a <custom-element> without defining the class because 'unknown' custom elements are by default treated by the browser as a <div> BUT <player-one> is a lot clearer than <div class = 'player'> (I try to avoid div soup)

  • You will need a global variable to track which score is selected (highlight and increment)

  • Its a whole lot easier to have global object tracking your scores and adjust the HTML off that object than storing all your data on the element dataset

  • In the refactor I used a switch(){} statement to filter click events and run the correct code. You could use a global object with methods but I kept it simple.

  • If you handle multiple events with one eventListener() consider having the clickable element CSS as {pointer-events: all} and all other elements *{ pointer-events: none} so that you don’t trigger a function if you click anywhere on the page.

If you have any questions on how it works, jump back on :slightly_smiling_face:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

<script>

// a memory object to keep track of scores
let score_data = {
    player_one: {
        badges : 0,
        tickets : 0
    },

    player_two: {
        badges : 0,
        tickets : 0
    }
}

// a pointer to the current selected score/ value
var selected_score;

// remove highlighting from the previous selected element
const remove_class = function(){
    document.querySelectorAll('.selected').forEach(el => el.classList.remove('selected'))
}

// filter out the click events with a switch statement "case" for each button
const click_handler = function(event){
    if (event.target !== event.currentTarget) {
        let clicked_Element = event.target;
        
        let clicked_ID = event.target.id;
        switch(clicked_ID){
            case "p1Badges" : {
                // remove the class of any other selected elements
                remove_class();
                // add selected to the clicked element
                clicked_Element.classList.add('selected');
                // set the global selected_score to the event.target
                selected_score = clicked_Element;
            }   break;
            
            case "p1Tickets" : {
                remove_class();
                clicked_Element.classList.add('selected');
                selected_score = clicked_Element;
            }   break;

             case "p2Badges" : {
                remove_class();
                clicked_Element.classList.add('selected')
                selected_score = clicked_Element;
            }   break;

             case "p2Tickets" : {
                remove_class();
                clicked_Element.classList.add('selected')
                selected_score = clicked_Element;
            }   break;

            case "btnAdd" : {
                // alert if score not selected
                if(!selected_score) return alert('Please select a score to increment');
                 // get the player for data-id
                 let player = selected_score.dataset.id;
                 // get the type from data-score
                 let type = selected_score.dataset.score;
                 // target and increment the score_data object in memory
                 score_data[player][type] ++;
                 // set the span value to the score_data value
                 selected_score.firstElementChild.textContent = score_data[player][type];
                  
            }   break;

             case "btnMinus" : {
                if(!selected_score) return alert('Please select a score to increment');
                 let player = selected_score.dataset.id;
                 let type = selected_score.dataset.score;
                 // stops incrementing below zero
                 if(score_data[player][type] !== 0) score_data[player][type] --;
                 selected_score.firstElementChild.textContent = score_data[player][type]
            }   break;    
        }
    }
    event.stopPropagation();
}

const loaded = function(){
    let body = document.querySelector('body');
    body.addEventListener('click', click_handler)
}

document.addEventListener('DOMContentLoaded', loaded)


</script>
<style>
*{pointer-events: none;}

body {
	background: #000;
	color:#fff;
	line-height: .7;
}

button {
	font-size: 2rem;
	
	color: #fff;
}

#btnMinus {
	background: red;
	margin: 1rem;
    width: 3rem;
}

#btnAdd {
	background: green;
	margin: 1rem;
	color: #fff;
    width: 3rem;
}

.selected {
	border: 2px solid yellow;
	display: inline-block;
}

.clickable{
    pointer-events: all;
    cursor: pointer;
    width: fit-content;
    padding: .5rem;

}

</style>


</head>
<body>

    <score-table>

        <player-one>
            <h2>Player 1</h2>
            <p id="p1Badges" class = 'clickable' data-id = "player_one" data-score = "badges" >Badges = <span >0</span></p>
            <p id="p1Tickets" class = 'clickable' data-id = "player_one" data-score = "tickets" >Tickets = <span>0</span></p>
        <player-one>

        <br>
        <player-two>
            <h2>Player 2</h2>
            <p id="p2Badges" class = 'clickable' data-id = "player_two"data-score = "badges" >Badges = <span >0</span></p>
            <p id="p2Tickets" class = 'clickable' data-id = "player_two" data-score = "tickets">Tickets = <span >0</span></p>
        </player-two>

    </score-table>
    <br>

    <button id="btnMinus" class = 'clickable'>-</button>
    <button id="btnAdd" class = 'clickable'>+</button>


 
</body>
</html>

Sorry, jumped the gun… :grin:

1 Like

Thanks guys!

It’s just an honor to have such a quick feedback from you.
Sorry for the late reply. I’ve been admiring, learning and trying to reproduce the same code by myself to train the art of it.

The most frustrating thing is understanding the solutions but never being able to reproduce or reach to them alone. Some detail is always missing.

@Kirupa, sorry, I forgot to say that the functionality was working just on the Console (and badly : )
Meanwhile I’ve made this image that reflects the real idea (in this case it has 4 different score types - Badges, Visa, Tickets and Passport.

@steve.mills
Thanks so much!

Such a clean solution. I’m still making it into my brain. And already using your tips.
And yep, I totally get it, with such a dirty code, you’re brave enough just for trying to read it… refactoring it seems very sane :slight_smile:

I used only two players and two score types for simplicity but if we have an undetermined nº of players (although 5 will probably be the max - added in a previous page <div>) - and more score types (like the image above), would you still advise on this solution or another one?

I will have more clickable elements outside the score table. Do you think it’s better to have multiple eventListeners instead?

I suppose keeping this object in local storage isn’t a problem? And resetting it, when needed?

Thanks, once again
and have a great weekend! :slight_smile:

2 Likes

Sorry mate,
I wasn’t clear in what I was saying… it’s not “dirty code”.

The code is imperative and hasn’t got comments so it would take a little while to work out what your variable/ functions are, what your logic is and how you tried to change it to make it work.

This is an imperitive loop in some of my code that hasn’t got comments (there is a commented version) and coming back to it even 6 months later would take a little while to work out what it’s doing… and I wrote it… :slightly_smiling_face:

for(let i = 0, len = elements.length -1; ; i++, node = iter.nextNode()){  
                    for(let prop of elements[i].props){ node[prop] = obj[prop] || '' };
                    if(i == len) break
                    if(elements[i+1].tag == 'textNode'){
                        let attr = elements[i+1].prop;
                        let child = node.childNodes[0]
                        child.data = obj[attr];  
                        i++;
                    }  
                }

(it uses the if() break as the loop [condition] IOT avoid the [final-expression] on the last iteration)

The switch() in the refactor was for clarity (so you could see how it works).

Another “cleaner” way to do it is to use an object {} for your functions instead of switch()

This uses the object[] bracket accesor notation:

So you could put you function in an object like this:

const event_repsonses = {
     p1Tickets : function(clicked_Element) {
                remove_class();
                clicked_Element.classList.add('selected');
                selected_score = clicked_Element;
            },
     btnAdd : function(clicked_Element) {
                // alert if score not selected
                if(!selected_score) return alert('Please select a score to increment');
                 // get the player for data-id
                 let player = selected_score.dataset.id;
                 // get the type from data-score
                 let type = selected_score.dataset.score;
                 // target and increment the score_data object in memory
                 score_data[player][type] ++;
                 // set the span value to the score_data value
                 selected_score.firstElementChild.textContent = score_data[player][type];            
            }
}

You could then call them like this:

const click_handler = function(event){
    if (event.target !== event.currentTarget) {
        let clicked_Element = event.target;
        let clicked_ID = event.target.id; // e.g. "p1Tickets"
// call the function on the object that has the key of "p1Tickets" 
     event_responses[clicked_ID](clicked_Element)
  }
}

You could also do a utility function like

const change_selected = function(clicked_Element){
     remove_class();
     clicked_Element.classList.add('selected');
     selected_score = clicked_Element;
}

So that your functions could all use the same code:

p1Tickets : function(clicked_Element) {
                return change_selected(clicked_Element)
            },

Having one listener is better for this type of thing. You will just have to add the appropriate class, data-id ect.

Local storage should be fine (SW cache would be more involved)

I hope this helps, good luck with your app :slightly_smiling_face:

Thanks @steve.mills !
You’re totally right. Comments are a must have.
Some challenging stuff here. I’m going to explore it.
If the App comes out ok, I’ll show you the results :slight_smile:

2 Likes