克隆/删除输入字段 - 保持元素ID唯一

时间:2014-01-31 20:32:06

标签: javascript jquery

我目前正在处理在表单中生成动态输入字段。我有一个复杂的例子,它使用复选框和选择框。它有两种类型的元素:main_itemssub_items。如上所述,我可以通过clone函数动态地添加一些jquery输入字段,该函数复制具有唯一id属性的一组新输入字段。但是我对两件事情有很大的困难:首先,保持id对于每个复制的元素都是唯一的,特别是对于选择框。其次,我只能得到第一个下拉菜单才能用于第一个项目,但我还没有想出办法为其他项目做这个。 JSFIDDLE

$('#btnAdd').click(function () {
    var num = $('.clonedSection').length;
    var newNum = num + 1;

    var newSection = $('#pq_entry_' + num).clone().attr('id', 'pq_entry_' + newNum);
    newSection.find('input[type="text"]').val('');
    newSection.find('select').val('');
    newSection.find('input[type="checkbox"]').prop('checked', false);
    //hide sub item
    newSection.find('.sub-item').hide();

    //change the input element selectors to use name
    newSection.find('input[name^="first_item_"]').attr('id', 'main_item_' + newNum).attr('name', 'main_item_' + newNum);
    newSection.find('input[name^="second_item_"]').attr('id', 'second_item_' + newNum).attr('name', 'second_item_' + newNum);
    newSection.find('input[name^="item_count_"]').attr('id', 'item_count_' + newNum).attr('name', 'item_count_' + newNum);
    newSection.find('input[name^="sub_item_"]').attr('id', 'sub_item_' + newNum).attr('name', 'sub_item_' + newNum);
    newSection.find('input[name^="other_item_"]').attr('id', 'other_item_' + newNum).attr('name', 'other_item_' + newNum);
    newSection.insertAfter('#pq_entry_' + num).last();

    $('#btnDel').click(function () {
        var num = $('.clonedSection').length; // how many "duplicatable" input fields we currently have
        $('#pq_entry_' + num).remove(); // remove the last element

        // enable the "add" button
        $('#btnAdd').prop('disabled', '');

        // if only one element remains, disable the "remove" button
        if (num - 1 == 1) $('#btnDel').prop('disabled', 'disabled');
    });
});


$('#btnDel').prop('disabled', 'disabled');

//Generate Dropdown
$('#item_count_1').change(function() {
    var option = $(this).val();
    showFields(option);
    return false;
});

function showFields(option){ 
    var content = '';
    for (var i = 1; i <= option; i++){
        content += '<div id="item_'+i+'"><label>Item # '+i+'</label><br /><label>Item Name:</label> <select id="item_name_'+i+'" name="item_name_'+i+'" class="course_list"><option value="" >--- Select ---</option><option value="apples" >apples</option><option value="banana" >banana</option><option value="mango" >mango</option></select></div>';  
    }
    $('#item_names_1').html(content);
}

HTML

<ul id="pq_entry_1" class="clonedSection">
  <li style="list-style-type: none;">
    <input id="first_item_1" class="main-item" name="main_item_1" type="checkbox"><label>First Item</label>
  </li>
  <li style="list-style-type: none;">
    <input id="second_item_1" class="main-item" name="main_item_1" type="checkbox"><label>Second Item</label>
  </li>
  <ul class="sub-item" style='display: none;'>
    <li style="list-style-type: none;">
      <label>
        How many items:
        <small>required</small>
      </label>
      <select id="item_count_1" name="item_count_1" class="medium" required>
        <option value="">---Select---</option>
        <option value="1">1</option>
        <option value="2">2</option>
      </select>
    </li>
    <li style="list-style-type: none;">
      <div id="item_name_1"></div>
    </li>
  </ul>
</ul>

3 个答案:

答案 0 :(得分:20)

所以,让我们谈谈如何构建基本的GUI应用程序。在我们开始之前,我希望您知道下面的代码可以用Knockout / Angular中的~20 LoC编写,但我选择不这样做,因为它不会真正教给任何人任何东西。

所以,让我们谈谈GUI。

这一切归结为两件事。

  • 演示文稿 - 这是您的HTML,CSS以及用户直接与之交互的内容。
  • 数据 - 这是您的实际数据和逻辑。

我们希望分开它们,以便它们可以独立行动。我们想要实际表示用户在JavaScript对象中看到的内容,以便它可以维护,可测试可读等等。有关详细信息,请参阅Separation of Concerns

让我们从数据开始。

那么,每个东西在你的应用程序中有什么作用?

  • First Item ,无论是真还是假
  • 子项目,无论是真还是假,但如果第一项不是真的,则永远不会为真。
  • 第二项,无论是真还是假。
  • 项数,这是一个数字
    • 这些项目中的每一项都是苹果,香蕉或芒果

最直观的是在那里开始

// our item, like we've just described it :) 
function Thing(){ //we use this as an object constructor.
    this.firstItem = false;
    this.subItem = false;
    this.secondItem = false;
    this.numItems = 0;
    this.items = []; // empty list of items
}

嗯,这是一件事,我们现在可以使用new Thing()创建它们,然后设置它们的属性,例如thing.firstItem = true

但我们没有 a Thing我们有东西。东西只是一个(有序的)一堆东西。有序集合通常由JavaScript中的数组表示,因此我们可以:

var stuff = []; // our list
var thing = new Thing(); // add a new item
stuff.push(thing); // add the thing we just created to our list

我们当然也可以在提交时将此信息传达给PHP。另一种方法是提交一个JSON对象并在PHP中读取它(这很好!),或者我们可以serialize it as form params(如果您对该问题中的方法有任何问题 - 请告诉我)。

现在我只有一堆物品......而且很头疼。

非常精明。到目前为止,您只有对象,您没有在任何地方指定它们的行为。我们有我们的数据&#39;图层,但我们还没有任何表示层。我们首先要删除所有ID并添加行为。

输入模板!

我们不想克隆现有的物品,而是希望拥有一个“切割器”。创建新元素外观的方法。为此,我们将使用模板。让我们首先提取您的商品列表&#39;查看HTML模板。基本上,考虑到你的HTML,它就像:

<script type='text/template' data-template='item'>

<ul class="clonedSection">
  <li style="list-style-type: none;">
    <label><input class="main-item" type="checkbox" />First Item</label>
    <ul class="sub-item" style="display: none;">
      <li style="list-style-type: none;">
        <label><input type="checkbox" />Sub Item</label>
      </li>
    </ul>
  </li>
  <li style="list-style-type: none;">
    <label>
      <input class="main-item" type="checkbox" />Second Item</label>
    <ul class="sub-item" style='display: none;'>
      <li style="list-style-type: none;">
        How many items:
        <select class="medium" required>
          <option value="">---Select---</option>
          <option value="1">1</option>
          <option value="2">2</option>
        </select>
      </li>
      <li style="list-style-type: none;"><div></div></li>
    </ul>
  </li>
</ul>
</script>

现在让我们创造一个“愚蠢的”。在屏幕上显示模板的方法。

var template;
function renderItem(){
    template = template || $("[data-template=item]").html();
    var el = $("<div></div>").html(template);
    return el; // a new element with the template
} 

