Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import tokens from '../src/compiled/tokens/js/tokens';
import 'focus-visible';
import '../src/index-with-dependencies.scss';
import './preview.scss';
import { makeTwigInclude } from '../src/make-twig-include';
const breakpoints = tokens.size.breakpoint;

// Extend the languages Storybook will highlight
Expand Down Expand Up @@ -67,6 +68,22 @@ export const parameters = {
// https://github.com/storybookjs/storybook/blob/v6.0.21/addons/docs/docs/docspage.md#inline-stories-vs-iframe-stories
inlineStories: true,
prepareForInline: (storyFn) => htmlToReactParser.parse(storyFn()),
transformSource(src, storyContext) {
try {
const storyFunction = storyContext.getOriginal();
const rendered = storyFunction(storyContext.args);
// The twing/source-inputs-loader.js file makes it so that whenever twig templates are rendered,
// the arguments and input path are stored in the window.__twig_inputs__ variable.
// __twig_inputs__ is a map between the output HTML and and objects with the arguments and input paths
// Here, since we have the rendered HTML, we can look up what the arguments and path were
// that correspond to that output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really interesting! Clever!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This is the second approach, the first approach was to have the twig function return an object like this:

{
  toString() {
    return '...html...'
  },
  path: '...',
  args: { ... }
}

which I think is a nicer solution, and it didn't have the limitation of not working for the stories which use useEffect / other hooks. But then I realized it breaks for the canvas page, because the canvas page has a typeof check and it doesn't allow non-strings (even though an object with a toString method can be implicitly coerced into a string)

const input = window.__twig_inputs__?.get(rendered);
if (!input) return src;
return makeTwigInclude(input.path, input.args);
} catch {
return src;
}
},
},
viewport: {
viewports: {
Expand Down
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,27 @@ This is generally not necessary, but in case you need to manually publish a vers
You can run `npm publish --dry-run` to see everything that _would_ happen during publish, without actually publishing to the npm registry.

Note the branch is `v-next` for now. When we we merge this branch to `master`, these instructions should be updated.

## Overriding source code previews in Storybook

For most stories, we are able to generate a twig source code snippet for Storybook to display automatically. When stories use `useEffect` or other hooks, the source code snippet cannot be generated automatically, so the JS that was passed to `<Story>` is shown instead (there may be other cases where this happens as well). In those cases, you can manually pass the source code to `<Story>`:

```jsx
import { makeTwigInclude } from '../../make-twig-include';
<Story
name="Story Name"
parameters={{
docs: { source: { code: makeTwigInclude('asdf', { foo: 'bar' }) } },
}}
>
{(args) => template(args)}
</Story>;
```

This generates a source code snippet like this:

```twig
{% include 'asdf' with {
"foo": "bar"
} only %}
```
13 changes: 13 additions & 0 deletions src/make-twig-include.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Generate a twig source string of an include with args, i.e.
* {% include '@cloudfour/components/button.twig' with { "type": "disabled" } only %}
* @param {string} path
* @param {any} args
*/
export const makeTwigInclude = (path, args) => {
const argsString =
Object.keys(args).length > 0
? ` with ${JSON.stringify(args, null, 2)}`
: '';
return `{% include '${path}'${argsString} only %}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there ever a use case where we wouldn't want to use only? 🤔

If so, we might want to add a param to control this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is best practice for includes to avoid implicitly passing through variables. For our docs, I put the only there so that if someone copy-pastes it it will probably "do the right thing", and they have to opt-in to the implicit passing through behavior. I can't think of any of our demos where someone using our patterns would always or usually want to have the implicit passing through behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that makes sense. We can always revisit later, and this is probably a safer starting point than leaving it off.

};
24 changes: 24 additions & 0 deletions twing/source-inputs-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { relative } = require('path');

// This loader changes the functions that are generated by importing .twig files
// It makes the functions attach the arguments and twig file path to a global __twig__inputs__ global
// __twig__inputs__ is a map between the output HTML and and objects with the arguments and input paths
// __twig__inputs__ is used in transformSource in .storybook/preview.js to generate the twig source code previews

module.exports = function (source) {
// Change it from an absolute path to a path starting with @cloudfour/
const path = relative(this.rootContext, this.resourcePath).replace(
/^src\//,
'@cloudfour/'
);
return source.replace(
`return template.render(context)`,
`const rendered = template.render(context);
const inputs = window.__twig_inputs__ || (window.__twig_inputs__ = new Map());
inputs.set(
rendered,
{ path: ${JSON.stringify(path)}, args: context }
);
return rendered;`
);
};
16 changes: 11 additions & 5 deletions twing/webpack-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ const twingVirtualEnvironmentPath = require.resolve(

const twingLoader = {
test: /\.twig$/,
use: {
loader: 'twing-loader',
options: {
environmentModulePath: twingEnvironmentNodePath,
use: [
// Webpack loaders run in reverse, so this top loader will run last
{
loader: require.resolve('./source-inputs-loader'),
},
},
{
loader: 'twing-loader',
options: {
environmentModulePath: twingEnvironmentNodePath,
},
},
],
};

const alias = {
Expand Down