Configuring same button for multiple events

前端 未结 1 957
礼貌的吻别
礼貌的吻别 2021-01-29 11:26

I have a form button in my web app, (purely made with Vanilla Js), The form is used to get data from user and when button is clicked, that data goes to a HTML table and so on.

相关标签:
1条回答
  • 2021-01-29 11:51

    The issue, as you're likely aware, is that you're assigning multiple click listeners to the same element (a behaviour that you want) at the same time (a behaviour you don't want). There are a couple of ways you could fix this.


    The fastest fix

    The quickest way to solve your problem would be to use .removeEventListener(). With this, you can remove the previous click listener (the one that adds a new element to the info array) before you create the second click listener that sets the edited data.

    At the end of the second click listener function, you would rebind the first click listener again so the behaviour returned to normal.

    Pros of this solution

    1. It's the fastest way to solve the problem you're having

    Cons of this solution

    1. Unbinding and rebinding event listeners can make it hard to reason about your code
    2. In order to remove an event listener, you need to supply the original function (like .removeEventListener("click", listenerFunction). As you are currently using an anonymous function expression, you'll have to move the functions currently inside click listeners elsewhere and name them (so that you can pass them to the removeEventListener function
    3. It's not actually clear which event listener is bound to addButton at any one time

    The solution

    We need to move the function declaration for addButton outside of .addEventListener and give it a name. I've called it addItemClickHandler but you can call it whatever you want:

    function addItemClickHandler(e) {
        var obj = {
            id : object.count,
            date : formatDate(inDate.value, inMonth.value),
            day : inDay.value,
            item : inItem.value,
            price : parseInt(inPrice.value),
        };
        object.info.push(obj);
        object.count += 1;
        refreshDOM();
    }
    
    addButton.addEventListener("click", addItemClickHandler);
    

    This should work exactly the same. Now we need to move the second event listener function you're trying to add into its own named function. As we're only referring to the name of the function from inside itself, we don't even need to move it out, just give it a name. I'm going to give it editItemClickHandler:

    addButton.removeEventListener("click", addItemClickHandler);
    addButton.addEventListener("click", function editItemClickHandler(e){
        object.info[eindex]['day'] = inDay.value;
        object.info[eindex]['date'] = formatDate(inDate.value, inMonth.value);
        object.info[eindex]['item'] = inItem.value;
        object.info[eindex]['price'] = parseInt(inPrice.value);
        refreshDOM();
    
        addButton.removeEventListener("click", editItemClickHandler);
        addButton.addEventListener("click", addItemClickHandler);
    });
    
    • As you can see, we first remove the listener addItemClickHandler so that when you click on the "Add" button, it won't do anything

    • We then bind a different click listener, that we give the name editItemClickHandler so we can remove it after the edit is complete

    • We do all the edits we need to do

    • Finally, we remove the new edit click listener we created and re-add the original click listener, so the functionality goes back to normal


    The more robust fix

    Here is a codepen of your application after applying the following fixes.

    The above solution is the fastest way to fix your problem, but there are more robust ways to ensure a working solution. In this solution, I'm going to tidy up some of your code in order to make it cleaner and easier to understand.

    Pros of this solution

    1. We won't have to unbind or rebind any click listeners
    2. It's easier to reason about what's happening

    Cons of this solution

    1. It'll take longer to implement, as it requires restructuring more of your code

    The solution

    Step 1: Keep track of whether we're editing or not

    Firstly, as we're not rebinding click listeners, we need to keep track of what we're editing. Let's create an object called editing just below object:

    var editing = {
        mode: false,
        index: null
    };
    

    This will let us keep track of whether or not we're editing anything (editing.mode), and what the index of the item we're editing is (editing.index).

    Step 2: Update the addButton event listener to use the editing object

    Next, we need to modify our addButton.addEventListener to use this new editing object:

    addButton.addEventListener("click", function(e){
        if (editing.mode) {
            var info = object.info[editing.index];
    
            info['day'] = inDay.value;
            info['date'] = formatDate(inDate.value, inMonth.value);
            info['item'] = inItem.value;
            info['price'] = parseInt(inPrice.value);
    
            editing.mode = false;
        } else {
            var obj = {
                id : object.count,
                date : formatDate(inDate.value, inMonth.value),
                day : inDay.value,
                item : inItem.value,
                price : parseInt(inPrice.value),
            };
            object.info.push(obj);
            object.count += 1;
        }
    
        refreshDOM();
    });
    
    • If editing.mode is true, when the addButton is clicked, it will update the values and then disable editing.mode, putting it back to the way it was before

    • If editing.mode is false, it will simply add the item to the array (same code as you had before)

    • No matter what happens, the DOM will get refreshed

    Step 3: Update the modifyBtn event listener to use the editing object

    Also, I've noticed you're using classes to modify programmatic behaviour instead of data- attributes. This is fine in a lot of cases, but for your exact use case of determining the behaviour, it's recommended to use data- attributes instead. We should also set the href's to #, as we don't need to use them as links:

    <td><a href="#" data-id={{id}} data-action="edit">Edit</a> | <a href="#" data-id={{id}} data-action="delete">Delete</a></td>
    

    Now, I've restructured your modifyBtn.addEventListener() to handle a these aspects differently:

    modifyBtn.addEventListener('click', function(e){
        e.preventDefault();
    
        var el = e.target;
        var id = parseInt(el.dataset.id);
        var index = object.info.findIndex(item => item.id === id);
        var info = object.info[index];
        var action = el.dataset.action;
    
        if (action === "edit") {
            editing.mode = true;
            editing.index = index;
    
            inDay.value = info['day'];
            inDate.value = parseInt(info['date'].substr(0,2));
            inMonth.value = info["date"].substr(4);
            inItem.value = info['item'];
            inPrice.value = info['price'];
        }
    
        if (action === "delete") {
            object.info.splice(index, 1);
        }
    
        refreshDOM();
    });
    
    • Using e.preventDefault() means that the browser wont navigate to the # href when clicking on the link

    • I've moved duplicate code where it retrieved eid and eindex no matter what action you were performing ('editing' or 'adding') outside of those functions

    • As we are now editing something, we set the editing object to have enabled: true and whatever the index of the current item we're editing is

    • Instead of using object.info[eindex] every time, I've assigned it to a variable

    • You no longer need to keep your getIndex function, as you can use Array.prototype.findIndex() instead

    • The recommended way to get data- attributes is to use element.dataset.name instead of element.getAttribute()

    • As is standard now, no matter what happens the DOM will get refreshed

    Step 4: Add a dynamic form header

    Alright, awesome! So this totally works. One final thing I'd like to do is make it more clear what is happening, so in your index.html under your <div class="form-modal">, I'm going to add an ID to your h2:

    <h2 id="form-header">Add Item</h2>
    

    Then, back at the top of index.js:

    formHeader = getElement('form-header');
    

    And then finally in refreshDOM():

    formHeader.textContent = editing.mode ? "Edit Item" : "Add Item";
    

    This will update the text inside <h2 id="form-header"></h2> to depending on whether or not it's being edited. This is a ternary operator, and is often used as a quick way to choose different outcomes depending on a boolean variable.


    I hope this wasn't too much information. I've spent a while looking at your code and really wanted to help with best practices and such! Let me know if you have any questions!

    0 讨论(0)
提交回复
热议问题