Skip to content

Instantly share code, notes, and snippets.

@ar-nelson
Last active May 19, 2025 03:06
Show Gist options
  • Save ar-nelson/e8af65532cf1330f7a03e0ebc6408143 to your computer and use it in GitHub Desktop.
Save ar-nelson/e8af65532cf1330f7a03e0ebc6408143 to your computer and use it in GitHub Desktop.
Mithril support in modern (v8.x) Storybook, using Web Components as a wrapper
import m from "mithril";
import { Meta, StoryObj as _StoryObj } from "@storybook/web-components";
/*
* Modern Storybook v8 doesn't support Mithril anymore, so this is a quick shim to cram Mithril
* components into Web Component wrappers and get all of the types right.
*
* Instead of importing Meta and StoryObj from @storybook/web-components and defining them directly,
* just use defineMetaMithril and StoryObj from this file.
*
* A FEW WARNINGS:
*
* 1. Make sure you define all of the attrs you might use, including "children" (if applicable), in
* the argTypes object of defineMetaMithril. Any attrs not defined here will not be defined on
* the web component, and will throw an exception if assigned.
*
* 2. You can't just `export default defineMetaMithril({ ... })`! Storybook expects the default
* export of stories to be an object literal. Luckily, this can be fixed with a spread operator:
*
* export default { ...defineMetaMithril({ ... }), ... }
*/
type AttrsAndChildren<Attrs extends object> = Attrs & {
children?: m.Children;
};
export type WrappedComponent<Attrs extends object> = {
new (props: AttrsAndChildren<Attrs>): HTMLElement;
};
export type StoryObj<Attrs extends object> = _StoryObj<AttrsAndChildren<Attrs>>;
export function defineMetaMithril<Attrs extends object>({
component,
argTypes,
...rest
}: Omit<Meta<WrappedComponent<Attrs>>, "component"> & {
component: m.ComponentTypes<Attrs>;
argTypes: Record<keyof AttrsAndChildren<Attrs>, any>;
}): Meta<WrappedComponent<Attrs>> {
return {
...rest,
component: wrapMithrilComponent(component, argTypes),
argTypes,
};
}
function wrapMithrilComponent<Attrs extends object>(
Component: m.ComponentTypes<Attrs>,
propNames: Record<keyof AttrsAndChildren<Attrs>, any>,
): string {
// Make sure outside styles are available inside the shadow DOM
const globalStyleSheets = Array.from(document.styleSheets)
.map((x) => {
const sheet = new CSSStyleSheet();
const css = Array.from(x.cssRules).map((rule) => rule.cssText).join(" ");
sheet.replaceSync(css);
return sheet;
});
class MithrilElement extends HTMLElement {
private readonly div: HTMLDivElement;
constructor(public props: Partial<AttrsAndChildren<Attrs>> = {}) {
super();
const shadow = this.attachShadow({ mode: "open" });
this.div = document.createElement("div");
shadow.appendChild(this.div);
for (let key of Object.keys(propNames)) {
Object.defineProperty(this, key, {
get: () => this.props[key as keyof AttrsAndChildren<Attrs>],
set: (value) => {
this.props[key as keyof AttrsAndChildren<Attrs>] = value;
},
});
}
shadow.adoptedStyleSheets.push(...globalStyleSheets);
}
connectedCallback() {
m.mount(this.div, {
view: () => {
if ("children" in this.props) {
const { children, ...rest } = this.props;
return m(Component, rest as Attrs, children);
} else {
return m(Component, this.props as Attrs);
}
},
});
}
}
const str = `element-${crypto.randomUUID()}`;
customElements.define(str, MithrilElement);
return str;
}
/*
// Example usage, assuming a button component where type ButtonAttrs = { title?: string }:
import { defineMetaMithril, StoryObj } from "./storybook-mithril";
import { Button, ButtonAttrs } from "./Button";
const meta = {
...defineMetaMithril({
component: Button,
argTypes: {
children: { control: "text" },
title: { control: "text" },
},
}),
title: "Button",
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<ButtonAttrs>;
export const Primary: Story = {
args: {
children: "Click me",
},
};
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment