Skip to main content
The ION Career app includes a pre-configured web form that dynamically displays screening questions. You can customize its behavior, appearance, and validation logic.

Web Form Structure

The ION Job Application web form (ion-job-application) includes the following fields:
Standard Fields:
├─ Job Opening (Link to Job Opening)
├─ Applicant Name (Data, Required)
├─ Email Address (Data/Email, Required)
├─ Phone Number (Data/Phone)
├─ Country (Link to Country)
├─ Cover Letter (Text)
├─ Resume Attachment (Attach)
└─ Source (Link to Job Applicant Source)

Custom Fields:
├─ custom_job_question_answers (Text, Hidden) - Stores JSON answers
└─ Screening Questions (HTML) - Dynamic question container
The web form is configured with doc_type: "Job Applicant" and creates Job Applicant records upon submission.

Client Script Overview

The web form uses a comprehensive client script to handle dynamic question loading and validation.

Lifecycle Hooks

The client script implements three key lifecycle hooks:
// Load questions when form loads (if job_title is already set)
frappe.web_form.after_load = function () {
    if (frappe.web_form.get_value('job_title')) {
        load_job_questions();
    }
};

// Load questions when job opening is selected
frappe.web_form.on('job_title', function (field, value) {
    if (value != ''){
        frappe.web_form.set_df_property('job_title', 'read_only', 1);
    }
    load_job_questions();
});

// Validate required questions before submission
frappe.web_form.validate = function () {
    let answers = JSON.parse(
        frappe.web_form.get_value('custom_job_question_answers') || '{}'
    );
    
    current_questions.forEach(q => {
        if (q.required && !answers[q.fieldname]) {
            frappe.throw(
                __('Please answer the required question: {0}', [q.question])
            );
        }
    });
};

Core Functions

Fetches questions for the selected job opening from the server.
function load_job_questions() {
    const job_opening = frappe.web_form.get_value('job_title');
    
    if (!job_opening) {
        clear_questions();
        return;
    }
    
    frappe.call({
        method: "ion_career.api.get_job_questions",
        args: { job_opening },
        freeze: true,
        callback: function (r) {
            current_questions = r.message || [];
            render_questions(current_questions);
        }
    });
}
Dynamically renders questions in the HTML field. This function:
  • Creates checkbox inputs for each question
  • Applies required styling and labels
  • Binds change event handlers
  • Sorts questions by the order property
Clears the questions container and resets the answers field when no job opening is selected.

Customizing Question Rendering

To customize how questions appear on the form, modify the render_questions() function in the web form’s client script.

Example: Custom Styling

Add custom CSS classes or styling to questions:
function render_questions(questions) {
    if (!questions || questions.length === 0) {
        clear_questions();
        return;
    }
    
    // Sort questions by order
    questions.sort((a, b) => (a.order || 0) - (b.order || 0));
    
    let html = '<div class="custom-screening-questions">';
    
    questions.forEach((q, idx) => {
        const required_mark = q.required ? '<span class="required-indicator">*</span>' : '';
        
        html += `
            <div class="question-item" data-fieldname="${q.fieldname}">
                <label class="question-label">
                    ${idx + 1}. ${q.question}${required_mark}
                </label>
                <div class="question-input">
                    <label class="checkbox-label">
                        <input type="checkbox" 
                               name="${q.fieldname}" 
                               data-required="${q.required}">
                        Yes
                    </label>
                </div>
            </div>
        `;
    });
    
    html += '</div>';
    
    // Render in the HTML field
    $('[data-fieldname="screening_questions"]').html(html);
    
    // Bind change handlers
    bind_answer_handlers();
}

Example: Different Input Types

Extend the rendering to support different question types:
function render_input(question) {
    switch (question.input_type) {
        case 'Checkbox':
            return `<input type="checkbox" name="${question.fieldname}">`;
        
        case 'Select':
            return `
                <select name="${question.fieldname}">
                    <option value="">Select...</option>
                    <option value="Yes">Yes</option>
                    <option value="No">No</option>
                </select>
            `;
        
        case 'Text':
            return `<textarea name="${question.fieldname}" rows="3"></textarea>`;
        
        default:
            return `<input type="text" name="${question.fieldname}">`;
    }
}

Answer Storage Format

Answers are stored in the hidden custom_job_question_answers field as JSON:
{
  "question-row-uuid-1": "Yes",
  "question-row-uuid-2": "No",
  "question-row-uuid-3": "Yes"
}
This JSON is then processed by the process_job_questions handler in handlers.py when the Job Applicant is created.

Custom Validation

Add custom validation logic in the frappe.web_form.validate hook:
frappe.web_form.validate = function () {
    let answers = JSON.parse(
        frappe.web_form.get_value('custom_job_question_answers') || '{}'
    );
    
    // Check required questions
    current_questions.forEach(q => {
        if (q.required && !answers[q.fieldname]) {
            frappe.throw(
                __('Please answer the required question: {0}', [q.question])
            );
        }
    });
    
    // Custom validation: ensure at least 3 "Yes" answers
    const yesCount = Object.values(answers).filter(a => a === 'Yes').length;
    if (yesCount < 3) {
        frappe.throw(
            __('You must answer "Yes" to at least 3 questions to proceed.')
        );
    }
};

Web Form Configuration

Access the web form configuration at: Desk View: Website → Web Form → ion-job-application

Key Settings

{
  "is_standard": 1,
  "published": 1,
  "route": "ion-job-application",
  "login_required": 0,
  "allow_multiple": 0,
  "show_sidebar": 0
}
Be careful when modifying standard web forms. Consider duplicating the web form and customizing the copy to preserve the original.

Custom CSS

Add custom CSS to style the web form in the Custom CSS field:
.custom-screening-questions {
    margin: 20px 0;
    padding: 15px;
    background: #f8f9fa;
    border-radius: 4px;
}

.question-item {
    margin-bottom: 15px;
    padding: 10px;
    background: white;
    border-left: 3px solid #007bff;
}

.question-label {
    font-weight: 500;
    display: block;
    margin-bottom: 8px;
}

.required-indicator {
    color: #dc3545;
    margin-left: 4px;
}

.checkbox-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
}

Modifying Field Order

To change the order of fields on the web form:
  1. Go to WebsiteWeb Formion-job-application
  2. Scroll to the Web Form Fields table
  3. Drag and drop fields to reorder them
  4. Save the form
Place the “Job Opening” field at the top so questions load immediately when a job is selected.

Adding Introduction Text

Set custom introduction text in the web form:
  1. Open the web form in edit mode
  2. Set the Introduction Text field:
    Welcome to our career portal! 
    
    Please complete all required fields and answer the screening questions honestly.
    Your responses help us match you with the right opportunities.
    

Success Message Customization

Customize the message shown after successful submission:
Success Title: Application Received!
Success Message: Thank you for applying. We'll review your application and contact you within 5 business days.

Next Steps

Build docs developers (and LLMs) love