ionic-team / stencil

A toolchain for building scalable, enterprise-ready component systems on top of TypeScript and Web Component standards. Stencil components can be distributed natively to React, Angular, Vue, and traditional web developers from a single, framework-agnostic codebase.
https://stenciljs.com
Other
12.51k stars 782 forks source link

Integrate with Storybook #2328

Open ericis opened 4 years ago

ericis commented 4 years ago

Stencil version:

All

I'm submitting a:

[ ] bug report [x] feature request [ ] support request

Current behavior:

Works great, but isn't explicitly supported on open-source Storybook.

Expected behavior:

Have an implementation supported explicitly by Storybook and listed in their "Guides".

Steps to reproduce: n/a (visit the website)

Related code: n/a

Other information: n/a

jpzwarte commented 4 years ago

My setup: https://gist.github.com/jpzwarte/0be6a491a3762f2ee2e784ab29669a2c

I still have to figure out how to get <Props> to work; something to do with the readme stencil outputs during the build.

dmartinjs commented 4 years ago

@jpzwarte you have to generate a custom-elements.json file by adding config in your stencil.config.ts

import { Config } from '@stencil/core';

export const config: Config = {
  namespace: 'roadtrip',
  outputTargets: [
    { 
      type: 'docs-vscode',
      file: 'custom-elements.json'
    }
  ]
};

import it in preview.js

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

setCustomElements(customElements);

then in a component story add the component: parameter in the default export with the name of the component.

export default {
  title: 'Forms|Input',
  component: 'my-input',
};

https://github.com/storybookjs/storybook/tree/next/addons/docs/web-components#custom-elementsjson

jpzwarte commented 4 years ago

@dmartinjs thanks, that helps. But your solution does not allow for JSX in an MDX story, right?

dmartinjs commented 4 years ago

@jpzwarte I didn't try in a MDX story.

jpzwarte commented 4 years ago

@dmartinjs there must be some part still missing. I generate the custom-elements.json and import that, but don't i need to import some generated JS from dist/ as well? Currently, none of my web-components are working.

dmartinjs commented 4 years ago

@jpzwarte you don't have to import something else, Storybook use the data generated in the custom-elements.json to display props.

wjureczka commented 3 years ago

Is the solution of @dmartinjs up to date? The link is not working now

dmartinjs commented 3 years ago

@wjureczka a folder has been removed or moved in the storybbok repository.

You can still find the link here: https://github.com/storybookjs/storybook/tree/v6.1.21/addons/docs/web-components

but the solution isn't up-to-date anymore, the docs-vscode output isn't working now, for generating the custom-elements.json you'll need to add a docs-custom output as follow:

stencil.config.ts

import { promises as fs } from 'fs';
import { Config } from '@stencil/core';
import { JsonDocs } from '@stencil/core/internal';

async function generateCustomElementsJson(docsData: JsonDocs) {
  const jsonData = {
    version: 1.2,
    tags: docsData.components.map((component) => ({
      name: component.tag,
      path: component.filePath,
      description: component.docs,

      attributes: component.props
        .filter((prop) => prop.attr)
        .map((prop) => ({
          name: prop.attr,
          type: prop.type,
          description: prop.docs,
          defaultValue: prop.default,
          required: prop.required,
        })),

      events: component.events.map((event) => ({
        name: event.event,
        type: event.detail,
        description: event.docs,
      })),

      methods: component.methods.map((method) => ({
        name: method.name,
        description: method.docs,
        signature: method.signature,
      })),

      slots: component.slots.map((slot) => ({
        name: slot.name,
        description: slot.docs,
      })),

      cssProperties: component.styles
        .filter((style) => style.annotation === 'prop')
        .map((style) => ({
          name: style.name,
          description: style.docs,
        })),

      cssParts: component.parts.map((part) => ({
        name: part.name,
        description: part.docs,
      })),
    })),
  };

  await fs.writeFile(
    './custom-elements.json',
    JSON.stringify(jsonData, null, 2),
  );
}

export const config: Config = {
  namespace: 'yourNamespace',
  outputTargets: [
    { 
      type: 'docs-custom',
      generator: generateCustomElementsJson
    },
  ],
};

import it in preview.js

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

setCustomElements(customElements);

then in a component story add the component: parameter in the default export with the name of the component.

export default {
  title: 'Forms|Input',
  component: 'my-input',
};
wjureczka commented 3 years ago

I will try it, thanks for your time! <3

deleonio commented 3 years ago

Hi together.

I've been working with stencil for a long time and I'm thrilled. In doing so, I also docked the storybook. But please don't include that in stencil. Because then there will be some collisions. The package Json is also mega large and confusing (> 50 deps). There are also file extension collisions, with tsx. I use tsx for my React stories. mdx is also possible, but linting is not optimal there.

How can I run the components in IE? So how do I use the ES5 modules from a React app? I think the documentation still has a gap. It doesn't work for me and I don't see why.

So far, I would really like to have more contact. Good JOB!

dmartinjs commented 3 years ago

@deleonio I think there is a misunderstanding, the point here isn't to integrate Storybook in Stencil codebase, but make the integration of Stencil components and documentation easier in Storybook.

But I think this issue can be closed since the Storybook team is investigating to build official support for Stencil.

marcolanaro commented 3 years ago

A first milestone would be to run stories writing jsx so to render them in stencil. A complete solution would include compiling stencil components through storybook.

I have a PR open for the first one. The only drawbacks are:

  1. you need to run npm run build:watch and npm run storybook separately.
  2. while editing stories leverage stencil HMR, editing components for the moment forces full refresh.

Here's my PR: https://github.com/storybookjs/storybook/pull/15479/files

In the mean time you can hack around and configure it standalone using @storybook/html. In preview.js define the following decorator:

import { renderVdom, registerHost, getHostRef, h } from '@stencil/core/internal/client';

import { defineCustomElements } from '../dist/esm/loader';

defineCustomElements();

const rootElement = document.getElementById('root');
const storyRoot = document.createElement('div');
rootElement.parentElement.appendChild(storyRoot);

registerHost(storyRoot, { $flags$: 0, $tagName$: 'story-root' })
const hostRef = getHostRef(storyRoot);

export const decorators = [
  (Story) => {
    renderVdom(hostRef, Story());
    return '<div />';
  }
];

In main.js define babel plugin:

  babelDefault: (config) => {
    return {
      ...config,
      plugins: [
        ...config.plugins,
        [require.resolve('@babel/plugin-transform-react-jsx'), { pragma: 'h' }, 'preset'],
      ],
    };
  }

Write your story like:

import { h } from '@stencil/core';

export default {
  title: 'Welcome',
};

export const Default = () => {
  return (
    <container-component>
      <div>Header</div>
      <div><data-component richData={{ foo: 'bar' }}></data-component></div>
      <div>Footer</div>
    </container-component>
  )
};
zimaah commented 2 years ago

My 5 cents: Storybook is a nightmare of dependencies, it does a lot, but bring also a lot of issues during the development. Integrating Stencil with Storybook is like integrating it with some UI/UX Angular-like framework (if you know what I mean haha).

That said, I'd love to see Stencil integrated with an "underdog" UI/UX framework, something like:

dmartinjs commented 2 years ago

@zimaah the cons of using an underdog is that they have almost no integrations with other tools and a smaller community to help you when you're stuck...

In the other side, Storybook is listed in all design system surveys, have a lot of integrations and can be extensible with plugins.

apurvaojas commented 2 years ago

Thanks @marcolanaro for workaround But I am facing below issue while following this process, Any Idea..?

Screenshot 2022-05-14 at 11 25 05 AM
tomwayson commented 2 years ago

@apurvaojas I'm experiencing a similar error while trying the decorator pattern from this blog post, which, similar to the above process utilizes renderVDom():

image

This is in stroybook 6.5. In earlier versions of storybook we were able to get stories work with stencil components by having the stories return a string, i.e.:

export const Default = args => `<my-component some-attr="${args.someAttr}" />`

Someone else on our team set up our storybook, but I assume that the above relied on stroybook's web component addon (as well as calling defineCusomElements() in preview.js. We'd also have to use custom story decorators to set object/array props after the story rendered. All in all kind of clunky, but it worked.... until it didn't. At some point the strings returned by the stories started being rendered in the dom as strings

image

instead of elements

image

