Symfony 4.4 如何使用 collectionType 从 0 个字段开始

Symfony 4.4 how to start with 0 fields using a collectionType

我的表单中有一个基于实体的 collectionType。我希望用户能够根据不同的字段动态添加新字段。所以在页面加载时我希望显示 0 个字段。在用户填写所有字段以使生成过程成为可能并且用户单击“下一步”按钮后,应添加新的集合字段。

目前,symfony 表单 总是 生成 1 个字段。但是由于我还没有给它任何数据,它只显示一个空的 div。但它打破了我的 Javascript :(

我知道我可以为此做一个解决方法,但我希望根本不生成此字段。在用户单击“下一步”按钮之前,不应制作 html 内容。

这是我收集的部分表格:

// Builder ...
->add('contracts', CollectionType::class, [
    'entry_type' => ContractsType::class,
    'entry_options' => ['label' => false],
    'allow_add' => true,
    'allow_delete' => true,
    'by_reference' => false,
])

这是合同类型:

->add('start_date', WeekType::class, [
    'label_attr' => [
        'class' => 'bold',
    ],
    'attr' => [
        'class' => 'input-margin',
    ],
    'input' => 'array',
    'widget' => 'choice',
])
->add('end_date', WeekType::class, [
    'label_attr' => [
        'class' => 'bold',
    ],
    'attr' => [
        'class' => 'input-margin',
    ],
    'input' => 'array',
    'widget' => 'choice',
])
->add('contract_hours', TextType::class, [
    'label_attr' => [
        'class' => 'bold',
    ],
    'attr' => [
        'class' => 'input-text input-margin',
        'placeholder' => 'Vul het aantal contract uren in',
        'min' => 0,
        'max' => 60
    ]
])

我的树枝:

{# Startdate, enddate and contract hours fields. Data sets are specific for each field since I need nice styling. #}
<div id="ContractFields" class="contracts"
    data-start-label="{{ form_label(userEntityForm.contracts.vars.prototype.start_date, 'Begintijd')|e('html_attr') }}" 
    data-start-date-year="{{ form_widget(userEntityForm.contracts.vars.prototype.start_date.year, { attr: { class: 'select-field mr-1' }})|e('html_attr') }}"
    data-start-date-week="{{ form_widget(userEntityForm.contracts.vars.prototype.start_date.week, { attr: { class: 'select-field' }})|e('html_attr') }}"
    data-end-label="{{ form_label(userEntityForm.contracts.vars.prototype.end_date, 'Eindtijd')|e('html_attr') }}"
    data-end-date-year="{{ form_widget(userEntityForm.contracts.vars.prototype.end_date.year, { attr: { class: 'select-field mr-1' }})|e('html_attr') }}"
    data-end-date-week="{{ form_widget(userEntityForm.contracts.vars.prototype.end_date.week, { attr: { class: 'select-field' }})|e('html_attr') }}"
    data-hours-label="{{ form_label(userEntityForm.contracts.vars.prototype.contract_hours, 'Contract uren')|e('html_attr') }}"
    data-hours="{{ form_widget(userEntityForm.contracts.vars.prototype.contract_hours, {'type' : 'number'})|e('html_attr') }}">
    
    // This if is here for testing, doesn't do much about the generating part.
    {% if userEntityForm.contracts is empty %}
        {# {{ form_widget(userEntityForm.contracts) }} #}
    {% else %}
        {% for contract in userEntityForm.contracts %}

            {# Class to make sure it is known in JS how many contract fields have been added. #}
            <div class="d-none class-to-count-amount-of-fields">

                <div class="d-flex">

                    {# Contract start date field #}
                    <div class="mr-2">
                        {{ form_label(contract.start_date, 'Begintijd') }}
                        <div class="input-margin">
                            {{- form_widget(contract.start_date.year, { attr: { class: 'select-field mr-1' }}) -}}
                            {{- form_widget(contract.start_date.week, { attr: { class: 'select-field' }}) -}}
                        </div>
                    </div>

                    {# Contract end date field. #}
                    <div>
                        {{ form_label(contract.end_date, 'Eindtijd') }}

                        <div class="input-margin">
                            {{- form_widget(contract.end_date.year, { attr: { class: 'select-field mr-1' }}) -}}
                            {{- form_widget(contract.end_date.week, { attr: { class: 'select-field' }}) -}}
                        </div>
                    </div>
                </div>

                {# Contract hours field. #}
                <div>
                    {{ form_label(contract.contract_hours, 'Contract uren') }}
                    {{ form_widget(contract.contract_hours, {'type' : 'number'}) }}
                </div>
            </div>

        {% endfor %}
    {% endif %}
</div>

我的Javascript:

// Only generate the remaining contracts. We dont want more than the user asks for.
for (let i = contracts.length; i < userEntityFormAmountOfContracts.value; i++) {

    // Get the data-prototype from _form.html.twig.
    var startLabel = contractFields.dataset.startLabel;
    var startDateYear = contractFields.dataset.startDateYear;
    var startDateWeek = contractFields.dataset.startDateWeek;
    var endLabel = contractFields.dataset.endLabel;
    var endDateYear = contractFields.dataset.endDateYear;
    var endDateWeek = contractFields.dataset.endDateWeek;
    var hoursLabel = contractFields.dataset.hoursLabel;
    var contractHours = contractFields.dataset.hours;

    // get the current index.
    var index = contractFields.dataset.index;

    // Replace '__name__' in the prototype's HTML to instead be a number based on how many items we have.
    startLabel = startLabel.replace(/__name__/g, index);
    startDateYear = startDateYear.replace(/__name__/g, index);
    startDateWeek = startDateWeek.replace(/__name__/g, index);
    endLabel = endLabel.replace(/__name__/g, index);
    endDateYear = endDateYear.replace(/__name__/g, index);
    endDateWeek = endDateWeek.replace(/__name__/g, index);
    hoursLabel = hoursLabel.replace(/__name__/g, index);
    contractHours = contractHours.replace(/__name__/g, index);

    // Create extra elements.
    var contractsContainer = document.createElement('div');
    contractsContainer.classList.add("d-none", "class-to-count-amount-of-fields");

    var datesContainer = document.createElement('div');
    datesContainer.classList.add("d-flex");

    // Start date container. (label + input).
    var startDateContainer = document.createElement('div');
    startDateContainer.classList.add("mr-2");

    // Start date container. (label + input).
    var startDateInputContainer = document.createElement('div');
    startDateInputContainer.classList.add("input-margin");

    // End date container. (label + input).
    var endDateContainer = document.createElement('div');

    // Start date container. (label + input).
    var endDateInputContainer = document.createElement('div');
    endDateInputContainer.classList.add("input-margin");

    // Contract hours container. (label + input).
    var contractHoursContainer = document.createElement('div');

    // Add labels and field container to time container.
    startDateContainer.insertAdjacentHTML('beforeend', startLabel);
    endDateContainer.insertAdjacentHTML('beforeend', endLabel);
    contractHoursContainer.insertAdjacentHTML('beforeend', hoursLabel);

    // Add date input fields. (from prototype). insertAdjacentHTML means that html in string format will be transformed.
    startDateInputContainer.insertAdjacentHTML('beforeend', startDateYear);
    startDateInputContainer.insertAdjacentHTML('beforeend', startDateWeek);
    endDateInputContainer.insertAdjacentHTML('beforeend', endDateYear);
    endDateInputContainer.insertAdjacentHTML('beforeend', endDateWeek);
    contractHoursContainer.insertAdjacentHTML('beforeend', contractHours);

    startDateContainer.appendChild(startDateInputContainer);
    endDateContainer.appendChild(endDateInputContainer);

    // Add time containers to clocking times container.
    datesContainer.appendChild(startDateContainer);
    datesContainer.appendChild(endDateContainer);

    // Adding the input field parts.
    contractsContainer.appendChild(datesContainer);
    contractsContainer.appendChild(contractHoursContainer);

    // Append all, sticking together, elements to the container with all other sticking elements (contractsContainers).
    contractFields.appendChild(contractsContainer);

    // Set default values.
    const currentDate = new Date();
    startDateInputContainer.children[0].value = 2021;
    startDateInputContainer.children[1].value = 1;
    endDateInputContainer.children[0].value = 2021;
    endDateInputContainer.children[1].value = isISOLeapYear(currentDate.getFullYear()) ? 53 : 52;

    // Increase the index with one for the next item.
    contractFields.dataset.index = parseInt(index) + 1;
}

如果您看到整个集合的标签(在您的情况下应该是“合同”),您可以通过将 'label' => false 添加到父表单来删除它,如下所示:

// Builder ...
->add('contracts', CollectionType::class, [
    'label' => false,
    ...
])

当调用 form_end(form) 时,Symfony 检查是否有您忘记手动渲染的字段。它通过在每个字段 (FormView) 上调用 isRendered 方法来完成此操作,然后将缺少的字段添加到表单的末尾。对于缺少的集合,这会添加(取决于表单主题)一个 <div> 和一个 <label> 和一个包含子表单(如果有)的容器 <div>

isRendered 方法 returns 如果集合作为一个整体(form_row(collection)form_widget(collection))或其所有子形式(form_row(child)form_widget(child)) 被渲染。在您使用循环单独呈现子表单但还没有子表单的特殊情况下,isRendered returns false 并且额外的字段被添加到表单的末尾。

上述解决方案仅从表单中删除了 <label>

更好的解决方案可能是将 {'render_rest': false} 添加到 form_end(form) 标记以禁用所有其他字段的呈现:{{ form_end(form, {'render_rest': false}) }}

编辑:当调用 form_end(form) 时,Symfony 也会渲染 crsf 字段,因此当使用 {'render_rest': false} 时,您需要使用 {{ form_widget(form._token) }}.

手动渲染它