Skip to content

Commit

Permalink
feat(demo): implement query parameters in search page
Browse files Browse the repository at this point in the history
This commit refactors the router to incorporate query parameters. A new `queryparams` event
has been introduced to facilitate the monitoring of state changes in query parameters.

The search page has been modified to update the query parameters for every new search and can
now trigger a search operations and update its state in response to changes in query parameters.
  • Loading branch information
jboix authored and amtins committed Dec 4, 2023
1 parent 01ffc74 commit fc3c13c
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 37 deletions.
95 changes: 67 additions & 28 deletions demo/src/core/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}

/**
Expand All @@ -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 });
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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'));
}

/**
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
45 changes: 37 additions & 8 deletions demo/src/search/search-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -40,7 +41,7 @@ class SearchPage {
* @private
* @type {AbortController}
*/
#abortController;
#abortController = new AbortController();
/**
* The search bar element.
*
Expand Down Expand Up @@ -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 :
*
Expand All @@ -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) => {
Expand Down Expand Up @@ -151,6 +161,12 @@ class SearchPage {
}
}

clearSearch() {
this.abortPreviousSearch();
this.#intersectionObserverComponent?.remove();
this.#resultsEl.replaceChildren();
}

/**
* Initializes the {@link IntersectionObserverComponent} for infinite scrolling.
*
Expand Down Expand Up @@ -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);
});

1 comment on commit fc3c13c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 98.37% 482/490
🟢 Branches 93.44% 228/244
🟢 Functions 100% 136/136
🟢 Lines 99.15% 465/469

Test suite run success

145 tests passing in 9 suites.

Report generated by 🧪jest coverage report action from fc3c13c

Please sign in to comment.