Skip to content

Commit

Permalink
feat: lazy-loaded routes
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinderoubaix committed Dec 5, 2023
1 parent 7645f10 commit 6d4993b
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 113 deletions.
17 changes: 13 additions & 4 deletions angular/demo/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type {Routes} from '@angular/router';
import {links, LinksComponent} from './links.component';
import {links} from './links.component';

const context = import.meta.webpackContext?.('./', {
regExp: /[^/]*\.route\.ts$/,
mode: 'lazy',
});

const componentRegExp = /samples\/([^/]*)\/([^/]*).route\.ts$/;
Expand All @@ -18,15 +19,23 @@ function replacePattern(webpackContext: __WebpackModuleApi.RequireContext) {
return directComponents;
}
const components = replacePattern(context!);

export const ROUTES: Routes = [
{
path: '',
pathMatch: 'full',
component: LinksComponent,
loadComponent: () => import('./links.component').then((c) => c.LinksComponent),
providers: [{provide: links, useValue: Object.keys(components)}],
},
...Object.entries(components).map(([path, component]) => {
return {path, loadComponent: async () => (await context!(component)).default};
return {
path,
loadComponent: async () => {
const comp = (await context!(component)).default;
if (window.parent) {
window.parent.postMessage({type: 'sampleload'});
}
return comp;
},
};
}),
];
60 changes: 4 additions & 56 deletions demo/src/lib/layout/Sample.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
import {tooltip} from '$lib/tooltip/tooltip';
import openLink from 'bootstrap-icons/icons/box-arrow-up-right.svg?raw';
import codeSvg from 'bootstrap-icons/icons/code.svg?raw';
import {onDestroy} from 'svelte';
import stackblitz from '../../resources/icons/stackblitz.svg?raw';
import type {Frameworks} from '../stores';
import {pathToRoot$, selectedFramework$} from '../stores';
import Lazy from './Lazy.svelte';
import Svg from './Svg.svelte';
import type {SampleInfo} from './sample';
import {createIframeHandler} from '$lib/layout/iframe';
/**
* iFrame title
Expand Down Expand Up @@ -74,71 +74,19 @@
$: sampleBaseUrl = `${$pathToRoot$}${$selectedFramework$}/samples/#/${path}`;
$: sampleUrl = sampleBaseUrl + (urlParameters ? `#${JSON.stringify(urlParameters)}` : '');
let iframeLoaded = true;
let resizeObserver: ResizeObserver;
let iframeHeight = 0;
const setupObserver = (iframe: HTMLIFrameElement) => {
if (!resizeObserver) {
resizeObserver = new ResizeObserver((entries) => {
if (entries.length === 1) {
iframeHeight = entries[0].contentRect.height || iframeHeight;
}
});
}
resizeObserver.disconnect();
const root = iframe.contentDocument?.getElementById('root');
if (root) {
resizeObserver.observe(root);
}
};
const updateLoaded = (iframe: HTMLIFrameElement, baseSrc: string) => {
const update = (baseSrc: string) => {
if (!iframe.contentWindow?.location?.href?.startsWith(baseSrc)) {
iframeLoaded = false;
}
};
update(baseSrc);
// the onLoad event is never called when loading a tab that was discarded through Chrome Tab Discarding
// so we use the first execution of this directive to check if the iframe is loaded and if we can setup a resize observer
if (iframe.contentDocument?.getElementById('root')) {
setupObserver(iframe);
}
return {
update,
};
};
function onLoad(event: Event) {
iframeLoaded = true;
if (event.target instanceof HTMLIFrameElement) {
setupObserver(event.target);
}
}
onDestroy(() => {
resizeObserver?.disconnect();
});
const {showSpinner$, handler} = createIframeHandler(height, !noresize);
</script>

<div class="mb-4 py-2 px-0 px-sm-3">
<div class="position-relative border">
{#if !iframeLoaded}
{#if $showSpinner$}
<div class="position-absolute top-50 start-50 translate-middle iframeSpinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
{/if}
<iframe
class="demo-sample d-block"
use:iframeSrc={sampleUrl}
{title}
height={noresize ? height : iframeHeight || height}
use:updateLoaded={sampleBaseUrl}
on:load={onLoad}
loading="lazy"
/>
<iframe class="demo-sample d-block" use:iframeSrc={sampleUrl} {title} use:handler={sampleBaseUrl} loading="lazy" />
</div>
<div class="btn-toolbar border border-top-0 d-flex align-items-center p-1" role="toolbar" aria-label="Toolbar with button groups">
{#if showCodeButton}
Expand Down
96 changes: 96 additions & 0 deletions demo/src/lib/layout/iframe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type {UnsubscribeFunction, UnsubscribeObject} from '@amadeus-it-group/tansu';
import {asReadable, writable} from '@amadeus-it-group/tansu';

/**
* Creates an iframe handler, allowing to enable dynamic resizing and detect if the parent should display a spinner.
* The returned spinner store includes a debounce for better user experience.
*
* In order to detect when the page is fully loaded, we listen to message events.
*
* @param defaultHeight the default height in pixels of the iframe
* @param resize enable dynamic resizing
* @param messageType the type of the message event we listen to
* @param spinnerDebounce the debounce in milliseconds before the spinner store is set to true
* @returns the handler and a spinner store
*/
export function createIframeHandler(defaultHeight: number, resize = true, messageType = 'sampleload', spinnerDebounce = 300) {
const _iframeLoaded$ = writable(true);
const _showSpinner$ = writable(false);
const _height$ = writable(0);

let spinnerTimer: any;
let resizeObserver: ResizeObserver | undefined;
let heightSubscription: (UnsubscribeFunction & UnsubscribeObject) | undefined;

const setupObserver = (iframe: HTMLIFrameElement) => {
if (!resizeObserver) {
resizeObserver = new ResizeObserver((entries) => {
if (entries.length === 1) {
if (_iframeLoaded$()) {
_height$.set(Math.ceil(entries[0].contentRect.height || _height$()));
}
}
});
}
resizeObserver.disconnect();
const root = iframe.contentDocument?.getElementById('root');
if (root) {
resizeObserver.observe(root);
}
};
const onLoad = (event: Event) => {
if (event.target instanceof HTMLIFrameElement) {
setupObserver(event.target);
}
};

return {
showSpinner$: asReadable(_showSpinner$),
handler: (iframe: HTMLIFrameElement, baseSrc: string) => {
iframe.onload = onLoad;
if (iframe.contentDocument?.getElementById('root')) {
setupObserver(iframe);
}
if (resize) {
heightSubscription = _height$.subscribe((height) => (iframe.height = (height || defaultHeight) + 'px'));
} else {
iframe.height = defaultHeight + 'px';
}

const update = (baseSrc: string) => {
if (!iframe.contentWindow?.location?.href?.startsWith(baseSrc)) {
_iframeLoaded$.set(false);
if (spinnerTimer) {
clearTimeout(spinnerTimer);
}
spinnerTimer = setTimeout(() => {
_showSpinner$.set(true);
spinnerTimer = undefined;
}, spinnerDebounce);
}
};
update(baseSrc);

const sampleLoad = (e: Event) => {
if (e instanceof MessageEvent && e.data.type === messageType && e.source === iframe.contentWindow) {
if (spinnerTimer) {
clearTimeout(spinnerTimer);
spinnerTimer = undefined;
}
_iframeLoaded$.set(true);
_showSpinner$.set(false);
}
};
window.addEventListener('message', sampleLoad, false);

return {
update,
destroy: () => {
window.removeEventListener('message', sampleLoad);
resizeObserver?.disconnect();
heightSubscription?.unsubscribe();
},
};
},
};
}
5 changes: 4 additions & 1 deletion e2e/accordion/accordion.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test.describe.parallel(`Accordion tests`, () => {
test(`Default accordion behavior`, async ({page}) => {
await page.goto('#/accordion/default');
const accordionPO = new AccordionPO(page, 0);
await accordionPO.locatorRoot.waitFor();

const itemsIds = await Promise.all((await accordionPO.locatorAccordionItems.all()).map((item) => item.getAttribute('id')));
const expectedState: State = {
Expand Down Expand Up @@ -70,7 +71,8 @@ test.describe.parallel(`Accordion tests`, () => {
const accordionDemoPO = new AccordionTogglePanels(page);
await page.goto('#/accordion/togglepanels');
const accordionPO = new AccordionPO(page, 0);
await accordionDemoPO.locatorRoot.waitFor();
await accordionPO.locatorRoot.waitFor();

const itemsIds = await Promise.all((await accordionPO.locatorAccordionItems.all()).map((item) => item.getAttribute('id')));
const expectedState: State = {
items: [
Expand Down Expand Up @@ -120,6 +122,7 @@ test.describe.parallel(`Accordion tests`, () => {
test(`Playground accordion behavior no destroy on hide`, async ({page}) => {
await page.goto('#/accordion/playground#{"config":{"itemDestroyOnHide":false}}');
const accordionPO = new AccordionPO(page, 0);
await accordionPO.locatorRoot.waitFor();

const itemsIds = await Promise.all((await accordionPO.locatorAccordionItems.all()).map((item) => item.getAttribute('id')));
const expectedState: State = {
Expand Down
2 changes: 1 addition & 1 deletion e2e/samplesMarkup.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test.describe.parallel(`Samples markup consistency check`, () => {

for (const route of allRoutes) {
test(`${route} should have a consistent markup`, async ({page}) => {
await page.goto(`#/${route}${routesExtraHash[route] ?? ''}`);
await page.goto(`#/${route}${routesExtraHash[route] ?? ''}`, {waitUntil: 'networkidle'});
await expect.poll(async () => (await page.locator('#root').innerHTML()).trim().length).toBeGreaterThan(0);
await routesExtraAction[route]?.(page);
await page.waitForSelector('.fade', {state: 'detached'}); // wait for fade transitions to be finished
Expand Down
28 changes: 10 additions & 18 deletions e2e/slider/slider.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,10 @@ const defaultExpectedHandleStateVertical: {[key: string]: string | null}[] = [
test.describe(`Slider tests`, () => {
test.describe(`Basic slider`, () => {
test(`should snap the handle to correct tick on the slider click event`, async ({page}) => {
const sliderDemoPO = new SliderDemoPO(page);
const sliderPO = new SliderPO(page, 0);

await page.goto('#/slider/default');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const expectedState = {...defaultExpectedState};
expectedState.value = '43';
Expand All @@ -83,11 +82,10 @@ test.describe(`Slider tests`, () => {
});

test(`should snap the handle on mouse drag event`, async ({page}) => {
const sliderDemoPO = new SliderDemoPO(page);
const sliderPO = new SliderPO(page, 0);

await page.goto('#/slider/default');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const expectedState = {...defaultExpectedState};
expectedState.value = '88';
Expand All @@ -106,11 +104,10 @@ test.describe(`Slider tests`, () => {
});

test(`should move handle on key strokes`, async ({page}) => {
const sliderDemoPO = new SliderDemoPO(page);
const sliderPO = new SliderPO(page, 0);

await page.goto('#/slider/default');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const expectedState = {...defaultExpectedState};
expectedState.value = '0';
Expand Down Expand Up @@ -148,7 +145,7 @@ test.describe(`Slider tests`, () => {
const sliderPO = new SliderPO(page, 2);

await page.goto('#/slider/default');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const sliderHandleLocator = sliderPO.locatorHandle;

Expand All @@ -164,7 +161,7 @@ test.describe(`Slider tests`, () => {
const sliderPO = new SliderPO(page, 2);

await page.goto('#/slider/default');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

await sliderDemoPO.disabledToggle.click();

Expand Down Expand Up @@ -196,11 +193,10 @@ test.describe(`Slider tests`, () => {

test.describe(`Range slider`, () => {
test(`should move the handle to correct tick on the slider click event`, async ({page}) => {
const sliderDemoPO = new SliderDemoPO(page);
const sliderPO = new SliderPO(page, 1);

await page.goto('#/slider/range');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const expectedState = {...defaultExpectedHandleState[1]};
expectedState.value = '88';
Expand All @@ -217,11 +213,10 @@ test.describe(`Slider tests`, () => {
});

test(`should interchange the handles on mouse drag event`, async ({page}) => {
const sliderDemoPO = new SliderDemoPO(page);
const sliderPO = new SliderPO(page, 1);

await page.goto('#/slider/range');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const expectedState = {...defaultExpectedHandleState};
expectedState[0].value = '40';
Expand All @@ -246,11 +241,10 @@ test.describe(`Slider tests`, () => {
});

test(`should move handle on key strokes`, async ({page}) => {
const sliderDemoPO = new SliderDemoPO(page);
const sliderPO = new SliderPO(page, 1);

await page.goto('#/slider/range');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const expectedState = {...defaultExpectedHandleState};
expectedState[0].value = '0';
Expand Down Expand Up @@ -297,11 +291,10 @@ test.describe(`Slider tests`, () => {

test.describe(`Vertical slider`, () => {
test(`should move the handle to correct tick on the slider click event`, async ({page}) => {
const sliderDemoPO = new SliderDemoPO(page);
const sliderPO = new SliderPO(page, 0);

await page.goto('#/slider/vertical');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const expectedState = {...defaultExpectedHandleStateVertical[1]};
expectedState.value = '80';
Expand All @@ -318,11 +311,10 @@ test.describe(`Slider tests`, () => {
});

test(`should move handle on key strokes`, async ({page}) => {
const sliderDemoPO = new SliderDemoPO(page);
const sliderPO = new SliderPO(page, 0);

await page.goto('#/slider/vertical');
await sliderDemoPO.locatorRoot.waitFor();
await sliderPO.locatorRoot.waitFor();

const expectedState = {...defaultExpectedHandleStateVertical};
expectedState[0].value = '0';
Expand Down
Loading

0 comments on commit 6d4993b

Please sign in to comment.