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

Web-components: Custom Elements Manifest v1 support #15138

Merged
merged 14 commits into from Jun 29, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
84 changes: 65 additions & 19 deletions addons/docs/src/frameworks/web-components/custom-elements.ts
Expand Up @@ -4,9 +4,10 @@ import { logger } from '@storybook/client-logger';

interface TagItem {
name: string;
type: string;
type: { text: string };
description: string;
default?: any;
kind?: string;
defaultValue?: any;
}

Expand All @@ -17,15 +18,25 @@ interface Tag {
properties?: TagItem[];
events?: TagItem[];
methods?: TagItem[];
members?: TagItem[];
slots?: TagItem[];
cssProperties?: TagItem[];
cssParts?: TagItem[];
}

interface CustomElements {
tags: Tag[];
modules?: [];
}

interface Module {
declarations?: [];
exports?: [];
}

interface Declaration {
tagName: string;
}
interface Sections {
attributes?: any;
properties?: any;
Expand All @@ -38,25 +49,32 @@ interface Sections {
function mapData(data: TagItem[], category: string) {
return (
data &&
data.reduce((acc, item) => {
const type = category === 'properties' ? { name: item.type } : { name: 'void' };
acc[item.name] = {
name: item.name,
required: false,
description: item.description,
type,
table: {
category,
type: { summary: item.type },
defaultValue: { summary: item.default !== undefined ? item.default : item.defaultValue },
},
};
return acc;
}, {} as ArgTypes)
data
.filter((item) => !!item)
.reduce((acc, item) => {
if (item.kind === 'method') return acc;

const type =
category === 'properties' ? { name: item.type?.text || item.type } : { name: 'void' };
acc[item.name] = {
name: item.name,
required: false,
description: item.description,
type,
table: {
category,
type: { summary: item.type?.text || item.type },
defaultValue: {
summary: item.default !== undefined ? item.default : item.defaultValue,
},
},
};
return acc;
}, {} as ArgTypes)
);
}

const getMetaData = (tagName: string, customElements: CustomElements) => {
const getMetaDataExperimental = (tagName: string, customElements: CustomElements) => {
if (!isValidComponent(tagName) || !isValidMetaData(customElements)) {
return null;
}
Expand All @@ -69,23 +87,51 @@ const getMetaData = (tagName: string, customElements: CustomElements) => {
return metaData;
};

const getMetaDataV1 = (tagName: string, customElements: CustomElements) => {
if (!isValidComponent(tagName) || !isValidMetaData(customElements)) {
return null;
}

let metadata;
customElements?.modules?.forEach((_module: Module) => {
_module?.declarations?.forEach((declaration: Declaration) => {
if (declaration.tagName === tagName) {
metadata = declaration;
}
});
});

if (!metadata) {
logger.warn(`Component not found in custom-elements.json: ${tagName}`);
}
return metadata;
};

export const extractArgTypesFromElements = (tagName: string, customElements: CustomElements) => {
const metaData = getMetaData(tagName, customElements);
return (
metaData && {
...mapData(metaData.attributes, 'attributes'),
...mapData(metaData.members, 'properties'),
...mapData(metaData.properties, 'properties'),
...mapData(metaData.events, 'events'),
...mapData(metaData.methods, 'methods'),
...mapData(metaData.slots, 'slots'),
...mapData(metaData.cssProperties, 'css custom properties'),
...mapData(metaData.cssParts, 'css shadow parts'),
}
);
};

const getMetaData = (tagName: string, manifest: any) => {
if (manifest.version === 'experimental') {
return getMetaDataExperimental(tagName, manifest);
}
return getMetaDataV1(tagName, manifest);
};

export const extractArgTypes = (tagName: string) => {
return extractArgTypesFromElements(tagName, getCustomElements());
const cem = getCustomElements();
return extractArgTypesFromElements(tagName, cem);
};

export const extractComponentDescription = (tagName: string) => {
Expand Down
63 changes: 48 additions & 15 deletions addons/docs/web-components/README.md
Expand Up @@ -12,10 +12,10 @@
- Add to your `.storybook/preview.js`

```js
import { setCustomElements } from '@storybook/web-components';
import { setCustomElementsManifest } from '@storybook/web-components';
import customElements from '../custom-elements.json';

setCustomElements(customElements);
setCustomElementsManifest(customElements);
```

- Add to your story files
Expand All @@ -33,8 +33,12 @@ In order to get [Props tables](..docs/../../docs/props-tables.md) documentation

You can hand write it or better generate it. Depending on the web components sugar you are choosing your milage may vary.

Known analyzers that output `custom-elements.json`:
Known analyzers that output `custom-elements.json` v1.0.0:

- [@custom-elements-manifest/analyzer](https://github.com/open-wc/custom-elements-manifest)
- Supports Vanilla, LitElement, FASTElement, Stencil, Catalyst, Atomico

Known analyzers that output older versions of `custom-elements.json`:
- [web-component-analyzer](https://github.com/runem/web-component-analyzer)
- Supports LitElement, Polymer, Vanilla, (Stencil)
- [stenciljs](https://stenciljs.com/)
Expand All @@ -53,22 +57,51 @@ The file looks something like this:

```json
{
"version": 2,
"tags": [
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"name": "demo-wc-card",
"properties": [
"kind": "javascript-module",
"path": "src/my-element.js",
"declarations": [
{
"name": "header",
"type": "String",
"attribute": "header",
"description": "Shown at the top of the card",
"default": "Your Message"
"kind": "class",
"description": "",
"name": "MyElement",
"members": [
{
"kind": "field",
"name": "disabled"
},
{
"kind": "method",
"name": "fire"
}
],
"events": [
{
"name": "disabled-changed",
"type": {
"text": "Event"
}
}
],
"superclass": {
"name": "HTMLElement"
},
"tagName": "my-element"
}
],
"events": [],
"slots": [],
"cssProperties": []
"exports": [
{
"kind": "custom-element-definition",
"name": "my-element",
"declaration": {
"name": "MyElement",
"module": "src/my-element.js"
}
}
]
}
]
}
Expand Down
13 changes: 11 additions & 2 deletions app/web-components/src/client/customElements.ts
Expand Up @@ -15,7 +15,11 @@ export function isValidMetaData(customElements: any) {
if (!customElements) {
return false;
}
if (customElements.tags && Array.isArray(customElements.tags)) {

if (
(customElements.tags && Array.isArray(customElements.tags)) ||
(customElements.modules && Array.isArray(customElements.modules))
) {
return true;
}
throw new Error(`You need to setup valid meta data in your config.js via setCustomElements().
Expand All @@ -30,7 +34,12 @@ export function setCustomElements(customElements: any) {
window.__STORYBOOK_CUSTOM_ELEMENTS__ = customElements;
}

export function setCustomElementsManifest(customElements: any) {
// @ts-ignore
window.__STORYBOOK_CUSTOM_ELEMENTS_MANIFEST__ = customElements;
}

export function getCustomElements() {
// @ts-ignore
return window.__STORYBOOK_CUSTOM_ELEMENTS__;
return window.__STORYBOOK_CUSTOM_ELEMENTS__ || window.__STORYBOOK_CUSTOM_ELEMENTS_MANIFEST__;
}
1 change: 1 addition & 0 deletions app/web-components/src/client/index.ts
Expand Up @@ -16,6 +16,7 @@ export {
export {
getCustomElements,
setCustomElements,
setCustomElementsManifest,
isValidComponent,
isValidMetaData,
} from './customElements';
Expand Down
6 changes: 3 additions & 3 deletions docs/essentials/auto-generated-controls/web-components.mdx
Expand Up @@ -12,10 +12,10 @@ Storybook uses this to auto-generate the `ArgTypes` for your component using you
You'll need to register that in `.storybook/preview.js`:

```js
import { setCustomElements } from '@storybook/web-components';
import { setCustomElementsManifest } from '@storybook/web-components';
import customElements from '../custom-elements.json';

setCustomElements(customElements);
setCustomElementsManifest(customElements);
```

You can generate a `custom-elements.json` using either [web-component-analyzer](https://github.com/runem/web-component-analyzer) or [stenciljs](https://stenciljs.com/) (if you're using Stencil).
You can generate a `custom-elements.json` using [@custom-elements-manifest/analyzer](https://github.com/open-wc/custom-elements-manifest). If you're using the pre-v1.0.0 version of `custom-elements.json` you can use either [web-component-analyzer](https://github.com/runem/web-component-analyzer) or [stenciljs](https://stenciljs.com/) (if you're using Stencil).
10 changes: 7 additions & 3 deletions examples/web-components-kitchen-sink/.storybook/preview.js
@@ -1,8 +1,12 @@
import { setCustomElements } from '@storybook/web-components';
import { setCustomElementsManifest } from '@storybook/web-components';

import customElements from '../custom-elements.json';
import customElementsManifest from '../custom-elements.json';

setCustomElements(customElements);
setCustomElementsManifest(customElementsManifest);

// TODO: Remove support of 0.x CustomElementManifest format in 7.0
// import customElements from '../custom-elements-experimental.json';
// setCustomElements(customElements);

export const parameters = {
a11y: {
Expand Down
@@ -0,0 +1,95 @@
{
"version": "experimental",
"tags": [
{
"name": "demo-wc-card",
"path": "./demo-wc-card.js",
"description": "This is a container looking like a card with a back and front side you can switch",
"attributes": [
{
"name": "back-side",
"description": "Indicates that the back of the card is shown",
"type": "boolean",
"default": "false"
},
{
"name": "header",
"description": "Header message",
"type": "string",
"default": "\"Your Message\""
},
{
"name": "rows",
"description": "Data rows",
"type": "object",
"default": "[]"
}
],
"properties": [
{
"name": "backSide",
"attribute": "back-side",
"description": "Indicates that the back of the card is shown",
"type": "boolean",
"default": "false"
},
{
"name": "header",
"attribute": "header",
"description": "Header message",
"type": "string",
"default": "\"Your Message\""
},
{
"name": "rows",
"attribute": "rows",
"description": "Data rows",
"type": "object",
"default": "[]"
}
],
"events": [
{
"name": "side-changed",
"description": "Fires whenever it switches between front/back"
}
],
"methods": [
{
"name": "testMethod",
"description": "Some web component frameworks like Stencil generate extra docs for methods. These are also displayed in the ArgsTable."
}
],
"slots": [
{
"name": "",
"description": "This is an unnamed slot (the default slot)"
}
],
"cssProperties": [
{
"name": "--demo-wc-card-header-font-size",
"description": "Header font size"
},
{
"name": "--demo-wc-card-front-color",
"description": "Font color for front"
},
{
"name": "--demo-wc-card-back-color",
"description": "Font color for back"
}
],
"cssParts": [
{
"name": "front",
"description": "Front of the card"
},
{
"name": "back",
"description": "Back of the card"
}
]
}
]
}