[这里是我们的第一个jsfiddle演示演示](http://jsfiddle.net/RLRtv/,它只添加了三个项目,没有任何行为到屏幕上。阅读代码,看到你理解它并且不是不敢问你不明白的事情:)

将它们绑在一起

接下来,我们会添加一些行为,当我们创建项目时,我们会将其与Thing相结合。因此,我们可以采用单向数据绑定方式(视图中的更改反映在模型中)。如果您感兴趣,我们可以稍后实施另一个绑定方向,但它不是原始问题的一部分,所以为了简洁起见,我们暂时跳过它。

function addItem(){
    var thing = new Thing(); // get the data
    var el = renderItem(); // get the element
    el. // WHOOPS? How do I find the things, you removed all the IDs!?!?
}

那么,我们在哪里被困?我们需要将行为附加到我们的模板,但普通的HTML模板没有钩子,所以我们必须手动完成。让我们首先使用&#39;数据绑定来改变我们的模板。属性。

<script type='text/template' data-template='item'>

<ul class="clonedSection">
    <li style="list-style-type: none;">
        <label>
            <input class="main-item" data-bind = 'firstItme' type="checkbox" />First Item</label>
        <ul class="sub-item" data-bind ='subItem' style="display: none;">
            <li style="list-style-type: none;">
                <label>
                    <input type="checkbox" />Sub Item</label>
            </li>
        </ul>
    </li>
    <li style="list-style-type: none;">
        <label>
            <input class="main-item" data-bind ='secondItem' type="checkbox" />Second Item</label>
        <ul class="sub-item" style='display: none;'>
            <li style="list-style-type: none;">How many items:
                <select class="medium" data-bind ='numItems' required>
                    <option value="">---Select---</option>
                    <option value="1">1</option>
                    <option value="2">2</option>
                </select>
            </li>
            <li style="list-style-type: none;">
                <div data-bind ='items'> 

                </div>
            </li>
        </ul>
    </li>
</ul>
</script>

查看我们添加的所有data-bind属性?让我们尝试选择那些。

function addItem() {
    var thing = new Thing(); // get the data
    var el = renderItem(); // get the element
    //wiring
    el.find("[data-bind=firstItem]").change(function(e){
       thing.firstItem = this.checked;
        if(thing.firstItem){//show second item
            el.find("[data-bind=subItem]").show(); //could be made faster by caching selectors
        }else{
            el.find("[data-bind=subItem]").hide();
        }
    });
    el.find("[data-bind=subItem] :checkbox").change(function(e){
        thing.subItem = this.checked;    
    });
    return {el:el,thing:thing}
}

this fiddle我们已经为第一个项目和子项目添加了属性,他们已经更新了元素。

让我们继续为第二个属性做同样的事情。它几乎是一样的,直接绑定。另外,有几个库会自动为您执行此操作 - Knockout for example

这里是设置了所有绑定的another fiddle,这结束了我们的表示层,我们的数据层及其绑定。

var template;

function Thing() { //we use this as an object constructor.
    this.firstItem = false;
    this.subItem = false;
    this.secondItem = false;
    this.numItems = 0;
    this.items = []; // empty list of items
}

function renderItem() {
    template = template || $("[data-template=item]").html();
    var el = $("<div></div>").html(template);
    return el; // a new element with the template
}

function addItem() {
    var thing = new Thing(); // get the data
    var el = renderItem(); // get the element
    el.find("[data-bind=firstItem]").change(function (e) {
        thing.firstItem = this.checked;
        if (thing.firstItem) { //show second item
            el.find("[data-bind=subItem]").show(); //could be made faster by caching selectors
        } else {
            el.find("[data-bind=subItem]").hide();
        }
    });
    el.find("[data-bind=subItem] :checkbox").change(function (e) {
        thing.subItem = this.checked;
    });
    el.find("[data-bind=secondItem]").change(function (e) {
        thing.secondItem = this.checked;
        if (thing.secondItem) {
            el.find("[data-bind=detailsView]").show();
        } else {
            el.find("[data-bind=detailsView]").hide();
        }
    });
    var $selectItemTemplate = el.find("[data-bind=items]").html();
    el.find("[data-bind=items]").empty();

    el.find("[data-bind=numItems]").change(function (e) {
        thing.numItems = +this.value;
        console.log(thing.items);
        if (thing.items.length < thing.numItems) {
            for (var i = thing.items.length; i < thing.numItems; i++) {
                thing.items.push("initial"); // nothing yet
            }
        }
        thing.items.length = thing.numItems;
        console.log(thing.items);
        el.find("[data-bind=items]").empty(); // remove old items, rebind
        thing.items.forEach(function(item,i){

            var container = $("<div></div>").html($selectItemTemplate.replace("{number}",i+1));
            var select = container.find("select");
            select.change(function(e){                
                thing.items[i] = this.value;
            });
            select.val(item);
            el.find("[data-bind=items]").append(container);

        })

    });
    return {
        el: el,
        thing: thing
    }
}

for (var i = 0; i < 3; i++) {
    var item = addItem();
    window.item = item;
    $("body").append(item.el);
}

按钮

有趣的是,既然我们完成了繁琐的部分,按钮就是小菜一碟。

让我们添加&#34;添加&#34;按钮

 <input type='button' value='add' data-action='add' />

和JavaScript:

var stuff = [];
$("[data-action='add']").click(function(e){
     var item = addItem();
     $("body").append(item.el);
     stuff.push(item);
});

男孩,that was easy

好的,所以删除应该很难,对吧?

HTML:

<input type='button' value='remove' data-action='remove' />

JS:

$("[data-action='remove']").click(function(e){
     var item = stuff.pop()
     item.el.remove();
});

好的,so that was pretty sweet.那么我们如何获取数据呢?让我们创建一个按钮,显示屏幕上的所有项目?

<input type='button' value='show' data-action='alertData' />

和JS

$("[data-action='alertData']").click(function(e){
    var things = stuff.map(function(el){ return el.thing;});
    alert(JSON.stringify(things));
});

Woah!我们在模型层中实际表示了我们的数据。我们可以随心所欲地做任何事情,这非常可爱。

如果我想将其作为表单提交,该怎么办? $.param救援。

<input type='button' value='formData' data-action='asFormData' />

和JS:

$("[data-action='asFormData']").click(function(e){
    var things = stuff.map(function(el){ return el.thing;});
    alert($.param({data:things}));
});

而且while this format is not very nice PHP(或任何其他流行技术)很乐意在服务器端阅读。

所以要把它包起来

  • 将数据与数据分开
  • 如果你有JS逻辑 - 有一个单一的事实来源 - JavaScript对象
  • 考虑更多地阅读它,了解像KnockoutJS或AngularJS这样的常见框架,这些框架对这个问题有更简单的解决方案(以假设为代价)。
  • 了解有关UI架构的更多信息。 This is a good (but hard for beginners) resource
  • 避免使用重复的ID,这些ID会很糟糕 - 而您还没有在您的dom中存储数据。
  • 不要害怕提问 - 这就是你学习的方式。
  • 你可以在这里轻松摆脱jQuery。

答案 1 :(得分:1)

我的方法是:

首先,正确使用<label>

<label><input ... /> My label</label>

<input><label>...</label>

通过第一种方式,确保标签可点击,就像点击复选框一样,保留了辅助功能


另一方面,字符串魔法过多。只需使用适合的data-xxx属性:

<ul class='pq_entry' data-id='1'>
     ....
</ul>

所以你可以通过data-id属性找到一个元素:

var myFirstSection = $("ul.pq_entry[data-id=1]");

通过这样做,在许多元素中根本不需要设置id属性,因为您可以简单地使用class并通过遍历DOM来查找单个项目。 例如,main_item变为:

 <input class="main-item" name="main_item[]" type="checkbox">

如果由于某种原因你需要在克隆的第3部分中找到这个项目,你可以这样做:

var mySection = 3;
$("ul.pq_entry[data-id=" + mySection + "] .menu_item").someFancyMethod(...);

克隆某个部分时,您可以动态分配data-xxx属性,如:

var myNewId = myOldId + 1;
$clonedSection.data("id", myNewId);

然后,我将使用main_item[]等名称数组,因此您无需在名称中手动指定id,但必须将此方法限制为仅在克隆部分中出现一次的元素。

名称数组意味着当您从表单中从服务器端检索值时(例如,在PHP中使用$ _POST),您将按照它们在表单中出现的确切顺序获取值数组。就像任何语言的常规数组一样,您可以访问(例如PHP)中的项目:

$_POST['main_item'][0] // for section 1
$_POST['main_item'][1] // for section 2
... and so on

答案 2 :(得分:0)

尝试分解代码以便更好地管理。

对于上述情况,

<强> HTML

在模板中隐藏了可恢复的html块:

<div class="form-template"> <!-- will pull form section template from here -->
    <ul data-custom-attributes="" data-id="formSectionIdPrefix" class="form-section">
        <li>
            <input data-custom-attributes="" data-id="firstCheckBoxIdPrefix" data-name="firstCheckBoxNamePrefix" class="main-item checkbox1" type="checkbox" />
            <label>First Item</label>

            <ul class="sub-item" style="display:none;">
                <li>
                    <input type="checkbox" />
                    <label>Sub Item</label>
                </li>
                <li>
                    <input class="main-item" data-id="checkBoxSubItem2IdPrefix" data-name="checkBoxSubItem2NamePrefix" type="checkbox" />
                    <label>Second Item</label>

                    <ul class="sub-item" style="display:none;">
                        <li>
                            <label>How many items:</label>
                            <select data-custom-attributes="" data-id="selectItem1IdPrefix" data-name="selectItem1IdPrefix" class="medium" required>
                                <option value="">---Select---</option>
                                <option value="1">1</option>
                                <option value="2">2</option>
                            </select>
                        </li>
                        <li style="list-style-type: none;">
                            <div data-custom-attributes="" class="dependant-select" data-id="selectItem2IdPrefix"></div>
                        </li>
                    </ul>
                </li>
            </ul>
        </li>
    </ul>
</div>

<div class="select-template hidden"> <!-- will pull dependant select template -->
    <select class="course_list" data-id="dependantSelectIdPrefix">
        <option value="">-- select --</option>
        <option value="apple">apples</option>
        <option value="apple">bananas</option>
    </select>
</div>

<div class="form-area"> <!-- main area to append form sections to -->
</div>

<div class="form-area-controls"> <!-- form controls -->
    <input type='button'  class="button tiny radius" id='btnAdd' value='Add Another' />
    <input type='button'   class="button tiny radius alert" id='btnDel' value='Delete Last' />
</div>

<强> CSS 一小部分CSS,以确保我们的模板永远不会出现在屏幕上

.form-template {
    display:none;
}
.form-area li,
#main-panel li {
    list-style-type: none;
}

