Last active
May 19, 2025 03:06
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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