if/else block with button clicked events that defy my wishes

Viewed 202

I'm creating a page with a form where user would enter name (iname) and price (iprice) for an item in a form that dynamically so the user could enter as many as the user would wish. So the flow goes as the following:

  1. the page load with a form that shows one input to enter item's name (iname)

  2. up on clicking outside the name input, it triggers onblur event that run another funciton to add an input to enter the price of the item

  3. 3- in that function I want to run if/else (which I'll describe shortly) where it will run onclick to submit the form results if the save button clicked or rerun the function to create another input for the user to keep entering items name and price.

  4. 4- that part with the if/else is what I need help with and just in plain js (no frameworks or jquery please)

my code:

 <script>
  import { onMount } from 'svelte';

  function getinfo(){

  let x = document.querySelectorAll('input')
  for (let i = 0; i < x.length; i++) {
                 let values = x[i].value;
    // persist the added values to db, localstorage...etc
    // for now, just log it
            console.log("input-value: ", values)
               }    
    } 
    
   onMount( () => {
        //create a name input, append it, add onblur fn that create price input  
       var iname = document.createElement('input')   
       iname.classList.add("name", "border", "space")      
       var parent = document.getElementById("parent");
       parent.appendChild(iname);         
        
       // Once you click outside or hit tab, create price input.       
       iname.onblur = function ()  {
           addprice()
       }
                    
           }  
     ) 
     
   function additem(){
   
    //Create an input, add classes and set its focus
       var parent = document.getElementById("parent"); 
       var iname = document.createElement('input')   
       iname.classList.add("name", "border", "space")      
       parent.appendChild(iname);         
       iname.focus()   
    
       // Once you click outside, add another price input(run addprice)   
       iname.onblur = function () {
               addprice()
       };
           
       }  
      
      
    function addprice() {
       
    //Create price input, add classes and set its focus
       var parent = document.getElementById("parent");
       var iprice = document.createElement('input');      
       parent.appendChild(iprice);
       iprice.classList.add("price", "border") 
       iprice.focus() 
      
    let btn = document.getElementById("button")
        
            //*****************************************//
               // Need Your Help With This Part //
                 // if btn is clicked, run getinfo()
    if (btn.clicked === true) {  
       btn.onclick = function () {
         getinfo();  
       }                     
   } else {
               // if btn not clicked, run onblur to add another item input
        iprice.onblur = function () {
             additem()
        }
    } // end of else block
   }  // end of addprice()       
    
</script>



<h1> Enter Menu Items </h1>

<form id="mainform" name="formcollect">

    <div class="parent" id="parent">

    </div>

    <button id="button" class="space" type="button" >SAVE</button>
</form>

I'm using svelte/sapper for this page but it's plain old js code that runs it. is btn.clicked=== true is the wrong condition. Because if I run the code with btn.onclick() part alone, it does what the code suppose to do and run the getinfo() with the entered value logged. If I run iprice.onblur() it adds a new input and I keep doing that loop (create iname input and iprice input) but if I add if/else blocks as I included here in the code above, it only runs the onblur function and I keep creating inputs and button doesn't fire the getinfo().

Please watch this video I created so the issue is clear because it is hard to describe. Watch Youtube video describing the issue Notice that when I click save (I'm done creating input and entering data) and want to save the form, the save button keeps creating inputs. How do I submit once I click save?

2 Answers

When you click the button the input first loses focus and then the click handler runs. Since onblur happens before onclick it's tricky to cancel the onblur handler from inside the click handler but it can be done using setTimeout to delay the onblur handling for a moment (so as to give the onclick handler an opportunity to cancel it).

$(() => { //using jQuery instead of onMount so I don't need to setup svelte
  const inputElem = document.querySelector('input');
  const btnElem = document.querySelector('button');
  
  let latestOnBlurHandlerTimeoutId = null;
  const onBlurHandler = () => {
    //wrap blur handler logic in a timeout so it that it won't run before onclick handler does (which means we can cancel the onblur handler from in the onclick handler)
    latestOnBlurHandlerTimeoutId = setTimeout(() => {
      console.log('handling blurred');
      const newInputElem = document.createElement('input');
      const container = document.querySelector(".inputsContainer");
      container.appendChild(newInputElem);
      newInputElem.focus();

      newInputElem.onblur = () => {
        console.log('blurred');
        onBlurHandler();
      };
    }, 250);
  };
  inputElem.onblur = () => {
    console.log('blurred');
    onBlurHandler();
  };
  
  btnElem.onclick = () => {
    console.log('clicked');
    if (latestOnBlurHandlerTimeoutId) {
      //cancel pending onblur handler
      clearTimeout(latestOnBlurHandlerTimeoutId);
    }
  };
});
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<div class="inputsContainer">
  <input type="text"></input>
</div>
<div>
  <button>Go</button>
</div>

One other unrelated but useful tip. Use https://beautifier.io/ to get your code indented correctly as your first step to solving any problem. You'd be surprised how much it will help you correctly understand your code and think clearly about it.

The input-blur event is triggered before the button-click event

The blur event from one of your inputs is triggered before the click event on your button. You respond to this blur event by creating a new input field and focusing on that input field.

A demo to prove that:

const button = document.getElementById('button');
button.addEventListener('click', () => {
  console.log('Submitting info..');
})

const input = document.getElementById('input');
input.addEventListener('blur', () => {
  console.log('Creating and focusing a new input');
  input.focus();
});

input.focus();
<input id='input' />
<button id='button'>Unreachable button</button>

Why timeouts are a bad solution

You could hack your way around it with timeouts and cancelling them in the onclick event. But what if an user wants to click anything outside the input? It would always create a new input, unless you clear the timeout set in the blur event.

Using an approach like this leads to a very very very poor user experience. I am pretty sure you are trying to achieve a good one.

A suggestion

It would be more acceptable to always have one empty input field. You would have to check if the last field is empty. The input event would be a better event to listen to.

Implement something along these lines:

  • listen to input events on the input
  • check if the last input field has an empty value
  • if not insert an empty field at the end (without focus)
  • otherwise do nothing
  • get the values on clicking the button, but filter out the empty one.

You should be good to go with this. Goodluck ;).

Related