feat: add demoing storybook

This commit is contained in:
Thomas Allmer
2019-03-02 20:29:09 +01:00
parent 88ee4f448c
commit 76878c8944
16 changed files with 653 additions and 1 deletions

View File

@@ -1 +1 @@
../../packages/storybook/README.md
../../packages/demoing-storybook/README.md

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Open Web Components
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,173 @@
# Demoing via storybook
[//]: # (AUTO INSERT HEADER PREPUBLISH)
For demoing and showcasing different states of your Web Component, we recommend using [storybook](https://storybook.js.org/).
::: tip
This is part of the default [open-wc](https://open-wc.org/) recommendation
:::
# Features
- Create API documentation/playground
- Show documentation (from markdown) with your element
- Show your source code to copy/paste
- Helper to setup transpilation for dependencies
- Setup that works down to IE11
## Setup
```bash
npm init open-wc demoing
```
### Manual
- `yarn add @open-wc/demoing-storybook --dev`
- Copy at minimum the [.storybook](https://github.com/daKmoR/create-open-wc/tree/master/src/generators/demoing-storybook/templates/static/.storybook) folder to `.storybook`
- If you want to bring along the examples, you may also copy the `stories` folder.
- Add the following scripts to your package.json
```js
"scripts": {
"storybook": "start-storybook -p 9001",
"storybook:build": "build-storybook -o _site",
"site:build": "npm run storybook:build",
},
```
## Usage
Create stories within the `stories` folder.
```bash
npm run storybook
```
### Create a Story
Create an `*.story.js` (for example `index.stories.js`) file within the `stories` folder.
```js
import {
storiesOf,
html,
} from '@open-wc/demoing-storybook';
storiesOf('Demo|Example Element', module)
.add(
'Alternative Header',
() => html`
<my-el .header=${'Something else'}></my-el>
`,
);
```
### Create API documentation/playground
If you have a single element then we can fully generate a usable api documentation/playground.
The data will be read from the elements properties.
So this should probably be your default story to gives users documentation, api and playground all in one.
For additional edge cases you should add more stories.
Note: you need to provide the Class (not a node or a template)
```js
import { storiesOf, withKnobs, withClassPropertiesKnobs } from '@open-wc/demoing-storybook';
storiesOf('Demo|Example Element', module)
.addDecorator(withKnobs)
.add(
'Documentation',
() => withClassPropertiesKnobs(MyEl),
{ notes: { markdown: notes } },
)
```
So with a configuration like this you will get this auto generated.
<img src="https://raw.githubusercontent.com/open-wc/open-wc/master/packages/demoing-storybook/dev_assets/storybook.gif" alt="storybook demo animation" />
For most types this works fine out of the box but if want to provide better knobs you can do so by overriding.
```js
() => withClassPropertiesKnobs(MyEl, {
overrides: el => [
// show a color selector
{ key: 'headerColor', fn: () => color('headerColor', el.headerColor, 'Element') },
// show dropdown
{
key: 'type',
fn: () => select('type', ['small', 'medium', 'large'], el.type, 'Element'),
},
// show textarea where you can input json
{ key: 'complexItems', fn: () => object('complexItems', el.complexItems, 'Inherited') },
// move property to a different group
{ key: 'locked', group: 'Security' },
],
}),
```
By default it will create one simple node from the given Class.
However for a nicer demo it may be needed to set properties or add more lightdom.
You can do so by providing a template.
```js
() => withClassPropertiesKnobs(MyEl, {
template: html`
<my-el .header=${'override it'}><p>foo</p></my-el>
`,
}),
```
### Show documentation (from markdown) with your element
The documentation will be visible when clicking on "notes" at the top menu.
```js
import notes from '../README.md';
.add(
'Documentation',
() => html`
<my-el></my-el>
`,
{ notes: { markdown: notes } },
)
```
### Helper to setup transpilation for dependencies
Whenever you add a dependency that is written in ES modules you will need to enable transpilation for it.
Open your configuration and add the new package to the array.
Below you see the default settings.
```js
const defaultConfig = require('@open-wc/demoing-storybook/default-storybook-webpack-config.js');
module.exports = ({ config }) => {
return defaultConfig({ config, transpilePackages: ['lit-html', 'lit-element', '@open-wc'] });
};
```
### Additional middleware config like a proxy
If you need additional configuration for the storybook dev server you can provide them via a config file `.storybook/middleware.js`.
```js
// example for a proxy middleware to use an api for fetching data to display
const proxy = require('http-proxy-middleware');
module.exports = function(app) {
app.use(proxy('/api/', {
target: 'http://localhost:9010/',
}));
};
```
<script>
export default {
mounted() {
const editLink = document.querySelector('.edit-link a');
if (editLink) {
const url = editLink.href;
editLink.href = url.substr(0, url.indexOf('/master/')) + '/master/packages/building-storybook/README.md';
}
}
}
</script>

View File

@@ -0,0 +1,33 @@
module.exports = ({ config, transpilePackages = ['lit-html', 'lit-element', '@open-wc'] }) => {
config.module.rules.push({
test: [/\.stories\.js$/, /index\.js$/],
loaders: [require.resolve('@storybook/addon-storysource/loader')],
enforce: 'pre',
});
// this is a separate config for only those packages
// the main storybook will use the .babelrc which is needed so storybook itself works in IE
config.module.rules.push({
test: new RegExp(`node_modules(\\/|\\\\)(${transpilePackages.join('|')})(.*)\\.js$`),
use: {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-object-rest-spread',
],
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'entry',
},
],
],
babelrc: false,
},
},
});
return config;
};

