<?php
use phpformbuilder\Form;
use phpformbuilder\Validator\Validator;
/* =============================================
Start session and include the autoloader
============================================= */
session_start();
include_once rtrim($_SERVER['DOCUMENT_ROOT'], DIRECTORY_SEPARATOR) . '/phpformbuilder/autoload.php';
// define the form name globally
$formId = 'dynamic-fields-form-2';
/* =============================================
Validation if posted
============================================= */
if ($_SERVER["REQUEST_METHOD"] == "POST" && Form::testToken($formId) === true) {
// create validator & auto-validate required fields
$validator = Form::validate($formId);
$validator->email()->validate('user-email');
// check for errors
if ($validator->hasErrors()) {
$_SESSION['errors'][$formId] = $validator->getAllErrors();
// register errors in a custom session variable to transmit to ajax dynamic form
// because errors will be registered & cleared when we construct the form
$_SESSION['ajax-errors'][$formId] = $_SESSION['errors'][$formId];
var_dump($_SESSION['ajax-errors']);
} else {
$emailConfig = array(
'sender_email' => 'contact@phpformbuilder.pro',
'sender_name' => 'Php Form Builder',
'recipient_email' => addslashes($_POST['user-email']),
'subject' => 'Php Form Builder - Dynamic fields Form 2',
'filter_values' => $formId
);
$sentMessage = Form::sendMail($emailConfig);
Form::clear($formId);
if (isset($_SESSION['ajax-errors'][$formId])) {
unset($_SESSION['ajax-errors'][$formId]);
}
}
}
// clear dynamic errors on page load
if ($_SERVER["REQUEST_METHOD"] !== "POST" && isset($_SESSION['ajax-errors'][$formId])) {
unset($_SESSION['ajax-errors'][$formId]);
}
/* ==================================================
The Form
================================================== */
$form = new Form($formId, 'horizontal', 'novalidate', 'bs5');
$form->setMode('development');
$form->setCols(2, 8, 'md');
$form->addInput('email', 'user-email', '', 'Your email', 'placeholder=Email, required');
$form->setCols(2, 3, 'md');
$form->groupElements('job-1', 'person-1');
$form->addOption('job-1', '', 'Choose one ...', '', 'disabled selected');
$form->addOption('job-1', 'Content writer', 'Content writer');
$form->addOption('job-1', 'Tech Support / Technical Leader', 'Tech Support / Technical Leader');
$form->addOption('job-1', 'Office Assistant', 'Office Assistant');
$form->addOption('job-1', 'Secretary', 'Secretary');
$form->addOption('job-1', 'Team Leader', 'Team Leader');
$form->addOption('job-1', 'Data Analyst', 'Data Analyst');
$form->addOption('job-1', 'Safety Officer', 'Safety Officer');
$form->addOption('job-1', 'Delivery Boy', 'Delivery Boy');
$form->addOption('job-1', 'Admin Assistant', 'Admin Assistant');
$form->addSelect('job-1', 'Job', 'data-slimselect=true, class=job, required');
$form->addOption('person-1', '', 'Choose one ...', '', 'disabled selected');
$form->addOption('person-1', 'Adam Bryant', 'Adam Bryant');
$form->addOption('person-1', 'Lillian Riley', 'Lillian Riley');
$form->addOption('person-1', 'Paula Day', 'Paula Day');
$form->addOption('person-1', 'Kelly Stephens', 'Kelly Stephens');
$form->addOption('person-1', 'Russell Hawkins', 'Russell Hawkins');
$form->addOption('person-1', 'Carl Watson', 'Carl Watson');
$form->addOption('person-1', 'Judith White', 'Judith White');
$form->addOption('person-1', 'Tina Cook', 'Tina Cook');
$form->addSelect('person-1', 'Person', 'data-slimselect=true, class=person, required');
// Dynamic fields - container + add button
$form->addHtml('<div id="ajax-elements-container"></div>');
$form->addHtml('<div class="row"><div class="col-md-2"><p> </p></div>');
$form->addHtml('<div class="col-md-3"><button type="button" class="btn btn-primary add-element-button"><i class="bi bi-plus-circle" aria-hidden="true"></i></button><p> </p></div>');
$form->addHtml('</div>');
// End Dynamic fields
// cancel/submit
$form->setCols(0, 12);
$form->centerContent();
$form->addBtn('button', 'cancel', 0, 'Cancel', 'class=btn btn-warning', 'btn-group');
$form->addBtn('submit', 'submit-btn', 1, 'Submit', 'class=btn btn-primary, data-ladda-button=true, data-style=zoom-in', 'btn-group');
$form->printBtnGroup('btn-group');
// Utility functions to extract the content retrieved with fetch() then run the built-in scripts
$form->addPlugin('ajax-data-loader', '#' . $formId);
// Javascript validation
$form->addPlugin('formvalidation', '#' . $formId);
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Bootstrap 5 Php Form with Ajax dynamic fields - How to create PHP forms easily</title>
<meta name="description" content="Bootstrap 5 Form Generator - how to load dynamic fields on the fly with Php Form Builder">
<link rel="canonical" href="https://www.phpformbuilder.pro/templates/bootstrap-5-forms/dynamic-fields-form-2.php" />
<!-- Bootstrap 5 CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<!-- Bootstrap icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
<?php $form->printIncludes('css'); ?>
</head>
<body>
<h1 class="text-center">Php Form Builder - Form with dynamic ajax-loaded fields<br><small>Click "+" or "-" buttons to add or remove fields</small></h1>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-11 col-lg-10">
<?php
if (isset($sentMessage)) {
echo $sentMessage;
}
$form->render();
?>
</div>
</div>
</div>
<!-- Bootstrap 5 JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<?php
$form->printIncludes('js');
$form->printJsCode();
?>
<script>
/* =============================================
Dynamic fields standard code
(No need to touch anything here)
============================================= */
document.addEventListener('DOMContentLoaded', function(event) {
const form = forms['<?php echo $formId; ?>'];
const dfIndexInput = document.createElement('input');
dfIndexInput.setAttribute('type', 'hidden');
dfIndexInput.setAttribute('name', 'dynamic-fields-index');
dfIndexInput.value = 1;
let parent = document.getElementById('ajax-elements-container').closest('form');
parent.insertBefore(dfIndexInput, parent.firstChild);
// first dynamic index is "1"
var firstDynamicIndex = 1,
postedDynamicIndex = 0,
loadPostedDynamicFields = true;
const countInput = document.querySelector('input[name="dynamic-fields-index"]');
var dfIndex = parseInt(countInput.value);
// hidden field to store dynamic fields index
document.querySelector('.add-element-button').addEventListener('click', function() {
// increment index & dynamic-fields-index
dfIndex++;
countInput.value = dfIndex;
let data = {
index: dfIndex
};
fetch('<?php echo $formId; ?>/dynamic-elements.php', {
method: 'post',
body: new URLSearchParams(data).toString(),
headers: {
'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'
},
cache: 'no-store',
credentials: 'include'
}).then(function(response) {
return response.text()
}).then(function(data) {
loadData(data, '#ajax-elements-container', true).then(() => {
// enable validator for the new fields
form.fv.addField(
'job-' + dfIndex, {
validators: {
notEmpty: {}
}
}
);
form.fv.addField(
'person-' + dfIndex, {
validators: {
notEmpty: {}
}
}
);
document.querySelector('select[name="job-' + dfIndex + '"]').addEventListener('change', function() {
form.fv.revalidateField('job-' + dfIndex);
});
document.querySelector('select[name="person-' + dfIndex + '"]').addEventListener('change', function() {
form.fv.revalidateField('person-' + dfIndex);
});
// load posted dynamic fields
if (loadPostedDynamicFields === true && postedDynamicIndex > dfIndex) {
setTimeout(function() {
const evt = new Event('click', {
bubbles: true
});
document.querySelector('.add-element-button').dispatchEvent(evt);
}, 100);
} else {
// all posted fields loaded
loadPostedDynamicFields = false;
}
// Remove action
// Array.from(document.querySelectorAll('.remove-element-button')).pop().closest('.dynamic')
const lastRemoveBtn = Array.from(document.querySelectorAll('.remove-element-button')).pop();
lastRemoveBtn.classList.remove('hidden');
lastRemoveBtn.addEventListener('click', function(e) {
let btnTarget = e.target;
if (btnTarget.nodeName !== 'BUTTON') {
btnTarget = btnTarget.closest('button');
}
var currentIndex = parseInt(btnTarget.dataset.index); // index of removed dynamic
// Transfer upper dynamics values to each previous
var transferUpperValues = function() {
var previousDynamic = document.querySelector('.dynamic[data-index="' + (currentIndex) + '"]');
var previousFields = previousDynamic.querySelectorAll('input, textarea, select, radio, checkbox');
previousFields.forEach((field, i) => {
if (field.dataset.activates === undefined) { // specific condition for material select
var followingField = '',
newValue = '';
if (field.tagName === 'INPUT' && field.type === 'RADIO') {
var followingFieldName = field.getAttribute('name').replace('-' + parseInt(currentIndex), '-' + parseInt(currentIndex + 1));
followingField = document.querySelector('input[name="' + followingFieldName + '"]:checked');
newValue = followingField.value;
if (field.value == newValue) {
field.checked = true;
} else {
field.checked = false;
}
} else {
var followingFieldId = field.id.replace('-' + parseInt(currentIndex), '-' + parseInt(currentIndex + 1));
followingField = document.getElementById(followingFieldId);
if (followingField !== null) {
if (field.tagName === 'SELECT') {
newValue = followingField.querySelector('option:checked').value;
} else {
newValue = followingField.value;
}
if (field.dataset.ssid !== undefined) {
// slimselect value
window.slimSelects[field.id].set(newValue);
} else {
field.value = newValue;
}
}
}
}
});
};
// if upper dynamic sections
var upperIndex = currentIndex - firstDynamicIndex;
var dynamicFields = document.querySelectorAll('.dynamic');
var dynamicFieldsArray = Object.keys(dynamicFields).map((key) => [Number(key), dynamicFields[key]]);
if (dynamicFieldsArray[upperIndex]) {
while (dynamicFieldsArray[upperIndex] !== undefined) {
transferUpperValues();
currentIndex++;
upperIndex++;
}
}
// Ajax call to unset removed fields from form required fields
let data = {
'index': dfIndex
};
fetch('<?php echo $formId; ?>/unset-ajax-elements.php', {
method: 'post',
body: new URLSearchParams(data).toString(),
headers: {
'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'
},
cache: 'no-store',
credentials: 'include'
}).then(function(response) {
return response.text()
}).then(function(data) {
form.fv.removeField('job-' + (dfIndex + 1));
form.fv.removeField('person-' + (dfIndex + 1));
}).catch(function(error) {
console.log(error);
});
// decrement dynamic-fields-index
dfIndex -= 1;
countInput.value = dfIndex;
// remove last dynamic container
Array.from(document.querySelectorAll('.remove-element-button')).pop().closest('.dynamic').remove();
});
});
}).catch(function(error) {
console.log(error);
});
});
// reload posted fields
<?php
$index = 1;
if (isset($_POST['dynamic-fields-index']) && isset($_SESSION['ajax-errors'][$formId])) {
$index = $_POST['dynamic-fields-index'];
}
if ($index > 1) {
?> postedDynamicIndex = <?php echo $index; ?>;
setTimeout(function() {
const evt = new Event('click', {
bubbles: true
});
document.querySelector('.add-element-button').dispatchEvent(evt);
}, 200);
<?php
} // end if
?> document.querySelector('select[name="job-' + dfIndex + '"]').addEventListener('change', function() {
form.fv.revalidateField(this.name);
});
document.querySelector('select[name="person-' + dfIndex + '"]').addEventListener('change', function() {
form.fv.revalidateField(this.name);
});
});
</script>
</body>
</html>