The WorkPEI Jobs page uses a two-pane "split layout": a FacetWP-filtered list of li.job_listing items on the left and the selected job's full detail on the right. Clicking a listing loads the detail pane over admin-ajax without a page reload. The first job auto-loads on desktop, and the Apply form works inside the pane. Each listing keeps a real permalink, so the page still works without JavaScript, for search engines, and for new-tab opens. The resume search uses the same pattern (see the last section).
How it works:
- Left pane: the child-theme override
job_manager/content-job_listing.phprenders each listing asa.wpjm-split-job-linkwith a realhrefanddata-job-permalink, inside.wpjm-split-layout__left. FacetWP re-renders this list when filters change. - Right pane:
.wpjm-split-layout__rightis filled by the AJAX actionmytheme_get_job_listing(handler infunctions.php). It is verified with the nonce actionworkpei_split_layout, passed to JS asmythemeWpjmSplit.nonce. The handler returns the HTML oftemplate-parts/job/job-main.php. - One template, two contexts:
job-main.phpis shared by the single job page and the AJAX pane. A global,$workpei_job_main_context, is set to'single'or'ajax'; the AJAX version is intentionally slimmed down (it leaves out items the single page shows). - JavaScript:
js/wpjm-split-layout.js(localized objectmythemeWpjmSplit) handles clicks and loading. It aborts the in-flight request when a new listing is clicked and ignores stale responses via acurrentJobIdguard, so a slow older response cannot overwrite a newer click. Below 768px the right pane is hidden by CSS, so a click opens the full job page in a new tab instead. It rebinds on FacetWP'sfacetwp-loadedevent and auto-loads the first job after each filter or page change. - CSS:
wpjm-ajax-jobs.cssandjob_manager/style.cssstyle the layout, the active (is-active) listing and the loading state. - Caching: the AJAX jobs page sends DONOTCACHE headers so the nonce and per-user content are never served from a page cache.
The inline application form:
Job listings are visible to logged-out visitors, but only logged-in users can apply. Applications' own hooks assume a normal front-end request, and admin-ajax is not one: the wp action never fires there, and is_admin() is true, so the Applications plugin does not hook its form. job-main.php therefore registers workpei_render_inline_application_form() itself on job_manager_application_details_email and job_manager_application_details_url (priority 20) and removes WPJM core's default handlers. That function instantiates WP_Job_Manager_Applications_Apply, calls init_fields() and renders the plugin's own application-form.php, catching any Throwable so a failure cannot break the pane. When Resume Manager's "force resume" option is enabled, the AJAX branch also calls $GLOBALS['resume_manager']->apply->init(); this is a no-op while that option is off.
Submitting: the JS intercepts the form's submit event and posts a FormData object by XHR to the form's action (the job permalink). Applications saves the application and redirects to permalink?application_success=1. Because that destination page still contains the application form, "the form is still present" cannot be used as a failure test. The JS reads the XHR's final responseURL instead: success means the URL contains application_success=1 and no .job-manager-error or .job-manager-applications-error element came back. Success replaces the form with a green confirmation message. Anything else re-enables the button and shows the returned error or a generic one.
Settings this relies on (see the Resume Settings documentation entry): Application Method is email only; Applications email form ON, URL form OFF; registered users only ON; Resume Manager "Apply With Resume" options all OFF.
Resume search (same pattern): resume-main.php, wprm-ajax-resumes.css and js/wpjm-split-layout-resumes.js mirror the jobs files using the AJAX action mytheme_get_resume_listing, with the same abort and stale-response guard. The contact block in the resume pane is a Gravity Forms shortcode (the employer contact/invite form) rather than the plugin's contact details, and visibility is controlled by Resume Manager's View/Browse/Contact capabilities (Administrator and Employer only).
Critical Configuration Note:
The resume dropdown only appears in the application form when the logged-in user has at least one published resume; Applications silently drops the field otherwise. With "force resume" off, a user with no resume can still submit, and the application is saved with no _resume_id, so the Kanban view shows no resume icon and the applicant name is not linked. That is expected behaviour, not a bug. The form's Name field is pre-filled from the user's first_name and last_name user meta, so accounts missing those show a blank name (email is pre-filled from the account). The detail endpoint for jobs checks the nonce; the resume endpoint does not yet. If you change the breakpoint, update both isMobileLayout() in the JS (768px) and the media query in the CSS together.