Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

lazy loaded routes for samples #79

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => {
Copy link
Member

@divdavem divdavem Dec 5, 2023

Choose a reason for hiding this comment

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

As discussed, this should be extracted (as part of another PR) in a reactive utility in the core (similar to the intersection utility) that could also be used in the slider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tracked in #291

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
Loading