Guilherme L. Moraes
Guilherme L. Moraes

Reputation: 122

Generating dynamic ID's in HTML elements with jQuery

I'm builtin an web resume-generator to learn how to develop for web. I've made a HTML form which the user can add more fields to add more information's about him. Example: he has more than one professional experience, but the form starts with a single prof-exp field to fill. So he clicks in a "add new exp" button and the JS creates a new field for it. I used the clone() method from jQuery to do this, but this gives me with the problems I've listed bellow. Also, here's the code I've made:

var index = 0;
$(document).ready(() => {
    $("#add-exp").click(() => {
        $("#professional").clone().attr("id", "professional" + index++).
        appendTo("#professional-info").find("select, input, textarea").val("");
    })
})
<!DOCTYPE html>
<html>


<body>

<form action="" method="GET" id="main">
  <fieldset id="professional-info">
                <legend><h2>professional experience</h2></legend>
                <div id="professional">
                    <label for="level">Nível: <select name="level" id="level" >
                        <option value="empty">Selecione</option>
                        <option value="estagio">Estágio</option>
                        <option value="junior-trainee">Junior/Trainee</option>
                        <option value="aux-opera">Auxiliar/Operacional</option>
                        <option value="pleno">Pleno</option>
                        <option value="senior">Sênior</option>
                        <option value="sup-coord">Supervisão/Coordenação</option>
                        <option value="gerencia">Gerência</option>
                    </select></label>
                    <label for="position"> Cargo: <input type="text" name="carrer" id="carrer" ></label><br>
                    <label for="company"> Empresa: <input type="text" name="company" id="company" ></label><br>
                    <label for="begin"> Início: <input type="month" name="begin" id="begin" ></label>
                    <label for="break"> Término: <input type="month" name="break" id="break" ></label>
                    <label for="stl-work"><input type="checkbox" name="stl-work" id="stl-work" >Ainda trabalho aqui</label><br>
                    <label for="job-desc"> Descrição: <textarea name="job-desc" id="job-desc"  placeholder="Conte um pouco sobre o que você fazia lá." cols="40" rows="1"></textarea></label>
                    <button type="button" id="remove-exp" >Remove this professional xp</button>
                </div>
                <button type="button" form="main" id="add-exp">Add other professional exp</button> 
            </fieldset>
</form>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</body>
</html>

The problems are:

I hope you guys could understand my problem and help me with it. Also, sorry for my english - i'm learning too. Thank you all!

Upvotes: 1

Views: 2148

Answers (4)

zer00ne
zer00ne

Reputation: 43880

Details of demo code are commented in the code itself. There are minor changes to some classes for <fieldset>s and <button>s. The structure is altered a little so keep that in mind. jQuery is versatile and it allows you to generalize DOM operations and do away with dependency on ids -- it's very possible to just use classes.

