Клонирование/удаление полей ввода - сохранение уникального идентификатора элемента

В настоящее время я работаю с созданием динамического поля ввода внутри формы. У меня сложный пример, который использует флажки и флажки. Он имеет два типа элементов: main_items и sub_items. Как уже упоминалось, я могу динамически добавлять поля ввода с некоторым jquery через функцию clone, которая реплицирует новый набор полей ввода с уникальными атрибутами 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>

Ответ 1

Итак, расскажите о том, как создавать базовые графические приложения. Прежде чем продолжить, я хочу, чтобы вы знали, что код ниже может быть написан в ~ 20 LoC в Knockout/ Angular, но я решил не делать этого, потому что это ничему не научило.

Итак, расскажите о графическом интерфейсе.

Все это сводится к двум вещам.

  • Презентация - это ваш HTML, css и все, с чем пользователь напрямую взаимодействует.
  • Данные - это ваши фактические данные и логика.

Мы хотим отделить их, чтобы они могли действовать независимо. Мы хотим получить реальное представление о том, что пользователь видит в объекте JavaScript, чтобы он был легко поддающимся проверке, читабельным для чтения и т.д. И т.д. Подробнее см. Разделение проблем.

Начнем с данных.

Итак, что каждая вещь имеет в вашем приложении?

  • A Первый элемент, true или false
  • A Sub Item либо true, либо false, но никогда не будет true, если первый элемент не соответствует true.
  • A Второй элемент, который является либо истинным, либо ложным.
  • A Число элементов, которое является числом
    • Каждый из этих элементов - это яблоко, банан или манго.

Самая интуитивная вещь - начать прямо там.

// 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 (это приятно!), Альтернативно мы можем сериализовать его как параметры формы (если у вас есть какие-либо проблемы с методами в этом вопросе - дайте мне знать).

Теперь у меня просто куча объектов... и головная боль.

Довольно проницательный. Пока у вас есть только объекты, вы не указали их поведение нигде. У нас есть наш "уровень данных", но пока у нас нет никакого уровня представления. Мы начнем с избавления от всех идентификаторов и добавления поведения.

Введите шаблоны!

Вместо клонирования существующих объектов мы хотим иметь способ "cookie cutter" для создания внешнего вида новых элементов. Для этого мы будем использовать шаблон. Начнем с того, что вы узнаете, как выглядит ваш "список элементов" в 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-шаблоны не имеют для этого крючка, поэтому мы должны сделать это вручную. Начнем с изменения нашего шаблона с помощью свойств привязки данных.

<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}
}

В эта скрипта мы добавили свойства к первому элементу и подпункту, и они уже обновили элементы.

Давайте продолжим делать то же самое для второго атрибута. Это почти то же самое, привязка напрямую. На стороне примечания есть несколько библиотек, которые делают это для вас автоматически - Нокаут например

Вот еще одна скрипка со всеми установленными связями, это завершило наш уровень представления, наш слой данных и их привязку.

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);
}

Кнопки

Самое интересное, что теперь мы закончили утомительную часть, кнопки - кусок пирога.

Добавьте кнопку "добавить"

 <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);
});

Мальчик, что было легко.

Хорошо, поэтому удаление должно быть довольно сложным, не так ли?

HTML:

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

JS:

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

Хорошо, так что это было очень мило. Итак, как мы получаем наши данные? Позвольте создать кнопку, которая отображает все элементы на экране?

<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}));
});

И в то время как этот формат не очень приятный, то что-то PHP (или любая другая популярная технология) с радостью прочитает на стороне сервера.

Итак, чтобы обернуть его

  • Отдельная презентация из данных
  • Если у вас есть JS-логика - есть один источник правды - объекты JavaScript
  • Подумайте об этом подробнее, узнайте об общих рамках, таких как KnockoutJS или AngularJS, которые имеют интересные менее подробные решения этой проблемы (за счет допущений).
  • Подробнее о архитектуре пользовательского интерфейса. Это хороший (но трудный для начинающих) ресурс
  • Избегайте дублирования идентификаторов, они плохие - пока вы там не храните данные в своем доме.
  • Не бойтесь задавать вопрос - так вы учитесь.
  • Вы можете легко избавиться от jQuery здесь.

Ответ 2

Мой подход:

Прежде всего, правильное использование <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[], поэтому вам не нужно указывать вручную имя в имени, но вы должны ограничить этот подход элементами, которые появляются только один раз в клонированных разделах.

Массив имен означает, что когда вы извлекаете значение из формы, с серверной стороны (например, используя $_POST в PHP), вы получаете массив значений в том порядке, в котором они появляются в форме. Подобно регулярному массиву на любом языке, вы можете получить доступ к элементам в разделах, например (пример в PHP):

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

Ответ 3

Попробуйте разбить код для лучшего управления.

Для сценариев, описанных выше,

HTML

У вас есть resuable chunks 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, который выполняет следующие действия:

раздел формы клонов

присоединяет события к этому клонированному разделу формы

увеличивает ионы клонированного раздела (подробнее об этом ниже)

 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;
}

Инкремент идентификаторов клонированной формы.

Ну, есть много способов приблизиться к нему (что во многом зависит от количества кофеина, которое у вас есть)

В нижнем бите мы ищем все элементы, которые обрезаны с помощью data-custom-attributes

итерируя все эти элементы и узнайте, какой идентификатор и ключ ключа мы должны искать в разделе config, а затем назначьте эти значения, добавляя приращение 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;
}

Получить поле "Зависимое" (tirggered по событию onchange в раскрывающемся списке выбора)

Он просто получает значение из поля выбора родителя и присваивает его идентификаторам клонированного идентификатора и т.д. с префиксами из объекта config.

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;
}

помещая все это togather

$("#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 уменьшает индекс, чтобы убедиться, что следующая вставка в форму секции содержит правильные идентификаторы.

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 );     
    }

});

Обновлено Fiddle: http://jsfiddle.net/Varinder/3VT2w/5/