Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(site): improve performance by replacing page content #1917

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions pages/src/localdev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import {ESLDemoSearchPageWrapper} from './search/search';
import {ESLDemoSidebar} from './navigation/navigation';
import {ESLDemoAnchorLink} from './anchor/anchor-link';
import {ESLDemoBanner} from './banner/banner';
import {ESLDemoRouteLink} from './route/route-link';
import {ESLDemoRouteInterceptor} from './route/route-interceptor';
import {ESLDemoSwipeArea} from './esl-swipe-demo/esl-swipe-demo-area';

ESLVSizeCSSProxy.observe();
Expand All @@ -72,6 +74,8 @@ ESLDemoSearchPageWrapper.register();
ESLDemoAnchorLink.register();
ESLDemoBackLink.register();
ESLDemoBanner.register();
ESLDemoRouteLink.register();
ESLDemoRouteInterceptor.register();
ESLDemoSwipeArea.register();

// Register ESL Components
Expand Down
33 changes: 33 additions & 0 deletions pages/src/route/route-interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {listen, attr, prop} from '@exadel/esl/src/modules/esl-utils/decorators';
import {ESLMixinElement} from '@exadel/esl/src/modules/esl-mixin-element/core';

import {ESLDemoRouterService} from './router-service';

import type {ESLPanel} from '@exadel/esl/src/modules/esl-panel/core/esl-panel';

export class ESLDemoRouteInterceptor extends ESLMixinElement {
static override is = 'esl-d-route-interceptor';

@attr() public mask: string;

@prop() public submenu: string = '.sidebar-nav-secondary';

public get $panel(): ESLPanel | null {
return this.$host.querySelector(this.submenu);
}

private updateNavActiveState(url: URL): boolean {
return url.pathname?.includes(this.mask);
}

@listen({event: 'esl:pushstate', target: () => ESLDemoRouterService.instance()})
protected _onPushState({detail}: CustomEvent): void {
const {oldURL, newURL} = detail;
this.updateNavActiveState(oldURL) && this.toggleNav(false);
this.updateNavActiveState(newURL) && this.toggleNav(true);
}

protected toggleNav(state: boolean): void {
this.$panel?.toggle(state);
}
}
44 changes: 44 additions & 0 deletions pages/src/route/route-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {listen, prop, attr, boolAttr, memoize} from '@exadel/esl/src/modules/esl-utils/decorators';
import {toAbsoluteUrl} from '@exadel/esl/src/modules/esl-utils/misc/url';
import {ESLMixinElement} from '@exadel/esl/src/modules/esl-mixin-element/core';
import {ESLTraversingQuery} from '@exadel/esl/src/modules/esl-traversing-query/core/esl-traversing-query';

import {ESLDemoRouterService} from './router-service';

export class ESLDemoRouteLink extends ESLMixinElement {
static override is = 'esl-d-route-link';

@attr() public href: string;

@boolAttr() public relatedLink: boolean;

@prop() public activeSelector: string = '::parent';
@prop() public activeCls: string = 'active';

@memoize()
public get $activeEl(): Element | null {
return ESLTraversingQuery.first(this.activeSelector, this.$host);
}

@listen({event: 'esl:pushstate', target: () => ESLDemoRouterService.instance()})
protected _onPushState(): void {
const linkURL = new URL(toAbsoluteUrl(this.href || ''));

const match = this.relatedLink ?
location.pathname === linkURL.pathname :
location.pathname.includes(linkURL.pathname);

this.toggle(match);
}

@listen('click')
protected async _onClick(e: Event): Promise<void> {
e.preventDefault();
if (!this.href || toAbsoluteUrl(this.href) === location.href) return;
ESLDemoRouterService.instance().routeContent(this.href);
}

public toggle(state?: boolean): void {
this.$activeEl?.classList.toggle(this.activeCls, state);
}
}
53 changes: 53 additions & 0 deletions pages/src/route/router-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {memoize} from '@exadel/esl/src/modules/esl-utils/decorators';
import {ESLEventUtils} from '@exadel/esl/src/modules/esl-event-listener/core';
import {SyntheticEventTarget} from '@exadel/esl/src/modules/esl-utils/dom';

