Skip to content

Commit

Permalink
feat(demo): lists page state management
Browse files Browse the repository at this point in the history
Closes #109

Refactor the state management of the lists page to introduce the `ListsPageStateManager`
class. This class facilitates navigation and retrieval of section data. Query parameters
have been added to the URL whenever a section is opened on the lists page, making it navigable
through the browser history stack. Examples of decoded urls :

```text
/lists?section=tv-topics&bu=rts&nodes=urn:rts:topic:tv:665
/lists?section=radio-shows&bu=rts&nodes=a9e7621504c6959e35c3ecbe7f6bed0446cdf8da,urn:rts:show:radio:9801398
```
  • Loading branch information
jboix committed Dec 6, 2023
1 parent ccfb056 commit 1c398a4
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 59 deletions.
4 changes: 4 additions & 0 deletions demo/scss/content-tree.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
color: inherit;
}
}

i {
font-size: var(--size-3);
}
}
98 changes: 85 additions & 13 deletions demo/src/core/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,35 @@ import { Minimatch } from 'minimatch';
* @class
*/
class Router extends EventTarget {
#fallback;
/**
* The fallback route to be used in case no matching route is found.
* @private
* @type {string|null}
*/
#fallback = null;

/**
* The currently active route.
* @private
* @type {object|null}
*/
#currentRoute = null;

/**
* The query parameters associated with the current route.
* @private
* @type {object}
*/
#currentQueryParams = {};

/**
* An array containing registered route objects with their patterns and associated actions.
* @private
* @type {Array<{ path: Minimatch, start: function, destroy: function }>}
*/
#routes = [];


constructor() {
super();

Expand All @@ -48,15 +72,15 @@ class Router extends EventTarget {
const queryParams = Object.fromEntries(url.searchParams.entries());

window.history.pushState({}, '', url.href);
this.handleRouteChange(path, queryParams);
this.#handleRouteChange(path, queryParams);
});

// Event listener for the popstate event
window.addEventListener('popstate', () => {
const entries = new URL(window.location.href).searchParams.entries();
const queryParams = Object.fromEntries(entries);

this.handleRouteChange(window.location.pathname, queryParams);
this.#handleRouteChange(window.location.pathname, queryParams, true);
});
}

Expand All @@ -67,7 +91,8 @@ 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 });
Expand Down Expand Up @@ -98,7 +123,7 @@ class Router extends EventTarget {
* 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.
* @param {object} [queryParams={}] - (Optional) query parameters to be associated with the route.
*/
navigateTo(path, queryParams = {}) {
const url = new URL(window.location.href);
Expand All @@ -107,24 +132,37 @@ class Router extends EventTarget {
url.search = new URLSearchParams(queryParams).toString();

window.history.pushState({}, '', url.href);
this.handleRouteChange(path, queryParams);
this.#handleRouteChange(path, queryParams);
}

/**
* Update the state of the current route (i.e. it's query params).
*
* @param {object} queryParams - query parameters to be associated with the route.
*/
updateState(queryParams) {
this.navigateTo(window.location.pathname, queryParams);
}

/**
* Handles a change in the route.
*
* @private
* @param {string} path - The path of the new route.
* @param {object} [queryParams={}] - (Optional) The query parameters associated with the route.
* @param {boolean} [popstate=false] - (Optional) if a popstate is at the origin of this route change.
*/
handleRouteChange(path, queryParams = {}) {
#handleRouteChange(path, queryParams = {}, popstate = false) {
if (this.isActiveRoute(path)) {
if (!this.#matchCurrentParams(queryParams)) {
this.#currentQueryParams = queryParams;
this.dispatchEvent(new Event('queryparams'));
this.dispatchEvent(new CustomEvent('queryparams', {
detail: {
route: this.#currentRoute,
popstate,
queryParams
}
}));
}

return;
Expand All @@ -133,13 +171,20 @@ class Router extends EventTarget {
const route = this.findRoute(path);

if (route)
this.#updateCurrentRoute(route, queryParams);
this.#updateCurrentRoute(route, queryParams, popstate);
else if (this.#fallback)
this.handleRouteChange(this.#fallback);
this.#handleRouteChange(this.#fallback, popstate);
else
throw Error(`No route found for '${path}'`);
}

/**
* Checks if the given query parameters match the current query parameters.
*
* @private
* @param {object} params - The query parameters to compare.
* @returns {boolean} - True if the given parameters match the current query parameters, false otherwise.
*/
#matchCurrentParams(params) {
const paramsKeys = Object.keys(params);

Expand All @@ -149,20 +194,47 @@ class Router extends EventTarget {
return paramsKeys.every(k => this.#currentQueryParams[k] === params[k]);
}

#updateCurrentRoute(route, queryParams) {
/**
* Updates the current route and dispatches the 'routechanged' event.
*
* @private
* @param {object} route - The route object.
* @param {object} queryParams - The query parameters associated with the route.
* @param {boolean} [popstate=false] - (Optional) if a popstate is at the origin of this route change.
*/
#updateCurrentRoute(route, queryParams, popstate = false) {
route.destroy();
this.#currentRoute = route;
this.#currentQueryParams = queryParams;
route.start(queryParams);
this.dispatchEvent(new Event('routechanged'));
this.dispatchEvent(new CustomEvent('routechanged', {
detail: {
route,
popstate,
queryParams
}
}));
}

/**
* Initiates the router based on the current window location.
*/
start() {
this.#initBase();

const url = new URL(window.location.href);
const path = url.pathname;
const queryParams = Object.fromEntries(url.searchParams.entries());

this.#handleRouteChange(path, queryParams);
}

/**
* Initializes the base path for the router based on the current window location pathname.
* If the current pathname matches a route, the base is set to the pathname with the last path segment removed.
* If there is no matching route, the base is set to the full pathname.
*/
initBase() {
#initBase() {
const pathname = window.location.pathname;

this.base = this.findRoute(pathname) ?
Expand Down
16 changes: 16 additions & 0 deletions demo/src/core/string-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Converts a given string to kebab case, spaces are replaced with hyphens
* and all letters are converted to lowercase.
*
* @param {string} str - The input string to be converted to kebab case.
* @returns {string} - The input string converted to kebab case.
*
* @example
* const result = toKebabCase("Hello World");
* console.log(result); // Output: "hello-world"
*
* @example
* const result = toKebabCase("CamelCase Example");
* console.log(result); // Output: "camelcase-example"
*/
export const toKebabCase = (str) => str.replace(/\s+/g, '-').toLowerCase();
8 changes: 1 addition & 7 deletions demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,5 @@ import './lists/lists-page';
import router from './core/router';

// Initialize the router with the current path or 'examples' if none is found
router.initBase();
router.fallback = 'examples';

const url = new URL(window.location.href);
const path = url.pathname;
const queryParams = Object.fromEntries(url.searchParams.entries());

router.handleRouteChange(path, queryParams);
router.start();
Loading

0 comments on commit 1c398a4

Please sign in to comment.