Events registered to dynamic tags fail unless you delegate events. To delegate a click event to all buttons existing currently and in the future, register an ancestor tag that the buttons commonly share (ex. #main). Then assign the selectors of the buttons in the second parameter (event data):

$('#main').on('click', '.add, .remove', function(e) {...  

As for removing a by clicking a nested button -- $(e.target) and $(this) can be used to reference the button that was currently clicked. When you need to find the appropriate ancestor of a clicked button (ex. .professional) use .closest() method like so:

$(e.target).closest('.professional').remove();

Demo

let index = 0;
// Hide the first .remove button
$('#remove').hide();
/*
Register the form to the click event 
Event data directs .add and .remove buttons
*/
$("#main").on('click', '.add, .remove', function(e) {
  // if the clicked button has .add
  if ($(this).hasClass('add')) {
    /*
    clone the first .professional
    increment counter
    Reference all form controls of the clone
    on each form control modify its id 
    */
    const dupe = $(".professional:first").clone(true, true);   
    index++;
    const formControls = dupe.find('select, button, input, textarea');
    formControls.each(function() {
      let ID = $(this).attr('id');
      $(this).attr('id', ID + index);
    });
    
    /*
    Remove the legend from clone
    Show the .add and .remove on clone
    Hide the clicked button
    Add clone to form
    Stop event bubbling
    */
    dupe.find('legend').remove();
    dupe.find('.add, .remove').show();
    $(e.target).hide();
    $('#main').append(dupe);
    e.stopPropagation();
  // Otherwise if clicked button has .remove...  
  } else if ($(e.target).hasClass('remove')) {
    /*
    Find clicked button ancestor .professional and remove 
    it.
    Hide all .add buttons
    Show the last .add
    Stop event bubbling
    */
    $(e.target).closest('.professional').remove();
    $('.add').hide();
    $('.add:last').show();
    e.stopPropagation();
    
  } else {
    // Otherwise just stop event bubbling
    e.stopPropagation();
  }
});
:root {
  font: 400 14px/1 Consolas
}

fieldset {
  width: fit-content
}

legend {
  margin-bottom: -15px
}

label {
  display: block
}

input,
select,
button {
  display: inline-block;
  font: inherit;
  height: 3ex;
  line-height: 3ex;
  vertical-align: middle
}

.text input {
  width: 24ch
}

select {
  line-height: 4ex;
  height: 4ex;
}

label b {
  display: inline-block;
  width: 7.5ch;
}

button {
  position: absolute;
  display: inline-block;
  height: initial;
  margin: 0;
}

.add {
  position: absolute;
  right: 0;
}

[for=level] b {
  width: 6ch
}

.btn-grp {
  position: relative;
  width: 97%;
  min-height: 26px;
  padding: 0
}
<!DOCTYPE html>
<html>

<head></head>

<body>
  <form action="" method="GET" id="main">
    <fieldset class="professional">
      <legend>
        <h2>Professional Experience</h2>
      </legend>
      <label for="level">
          <b>Nível: </b>
          <select name="level" id="level">
            <option value="empty">Selecione</option>
            <option value="estagio">Estágio</option>
            <option value="junior-trainee">
              Junior/Trainee
            </option>
            <option value="aux-opera">
              Auxiliar/Operacional
            </option>
            <option value="pleno">Pleno</option>
            <option value="senior">Sênior</option>
            <option value="sup-coord">
              Supervisão/Coordenação
            </option>
            <option value="gerencia">
              Gerência
            </option>
          </select>
        </label>
      <fieldset class='text'>
        <label for="carrier"><b>Cargo: </b> 
          <input type="text" name="carrer" id="carrer">
          </label>
        <label for="company"><b>Empresa: </b>
          <input type="text" name="company" id="company">
          </label>
        <label for="begin"><b>Início: </b> 
          <input type="month" name="begin" id="begin">
          </label>
        <label for="break"><b>Término: </b> 
          <input type="month" name="break" id="break">
          </label>
      </fieldset>
      <label for="stl-work">
        <input type="checkbox" name="stl-work" id="stl-work" >Ainda trabalho aqui
        </label>
      <label for="job-desc"><b>Descrição: </b></label>
      <textarea name="job-desc" id="job-desc" placeholder="Conte um pouco sobre o que você fazia lá." cols="35" rows="1"></textarea>
      <fieldset class='btn-grp'>
        <button type="button" id='remove' class='remove'>Remove</button>
        <button type="button" id='add' class="add">Add</button>
      </fieldset>
    </fieldset>
  </form>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</body>

</html>

Upvotes: 1

Roko C. Buljan
Roko C. Buljan

Reputation: 206111

As suggested, Vue.js is cool, but jQuery has some forgotten powers too.

And, since you create elements dynamically, don't use IDs.
And submit to the backend your experiences as arrays []: i.e: name="carrer[]", name="company[]" etc. Than on the backend loop those data arrays to retrieve all the user experiences.

const new_exp = () => $('<div>', {
  'class': 'professional-exp',
  html: `
       <label>Nível:
         <select name="level[]">
           <option value="empty">Selecione</option>
           <option value="estagio">Estágio</option>
           <!-- etc... -->
         </select>
      </label>
      <label>Cargo: <input type="text" name="carrer[]"></label><br>
      <label>Empresa: <input type="text" name="company[]"></label><br>
      <label>Início: <input type="month" name="begin[]"></label>
      <label>Término: <input type="month" name="break[]" ></label>
      <label><input type="checkbox" name="stl-work[]"> Ainda trabalho aqui</label><br>
      <label>Descrição: <textarea name="job-desc[]" placeholder="Conte um pouco sobre o que você fazia lá." cols="40" rows="1"></textarea></label><br>
     `,
  append: $('<button>', {
    type: 'button',
    text: 'Remove',
    click() {
      $(this).closest('.professional-exp').remove();
    }
  }),
  appendTo: '#professional',
});


jQuery($ => { // DOM ready and $ alias in scope

  new_exp();                          // On init (Create first exp)
  $("#new_exp").on('click', new_exp); // On click

});
.professional-exp {
  padding: 10px;
  margin-bottom: 10px;
  background: #eee;
}
<form action="" method="POST" id="main">
  <fieldset>
    <legend>
      <h2>Professional experience</h2>
    </legend>
    <div id="professional"></div>
    <button type="button" id="new_exp">+ Add more</button>
  </fieldset>
</form>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

Above we're defining the Remove's button action right within the template, but if you want you can also hardcode the button into the template and create a dynamic click handler (using jQuery's .on()) like:

const exp_new = () => $('<div>', {
  'class': 'professional-exp',
  html: `
     <label>Nível:
       <select name="level[]">
         <option value="empty">Selecione</option>
         <option value="estagio">Estágio</option>
         <!-- etc... -->
       </select>
    </label>
    <label>Cargo: <input type="text" name="carrer[]"></label><br>
    <label>Empresa: <input type="text" name="company[]"></label><br>
    <label>Início: <input type="month" name="begin[]"></label>
    <label>Término: <input type="month" name="break[]" ></label>
    <label><input type="checkbox" name="stl-work[]"> Ainda trabalho aqui</label><br>
    <label>Descrição: <textarea name="job-desc[]" placeholder="Conte um pouco sobre o que você fazia lá." cols="40" rows="1"></textarea></label><br>
    <button class="exp_delete">REMOVE</button>
  `,
  appendTo: '#professional',
});



jQuery($ => { // DOM ready and $ alias in scope

  exp_new();                          // On init (Create first exp)
  $("#exp_new").on('click', exp_new); // and on click.
  $('#main').on('click', '.exp_delete', ev => $(ev.target).closest('.professional-exp').remove());

});
.professional-exp {
  padding: 10px;
  margin-bottom: 10px;
  background: #eee;
}
<form action="" method="POST" id="main">
  <fieldset>
    <legend>
      <h2>Professional experience</h2>
    </legend>
    <div id="professional"></div>
    <button type="button" id="exp_new">+ Add more</button>
  </fieldset>
</form>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

Upvotes: 1

Cat
Cat

Reputation: 4226

One way to connect your new "remove" button with its "professional" div would be to add an extra statement in your event handler to update its id parallel to the new div's id, something like:

let lastIndex = index - 1;
$("#professional" + lastIndex).find("button").attr("id", "add-exp" + lastIndex);

(This code may not have the correct syntax -- I don't use jQuery very much -- but you can see the idea.)

A better way might be, when the "remove" button is clicked, don't remove according to ID, but instead find the closest ancestor div and remove that div.

For labels, you should leave out the ids (because no two elements on the same page should ever have the same id). And because the inputs are nested in the labels, you should be able to leave out the for attribute as well and let the the association be implicit. (See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label.)

Upvotes: -1

scott6
scott6

Reputation: 766

if you already wrap your input inside a label, you dont need id anymore, and you can use this as a parameter of delete button, so you can use it to delete your block.

Please check the following example

$(function(){
	// keep the first block hidden as an empty template 
	$('.form-row:first').hide();
	// trigger add new item
	AddItem();
})
function AddItem(){
	var container = $('#container');
	// clone the form, show it & append before add button
	var cloned = $('.form-row:first').clone().show().insertBefore($('#addBtn'));
}
function RemoveItem(elm){
	// get form element & remove it
	$(elm).closest('.form-row').remove()
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<style type="text/css">
.form-row {border:1px solid #ccc; margin:5px 0;padding:10px;}
</style>
<div id="container">
	<div class="form-row">
		<!-- wrap your input inside label tag to avoid using id as reference -->
		<label>Field 1 : <input type="text" name="field1"></label>
		<label>Field 2 : <input type="text" name="field2"></label>
		<input type="button" value="Remove this item" onclick="RemoveItem(this)">
	</div>
	<input id="addBtn" type="button" value="Add new item" onclick="AddItem()">
</div>

Upvotes: 0

Related Questions