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 { Controller } from '@hotwired/stimulus';
|
||||||
import { toggle, delegate } from '@utils';
|
import { toggle, delegate } from '@utils';
|
||||||
|
import Highcharts from 'highcharts';
|
||||||
|
import Chartkick from 'chartkick';
|
||||||
|
|
||||||
export class ChartkickController extends Controller {
|
export class ChartkickController extends Controller {
|
||||||
async connect() {
|
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) =>
|
delegate('click', '[data-toggle-chart]', (event) =>
|
||||||
toggleChart(event as MouseEvent, reflow)
|
toggleChart(event as MouseEvent)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleChart(
|
Chartkick.use(Highcharts);
|
||||||
event: MouseEvent,
|
function reflow(nextChartId?: string) {
|
||||||
reflow: (nextChartId?: string) => void
|
nextChartId && Chartkick.charts[nextChartId]?.getChartObject()?.reflow();
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
function toggleChart(event: MouseEvent) {
|
||||||
const nextSelectorItem = event.target as HTMLButtonElement,
|
const nextSelectorItem = event.target as HTMLButtonElement,
|
||||||
chartClass = nextSelectorItem.dataset.toggleChart,
|
chartClass = nextSelectorItem.dataset.toggleChart,
|
||||||
nextChart = chartClass
|
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 { Application } from '@hotwired/stimulus';
|
||||||
import invariant from 'tiny-invariant';
|
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 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) {
|
export function registerControllers(application: Application) {
|
||||||
for (const [path, mod] of Object.entries(controllers)) {
|
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();
|
const [filename] = path.split('/').reverse();
|
||||||
const name = filename
|
return filename.replace(/_/g, '-').replace(/-controller\.(ts|tsx)$/, '');
|
||||||
.replace(/_/g, '-')
|
}
|
||||||
.replace(/-controller\.(ts|tsx)$/, '');
|
|
||||||
if (name != 'application') {
|
function registerController(
|
||||||
if (mod.default) {
|
name: string,
|
||||||
|
module: Awaited<ReturnType<Loader>>,
|
||||||
|
application: Application
|
||||||
|
) {
|
||||||
|
if (module.default) {
|
||||||
console.debug(`Registered default export for "${name}" controller`);
|
console.debug(`Registered default export for "${name}" controller`);
|
||||||
application.register(name, mod.default);
|
application.register(name, module.default);
|
||||||
} else {
|
} else {
|
||||||
const exports = Object.entries(mod);
|
const exports = Object.entries(module);
|
||||||
invariant(
|
invariant(
|
||||||
exports.length == 1,
|
exports.length == 1,
|
||||||
`Expected a single export but ${exports.length} exports were found for "${name}" controller`
|
`Expected a single export but ${exports.length} exports were found for "${name}" controller`
|
||||||
);
|
);
|
||||||
const [exportName, exportMod] = exports[0];
|
const [exportName, exportModule] = exports[0];
|
||||||
console.debug(
|
console.debug(
|
||||||
`Registered named export "${exportName}" for "${name}" controller`
|
`Registered named export "${exportName}" for "${name}" controller`
|
||||||
);
|
);
|
||||||
application.register(name, exportMod);
|
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