Neutral TS detects AJAX requests via the Requested-With-Ajax header. The template component at src/component/cmp_XXXX_template/neutral/layout/index.ntpl detects if the request is an AJAX request:
{:bool; CONTEXT->HEADERS->Requested-With-Ajax >>
{:include; {:flg; require :} >> #/template-ajax.ntpl :}
:}{:else;
{:include; {:flg; require :} >> #/template.ntpl :}
:}
When an AJAX request is detected, only the content of the snippet current:template:body-main-content is rendered — without the surrounding <html>, <head>, or <body> elements. This behavior is automatic and transparent for the developer: the same content-snippets.ntpl file works for both full-page and AJAX responses.
{:fetch;} BIF ReferenceThe {:fetch;} BIF (Built-In Function) is Neutral TS’s declarative way to create AJAX-enabled elements. It generates the appropriate HTML element with Neutral JS classes and attributes, handling all request mechanics automatically.
{:fetch; |url|event|wrapperId|class|id|name| >> initial_content :}
| Position | Parameter | Required | Description |
|---|---|---|---|
| 1 | url |
Yes | Endpoint URL to fetch |
| 2 | event |
Yes | Trigger event type |
| 3 | wrapperId |
No | ID of the element to replace with the response |
| 4 | class |
No | CSS class(es) added to the generated element |
| 5 | id |
No | ID attribute for the generated element |
| 6 | name |
No | Name attribute for the generated element |
The content between >> and :} is the initial content — what the user sees before the fetch resolves. For forms this is the server-rendered form fields; for lazy-loaded sections it is typically a loading spinner.
| Event | Generated Element | HTTP Method | Trigger | Primary Use Case |
|---|---|---|---|---|
form |
<form> |
POST | Form submit | Form submissions |
visible |
<div> |
GET | Element enters viewport | Modal content, lazy loading |
click |
<div> |
GET | User click | Load-on-demand buttons |
auto |
<div> |
GET | Immediate on page load | Auto-loading content blocks |
none |
<div> |
— | Manual JS trigger only | Custom JavaScript control |
AJAX URLs typically include the component route (from the manifest) and the LTOKEN security token:
{:fetch; |{:;cmp_uuid->manifest->route:}/subroute/ajax/{:;LTOKEN:}|form||{:;local::current->forms->class:}| >>
...
:}
{:;cmp_uuid->manifest->route:} — Resolves to the component’s base URL path (e.g., /example-sign).{:;LTOKEN:} — A per-page security token used to validate that the AJAX request originated from a legitimately rendered page.{:fetch; ... :} Maps to HTMLWhen event is form, the BIF generates a <form> element:
<form id="my-id" name="my-name" class="neutral-fetch-form my-class"
method="POST" action="/url" data-wrap="form-wrapper">
<!-- initial_content rendered here -->
</form>
For all other events, it generates a <div> with the corresponding Neutral JS class (neutral-fetch-auto, neutral-fetch-visible, etc.) and a data-url attribute.
AJAX response templates live in an ajax/ subdirectory beneath the route they serve:
neutral/route/root/
├── subroute/
│ ├── content-snippets.ntpl ← Full-page template (GET /subroute)
│ ├── data.json
│ └── ajax/
│ └── content-snippets.ntpl ← AJAX fragment template (GET|POST /subroute/ajax/<ltoken>)
The URL-to-directory mapping follows the same pattern as all Neutral TS routes. If the component’s manifest route is /my-component:
| URL Path | Template Directory |
|---|---|
/my-component/subroute |
root/subroute/ |
/my-component/subroute/ajax/<ltoken> |
root/subroute/ajax/ |
An AJAX content-snippets.ntpl defines the current:template:body-main-content snippet with only the fragment that should replace the existing content:
{:snip; current:template:body-main-content >>
{:snip; cmp_uuid:my-content-snippet :}
:}
{:^;:}
Critical rules:
| Rule | Reason |
|---|---|
Do NOT include {:data; ... :} |
Data is provided by the handler and schema, not by a data file |
Do NOT add structural HTML (<html>, <head>, <body>) |
The AJAX layout (template-ajax.ntpl) handles this |
Do NOT add extra wrapper <div> elements |
The content snippet already provides its own wrapper |
MUST end with {:^;:} |
The render trigger is required for every content-snippets.ntpl |
The key architectural pattern is that both the full-page template and the AJAX template render the same snippet. The snippet is defined once in form-snippets.ntpl (or any shared file included by index-snippets.ntpl) and referenced from both places:
Full-page template (root/subroute/content-snippets.ntpl):
{:data; #/data.json :}
{:snip; current:template:body-main-content >>
<div class="{:;current->theme->class->container:}">
{:snip; cmp_uuid:my-content-snippet :}
</div>
:}
{:^;:}
AJAX template (root/subroute/ajax/content-snippets.ntpl):
{:snip; current:template:body-main-content >>
{:snip; cmp_uuid:my-content-snippet :}
:}
{:^;:}
The full-page version loads its data.json and adds page-level wrappers (container, cards, headings). The AJAX version returns only the inner content fragment. Both call the same cmp_uuid:my-content-snippet, so form logic, coalesce patterns, and error handling are defined in one place.
Neutral TS auto-loads certain template files based on their location:
| File | Scope | Auto-loaded |
|---|---|---|
component-init.ntpl |
App-wide (all components) | Yes — do NOT {:include;} it |
index-snippets.ntpl |
Component-wide (all routes in this component) | Yes — do NOT {:include;} it |
form-snippets.ntpl |
Component-wide | No — must be included from index-snippets.ntpl |
A typical index-snippets.ntpl:
{:* Include shared form/content snippets *:}
{:include; {:flg; require :} >> #/form-snippets.ntpl :}
{:* Move modals to end of body for proper Bootstrap z-indexing *:}
{:moveto; </body >>
{:snip; cmp_uuid:modals :}
:}
This section covers non-form AJAX patterns. For form-specific implementation, see Section 5.
Use the auto event to load content immediately when the page renders:
{:fetch; |{:;cmp_uuid->manifest->route:}/dashboard/ajax/{:;LTOKEN:}|auto|dashboard-content|my-class| >>
<div class="text-center p-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{:trans; Loading... :}</span>
</div>
</div>
:}
The spinner is shown immediately; as soon as the page loads, a GET request fires to the URL and the response replaces the spinner.
Use the visible event to defer loading until the element scrolls into the viewport:
{:fetch; |{:;cmp_uuid->manifest->route:}/stats/ajax/{:;LTOKEN:}|visible|stats-section|| >>
<div class="placeholder-glow p-3">
<span class="placeholder col-12"></span>
</div>
:}
This is the primary pattern for modal content: the {:fetch;} is placed inside .modal-body, and the visible event fires when the modal opens and the element becomes visible.
Use the click event to load content on user interaction:
{:fetch; |{:;cmp_uuid->manifest->route:}/details/ajax/{:;LTOKEN:}|click|details-area|btn btn-outline-primary| >>
{:trans; Load more details :}
:}
The generated <div> behaves like a clickable element. On click, it sends a GET request and replaces itself (or the element identified by wrapperId) with the response.
Modals are the most common use of visible loading. The complete pattern involves three parts:
1. Define the modal snippet (in form-snippets.ntpl or a shared snippets file):
{:snip; cmp_uuid:modals >>
<div class="modal fade" id="contentModal" tabindex="-1"
aria-labelledby="contentModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="contentModalLabel">
{:trans; Details :}
</h5>
<button type="button" class="btn-close"
data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{:fetch; |{:;cmp_uuid->manifest->route:}/details/ajax/{:;LTOKEN:}|visible||{:;local::current->forms->class:}| >>
<div class="text-center p-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
:}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light btn-sm"
data-bs-dismiss="modal">{:trans; Close :}</button>
</div>
</div>
</div>
</div>
:}
2. Move modals to end of <body> (in index-snippets.ntpl):
{:moveto; </body >>
{:snip; cmp_uuid:modals :}
:}
3. Add the trigger button (in any page template):
<button type="button" class="btn btn-primary"
data-bs-toggle="modal" data-bs-target="#contentModal">
{:trans; View Details :}
</button>
Flow: User clicks trigger → modal opens → visible event fires → GET request loads content → spinner is replaced with response.
For simple content loading (no form processing), the backend route is straightforward:
from flask import Response, g
from core.request_handler_form import FormRequestHandler
from . import bp
@bp.route("/details/ajax/<ltoken>", defaults={"route": "details/ajax"}, methods=["GET"])
def details_ajax(route, ltoken) -> Response:
"""AJAX route — load details content"""
handler = FormRequestHandler(g.pr, route, bp.neutral_route, ltoken, "_unused_form")
handler.schema_data["ajax_result"] = True
return handler.render_route()
The route parameter value ("details/ajax") maps directly to the template directory root/details/ajax/ where content-snippets.ntpl lives.
Forms in Neutral TS follow a structured pattern that combines the {:fetch;} BIF with server-side validation, a coalesce-based lifecycle, and a handler class.
route/schema.json)Every form needs validation rules defined in the route’s schema.json under data.current_forms:
{
"data": {
"current_forms": {
"contact_form": {
"check_fields": ["name", "email", "message"],
"validation": {
"minfields": 3,
"maxfields": 4,
"allow_fields": ["name", "email", "message", "send"]
},
"rules": {
"name": {
"required": true,
"minlength": 2,
"maxlength": 100
},
"email": {
"required": true,
"pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+",
"regex": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"
},
"message": {
"required": true,
"minlength": 10,
"maxlength": 1000
}
}
}
}
}
}
| Field | Description |
|---|---|
check_fields |
Array of field names to validate on POST |
validation.minfields / maxfields |
Expected range of POST field count |
validation.allow_fields |
Whitelist of allowed field names (use ftoken.* for anti-bot token fields) |
rules.<field>.required |
Field must have a value |
rules.<field>.minlength / maxlength |
String length constraints |
rules.<field>.pattern |
HTML5 pattern attribute (client-side) |
rules.<field>.regex |
Server-side regex validation |
rules.<field>.value |
Exact value match required |
rules.<field>.set |
If false, field must NOT be present (honeypot anti-bot) |
Note:
patternandregexcan both be defined for the same field.patterndrives the browser’s built-in validation;regexis checked server-side in the handler.
Form snippets are defined in form-snippets.ntpl and follow a layered structure. All user-facing text must be wrapped in {:trans; ... :}.
Required include at the top:
{:include; {:flg; require :} >> {:;forms_0yt2sa->path:}/neutral/snippets.ntpl :}
Error snippet generation — for each form, iterate over validation errors and generate per-field error snippets:
{:+each; contact_form->error->field field msg >>
{:+code;
{:param; field >> {:;field:} :}
{:param; msg >> {:;msg:} :}
{:param; help-item >> contact_form-{:;field:} :}
{:snip; forms:set-form-field-error :}
:}
:}
This generates two snippets per errored field:
{:snip; is-invalid:fieldname :} → outputs the CSS class is-invalid{:snip; error-msg:fieldname :} → outputs the error message HTMLIf a field has no error, both snippets render as empty strings.
The form snippet — wraps form fields in {:fetch;} with event=form:
{:snip; cmp_uuid:contact_form-form >>
{:fetch; |{:;cmp_uuid->manifest->route:}/contact/ajax/{:;LTOKEN:}|form||{:;local::current->forms->class:}| >>
<div class="form-floating">
<input type="text" id="contact_form-name" name="name"
value="{:;CONTEXT->POST->name:}"
class="form-control {:snip; is-invalid:name :}"
placeholder="{:trans; Your name :}"
minlength="{:;current_forms->contact_form->rules->name->minlength:}"
maxlength="{:;current_forms->contact_form->rules->name->maxlength:}"
{:bool; current_forms->contact_form->rules->name->required >> required :}
>
<label for="contact_form-name">{:trans; Your name :}</label>
</div>
{:snip; error-msg:name :}
<div class="{:;local::current->forms->field-spacing:}"></div>
{:* ... more fields ... *:}
<button type="submit" name="send" value="1"
class="w-100 btn btn-primary">{:trans; Send :}</button>
:}
:}
Key patterns used in form fields:
| Pattern | Purpose |
|---|---|
value="{:;CONTEXT->POST->fieldname:}" |
Preserves user input after failed submission |
class="form-control {:snip; is-invalid:fieldname :}" |
Adds is-invalid class on validation error |
{:snip; error-msg:fieldname :} |
Shows per-field error message |
{:bool; ...->required >> required :} |
Conditionally adds required attribute from schema |
class="fetch-form-button-reset" |
Makes a button reload/reset the form via AJAX |
{:;local::current->forms->field-spacing:} |
Theme-provided spacing class |
The container snippet uses {:coalesce;} to control which state the user sees. {:coalesce;} evaluates each option in order and renders the first one that produces non-empty output:
{:snip; cmp_uuid:contact_form-form-container >>
<div id="contact_form-container">
{:coalesce;
{:* Option 1: Success *:}
{:same; /{:;form_result->status:}/success/ >>
<div class="alert alert-success">
<h5>{:trans; Success! :}</h5>
<p>{:trans; Your message has been sent. :}</p>
</div>
:}
{:* Option 2: General failure *:}
{:same; /{:;form_result->status:}/fail/ >>
<div class="alert alert-danger">
<h5>{:trans; ERROR :}</h5>
<p>{:;form_result->message:}</p>
</div>
:}
{:* Option 3: Default — show the form *:}
{:snip; cmp_uuid:contact_form-form :}
:}
</div>
:}
Lifecycle states:
| State | Condition | What renders |
|---|---|---|
| Initial load | form_result not set |
Option 3: empty form (no errors) |
| Field validation errors | post() returns False, field errors set |
Option 3: form with inline errors |
| General failure | form_result->status = "fail" |
Option 2: error alert |
| Success | form_result->status = "success" |
Option 1: success message |
Alternative success actions include {:snip; util:reload-page-self :} to trigger a page reload (useful for login flows).
Each form requires three Flask routes: the full-page GET, the AJAX GET, and the AJAX POST:
from flask import Response, g
from .handler_module import FormRequestHandlerContact
from . import bp
# Full page (GET)
@bp.route("/contact", defaults={"route": "contact"}, methods=["GET"])
def contact_page(route) -> Response:
handler = FormRequestHandlerContact(
g.pr, route, bp.neutral_route,
None, # ltoken: None for page routes
"contact_form" # form_name: key in schema.json current_forms
)
handler.schema_data["dispatch_result"] = True
return handler.render_route()
# AJAX GET (initial form load for modals / fetch)
@bp.route("/contact/ajax/<ltoken>", defaults={"route": "contact/ajax"}, methods=["GET"])
def contact_ajax_get(route, ltoken) -> Response:
handler = FormRequestHandlerContact(
g.pr, route, bp.neutral_route, ltoken, "contact_form"
)
handler.schema_data["dispatch_result"] = handler.get()
return handler.render_route()
# AJAX POST (form submission)
@bp.route("/contact/ajax/<ltoken>", defaults={"route": "contact/ajax"}, methods=["POST"])
def contact_ajax_post(route, ltoken) -> Response:
handler = FormRequestHandlerContact(
g.pr, route, bp.neutral_route, ltoken, "contact_form"
)
handler.schema_data["dispatch_result"] = handler.post()
return handler.render_route()
The route parameter maps to the template directory under root/. The ltoken parameter from the URL is passed to the handler for security validation.
The handler subclasses FormRequestHandler and implements get() and post():
from core.request_handler_form import FormRequestHandler
class FormRequestHandlerContact(FormRequestHandler):
"""Handles contact_form processing."""
def __init__(self, req, comp_route, neutral_route=None, ltoken=None,
form_name="contact_form"):
super().__init__(req, comp_route, neutral_route, ltoken, form_name)
self.schema_data["dispatch_result"] = True
def get(self) -> bool:
if not self.valid_form_tokens_get():
return False
return True
def post(self) -> bool:
# 1. Validate tokens (CSRF / LTOKEN)
if not self.valid_form_tokens_post():
return False
# 2. Validate fields against schema rules
if not self.valid_form_validation():
return False
# 3. Check for field-level validation errors
if self.any_error_form_fields("ref:contact_form_error"):
return False
# 4. Custom business logic
email = self.schema_data["CONTEXT"]["POST"].get("email")
if email and "@blocked.com" in email:
self.error["form"]["email"] = "true"
self.error["field"]["email"] = "ref:contact_form_error_invalid_domain"
return False
# 5. Set success
self.schema_data["form_result"] = {
"status": "success",
"message": "Thank you!"
}
return True
Inherited methods:
| Method | Purpose |
|---|---|
valid_form_tokens_get() |
Validates LTOKEN for GET requests |
valid_form_tokens_post() |
Validates CSRF/LTOKEN for POST requests |
valid_form_validation() |
Validates POST fields against schema.json rules |
any_error_form_fields(prefix) |
Checks for field errors; generates translation keys by appending error type (e.g., _required, _minlength, _regex) |
Error handling:
| Property | Usage |
|---|---|
self.error["form"]["fieldname"] = "true" |
Marks a field as having an error |
self.error["field"]["fieldname"] = "ref:..." |
Sets the translation reference for the field’s error message |
self.schema_data["form_result"] |
Dict with status ("success" / "fail") and optional message |
self.schema_data["CONTEXT"]["POST"] |
Dict of submitted POST data |
Initial page load (GET):
/component-route/contactltoken=None, dispatch_result=Truedata.json → content-snippets.ntpl → content snippet → form-container → form{:fetch;} generates a <form> with fields rendered server-side; no errors, empty fieldsForm submission (POST via AJAX):
{:fetch;} intercepts and POSTs to /contact/ajax/<ltoken>post() returns False; errors populate self.error; template re-renders the form with inline error messagesform_result = {"status": "fail", ...}; coalesce shows error alertform_result = {"status": "success"}; coalesce shows success messageModal flow:
visible event fires → GET to /contact/ajax/<ltoken>Neutral TS dispatches custom events you can listen to:
// Event: Fetch completed successfully
window.addEventListener('neutralFetchCompleted', function(evt) {
console.log('Loaded URL:', evt.detail.url);
console.log('Container element:', evt.detail.element);
});
// Event: Fetch error
window.addEventListener('neutralFetchError', function(evt) {
console.error('Error loading:', evt.detail.url);
// Show error message to user
});
Before loading neutral.min.js, you can configure behavior:
<script nonce="{:;CSP_NONCE:}">
// Spinner shown during load
var neutral_submit_loading = '<span class="spinner-border spinner-border-sm"></span>';
// Request timeout (ms)
var neutral_submit_timeout = 30000;
// Error message
var neutral_submit_error = '{:trans; Connection error. Please try again. :}';
// Time error message remains visible (ms)
var neutral_submit_error_delay = 3500;
// Delay to prevent double-click (ms)
var neutral_submit_delay = 250;
</script>
Neutral TS provides two approaches for AJAX functionality: the {:fetch; BIF for automatic handling, or manual HTML with Neutral JS classes for full control.
{:fetch; BIFThe {:fetch; BIF automatically wraps content and adds necessary classes:
{:* Neutral TS automatically generates the appropriate wrapper *:}
{:fetch; |/url|form|form-wrapper|my-class|my-form-id|my-name| >>
<input type="text" name="paramValue">
<button type="submit">Send</button>
:}
Generated HTML (form event):
<form id="my-id" name="my-name" class="neutral-fetch-form my-class"
method="POST" action="/url" data-wrap="form-wrapper">
<input type="text" name="paramValue">
<button type="submit">Send</button>
</form>
For cases requiring full control over the HTML structure, use Neutral JS classes directly:
| Class | Behavior | Use Case |
|---|---|---|
neutral-fetch-form |
Submits form via AJAX on submit event | Forms needing custom structure |
neutral-fetch-auto |
Fetches content immediately on page load | Auto-loading content |
neutral-fetch-click |
Fetches content on click event | Buttons, links, clickable elements |
neutral-fetch-visible |
Fetches when element enters viewport | Lazy loading, infinite scroll |
neutral-fetch-none |
No automatic event, manual JS trigger | Custom JavaScript control |
Required attributes for manual classes:
data-url - Endpoint URL to fetchdata-wrap (optional) - ID of container to receive responseForm with {:fetch;:
{:fetch; |/submit|form|wrapper|my-form| >>
<input type="text" name="username">
<button type="submit">Submit</button>
:}
Same form with manual class (full control):
<form class="neutral-fetch-form my-form"
method="POST"
action="/submit"
data-wrap="wrapper">
<input type="text" name="username">
<button type="submit">Submit</button>
</form>
Use manual Neutral JS classes when:
Access Neutral JS functions directly for custom behavior:
// Re-initialize events after dynamic content loads
neutral_fev();
// Manual fetch trigger
neutral_fetch(element, url, wrapperId);
// Manual form submission
neutral_fetch_form(formElement, url, wrapperId);
| Step | Verification |
|---|---|
| 1 | Create reusable snippet in shared file |
| 2 | Container page uses {:fetch; |url|auto|... for initial load |
| 3 | /ajax route exists and responds to GET (or POST for forms) |
| 4 | AJAX template only defines current:template:body-main-content |
| 5 | No structural HTML (html, body, head) in AJAX response |
| 6 | Include {:^;:} at end of both templates |
| 7 | Configure neutral_submit_* variables if needed |
| 8 | Add event listeners for neutralFetchCompleted if post-load JS needed |
| 9 | Protect AJAX routes with @require_header_set if necessary |
| 10 | Verify disable_js is false in schema or include neutral.min.js manually |
Reset / reload button inside a form:
Add a button with the class fetch-form-button-reset. Neutral JS automatically handles it by re-fetching the form via GET, restoring it to its initial state:
<button type="button" title="{:trans; Reload form :}"
class="fetch-form-button-reset btn btn-light">
<span class="{:;x-icons->x-icon-reload:}"></span>
</button>
Multiple modals sharing a single snippet:
Define all modals inside one snippet and move them together:
{:snip; cmp_uuid:modals >>
<div class="modal fade" id="formOneModal" ...>
<div class="modal-body">
{:fetch; |{:;cmp_uuid->manifest->route:}/form-one/ajax/{:;LTOKEN:}|visible||{:;local::current->forms->class:}| >>
<div class="spinner-border" role="status"></div>
:}
</div>
</div>
<div class="modal fade" id="formTwoModal" ...>
...
</div>
:}
Setting cookies from the handler:
self.view.add_cookie({
"session-name": {
"key": "session-name",
"value": "session-value",
"path": "/"
}
})
Page reload on success (e.g., after login):
Replace the success message in the coalesce with:
{:same; /{:;form_result->status:}/success/ >>
{:snip; util:reload-page-self :}
:}
FToken is an optional client-side anti-bot mechanism. When enabled:
"set": false and "value" rules.ftoken-field-key, ftoken-field-value, data-ftokenid, and related attributes to tracked input fields.{:fetch;} tag explicit wrapperId and id parameters.ftoken_check() in the handler after standard validation.See the cmp_6000_examplesign component for a complete working example.
| Symptom | Likely Cause | Fix |
|---|---|---|
| Full page returned instead of fragment | Requested-With-Ajax header missing |
If using manual fetch(), add "Requested-With-Ajax": "true" header |
| AJAX response is empty | Missing {:^;:} at end of template |
Add {:^;:} as the last line of content-snippets.ntpl |
| Form shows no validation errors | Error snippet generation block missing or misnamed | Verify {:+each; form_name->error->field ...} block exists and uses the correct form name |
| Form fields lose values after error | Missing value="{:;CONTEXT->POST->fieldname:}" |
Add the value attribute referencing POST context |
| Snippet not found / empty | Snippet defined in a file that isn’t included | Ensure form-snippets.ntpl is included from index-snippets.ntpl |
| Modal appears behind backdrop | Modal HTML not at end of <body> |
Use {:moveto; </body >> ... :} in index-snippets.ntpl |
{:data; ... :} has no effect in AJAX template |
Data files are only loaded for page-level templates | Remove {:data;} from AJAX templates; data comes from the handler |
| Route returns 404 | route default doesn’t match directory structure |
Verify defaults={"route": "..."} matches root/<path>/ |
Requested-With-Ajax HeaderWhen Neutral TS initiates AJAX requests via {:fetch; or its JavaScript classes, it automatically sets the Requested-With-Ajax header to fetch. This header is essential for the backend to detect AJAX mode and respond appropriately (rendering template-ajax.ntpl instead of the full layout).
However, if you need to make manual AJAX requests using vanilla JavaScript (outside of Neutral’s automatic handling), you must explicitly set this header:
fetch("/component/route", {
method: "POST",
headers: {
"Requested-With-Ajax": "true",
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({ key: "value" })
});
Important notes:
fetch(), XMLHttpRequest, or third-party libraries like AxiosThis convention is specific to routes handled through the Neutral + RequestHandler flow and is not required for independent API endpoints outside that rendering pipeline.
| Syntax | Purpose |
|---|---|
{:;variable->path:} |
Output variable value |
{:;local::variable:} |
Output local/theme variable |
{:;LTOKEN:} |
Current page security token |
{:;CONTEXT->POST->field:} |
Submitted POST field value |
{:;cmp_uuid->manifest->route:} |
Component base route from manifest |
{:;current_forms->form->rules->field->prop:} |
Schema validation rule value |
{:trans; text :} |
Translation wrapper |
{:snip; namespace:name >> content :} |
Define a snippet |
{:snip; namespace:name :} |
Render a snippet |
{:fetch; \|url\|event\|wrap\|class\|id\| >> content :} |
AJAX fetch wrapper |
{:include; {:flg; require :} >> path :} |
Include a file (required) |
{:data; #/data.json :} |
Load data file (relative path with #/) |
{:coalesce; opt1 opt2 opt3 :} |
Render first non-empty option |
{:bool; variable >> content :} |
Render content if variable is truthy |
{:!bool; variable >> content :} |
Render content if variable is falsy |
{:bool; var >> if_true :}{:else; if_false :} |
Conditional with else |
{:same; /val1/val2/ >> content :} |
Render content if values are equal |
{:eval; expression >> content :} |
Render content if expression is truthy |
{:moveto; </body >> content :} |
Move content to end of body |
{:+each; collection key val >> template :} |
Iterate over collection |
{:+code; ... :} |
Code block with parameters |
{:param; name >> value :} |
Set parameter (inside {:+code;:}) |
{:* comment *:} |
Template comment (not rendered) |
{:^;:} |
Render trigger (required at end of content-snippets.ntpl) |