.hidden {
    display:none;
}

<强> JS

从配置对象开始,轻松管理属性

var config = {};

config.formSectionIdPrefix = "pq_entry_";

config.firstCheckBoxIdPrefix = "first_item_";
config.firstCheckBoxNamePrefix = "main_item_";

config.checkBoxSubItem1IdPrefix = "sub_item_";
config.checkBoxSubItem1NamePrefix = "sub_item_";

config.checkBoxSubItem2IdPrefix = "second_item_";
config.checkBoxSubItem2NamePrefix = "main_item_";

config.selectItem1IdPrefix = "item_count_";
config.selectItem2IdPrefix = "item_names_";
config.dependantSelectIdPrefix = "item_";

缓存对FormSectionTemplate,SelectDropdownTemplate和FormArea

的引用
var $formTemplate = $(".form-template");
var $selectTemplate = $(".select-template");
var $formArea = $(".form-area");

可能是一个跟踪Id增量的索引变量

var index = 0;

使用辅助方法getFormTemplate执行以下操作:

克隆表单部分

将事件附加到克隆的表单部分

增加克隆部分的ID(更多关于此进一步向下)

 function getFormTemplate() {
    var $newTemplate = $formTemplate.children().clone(true);

    var $formSectionWithEvents = attachEvents( $newTemplate );

    var $formSectionWithUpdatedAttributes = incrementAttributes( $formSectionWithEvents );

    return $formSectionWithUpdatedAttributes;
}

