Turbo Router
Learn how links are routed using the AJAX framework.
- Routing Links
- Disable Routing
- Disable for Specific Links
- Disable Visit Scrolling
- Persisting Page Elements
- Setting a Root Path
- Native Error Pages
- Page Caching
- Listening for the Cache Event
- Detecting a Cached Page Load
- Disabling the Cache
- Working with JavaScript
- Inline Script Elements
- Making Controls Idempotent
- Disposing Controls
- Pause Rendering
- Global Events
- Usage Examples
- Working with Hot Reloading
Turbo routing is an implementation of PJAX (push state and AJAX) that gives the performance benefits of a single page application without the added complexity of a client-side framework. When you click a link, the page is automatically swapped client-side without the cost of a full page load.
{% framework turbo %}
# Routing Links
You may programmatically visit a link with the following.
oc.visit(location);
To replace the current URL without adding it to the navigation history, similar to window.history.replaceState
, set the action
option to replace.
oc.visit(location, { action: 'replace' });
To check if the turbo router is enabled and should be used.
if (oc.useTurbo && oc.useTurbo()) {
// Use PJAX
}
# Disable Routing
To disable PJAX routing on a specific page, you may trigger a full reload by including the turbo-visit-control
meta tag in the head section of the page with the reload
value. This will disable the feature for incoming requests only.
<head>
<meta name="turbo-visit-control" content="reload" />
</head>
To completely disable PJAX in your website, set the value to disable
. This will disable the feature for all incoming and outgoing requests.
<meta name="turbo-visit-control" content="disable" />
# Disable for Specific Links
By default, all internal HTML links will be routed using PJAX, but you can disable this by marking links or their parent container with data-turbo="false"
. Links that are disabled are handled normally by the browser.
<a href="/" data-turbo="false">Disabled</a>
You may re-enable when an ancestor has disabled:
<div data-turbo="false">
<a href="/" data-turbo="true">Enabled</a>
</div>
# Disable Visit Scrolling
Every visit scrolls to the top of the page like most links in a browser. However, in some cases preserving the scroll position is useful, such as situations where links act like filters. You may disable visit scrolling using the data-turbo-no-scroll
attribute on the link element.
<a href="/" data-turbo-no-scroll>Filter</a>
# Persisting Page Elements
In some cases you may want to include static elements on the page, these are elements that should not be refreshed when the page updates. Use the data-turbo-permanent
attribute on the parent element. The element must also supply and id
attribute so the original page can be matched with the new page, including event listeners.
<div id="main-navigation" data-turbo-permanent>...</div>
# Setting a Root Path
By default, PJAX is used for all links within the same domain name and a visit to any other URL will fallback to a full page load. In some cases, your application may live in a subdirectory and the links should only apply to the root path.
For example, if you website lives in /app
and you don't want the links to apply to a different site in /docs
then you may restrict the links to a root path. You may set the root path by including the turbo-root
meta tag in the page's head section.
<head>
<meta name="turbo-root" content="/app">
</head>
# Native Error Pages
When using PJAX and the server responds with an error code, such as 404 or 500 status, the complete html element is replaced, including scripts and stylesheets. This prevents accidentally replacing the body element with content not produced by the same application code.
You may disable this behavior by including the turbo-visit-control
meta tag in the head section of the page with the error
value. This will tell the turbo router that the error page content is produced by the native application.
<meta name="turbo-visit-control" content="error">
# Page Caching
With caching enabled, the turbo router speeds up a website's performance by displaying revisited pages without accessing the network, making the website feel faster. When clicking a link, the contents are shown from the browser's local storage while the page requests the background. The latest page shows when the network request is complete, meaning the page renders twice.
# Listening for the Cache Event
You may listen to the page:before-cache
event if you need to prepare the document before it enters the cache. You can use this for things like resetting forms, collapsing UI elements or tearing down any third-party controls, so the page is ready to be displayed again.
addEventListener('page:before-cache', function() {
// Close any open submenus, etc.
});
# Detecting a Cached Page Load
You can detect when the page contents are sourced from the cache with the data-turbo-preview
attribute on the HTML element. Expressed in JavaScript as the following.
if (document.documentElement.hasAttribute('data-turbo-preview')) {
// Page shown is loaded from cache
}
Or using a StyleSheet with the following.
html[data-turbo-preview] {
/* Hide overlays from previous view */
}
# Disabling the Cache
You can disable the page cache for individual pages by using the turbo-cache-control
meta tag in the page's head section. Settings this value to no-cache will disable the cache entirely. You can also set this to no-preview to keep the cached version when navigating using the browser's Back and Forward buttons.
<head>
<meta name="turbo-cache-control" content="no-cache">
</head>
# Working with JavaScript
When working with PJAX, the page contents may load dynamically, which differs from the usual browser behavior. To overcome this, use the render
event handler is called every time the page loads, including AJAX updates.
addEventListener('render', function() {
// Page has rendered something new
});
The oc.pageReady
function is used call code when the page and scripts are ready. The function returns a promise that is resolved after all the page scripts have loaded, or immediately if they are already loaded.
oc.pageReady().then(() => {
// Page has finished loading scripts
});
The oc.waitFor
is another useful function that will wait for an object or variable to exist. The function returns a promise that is resolved when the variable is found.
oc.waitFor(() => window.propName).then(() => [
// window.propName is now available
]);
The second argument provides a timeout interval in milliseconds, the following will stop waiting after two seconds.
oc.waitFor(() => window.propName, 2000).then(() => {
console.log('Found the variable!')
}).catch(() => {
console.error('Gave up waiting...')
});
# Inline Script Elements
The turbo router maintains the scripts within the <head>
tag of the page by comparing the differences. If you use script tags in the <body>
tag then the script will be executed every time the page renders, which may be undesirable.
You may include data-turbo-eval="false"
to only allow the script to be executed on the first page load. The script will not be called for any PJAX requests.
<body>
<script data-turbo-eval="false" src="{{ ['@framework.bundle']|theme }}"></script>
</body>
If you are placing scripts in the <body>
tag for performance reasons, consider moving it to the <head>
tag and using <script defer>
instead.
To execute inline JavaScript code only once, regardless of first page load or PJAX request, set a unique value to the data-turbo-eval-once
attribute. The unique value (e.g myAjaxPromise
) is used to determine if the script has been seen before.
<script data-turbo-eval-once="myAjaxPromise">
// This script will run once only
addEventListener('ajax:promise', function(event) {
//
});
</script>
# Making Controls Idempotent
October CMS provides a complimentary library that is used to make building idempotent controls easy.
When a page visit occurs and JavaScript components are initialized, it is important that these function are idempotent. In simple terms, an idempotent function is safe to apply multiple times without changing the result beyond its initial application.
One technique for making a function idempotent is to keep track of whether you've already performed it by adding a value to the dataset
property on each processed element. This is useful for external scripts.
addEventListener('page:loaded', function() {
// Find my control
var myControl = document.querySelector('.my-control');
// Check if control has already been initialized
if (!myControl.dataset.hasMyControl) {
myControl.dataset.hasMyControl = true;
// Initialize since this is the first time
initializeMyControl(myControl);
}
});
As general advice, a simpler approach is to allow the function to run multiple times and apply idempotence techniques internally. For example, check to see if a menu divider already exists first before creating a new one.
# Disposing Controls
In some cases you may bind global events for a specific page only, for example, binding a hot key to a certain action.
addEventListener('keydown', myKeyDownFunction);
To prevent this event from leaking to other pages, you should remove the event using the page:unload
method, which will destroy any events and controls. The event can be used once to dispose of controls and events safely.
addEventListener('page:unload', function() {
removeEventListener('keydown', myKeyDownFunction);
}, { once: true });
October CMS includes a complimentary library for building disposable controls.
# Pause Rendering
If you would like to animate some elements like dropdowns or off-canvas menus before loading a new page, you can pause the page:before-render
event by preventing the default behavior and resuming it with the resume()
function in the event detail.
addEventListener('page:before-render', async (event) => {
event.preventDefault();
await animateOut();
event.detail.resume();
});
Keep in mind that the page:before-render event may fire twice, once from cache and once again after requesting the new page content.
# Global Events
The AJAX framework triggers several events during the navigation life cycle and page responses. The events are usually triggered on the document
object with details available on the event.detail
property.
Event | Description |
---|---|
render | triggered when the page updates via PJAX or AJAX. |
page:click | triggered when a turbo-routed link is clicked. |
page:before-visit | triggered before visiting a location, except when navigating using browser history. |
page:visit | triggered after a clicked visit starts. |
page:request-start | triggered before the request for a page. |
page:request-end | triggered after the page request ends. |
page:before-cache | triggered before a page is cached. |
page:before-render | triggered before the page content is rendered. |
page:render | triggered after the page is rendered. This is fired twice, once from cache and once again after requesting the new page content. |
page:load | triggered once after the initial page load and again every time a page is visited. |
page:loaded | identical to page:load except will wait for all newly added scripts to load. |
page:updated | similar to DOMContentLoaded except triggered only when a page is visited. |
page:unload | called when a previously loaded page should be disposed. |
# Usage Examples
The following JavaScript will run every time a page loads, including scripts.
addEventListener('page:loaded', function() {
// ...
});
# Working with Hot Reloading
In some cases, the turbo router may interfere when you are developing your website with hot reloading or browser sync technology, such as with Laravel Mix (opens new window) in development mode using laravel-mix & browsersync
. To overcome this add the following code to your webpack browserSync
configuration.
snippetOptions: {
rule: {
match: /<\/head>/i,
fn: function (snippet, match) {
return '<meta name="turbo-visit-control" content="disable" />';
}
}
}