Multi-Step Feedback Form Development on 1C-Bitrix
A multi-step feedback form differs from a regular step-by-step form in that its goal is not a sale, but obtaining a structured inquiry. Technical requests, complaints, support tickets, return applications — situations where you need to collect context sequentially. The inquiry type determines which fields to show next. This is conditional logic, closer to a quiz funnel, but oriented toward service rather than sales scenarios.
Architecture with Conditional Fields
The key feature: on the first step, the user selects the inquiry type — and the subsequent steps change accordingly. This requires conditional rendering.
Step configuration with conditions:
const FEEDBACK_FORM_CONFIG = {
steps: [
{
id: 'step_type',
title: 'Inquiry type',
fields: [
{
name: 'type',
type: 'radio-cards',
required: true,
options: [
{value: 'technical', label: 'Technical question', icon: '⚙️'},
{value: 'billing', label: 'Billing question', icon: '💳'},
{value: 'complaint', label: 'Complaint', icon: '⚠️'},
{value: 'other', label: 'Other', icon: '💬'},
],
},
],
},
{
id: 'step_technical',
showWhen: {field: 'type', value: 'technical'},
title: 'Problem description',
fields: [
{name: 'product', type: 'select', label: 'Product', required: true,
options: ['Mobile app', 'Website', 'API']},
{name: 'error_text', type: 'textarea', label: 'Describe the error', required: true},
{name: 'screenshot', type: 'file', label: 'Screenshot (optional)', accept: 'image/*'},
],
},
{
id: 'step_billing',
showWhen: {field: 'type', value: 'billing'},
title: 'Payment details',
fields: [
{name: 'order_id', type: 'text', label: 'Order number', required: true},
{name: 'amount', type: 'number', label: 'Payment amount', required: true},
{name: 'issue_type', type: 'select', label: 'Issue', required: true,
options: ['Double charge', 'No receipt', 'Wrong amount', 'Other']},
],
},
{
id: 'step_contact',
title: 'Your contacts',
fields: [
{name: 'name', type: 'text', label: 'Name', required: true},
{name: 'email', type: 'email', label: 'Email', required: true},
{name: 'phone', type: 'tel', label: 'Phone', required: false},
],
},
],
};
JavaScript Controller with Conditional Logic
class ConditionalMultistepForm {
constructor(config) {
this.config = config;
this.answers = {};
this.stepsPath = this.calculatePath();
this.currentIndex = 0;
}
// Calculate the actual step path based on current answers
calculatePath() {
return this.config.steps.filter(step => {
if (!step.showWhen) return true;
const {field, value} = step.showWhen;
const answer = this.answers[field];
return Array.isArray(value) ? value.includes(answer) : answer === value;
});
}
handleAnswer(fieldName, value) {
this.answers[fieldName] = value;
// Recalculate path — the set of steps may have changed
this.stepsPath = this.calculatePath();
}
get currentStep() {
return this.stepsPath[this.currentIndex] ?? null;
}
get totalSteps() {
return this.stepsPath.length;
}
canGoNext() {
const step = this.currentStep;
if (!step) return false;
return step.fields
.filter(f => f.required)
.every(f => !!this.answers[f.name]);
}
next() {
if (!this.canGoNext()) return false;
if (this.currentIndex < this.stepsPath.length - 1) {
this.currentIndex++;
return true;
}
return false; // Last step
}
prev() {
if (this.currentIndex > 0) {
this.currentIndex--;
}
}
isLastStep() {
return this.currentIndex === this.stepsPath.length - 1;
}
getSubmitData() {
return {
type: this.answers.type,
answers: this.answers,
};
}
}
File Handling (Screenshot)
File uploads in a multi-step form require either immediate upload on file selection, or sending together with the rest of the data.
Immediate upload on file selection — via AJAX, returns the ID of the uploaded file in Bitrix:
// /local/ajax/upload_temp_file.php
if (!$_FILES['file'] || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['error' => 'Upload error']);
exit;
}
// File type check
$allowedMime = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($_FILES['file']['type'], $allowedMime)) {
echo json_encode(['error' => 'Invalid file type']);
exit;
}
// Save as a Bitrix temporary file
$fileId = \CFile::SaveFile([
'name' => $_FILES['file']['name'],
'size' => $_FILES['file']['size'],
'tmp_name' => $_FILES['file']['tmp_name'],
'type' => $_FILES['file']['type'],
], 'feedback_forms');
echo json_encode(['file_id' => $fileId]);
On final submission — pass file_id along with the rest of the data.
Saving and Creating a CRM Inquiry
Inquiries are routed depending on type:
// /local/ajax/feedback_form_submit.php
$data = json_decode(file_get_contents('php://input'), true);
$type = $data['type'] ?? 'other';
\Bitrix\Main\Loader::includeModule('crm');
switch ($type) {
case 'technical':
// Technical inquiries → deal in the "Support" pipeline
$deal = new \CCrmDeal(false);
$deal->Add([
'TITLE' => 'Tech. inquiry: ' . ($data['answers']['product'] ?? ''),
'CATEGORY_ID' => CRM_SUPPORT_PIPELINE_ID,
'STAGE_ID' => 'C' . CRM_SUPPORT_PIPELINE_ID . ':NEW',
'COMMENTS' => $data['answers']['error_text'] ?? '',
'CONTACT_ID' => $this->findOrCreateContact($data),
]);
break;
case 'billing':
// Financial questions → lead with a tag
$lead = new \CCrmLead(false);
$lead->Add([
'TITLE' => 'Billing: order #' . ($data['answers']['order_id'] ?? ''),
'COMMENTS' => 'Issue: ' . ($data['answers']['issue_type'] ?? ''),
]);
break;
default:
// Others — lead
$lead = new \CCrmLead(false);
$lead->Add([
'TITLE' => 'Inquiry: ' . $data['answers']['name'],
'SOURCE_ID' => 'WEB',
]);
}
echo json_encode(['success' => true]);
Notifications
When an inquiry is received — notify the responsible person. Using Bitrix mail events (b_event_log):
\Bitrix\Main\Mail\Event::send([
'EVENT_NAME' => 'FEEDBACK_NEW_TICKET',
'LID' => SITE_ID,
'C_FIELDS' => [
'TYPE' => $typeLabel,
'NAME' => $data['answers']['name'],
'EMAIL' => $data['answers']['email'],
'TEXT' => json_encode($data['answers'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
],
'TO_EMAIL' => FEEDBACK_MANAGER_EMAIL,
]);
The mail event template is created in Settings → Mail events → Mail templates.
Development Timeline
| Option | Scope | Timeline |
|---|---|---|
| Basic multi-step | 2–3 steps, inquiry type, lead in CRM | 3–5 days |
| With conditional logic | Different fields by type, file uploads | 6–10 days |
| Full Service Desk | + Personal account, inquiry statuses, SLA | 15–25 days |