将事件附加到克隆表单部分attachEvents

function attachEvents( $formSection ) {
    var $mainCheckBoxes = $formSection.find( ".main-item" );
    var $selectBox = $formSection.find( ".medium" );
    var $dependantSelectSection = $formSection.find( ".dependant-select" );

    $mainCheckBoxes.on("click", function() {
        var $this = $( this );
        var $subItem = $this.siblings(".sub-item");
        if ( $this.is(":checked") ) {
            $subItem.show();
        } else {
            $subItem.hide();   
        }
    });

    $selectBox.on("change", function() {
        var option = $(this).val();

        var $dependantSelect = getSelectField( option );

        $dependantSelectSection.children().remove();
        $dependantSelectSection.append( $dependantSelect );
    });

    return $formSection;
}

增加克隆表单部分的ID。

嗯,有很多方法可以接近它(这在很大程度上取决于你的咖啡因含量)

在下面的位中,我们正在寻找所有带有data-custom-attributes

的元素

遍历所有这些元素,找出我们应在config部分中查找的ID和名称密钥,然后分配附加index增量器的值。

function incrementAttributes( $formSection ) {
    index = index + 1;
    var $customAttributeElements = $formSection.find("[data-custom-attributes]");

    $customAttributeElements.each( function() {
        var $this = $(this);

        var idNamePrefix = $this.attr( "data-id" );
        var namePrefix = $this.attr( "data-name" );

        var idName = config[idNamePrefix] + index;
        var name = config[namePrefix] + index;

        $this.attr( "id", idName );
        $this.attr( "name", name );
    });

    return $formSection;
}

获取相关选择字段(在选择下拉列表中由onchange事件处理)

它只是从父选择框中获取值,并使用config对象的前缀将其分配给克隆的选择框的ID等。

function getSelectField( indexValue ) {
    var $selectItem = $selectTemplate.find("select").clone();

    var selectElementIdPrefix = $selectItem.attr("data-id");
    var selectElementId = config[selectElementIdPrefix] + indexValue;

    $selectItem.attr( "id", selectElementId );

    return $selectItem;
}

把它全部收集起来

$("#btnAdd").on("click", function(e) {
     e.preventDefault();

    var $formSection = getFormTemplate();
    $formArea.append($formSection);

});

$("#btnDel").on("click", function(e) {
    e.preventDefault();
    $formArea.children().last().remove();

    if ( index > 0 ) {
        index = index - 1;    
    }
});

唯一要提及的事件是#btnDel递减索引以确保下一个表单部分插入时附加了正确的ID。

JS小提琴:http://jsfiddle.net/Varinder/3VT2w/3/

修改

刚刚注意到上面的小提琴中有一些HTML标签不匹配(已修复)

下拉选择应根据选择添加1个或更多子项下拉列表。

可以通过将change上的$selectBox事件更改为以下内容来完成:

$selectBox.on("change", function() {
    var option = $(this).val();

    var optionInt = parseInt( option );

    $dependantSelectSection.children().remove();

    for ( var i = 0; i < optionInt; i++ ) {
        var $dependantSelect = getSelectField( option );
        $dependantSelectSection.append( $dependantSelect );     
    }

});

更新小提琴:http://jsfiddle.net/Varinder/3VT2w/4/

编辑2

使用增量添加子选择项名称:

$selectBox.on("change", function() {
    var option = $(this).val();

    var optionInt = parseInt( option );

    $dependantSelectSection.children().remove();

    for ( var i = 1; i <= optionInt; i++ ) {
        var $dependantSelect = getSelectField( option );
        $dependantSelectSection.append( "item" + i );
        $dependantSelectSection.append( $dependantSelect );     
    }

});

更新小提琴:http://jsfiddle.net/Varinder/3VT2w/5/