Use Framework7 Page Lifecycle Events Instead of DOMContentLoaded
Router-managed pages swap in and out of the DOM, so DOMContentLoaded is not enough. Use scoped Framework7 lifecycle events to initialize and clean up pages safely.
11 Jun 2026, 10:04 UTC

Pages are swapped, not reloaded
In a traditional multi-page website, each URL loads a fresh document and DOMContentLoaded runs once per visit. Framework7's router works differently: it keeps a single application shell and swaps page fragments in and out of the DOM as the user navigates. If you attach page-specific behavior inside DOMContentLoaded, it runs only when the app first loads. Pages loaded later never initialize, and behavior attached globally can accumulate across visits.
The practical takeaway is to scope initialization and cleanup to Framework7's page lifecycle events instead of the document load event.
Lifecycle events for router-managed pages
Framework7 fires a sequence of events whenever the router mounts or unmounts a page. These names reflect Framework7 v7; event names and registration syntax changed between major versions, so confirm them against your installed version's documentation.
page:init— Fires once when a page is first initialized. Use it to bind buttons, fetch data, or set up page-specific UI.page:beforein— Fires before the page becomes visible.page:afterin— Fires after the page has transitioned into view.page:beforeout— Fires before the page is removed or hidden. Use it to remove listeners and clear timers.page:afterout— Fires after the page has left the DOM or been hidden.
Events can be scoped to a specific page name using a CSS selector, so a handler for the products page does not run on the settings page.
Worked example: product list and detail pages
This example assumes a Framework7 v7 app with two routes. Place the script in your application entry file, such as src/app.js. No special permissions are required beyond read/write access to the project source.
import Framework7 from 'framework7';
const app = new Framework7({
root: '#app',
name: 'Store',
id: 'com.example.store',
routes: [
{
path: '/products/',
url: './pages/products.html',
name: 'products',
},
{
path: '/product/:id/',
url: './pages/product.html',
name: 'product',
},
],
});
// Initialize behavior only on the products page
app.on('page:init', '.page[data-name=\'products\']', function (page) {
console.log('[products] init');
const addButtons = page.el.querySelectorAll('.add-to-cart');
addButtons.forEach((button) => {
button.addEventListener('click', function () {
const productId = this.dataset.id;
console.log('add to cart', productId);
});
});
});
// Clean up when leaving the products page
app.on('page:beforeout', '.page[data-name=\'products\']', function (page) {
console.log('[products] beforeout');
// Remove dynamically attached listeners here if they were not scoped to page.el
});
// Read a route parameter on the product detail page
app.on('page:init', '.page[data-name=\'product\']', function (page) {
const productId = page.route.params.id;
console.log('[product] init for id', productId);
// Fetch product details using productId
});
Expected checks: open browser DevTools, navigate to /products/, then open a product, then go back. The console should show products page:init, then product page:init with the correct ID, then products page:beforeout when leaving. If you see page:init fire only once or not at all on later visits, the selector or page name likely does not match the rendered page markup.
Risk: attaching listeners outside page.el without removing them in page:beforeout can leak memory as users navigate back and forth.
Limitations and what to verify
- AJAX-loaded pages must return HTML fragments only. A full document with
<html>and<body>tags can prevent the router from mounting the page correctly. - The core router does not provide built-in lazy-loading of route components in the same way as some modern SPA frameworks. All routes are typically registered at startup, which can increase the initial bundle size.
- Storing page-specific data in the global Framework7 app instance works, but objects should be released in
page:beforeoutto avoid retaining detached DOM nodes.
Practical verification: insert a temporary console.log in each lifecycle handler, click through the app, and confirm the event order matches page:init → page:beforein → page:afterin on entry and page:beforeout → page:afterout on exit. Also inspect the DOM after an AJAX navigation to confirm no duplicate document wrappers were inserted.
Closing
Treat Framework7 pages as temporary components that enter and leave the DOM, not as static documents. Bind behavior to scoped lifecycle events, clean up on exit, and pass data through route parameters. Check your installed Framework7 version before adopting the exact event names, since they changed between major releases.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.