View File

@@ -0,0 +1,14 @@
{
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-object-rest-spread"
],
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry"
}
]
]
}

View File

@@ -0,0 +1,6 @@
import '@storybook/addon-storysource/register';
import '@storybook/addon-actions/register';
import '@storybook/addon-notes/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-links/register';
import '@storybook/addon-viewport/register';

View File

@@ -0,0 +1,9 @@
import { configure } from '@storybook/polymer';
import '@storybook/addon-console';
const req = require.context('../stories', true, /\.stories\.js$/);
function loadStories() {
req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);

View File

@@ -0,0 +1,5 @@
const defaultConfig = require('../../default-storybook-webpack-config.js');
module.exports = ({ config }) => {
return defaultConfig({ config, transpilePackages: ['lit-html', 'lit-element', '@open-wc'] });
};

View File

@@ -0,0 +1,38 @@
# Example Element
<style>
iframe {
width: 100%;
border: 1px solid rgba(0,0,0,.1);
min-height: 300px;
}
</style>
This example element display some information about...
## Features:
- bigger then you would expect
- still fast
- awesome to learn something
## How to use
### Installation
```bash
yarn add @foo/my-el
```
```js
import '@foo/my-el/my-el.js';
```
### Example:
<iframe src="iframe.html?id=demo-example-element--documentation"></iframe>
```html
<my-el></my-el>
```
## API
Please look at "Canvas" and select "Knobs" in the addon bar (usually to the right) to see all available properties.

View File

@@ -0,0 +1,124 @@
/* eslint-disable no-console */
// eslint-disable-next-line import/no-extraneous-dependencies
import { LitElement, html, css } from 'lit-element';
class Base extends LitElement {
static get properties() {
return {
complexOption: { type: Object },
complexItems: { type: Array },
};
}
constructor() {
super();
this.complexOption = { foo: 'is foo', bar: 'is bar' };
this.complexItems = [
{ name: 'foo', age: 1000, message: 'knows all' },
{ name: 'bar', age: 1, message: 'is new here' },
];
}
}
export class MyEl extends Base {
static get styles() {
return css`
p {
margin: 0;
}
h2 {
color: var(--my-el-header-color);
}
`;
}
static get properties() {
return {
header: { type: String },
headerColor: { type: String },
locked: { type: Boolean },
items: { type: Array },
type: { type: String },
time: { type: Date },
age: { type: Number },
};
}
constructor() {
super();
this.header = 'Default Header';
this.headerColor = '#ff0000';
this.disabled = false;
this.items = ['A', 'B', 'C'];
this.type = 'medium';
this.age = 10;
this.time = new Date();
// time needs to stay the same so storybook knows can work with them
this.time.setHours(0, 0, 0, 0);
this.complexOption = { foo: 'is foo', bar: 'is bar' };
this.complexItems = [
{ name: 'foo', age: 1000, message: 'knows all' },
{ name: 'bar', age: 1, message: 'is new here' },
];
}
update(oldValues) {
super.update(oldValues);
if (oldValues.has('type')) {
const { type } = this;
if (!(type === 'small' || type === 'medium' || type === 'large')) {
throw new Error('Type needs to be either small, medium or large');
}
}
if (oldValues.has('headerColor')) {
this.style.setProperty('--my-el-header-color', this.headerColor);
}
}
render() {
return html`
<h2>${this.header}</h2>
<p>My size is: ${this.type}.</p>
<p>I am ${this.locked ? 'locked' : 'free'} since ${this.age} years.</p>
<p>Time: ${this.time.toDateString()}</p>
<p>List:</p>
<ul>
${this.items.map(
item =>
html`
<li>${item}</li>
`,
)}
</ul>
<p>This is the light dom:</p>
<slot></slot>
<p>Complex Option:</p>
<pre>${JSON.stringify(this.complexOption, null, 2)}</pre>
<p>Complex List:</p>
<table>
<tr>
<th>Name</th>
<th>Age</th>
<th>Message</th>
</tr>
${this.complexItems.map(
item =>
html`
<tr>
<td>${item.name}</td>
<td>${item.age}</td>
<td>${item.message}</td>
</tr>
`,
)}
</table>
<button @click=${() => console.log('my-el button clicked', this)}>
log me to Actions/console
</button>
`;
}
}
customElements.define('my-el', MyEl);

View File

@@ -0,0 +1,48 @@
import {
storiesOf,
html,
withKnobs,
select,
object,
color,
addParameters,
withClassPropertiesKnobs,
} from '../../index.js';
import { MyEl } from '../my-el.js';
import notes from '../README.md';
addParameters({
options: {
isFullscreen: false,
panelPosition: 'right',
},
});
storiesOf('Demo|Example Element', module)
.addDecorator(withKnobs)
.add(
'Documentation',
() =>
withClassPropertiesKnobs(MyEl, {
overrides: el => [
{ key: 'headerColor', fn: () => color('headerColor', el.headerColor, 'Element') },
{
key: 'type',
fn: () => select('type', ['small', 'medium', 'large'], el.type, 'Element'),
},
{ key: 'complexItems', fn: () => object('complexItems', el.complexItems, 'Inherited') },
{ key: 'locked', group: 'Security' },
],
template: html`
<my-el><p>foo</p></my-el>
`,
}),
{ notes: { markdown: notes } },
)
.add(
'Alternative Header',
() => html`
<my-el .header=${'Something else'}></my-el>
`,
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

23
packages/demoing-storybook/index.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
export { html } from 'lit-html';
// NB: @types/storybook__polymer doesn't yet exist
// export { storiesOf, addParameters } from '@storybook/polymer';
export { action } from '@storybook/addon-actions';
export { linkTo } from '@storybook/addon-links';
// NB: @types/storybook__addon-backgrounds is 2 major versions behind
// export { withBackgrounds } from '@storybook/addon-backgrounds';
export { withNotes } from '@storybook/addon-notes';
export {
withKnobs,
text,
button,
number,
select,
date,
color,
array,
boolean,
} from '@storybook/addon-knobs';
export { withClassPropertiesKnobs } from './withClassPropertiesKnobs.js';

View File

@@ -0,0 +1,21 @@
export { html } from 'lit-html';
export { storiesOf, addParameters } from '@storybook/polymer';
export { action } from '@storybook/addon-actions';
export { linkTo } from '@storybook/addon-links';
export { withNotes } from '@storybook/addon-notes';
export { document } from 'global';
export {
withKnobs,
text,
button,
number,
select,
date,
object,
color,
array,
boolean,
} from '@storybook/addon-knobs';
export { withClassPropertiesKnobs } from './withClassPropertiesKnobs.js';

View File

@@ -0,0 +1,55 @@
{
"name": "@open-wc/demoing-storybook",
"version": "0.0.1",
"description": "Storybook configuration following open-wc recommendations",
"author": "open-wc",
"homepage": "https://github.com/open-wc/open-wc/",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/open-wc/open-wc.git",
"directory": "packages/building-storybook"
},
"scripts": {
"storybook": "start-storybook -p 9001 -c demo/.storybook",
"prepublishOnly": "../../scripts/insert-header.js"
},
"main": "index.js",
"files": [
"*.js",
"*.ts"
],
"dependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@storybook/addon-actions": "^5.0.0-rc.8",
"@storybook/addon-console": "^1.1.0",
"@storybook/addon-knobs": "^5.0.0-rc.8",
"@storybook/addon-links": "^5.0.0-rc.8",
"@storybook/addon-notes": "^5.0.0-rc.8",
"@storybook/addon-storysource": "^5.0.0-rc.8",
"@storybook/addon-viewport": "^5.0.0-rc.8",
"@storybook/polymer": "^5.0.0-rc.8",
"@types/storybook__addon-actions": "^3.4.1",
"@types/storybook__addon-backgrounds": "^3.2.1",
"@types/storybook__addon-knobs": "^3.4.1",
"@types/storybook__addon-links": "^3.3.3",
"@types/storybook__addon-notes": "^3.3.3",
"@webcomponents/webcomponentsjs": "^2.0.0",
"babel-loader": "^8.0.0",
"moment": "^2.0.0",
"polymer-webpack-loader": "^2.0.0"
},
"peerDependencies": {
"lit-html": "^1.0.0"
},
"devDependencies": {
"lit-element": "^2.0.1",
"lit-html": "^1.0.0"
}
}

View File

@@ -0,0 +1,82 @@
import { text, number, date, object, array, boolean } from '@storybook/addon-knobs';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render } from 'lit-html';
/**
* @example
* class MyEl extends LitElement { ... }
*
* .add('Playground', () => {
* return withClassPropertiesKnobs(MyEl);
* });
*
* @example
* .add('Playground', () => {
* return withClassPropertiesKnobs(MyEl, el => ([
* { key: 'type', fn: () => select('type', ['small', 'medium', 'large'], el.type, 'Element') },
* { key: 'complexItems', fn: () => object('complexItems', el.complexItems, 'Inherited') },
* { key: 'locked', group: 'Security' }, // change group of an default Element property
* ]));
* });
*/
export function withClassPropertiesKnobs(Klass, { overrides: overrideFunction, template } = {}) {
let el;
if (template) {
const wrapper = document.createElement('div');
render(template, wrapper);
el = wrapper.children[0]; // eslint-disable-line prefer-destructuring
} else {
el = new Klass();
}
const overrides = overrideFunction ? overrideFunction(el) : [];
const elProperties = Object.keys(Klass.properties);
const properties = Array.from(elProperties);
if (Klass._classProperties) {
Array.from(Klass._classProperties.keys()).forEach(propName => {
if (!elProperties.includes(propName)) {
properties.push(propName);
}
});
}
properties.forEach(propName => {
const override = overrides.find(item => item.key === propName);
if (override && override.fn) {
el[propName] = override.fn();
} else {
const isElProperty = elProperties.includes(propName);
let group = isElProperty ? 'Element' : 'Inherited';
if (override && override.group) {
group = override.group; // eslint-disable-line prefer-destructuring
}
const prop = isElProperty ? Klass.properties[propName] : Klass._classProperties.get(propName);
if (prop.type.name) {
// let method = false;
switch (prop.type.name) {
case 'String':
el[propName] = text(propName, el[propName], group);
break;
case 'Number':
el[propName] = number(propName, el[propName], {}, group);
break;
case 'Array':
el[propName] = array(propName, el[propName], ',', group);
break;
case 'Boolean':
el[propName] = boolean(propName, el[propName], group);
break;
case 'Object':
el[propName] = object(propName, el[propName], group);
break;
case 'Date':
el[propName] = new Date(date(propName, el[propName], group));
break;
default:
}
}
}
});
return el;
}