chore(stimulus): stimulus controllers can be lazy loaded
This commit is contained in:
parent
34d4bb5e93
commit
f5e75d5e8e
4 changed files with 122 additions and 40 deletions
|
@ -1,25 +1,22 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { toggle, delegate } from '@utils';
|
||||
import Highcharts from 'highcharts';
|
||||
import Chartkick from 'chartkick';
|
||||
|
||||
export class ChartkickController extends Controller {
|
||||
async connect() {
|
||||
const Highcharts = await import('highcharts');
|
||||
const Chartkick = await import('chartkick');
|
||||
|
||||
Chartkick.use(Highcharts);
|
||||
const reflow = (nextChartId?: string) =>
|
||||
nextChartId && Chartkick.charts[nextChartId]?.getChartObject()?.reflow();
|
||||
|
||||
delegate('click', '[data-toggle-chart]', (event) =>
|
||||
toggleChart(event as MouseEvent, reflow)
|
||||
toggleChart(event as MouseEvent)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleChart(
|
||||
event: MouseEvent,
|
||||
reflow: (nextChartId?: string) => void
|
||||
) {
|
||||
Chartkick.use(Highcharts);
|
||||
function reflow(nextChartId?: string) {
|
||||
nextChartId && Chartkick.charts[nextChartId]?.getChartObject()?.reflow();
|
||||
}
|
||||
|
||||
function toggleChart(event: MouseEvent) {
|
||||
const nextSelectorItem = event.target as HTMLButtonElement,
|
||||
chartClass = nextSelectorItem.dataset.toggleChart,
|
||||
nextChart = chartClass
|
5
app/javascript/controllers/lazy/trix_controller.ts
Normal file
5
app/javascript/controllers/lazy/trix_controller.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import 'trix';
|
||||
import '@rails/actiontext';
|
||||
|
||||
export class TrixController extends Controller {}
|
|
@ -1,8 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export class TrixController extends Controller {
|
||||
connect() {
|
||||
import('trix');
|
||||
import('@rails/actiontext');
|
||||
}
|
||||
}
|
|
@ -1,30 +1,118 @@
|
|||
import { Application } from '@hotwired/stimulus';
|
||||
import invariant from 'tiny-invariant';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Loader = () => Promise<{ [key: string]: any }>;
|
||||
|
||||
const controllerAttribute = 'data-controller';
|
||||
const controllers = import.meta.globEager('../controllers/*.{ts,tsx}');
|
||||
const lazyControllers = import.meta.glob('../controllers/lazy/*.{ts,tsx}');
|
||||
const controllerLoaders = new Map<string, Loader>();
|
||||
|
||||
export function registerControllers(application: Application) {
|
||||
for (const [path, mod] of Object.entries(controllers)) {
|
||||
const [filename] = path.split('/').reverse();
|
||||
const name = filename
|
||||
.replace(/_/g, '-')
|
||||
.replace(/-controller\.(ts|tsx)$/, '');
|
||||
if (name != 'application') {
|
||||
if (mod.default) {
|
||||
console.debug(`Registered default export for "${name}" controller`);
|
||||
application.register(name, mod.default);
|
||||
} else {
|
||||
const exports = Object.entries(mod);
|
||||
invariant(
|
||||
exports.length == 1,
|
||||
`Expected a single export but ${exports.length} exports were found for "${name}" controller`
|
||||
);
|
||||
const [exportName, exportMod] = exports[0];
|
||||
console.debug(
|
||||
`Registered named export "${exportName}" for "${name}" controller`
|
||||
);
|
||||
application.register(name, exportMod);
|
||||
for (const [path, module] of Object.entries(controllers)) {
|
||||
registerController(controllerName(path), module, application);
|
||||
}
|
||||
for (const [path, loader] of Object.entries(lazyControllers)) {
|
||||
registerControllerLoader(controllerName(path), loader);
|
||||
}
|
||||
lazyLoadExistingControllers(application);
|
||||
lazyLoadNewControllers(application);
|
||||
}
|
||||
|
||||
function lazyLoadExistingControllers(
|
||||
application: Application,
|
||||
element?: Element
|
||||
) {
|
||||
queryControllerNamesWithin(element ?? application.element).forEach(
|
||||
(controllerName) => loadController(controllerName, application)
|
||||
);
|
||||
}
|
||||
|
||||
function lazyLoadNewControllers(application: Application) {
|
||||
new MutationObserver((mutationsList) => {
|
||||
for (const { attributeName, target, type } of mutationsList) {
|
||||
const element = target as Element;
|
||||
switch (type) {
|
||||
case 'attributes': {
|
||||
if (
|
||||
attributeName == controllerAttribute &&
|
||||
element.getAttribute(controllerAttribute)
|
||||
) {
|
||||
extractControllerNamesFrom(element).forEach((controllerName) =>
|
||||
loadController(controllerName, application)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'childList':
|
||||
lazyLoadExistingControllers(application, element);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).observe(application.element, {
|
||||
attributeFilter: [controllerAttribute],
|
||||
subtree: true,
|
||||
childList: true
|
||||
});
|
||||
}
|
||||
|
||||
function queryControllerNamesWithin(element: Element) {
|
||||
return Array.from(
|
||||
element.querySelectorAll(`[${controllerAttribute}]`)
|
||||
).flatMap(extractControllerNamesFrom);
|
||||
}
|
||||
|
||||
function extractControllerNamesFrom(element: Element) {
|
||||
return (
|
||||
element
|
||||
.getAttribute(controllerAttribute)
|
||||
?.split(/\s+/)
|
||||
.filter((content) => content.length) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function loadController(name: string, application: Application) {
|
||||
const loader = controllerLoaders.get(name);
|
||||
controllerLoaders.delete(name);
|
||||
if (loader) {
|
||||
loader()
|
||||
.then((module) => registerController(name, module, application))
|
||||
.catch((error) =>
|
||||
console.error(`Failed to autoload controller: ${name}`, error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function controllerName(path: string) {
|
||||
const [filename] = path.split('/').reverse();
|
||||
return filename.replace(/_/g, '-').replace(/-controller\.(ts|tsx)$/, '');
|
||||
}
|
||||
|
||||
function registerController(
|
||||
name: string,
|
||||
module: Awaited<ReturnType<Loader>>,
|
||||
application: Application
|
||||
) {
|
||||
if (module.default) {
|
||||
console.debug(`Registered default export for "${name}" controller`);
|
||||
application.register(name, module.default);
|
||||
} else {
|
||||
const exports = Object.entries(module);
|
||||
invariant(
|
||||
exports.length == 1,
|
||||
`Expected a single export but ${exports.length} exports were found for "${name}" controller`
|
||||
);
|
||||
const [exportName, exportModule] = exports[0];
|
||||
console.debug(
|
||||
`Registered named export "${exportName}" for "${name}" controller`
|
||||
);
|
||||
application.register(name, exportModule);
|
||||
}
|
||||
}
|
||||
|
||||
function registerControllerLoader(name: string, loader: Loader) {
|
||||
console.debug(`Registered loader for "${name}" controller`);
|
||||
controllerLoaders.set(name, loader);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue