diff --git a/demo/src/core/router.js b/demo/src/core/router.js index 5e768e0..7d3934b 100644 --- a/demo/src/core/router.js +++ b/demo/src/core/router.js @@ -31,28 +31,33 @@ import { Minimatch } from 'minimatch'; */ class Router extends EventTarget { #fallback; + #currentRoute = null; + #currentQueryParams = {}; + #routes = []; constructor() { super(); - this.routes = []; - this.currentRoute = null; - // Event listener for click events on the document document.addEventListener('click', (event) => { if (!('spaRoute' in event.target.dataset)) return; event.preventDefault(); - const path = new URL(event.target.href).pathname; + const url = new URL(event.target.href); + const path = url.pathname; + const queryParams = Object.fromEntries(url.searchParams.entries()); - if (!this.isActiveRoute(path)) { - window.history.pushState({}, '', event.target.href); - this.handleRouteChange(path); - } + window.history.pushState({}, '', url.href); + this.handleRouteChange(path, queryParams); }); // Event listener for the popstate event - window.addEventListener('popstate', () => this.handleRouteChange(window.location.pathname)); + window.addEventListener('popstate', () => { + const entries = new URL(window.location.href).searchParams.entries(); + const queryParams = Object.fromEntries(entries); + + this.handleRouteChange(window.location.pathname, queryParams); + }); } /** @@ -62,11 +67,10 @@ class Router extends EventTarget { * @param {function} start - The function to be called when the route is navigated to. * @param {function} destroy - The function to be called when the route is navigated away from. */ - addRoute(pattern, start, destroy = () => { - }) { + addRoute(pattern, start, destroy = () => {}) { const path = new Minimatch(pattern, { matchBase: true }); - this.routes.push({ path, start, destroy }); + this.#routes.push({ path, start, destroy }); } /** @@ -76,8 +80,8 @@ class Router extends EventTarget { * @returns {boolean} - True if the given path is the current active route, false otherwise. */ isActiveRoute(path) { - return this.currentRoute && - this.currentRoute.path === this.findRoute(path).path; + return this.#currentRoute && + this.#currentRoute.path === this.findRoute(path).path; } /** @@ -87,39 +91,70 @@ class Router extends EventTarget { * @returns {object} - The route object if found, otherwise undefined. */ findRoute(path) { - return this.routes.find(r => r.path.match(path)); + return this.#routes.find(r => r.path.match(path)); } /** * Navigates to the specified path. * * @param {string} path - The path to navigate to. + * @param {object} queryParams - Optional query parameters to be associated with the route. */ - navigateTo(path) { - if (this.isActiveRoute(path)) return; + navigateTo(path, queryParams = {}) { + const url = new URL(window.location.href); + + url.pathname = path; + url.search = new URLSearchParams(queryParams).toString(); - window.history.pushState({}, '', this.base + path); - this.handleRouteChange(path); + window.history.pushState({}, '', url.href); + this.handleRouteChange(path, queryParams); + } + + updateState(queryParams) { + this.navigateTo(window.location.pathname, queryParams); } /** * Handles a change in the route. * * @param {string} path - The path of the new route. + * @param {object} [queryParams={}] - (Optional) The query parameters associated with the route. */ - handleRouteChange(path) { + handleRouteChange(path, queryParams = {}) { + if (this.isActiveRoute(path)) { + if (!this.#matchCurrentParams(queryParams)) { + this.#currentQueryParams = queryParams; + this.dispatchEvent(new Event('queryparams')); + } + + return; + } + const route = this.findRoute(path); - if (route) { - route.destroy(); - this.currentRoute = route; - route.start(); - this.dispatchEvent(new Event('routechanged')); - } else if (this.#fallback) { + if (route) + this.#updateCurrentRoute(route, queryParams); + else if (this.#fallback) this.handleRouteChange(this.#fallback); - } else { + else throw Error(`No route found for '${path}'`); - } + } + + #matchCurrentParams(params) { + const paramsKeys = Object.keys(params); + + if (paramsKeys.length !== Object.keys(this.#currentQueryParams).length) + return false; + + return paramsKeys.every(k => this.#currentQueryParams[k] === params[k]); + } + + #updateCurrentRoute(route, queryParams) { + route.destroy(); + this.#currentRoute = route; + this.#currentQueryParams = queryParams; + route.start(queryParams); + this.dispatchEvent(new Event('routechanged')); } /** @@ -142,6 +177,10 @@ class Router extends EventTarget { set fallback(fallback) { this.#fallback = fallback; } + + get queryParams() { + return this.#currentQueryParams; + } } // Export a singleton instance of the router diff --git a/demo/src/index.js b/demo/src/index.js index 63090a0..6da8dfe 100644 --- a/demo/src/index.js +++ b/demo/src/index.js @@ -13,4 +13,9 @@ import router from './core/router'; // Initialize the router with the current path or 'examples' if none is found router.initBase(); router.fallback = 'examples'; -router.handleRouteChange(window.location.pathname); + +const url = new URL(window.location.href); +const path = url.pathname; +const queryParams = Object.fromEntries(url.searchParams.entries()); + +router.handleRouteChange(path, queryParams); diff --git a/demo/src/search/search-page.js b/demo/src/search/search-page.js index c002403..3aa1ebd 100644 --- a/demo/src/search/search-page.js +++ b/demo/src/search/search-page.js @@ -10,7 +10,8 @@ import { openPlayerModal } from '../player/player-dialog'; import ilProvider from '../core/il-provider'; import SpinnerComponent from '../core/spinner-component'; import Pillarbox from '../../../src/pillarbox'; -import IntersectionObserverComponent from '../core/intersection-observer-component'; +import IntersectionObserverComponent + from '../core/intersection-observer-component'; /** * Represents the search page. @@ -40,7 +41,7 @@ class SearchPage { * @private * @type {AbortController} */ - #abortController; + #abortController = new AbortController(); /** * The search bar element. * @@ -74,10 +75,19 @@ class SearchPage { this.#resultsEl = document.querySelector('#results'); this.#dropdownEl = document.querySelector('#bu-dropdown'); this.#searchBarEl = document.querySelector('#search-bar'); - this.#abortController = new AbortController(); + this.initListeners(); } + async onStateChanged({ query, bu }) { + this.clearSearch(); + this.#searchBarEl.value = query || ''; + this.#dropdownEl.value = bu || 'rsi'; + + if (query) + await this.search(this.#dropdownEl.value, this.#searchBarEl.value); + } + /** * Initializes the event listeners for the search bar and search results : * @@ -87,25 +97,25 @@ class SearchPage { initListeners() { let lastQuery; - this.#searchBarEl.addEventListener('keyup', Pillarbox.fn.debounce(async (event) => { + this.#searchBarEl.addEventListener('keyup', Pillarbox.fn.debounce((event) => { const query = event.target.value.trim(); if (!query || query === lastQuery) return; const bu = this.#dropdownEl.value; - await this.search(bu, query); + router.updateState({ query, bu }); lastQuery = query; }, 500)); - this.#dropdownEl.addEventListener('change', async () => { + this.#dropdownEl.addEventListener('change', () => { const query = this.#searchBarEl.value.trim(); if (!query) return; const bu = this.#dropdownEl.value; - await this.search(bu, query); + router.updateState({ query, bu }); }); this.#resultsEl.addEventListener('click', (event) => { @@ -151,6 +161,12 @@ class SearchPage { } } + clearSearch() { + this.abortPreviousSearch(); + this.#intersectionObserverComponent?.remove(); + this.#resultsEl.replaceChildren(); + } + /** * Initializes the {@link IntersectionObserverComponent} for infinite scrolling. * @@ -242,4 +258,17 @@ class SearchPage { } -router.addRoute('search', () => new SearchPage()); +let onStateChangedListener; + +router.addRoute('search', async (queryParams) => { + const searchPage = new SearchPage(); + + onStateChangedListener = async () => { + await searchPage.onStateChanged(router.queryParams); + }; + + router.addEventListener('queryparams', onStateChangedListener); + await searchPage.onStateChanged(queryParams); +}, () => { + router.removeEventListener('queryparams', onStateChangedListener); +});