En este tutorial aprenderás a implementar un paso de resumen de datos en formularios multistep de JetFormBuilder, ideal para reservas con JetAppointment, totalmente personalizable y fácil de adaptar a los campos que tú requieras.
📋 ¿Qué logramos?
- Formularios multipaso y de reserva (servicios, horarios, datos de contacto, etc.), totalmente personalizados.
- Resumen de datos editable, que refleja exactamente lo que el usuario llenó antes de enviar.
- Visual limpio: los campos vacíos simplemente desaparecen del resumen.
- 100% compatible y visualmente integrado con Gutenberg, JetFormBuilder y JetAppointment.
🚦 Paso a Paso
1️⃣ Prepara tu formulario JetFormBuilder
- Crea o edita tu formulario JetFormBuilder normal.
- Usa bloques multipasos («Page Break») para dividir el llenado por secciones.
- Incluye todos los campos que desees: nombre, correo, dirección, área, fecha, hora, servicios extra, etc.
- Si usas JetAppointment, incluye también el calendario y los campos relacionados.
2️⃣ Agrega el bloque de Resumen
- Al final de tu formulario, antes del botón de envío, añade un «Custom HTML block» en el editor de Gutenberg.
- Pega el siguiente código HTML para la estructura visual del resumen.
- Modifica los ids de los
<span>según los campos de tu formulario.
<div id="booking-review-summary" class="summary-card" style="margin-bottom:32px; display:none;">
<div class="dateselected">
<div class="summary-main-service">
<h3 class="summary-main-title">Date Selected : <span id="summary-date"></span> <span id="summary-time"></span></h3>
</div>
</div>
<div class="summary-main-service">
<h3 class="summary-main-title">Main Service Selected</h3>
<ul id="summary-main-list" class="summary-main-list"></ul>
<ul id="summary-secondary-list" class="summary-secondary-list"></ul>
</div>
<div class="summary-row">
<div class="summary-location">
<h3 class="summary-location-title">Location & Schedule</h3>
<ul class="summary-location-list">
<li><span>Area:</span> <span id="summary-zona"></span></li>
<li><span>Address:</span> <span id="summary-address-line"></span></li>
<li><span>Address Notes:</span> <span id="summary-address"></span></li>
</ul>
</div>
<div class="summary-contact">
<h3 class="summary-contact-title">Your Contact Details</h3>
<ul class="summary-contact-list">
<li><span>Full Name:</span> <span id="summary-name"></span></li>
<li><span>Phone:</span> <span id="summary-phone"></span></li>
<li><span>Email:</span> <span id="summary-email"></span></li>
<li><span>Preferred Contact:</span> <span id="summary-contact"></span></li>
</ul>
</div>
</div>
</div>
Recuerda: Puedes cambiar, agregar o eliminar <li> según los campos que quieras mostrar. Lo importante es que el id del <span> coincida con el campo a inyectar desde JS.
3️⃣ El JavaScript responsable del resumen dinámico
- Pega este bloque en el mismo Custom HTML block (o usa un plugin para código JS en la página).
<script>
document.addEventListener('DOMContentLoaded', function() {
function isLastStepActive() {
let activeStep = document.querySelector('.jet-form-builder-page:not(.jet-form-builder-page--hidden)');
return activeStep && activeStep.dataset.page === "4";
}
function printOrEmpty(val) {
if (val && val.trim() !== '') return val;
return '<span class="empty-value">Empty</span>';
}
function getMainService() {
const node = document.querySelector('.primary-service');
if (!node) return null;
const title = node.querySelector('.elementor-heading-title, .service-title, .jet-listing-dynamic-field__content, span')?.innerText?.trim() || '';
const img = node.querySelector('img')?.src || '';
return {img, title};
}
function getSecondaryServices() {
const checked = document.querySelectorAll('input[name="related_service_ids"]:checked, input[name="related_service_ids[]"]:checked');
let result = [];
checked.forEach(input => {
const wrap = input.closest('.jet-form-builder__field-wrap');
const serviceBlock = wrap?.querySelector('.service-related');
if (serviceBlock) {
const img = serviceBlock.querySelector('.jet-listing-dynamic-image img')?.src || '';
const title = serviceBlock.querySelector('.jet-listing-dynamic-field__content, .service-title, span')?.innerText?.trim() || '';
result.push({img, title});
}
});
return result;
}
function showOrHideField(spanId, value) {
const span = document.getElementById(spanId);
if (!span) return;
const li = span.closest('li');
if (value && value.trim() !== '') {
span.textContent = value;
if(li) li.style.display = '';
} else {
span.textContent = '';
if(li) li.style.display = 'none';
}
}
function updateSummaryCard() {
const resumen = document.getElementById('booking-review-summary');
if (!resumen) return;
resumen.style.display = isLastStepActive() ? 'block' : 'none';
// Date Selected (caso colocado en el h3, como en tu ejemplo)
let dateStr = '', timeStr = '';
const bookField = document.querySelector('[name="appointment_date"]');
if (bookField && bookField.value) {
try {
let obj = JSON.parse(bookField.value)[0];
dateStr = obj.friendlyDate || '';
timeStr = obj.friendlyTime || '';
} catch(e){}
}
document.getElementById('summary-date').textContent = dateStr;
document.getElementById('summary-time').textContent = timeStr;
// Main Service
const main = getMainService();
const mainUl = document.getElementById('summary-main-list');
mainUl.innerHTML = '';
if (main && (main.title || main.img)) {
mainUl.innerHTML = `<li>
<img src="${main.img || 'https://via.placeholder.com/50x50?text=Empty'}" alt="Main service" />
<span class="summary-service-title">${printOrEmpty(main.title)}</span>
</li>`;
} else {
mainUl.innerHTML = '';
}
// Secondary Services
const secondary = getSecondaryServices();
const secUl = document.getElementById('summary-secondary-list');
secUl.innerHTML = '';
if (secondary.length) {
secUl.innerHTML = secondary.map(serv =>
`<li>
<img src="${serv.img || 'https://via.placeholder.com/50x50?text=Empty'}" alt="Secondary service" />
<span class="summary-service-title">${printOrEmpty(serv.title)}</span>
</li>`
).join('');
} else {
secUl.innerHTML = '';
}
// LOCATION/SCHEDULE
const zonaSel = document.querySelector('[name="address_area"]');
const zonaTxt = zonaSel?.options[zonaSel.selectedIndex]?.text || '';
showOrHideField('summary-zona', zonaTxt);
showOrHideField('summary-address-line', document.getElementById('address_line')?.value || '');
showOrHideField('summary-address', document.getElementById('address_notes')?.value || '');
¿Cómo conectar cada campo?
Cada campo que quieras mostrar en el resumen debe tener un <span id="summary-xxxx">.
El JavaScript busca el valor desde el formulario y lo inserta allí.
Ejemplo:
Si tu formulario tiene un campo con id customer_name (input para el nombre del usuario), el resumen debe contener <span id="summary-name"></span>,
y el JS hace:
showOrHideField('summary-name', document.getElementById('customer_name')?.value || '');
¿Cómo agregar, quitar o cambiar campos?
- Para agregar un campo:
- Agrega un
<li><span>Etiqueta:</span> <span id="summary-nuevo"></span></li>en el HTML. - Agrega la línea equivalente en tu JS, por ejemplo:
showOrHideField('summary-nuevo', document.getElementById('campo_nuevo')?.value || '');
- Agrega un
- Para quitar un campo:
- Borra el
<li>...</li>correspondiente del resumen HTML. - Borra o comenta la línea respectiva en el JS.
- Borra el
- Para cambiar etiqueta/orden:
- Ajusta sólo el HTML, no es necesario cambiar el JS si los
idcoinciden.
- Ajusta sólo el HTML, no es necesario cambiar el JS si los
Tip: El script no depende del idioma ni del nombre, solo del id del <span> y del input en el formulario.
Puedes adaptar esto a cualquier campo y tipo de dato.
🎯 Notas Finales y Mejoras
Este método es escalable, fácil de mantener y 100% seguro ante futuros cambios de JetFormBuilder o JetAppointment. Además, los campos vacíos simplemente no aparecen, el usuario solo ve lo que de verdad llenó.