So we're now in a spot where some patch update in some dependency of some storybook addon made all our stories useless (just rendering the element tag as a string), so we're trying to change our stories not to return strings and use what appears to be a more robust approach to integrating stencil into storybook (i.e. using renderVDom(), but we're getting a similar error to @apurvaojas. We've invested in dozens of stories, and now none of them work.

I'd have hoped that the stencil team would have a better, less hacky, more first class story for storybook integration given the intent of Stencil is to author UI libraries.

tomwayson commented 2 years ago

For anyone else who ends up here, it seems like @marcolanaro has a more recent solution here: https://github.com/storybookjs/storybook/issues/4600#issuecomment-899055226

And it looks like the storybook and stencil teams were interested in using that as the basis for an official integration, but it's not clear to me where that effort ended up.

kerryj89 commented 1 year ago

I feel like having official support for Storybook would bring more people over to Stencil. Many design systems use Storybook and having that logo anywhere on the Stencil page would seal the deal for many looking towards componentization. What initially attracted me to Stencil was that much of the tooling was already done for me (scss, framework bindings, etc.).

Storybook 7 was just released today. How awesome it would be to be able to strap on the Storybook jetpack to jump between development and design space. I'm hoping there's someone well versed enough in both tools to make this happen. I would donate to that end.

jared-christensen commented 1 year ago

I'm working with Storybook 7 and I just added the below but it does not seem to work. I don't get any errors but I still have to add my arg types manually.

/* preview.js */

import { setCustomElementsManifest } from '@storybook/web-components';
import customElements from '../vscode-data.json';

setCustomElementsManifest(customElements);
junelau commented 1 year ago

inspired by this post, we managed to get it working in by loading stencil's compiler within storybook.

/* main.js */

module.exports = {
  ...,
  webpack: config => {
    return {
      ...config,
      module: {
        ...config.module,
        rules: [
          ...config.module.rules,
          {
            test: /\.(tsx)$/,
            loader: path.resolve('./.storybook/loader.js'),
          },
        ],
      },
    };
  },
...
};
/* loader.js */

const stencil = require('@stencil/core/compiler');

module.exports = function (source) {
  const callback = this.async();

  patchedSource = patchSourceWithFragment(source);
  const compiled = stencil.transpileSync(patchedSource, { sourceMap: false });
  callback(null, compiled.code);
};

function patchSourceWithFragment(source) {
  const regex = /import\s*\{([\w\s,]+)\}\s*from\s*'@stencil\/core';/;
  const match = source.match(regex);
  const fragment = 'Fragment';

  if (match) {
    const existingImports = match[1].split(',').map(item => item.trim());

    if (!existingImports.includes(fragment)) {
      const newImports = [...existingImports, fragment].join(', ');
      return source.replace(regex, `import { ${newImports} } from '@stencil/core';`);
    }

    console.error('Import already exists.');
    return source;
  }

  console.error('No matching import found.');
  return source;
}

with this setup, stories can be written as per the storybook docs if your props are nice and simple.

To use a callback (e.g. searching/filtering in a table of data), and allow the parent to govern the state, we can use renderVdom and some jsx like below. this is analagous to using React's useState.

/* myComponent.stories.tsx */
import { getHostRef, registerHost, renderVdom } from '@stencil/core/internal/client';

export const WithRenderVdom: Story = {
  render: () => {
    const rootRef = document.getElementById('storybook-root').appendChild(document.createElement('div'));
    registerHost(rootRef, { $flags$: 0, $tagName$: 'host' });
    const hostRef = getHostRef(rootRef);

    const render = () => {
      renderVdom(
        hostRef,
        <div style={{ margin: '50px' }}>
          <my-component data={data} searchHandler={searchHandler}></my-component>
        </div>,
      );
    };

    // An example arg which we want to update via a callback
    let data = getTableData();

    const searchHandler = searchValue => {
      const d = getTableData();
      const filteredData = {
        columns: d.columns,
        rows: d.rows.filter(r => {
          return String(r.data.Name).toLowerCase().includes(String(searchValue.value).toLowerCase());
        }),
      };
      data = filteredData;
      render();
    };

    render();
    return rootRef;
  },
};
*/

using storybook 7.0.7. and these framework builders:

    "@storybook/web-components": "7.3.2",
    "@storybook/web-components-webpack5": "7.3.2",

doing it this way, we've found 3 annoying things so far:

  1. you can't kill the npm run storybook process in the terminal using ctrl-C. the terminal session has to be killed entirely.
  2. due to using the stencil compiler, the component is rendered twice in storybook. so there's a short "flicker" as it reloads twice.
  3. interaction tests (storybook Plays) aren't seamless. we are finding we have to add arbitrary waits, due to waiting for the stencil compiler to finish and then attach the elements

I would really like feedback from the ionic/stencil team please if this is an appropriate use of getHostRef, registerHost, renderVdom.

and, if any expert storybook users can suggest improvements (such as moving our render to somewhere with global effect ) it would be most appreciated!