export class ESLDemoRouterService extends SyntheticEventTarget {
public contentSelector: string = '#content-routable';

public previousUrl: URL;

@memoize()
public static instance(): ESLDemoRouterService {
return new ESLDemoRouterService();
}

public constructor() {
super();
this.previousUrl = this.copyLocation();
ESLEventUtils.subscribe(this, {target: window, event: 'popstate'}, () => this.routeContent(location.href, false));
}

protected copyLocation(): URL {
return new URL(location.href);
}

public async routeContent(url: string, pushState = true): Promise<void> {
try {
const response = await fetch(url);
const text = await response.text();

pushState && history.pushState(null, '', url);
this.replaceContent(document.body.querySelector(this.contentSelector), this.retrieveTextContent(text));
} catch (error) {
throw new Error(`[ESL] Failed to fetch resource: ${url}`);
}
}

protected retrieveTextContent(text: string): Element | null {
return new DOMParser().parseFromString(text, 'text/html').querySelector(this.contentSelector);
}

protected replaceContent($currentContent: Element | null, $content: Element | null): void {
if (!$content || !$currentContent) {
location.reload();
return;
}

const newURL = this.copyLocation();
this.dispatchEvent(new CustomEvent('esl:pushstate', {detail: {oldURL: this.previousUrl, newURL}}));
this.previousUrl = newURL;

$currentContent.replaceWith($content);
}
}
6 changes: 4 additions & 2 deletions pages/views/_includes/navigation/sidebar-item.njk
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
{% set isPrimaryActive = functions.isActivePath(page.url, collection) %}
{% set items = collections[collection] | released %}
{% set isDraftCollection = collection == 'draft' %}
{% set headingURL = ('/' + collection + '/') | url %}

{% if items.length %}
<div class="sidebar-nav-item-heading {{ 'active' if isPrimaryActive }}">
<a class="sidebar-nav-item-link icon-link" href="{{ ('/' + collection) | url }}"
<a esl-d-route-link class="sidebar-nav-item-link icon-link" href="{{ headingURL }}"
aria-label="{{ title }} home page">
{% include icon %}
</a>
Expand Down Expand Up @@ -38,8 +39,9 @@
{% set isBeta = [].concat(item.data.tags).includes('beta') %}
<li class="sidebar-nav-secondary-item {{ 'active' if isActive }} {{ 'draft' if isDraft }}"
{% if isActive %}aria-selected="true"{% endif %}>
<a class="sidebar-nav-secondary-link"
<a esl-d-route-link class="sidebar-nav-secondary-link"
{% if isActive %}aria-current="page"{% endif %}
{% if item.url === headingURL %}related-link{% endif %}
href="{{ item.url | url }}">
{% if isNew %}
<sup class="badge badge-sup badge-success badge-sidebar">new</sup>
Expand Down
15 changes: 8 additions & 7 deletions pages/views/_includes/navigation/sidebar.njk
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<esl-scrollbar class="sidebar-scrollbar" target="::next"></esl-scrollbar>

<ul class="sidebar-nav-list esl-scrollable-content">
<li class="sidebar-nav-item">
<li esl-d-route-interceptor mask="search" class="sidebar-nav-item">
<div class="sidebar-nav-item-heading {{ 'active' if functions.isActivePath(page.url, 'search') }}">
<a class="sidebar-nav-item-heading-inner"
href="{{ '/search.html' | url }}"
Expand All @@ -23,26 +23,27 @@
</a>
</div>
</li>
<li class="sidebar-nav-item">
<li esl-d-route-interceptor mask="introduction" class="sidebar-nav-item">
{{ navitem ('Introduction', 'introduction', 'static/assets/sidebar/intro.svg') }}
</li>
<li class="sidebar-nav-item">
<li esl-d-route-interceptor mask="core" class="sidebar-nav-item">
{{ navitem ('Core', 'core', 'static/assets/sidebar/utils.svg') }}
</li>
<li class="sidebar-nav-item">
<li esl-d-route-interceptor mask="components" class="sidebar-nav-item">
{{ navitem ('Components', 'components', 'static/assets/sidebar/components.svg') }}
</li>
<li class="sidebar-nav-item">
<li esl-d-route-interceptor mask="examples" class="sidebar-nav-item">
{{ navitem ('Examples', 'examples', 'static/assets/sidebar/examples.svg') }}
</li>

{% set releasedBlogs = collections.blogs | released %}
{% if releasedBlogs.length %}
<li class="sidebar-nav-item">
<li esl-d-route-interceptor mask="blogs" class="sidebar-nav-item">
{{ navitem ('Blogs', 'blogs', 'static/assets/sidebar/blogs.svg') }}
</li>
{% endif %}
{% if env.isDev %}
<li class="sidebar-nav-item">
<li esl-d-route-interceptor mask="draft" class="sidebar-nav-item">
{{ navitem ('Drafts', 'draft', 'static/assets/common/flask.svg') }}
</li>
{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion pages/views/_layouts/content.njk
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{% include 'navigation/sidebar.njk' %}

<main class="esl-scrollable-content">
<div class="content {{ containerCls or 'container' }} {{ 'container-aside' if aside }}">
<div id="content-routable" class="content {{ containerCls or 'container' }} {{ 'container-aside' if aside }}">
{% if title %}
<h1 class="page-title">
{% if collectionIcon %}{% include "static/assets/" + collectionIcon %}{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion pages/views/_layouts/forward.njk
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{% include 'navigation/sidebar.njk' %}

<main class="esl-scrollable-content">
<div class="content system-page">
<div id="content-routable" class="content system-page">
<div class="m-auto text-center">
<h1>🦆 301 🦆</h1>
<p class="h2 text-warning">This isn't here anymore!</p>
Expand Down