How to implement a "Save as draft" button that bypasses validation — Form + Directory
Context
This guide explains how to add a "Save as draft" button to an RSForm! Pro form and its Directory, allowing users to save incomplete submissions without triggering validation errors. The "Send definitively" button keeps full validation.
PART 1 — The Form (creation)
Step 1 — Add a hidden field
Add a Hidden field to your form named
is_draft with default value
0.
Step 2 — Add a Button field (not a Submit Button)
Add a component of type
Button (not Submit Button) named
btn_save.
In Additional Attributes:
type="button" class="uk-button uk-button-secondary"
Label: Save as draft
Step 3 — Scripts called on form process (ScriptProcess)
This script runs during form processing — it clears $invalid when saving as draft, keeping only the fields you always require:
$isDraft = $_POST['form']['is_draft'] ?? '0';
if ($isDraft === '1') {
$invalid = array();
// Fields that are always required even for draft
$alwaysRequired = array('field1', 'field2', 'field3');
foreach ($alwaysRequired as $fieldName) {
$val = isset($_POST['form'][$fieldName]) ? trim($_POST['form'][$fieldName]) : '';
if ($val === '') {
$invalid[] = RSFormProHelper::getComponentId($fieldName);
}
}
}
Step 4 — User Email Script and Admin Email Script
Block emails when saving as draft:
// UserEmailScript
$d = JFactory::getApplication()->input->get('form', array(), 'array');
if ($d['is_draft'] == '1') { $userEmail['to'] = ''; }
// AdminEmailScript
$d = JFactory::getApplication()->input->get('form', array(), 'array');
if ($d['is_draft'] == '1') { $adminEmail['to'] = ''; }
Step 5 — ScriptProcess2 (optional — custom thank you message for draft)
$d = JFactory::getApplication()->input->get('form', array(), 'array');
if ($d['is_draft'] == '1') {
$thankYouMessage = '<h2>Draft saved</h2><p>Your draft has been saved without sending emails.</p>';
}
Step 6 — CSS and Javascript tab of the form
Add this JavaScript. It intercepts the btn_save click, sets is_draft=1, bypasses client-side AJAX validation, and submits.
Note: Replace rsform_error_YOUR_FORM_ID with your actual form ID, e.g. rsform_error_6.
<script type="text/javascript">
window.addEventListener('load', function () {
var btnSave = document.getElementById('btn_save');
if (btnSave) {
btnSave.addEventListener('click', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var form = document.getElementById('userForm');
var draftField = document.querySelector('[name="form[is_draft]"]');
if (!form || !draftField) return;
draftField.value = '1';
form.querySelectorAll('.rsform-error').forEach(function (el) { el.classList.remove('rsform-error'); });
form.querySelectorAll('.formError').forEach(function (el) { el.classList.remove('formError'); el.classList.add('formNoError'); });
form.querySelectorAll('[aria-invalid="true"]').forEach(function (el) { el.removeAttribute('aria-invalid'); el.removeAttribute('aria-describedby'); });
var errDiv = document.getElementById('rsform_error_YOUR_FORM_ID');
if (errDiv) errDiv.style.display = 'none';
form.querySelectorAll('input[type="file"]').forEach(function (input) {
input.removeAttribute('data-rsfp-required');
input.removeAttribute('data-rsfp-minfiles');
});
form.querySelectorAll('[aria-required="true"]').forEach(function (f) {
f.setAttribute('aria-required', 'false');
});
var originalDisplay = RSFormPro.Ajax.displayValidationErrors;
RSFormPro.Ajax.displayValidationErrors = function() { return; };
try {
RSFormPro.Ajax.validate(form);
} catch(err) {}
setTimeout(function () {
RSFormPro.Ajax.displayValidationErrors = originalDisplay;
form.querySelectorAll('input[type="file"]').forEach(function (input) {
input.setAttribute('data-rsfp-required', 'true');
input.setAttribute('data-rsfp-minfiles', '1');
});
form.querySelectorAll('[aria-required="false"]').forEach(function (f) {
f.setAttribute('aria-required', 'true');
});
}, 2000);
}, true);
}
});
</script>
PART 2 — The Directory (editing existing submissions)
Step 1 — Scripts called on Edit Layout
This script handles the "Save as draft" button (data-directory-task="apply"). It bypasses directory-edit.min.js validation and injects is_draft=1 before submitting:
$doc = JFactory::getDocument();
$js = "
document.addEventListener('DOMContentLoaded', function() {
var btnApply = document.querySelector('[data-directory-task=\"apply\"]');
if (btnApply) {
btnApply.addEventListener('click', function(e) {
e.stopImmediatePropagation();
e.preventDefault();
var form = document.getElementById('directoryEditForm');
if (!form) return;
var taskField = form.querySelector('[name=\"task\"]');
if (taskField) taskField.value = 'apply';
var existing = form.querySelector('[name=\"form[is_draft]\"]');
if (existing) {
existing.value = '1';
} else {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'form[is_draft]';
input.value = '1';
form.appendChild(input);
}
form.submit();
}, true);
}
});
";
$doc->addScriptDeclaration($js);
Step 2 — Scripts called when submission is updated
Clear the validation array when saving as draft:
$isDraft = $_POST['form']['is_draft'] ?? '0';
if ($isDraft === '1') {
$validation = array();
}
Step 3 — Script called before directory emails are sent
Block emails when saving as draft:
$app = JFactory::getApplication();
$task = $app->input->get('task');
if ($task == 'submissions.apply' || $task == 'apply') {
$directoryEmail['to'] = '';
$directoryEmail['_no_p_run'] = 1;
}
Key differences between Form and Directory
| . | Form | Directory |
| Validation variable | $invalid | $validation |
| Script location | ScriptProcess | Scripts called when submission is updated |
| Button type | Button field (type="button") | data-directory-task="apply" |
| JS approach | Bypass RSFormPro.Ajax.validate | stopImmediatePropagation() + form.submit() |
Critical note confirmed by RSJoomla support: In the Directory context, the variable to clear is $validation, NOT $invalid. This is the key difference that is not documented anywhere.