I have a entity based collectionType in my form. I want users to be able to dynamicly add new fields based on a different field. So at the page load I want 0 fields to be shown. After the user filled in all the fields to make the generation process possible and the user clicked the "next" button the new collection fields should be added.
Currently, symfony forms always generates 1 field. But since I gave no data to it yet, it only shows a empty div. But it breaks my Javascript :(
I know I could just do a workaround for this, but I want this field to not be generated at all. No html content should be made before the users clicked the "next button".
This is my collection part of the form:
// Builder ...
->add('contracts', CollectionType::class, [
'entry_type' => ContractsType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
])
This is the ContractsType:
->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
]
])
My twig:
{# 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>
My 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;
}