Adding External Script to Specific Story in Storybook - storybook

I wanted to know how I can load in external javascript into a specific story in storybook. The only documentation I can find right now is how to do it globally https://storybook.js.org/docs/react/configure/story-rendering. Doing this works, i would just like to save on performance since only one of my stories uses an external js script.

No, there isn't a standard way to do this in storybook currently (version 6.5).
However you can achieve it with a decorator.
Depending on your needs it could look something like this (this is for a React story):
import { Story, Meta } from '#storybook/react';
import { useEffect } from '#storybook/addons';
export default {
title: 'My Story',
component: MyStory,
decorators: [
(Story) => {
useEffect(() => {
const script = document.createElement('script');
script.src = '/my-script';
document.body.appendChild(script);
}, []);
return <Story />;
},
],
};
There are some caveats here though:
The scripts will remain loaded as you navigate to other stories.
The story will render before the script has loaded.
To handle these caveats you can:
Add a cleanup handler to useEffect.
Don't render your <Story/> until the script has loaded.
For example:
import { Story, Meta } from '#storybook/react';
import { useEffect, useState } from '#storybook/addons';
export default {
title: 'My Story',
component: MyStory,
decorators: [
(Story) => {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const script = document.createElement('script');
script.onload = () => {
setIsLoaded(true);
};
script.src = '/my-script';
document.body.appendChild(script);
return () => {
// clean up effects of script here
};
}, []);
return isLoaded ? <Story /> : <div>Loading...</div>;
},
],
};
If you have multiple scripts you'll have to wrap all the onload events into a Promise.all.
This could be wrapped up in an addon similar to storybook-addon-run-script.

Related

Why isn't my t() texts refreshing in localhost/en but refreshing in localhost/fr on i18n.changeLanguage()?

Hi
I just made a website with a darkmode and multilanguage support to test around but I ran into an issue.
the code
I got rid of all things that aren't an issue
portfolio/src/pages/index.tsx
import { useTranslation } from 'react-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
export default () => {
const { t,i18n } = useTranslation('common')
return <div onClick={()=>i18n.changeLanguage(i18n.language=='fr'?'en':'fr')}>
<div>{i18n.language}</div>
<span>{t('debug')}</span>
</div>
}
export async function getStaticProps({ locale }:any) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},
};
}
portfolio/src/public/locales/en/common.js
{"debug":"english"}
portfolio/src/public/locales/fr/common.js
{"debug":"français"}
portfolio/next-i18next.config.js
const path = require("path");
module.exports = {
debug: false,
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
localePath: path.resolve('./src/public/locales'),
};
portfolio/src/pages/_app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import {appWithTranslation} from 'next-i18next'
export default appWithTranslation(({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />
})
The issue
When I do npm run dev and go to http://localhost:3000/fr, the page defaults to french and works good I can swap between languages without problems but when i go to http://localhost:3000/en the t('debug') doesn't translate when the i18n.language changes as intended.
Found what I wanted
So basicaly I need to use a next Link that will change the local and the link
Code application
index.js
//...
export default () => {
const { t,i18n } = useTranslation('common')
return (
<div>
<Link
href={i18n.language=='fr'?'/en':'/fr'}
locale={i18n.language=='fr'?'en':'fr'}
>{i18n.language}</Link>
<div>{t('debug')}</div>
</div>
)
}
//...
result
Now the text changes as intended both in the /fr and /en because it switches between the 2 however the result is far from smooth. It reloads the page and i'd like to avoid that because I use some animations on it.
Found what i wanted part 2
Browsing through the next-i18next documentation I found what I wanted.
solution
I needed to load the props using getStaticProps and in the serverSideTranslation function i needed to pass as argument the array off ALL the language necessary to load the page ['en','fr'] because i switched between the 2

How to use SSR with Stencil in a Nuxt 3 Vite project?

In Nuxt 2 I could use server-side rendered Stencil components by leveraging the renderToString() method provided in the Stencil package in combination with a Nuxt hook, like this:
import { renderToString } from '[my-components]/dist-hydrate'
export default function () {
this.nuxt.hook('generate:page', async (page) => {
const render = await renderToString(page.html, {
prettyHtml: false
})
page.html = render.html
})
}
Since the recent release of Stencil 2.16.0 I'm able to use native web components in Nuxt 3 that is powered by Vite. However I haven't found a way to hook into the template hydration process. Unfortunately there is no documentation for the composable useHydration() yet.
Does anybody know how I could get this to work in Nuxt 3?
I had the same problem. I solved it via a module.
Make a new custom nuxt module. documentation for creating a module
In the setup method hook into the generate:page hook:
nuxt.hook('generate:page', async (page) => {
const render = await renderToString(page.html, {
prettyHtml: true,
});
page.html = render.html;
});
documentation for nuxt hooks
documentation for stencil hydration (renderToString)
Register the css classes you need via nuxt.options.css.push(PATH_TO_CSS)
Register the module in the nuxt config.
Note: Make sure in the nuxt.config.ts the defineNuxtConfig gets exported as default.
Tap the vue compiler options in the nuxt config:
vue: {
compilerOptions: {
isCustomElement: (tag) => TEST_TAG_HERE,
},
},
This depends on how you wan't to use the custom elements. In my case I defined the elements over the stencil loader in my app.vue file:
import { defineCustomElements } from '<package>/<path_to_loader>';
defineCustomElements();
You could also import the elements you need in your component and then define them right there, for example in a example.vue component:
import { CustomElement } from '<package>/custom-elements';
customElements.define('custom-element', CustomElement);
Here is an example from my module and config:
./modules/sdx.ts
import { defineNuxtModule } from '#nuxt/kit';
import { renderToString } from '#swisscom/sdx/hydrate';
export default defineNuxtModule({
meta: {
name: '#nuxt/sdx',
configKey: 'sdx',
},
setup(options, nuxt) {
nuxt.hook('generate:page', async (page) => {
const render = await renderToString(page.html, {
prettyHtml: true,
});
page.html = render.html;
});
nuxt.options.css.push('#swisscom/sdx/dist/css/webcomponents.css');
nuxt.options.css.push('#swisscom/sdx/dist/css/sdx.css');
},
});
Important: This only works if the stenciljs package supports hydration or in other words has a hydrate output. Read more here
./nuxt.config.ts
import { defineNuxtConfig } from 'nuxt';
//v3.nuxtjs.org/api/configuration/nuxt.config export default
export default defineNuxtConfig({
typescript: { shim: false },
vue: {
compilerOptions: {
isCustomElement: (tag) => /sdx-.+/.test(tag),
},
},
modules: ['./modules/sdx'],
});
./app.vue
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<script setup lang="ts">
import { defineCustomElements } from '#swisscom/sdx/dist/js/webcomponents/loader';
defineCustomElements();
// https://v3.nuxtjs.org/guide/features/head-management/
useHead({
title: 'demo',
viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
charset: 'utf-8',
meta: [{ name: 'description', content: 'demo for using a stencil package in a nuxt ssr app' }],
bodyAttrs: {
class: 'sdx',
},
});
</script>
Update
I tested my setup with multiple components and it looks like you cannot define your components in the module. I updated the answer to my working solution.
I've found defining a plugin using the 'render:response' hook to work for me:
server/plugins/ssr-components.plugin.ts
import { renderToString } from '#my-lib/components/hydrate';
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:response', async (response) => {
response.body = (await renderToString(response.body)).html;
});
});
Perhaps it will work for you :)
Try this in defineNuxtPlugin
nuxtApp.hook('app:rendered', () => {
const response = nuxtApp.ssrContext?.res
if (!response)
return
const end = response.end
response.end = function(chunk) {
chunk = 'hijacked'
end(chunk)
}
})

Dynamic Importing of an unknown component - NextJs

I want to load a component dynamically based on the route. I'm trying to make a single page which can load any individual component for testing purposes.
However whenever I try to do import(path) it shows the loader but never actually loads. If I hard code the exact same string that path contains then it works fine. What gives? How can I get nextjs to actually dynamically import the dynamic import?
// pages/test/[...component].js
const Test = () => {
const router = useRouter();
const { component } = router.query;
const path = `../../components/${component.join('/')}`;
console.log(path === '../../components/common/CircularLoader'); // prints true
// This fails to load, despite path being identical to hard coded import
const DynamicComponent = dynamic(() => import(path), {
ssr: false,
loading: () => <p>Loading...</p>,
});
// This seems to work
const DynamicExample = dynamic(() => import('../../components/Example'), {
ssr: false,
loading: () => <p>Loading...</p>,
});
return (
<Fragment>
<h1>Testing {path}</h1>
<div id="dynamic-component">
<DynamicComponent /> <!-- this always shows "Loading..." -->
<DynamicExample /> <!-- this loads fine. -->
</div>
</Fragment>
);
};
export default Test;
I put dynamic outside of the component, and it work fine.
const getDynamicComponent = (c) => dynamic(() => import(`../components/${c}`), {
ssr: false,
loading: () => <p>Loading...</p>,
});
const Test = () => {
const router = useRouter();
const { component } = router.query;
const DynamicComponent = getDynamicComponent(component);
return <DynamicComponent />
}
I had the same issue like the thread opener.
The Documentation describe, that it's not possible to use template strings in the import() inside dynamic:
In my case it was also impossible to add an general variable with the path there...
Solution
I've found an easy trick to solve this issue:
// getComponentPath is a method which resolve the path of the given Module-Name
const newPath = `./${getComponentPath(subComponent)}`;
const SubComponent = dynamic(() => import(''+newPath));
All the MAGIC seems to be the concatenation of an empty String with my generated Variable newPath: ''+newPath
Another Solution:
Another Solution (posted by bjn from the nextjs-Discord-Channel):
const dynamicComponents = {
About: dynamic(() => import("./path/to/about")),
Other: dynamic(() => import("./path/to/other")),
...
};
// ... in your page or whatever
const Component = dynamicComponents[subComponent];
return <Component />
This example might be useful, if you know all dynamically injectable Components.
So you can list them all and use it later on in your code only if needed)
The below code worked for me with dynamic inside the component function.
import dynamic from "next/dynamic";
export default function componentFinder(componentName, componentPath) {
const path = componentPath; // example : "news/lists"
const DynamicComponent = dynamic(() => import(`../components/${path}`),
{
ssr: false,
loading: () => <p>Loading Content...</p>,
});
return <DynamicComponent />;
}
It happens because router.query is not ready and router.query.component is undefined at the very first render of dynamic page.
This would print false at first render and true at the following one.
console.log(path === '../../components/common/CircularLoader');
You can wrap it with useEffect to make sure query is loaded.
const router = useRouter();
useEffect(() => {
if (router.asPath !== router.route) {
// router.query.component is defined
}
}, [router])
SO: useRouter receive undefined on query in first render
Github Issue: Add a ready: boolean to Router returned by useRouter
As it was said here before the dynamic imports need to be specifically written without template strings. So, if you know all the components you need beforehand you can dynamically import them all and use conditionals to render only those you want.
import React from 'react';
import dynamic from 'next/dynamic';
const Component1 = dynamic(() => import('./Component1').then((result) => result.default));
const Component2 = dynamic(() => import('./Component2').then((result) => result.default));
interface Props {
slug: string;
[prop: string]: unknown;
}
export default function DynamicComponent({ slug, ...rest }: Props) {
switch (slug) {
case 'component-1':
return <Component1 {...rest} />;
case 'component-2':
return <Component2 {...rest} />;
default:
return null;
}
}

How can I implement in styled components and interpolations with emotion theming?

I have been working on a React web application with a dynamic theme using the emotion-theming library. So a user can switch between environments and each environment has its own theme.
I have created my own CustomThemeProvider which I use to dynamicly change the theme. Below is the code.
export interface CustomThemeContextValue {
customTheme?: Theme;
setCustomTheme: (theme: Theme) => void;
};
const CustomThemeContext = React.createContext<CustomThemeContextValue>({
customTheme: undefined,
setCustomTheme: (theme) => { }
});
interface CustomThemeProviderProps {
}
export const CustomThemeProvider: FC<CustomThemeProviderProps> = (props) => {
const [customTheme, setCustomTheme] = useState<Theme>(theme);
const context: CustomThemeContextValue = React.useMemo(() => ({
customTheme,
setCustomTheme
}), [customTheme, setCustomTheme]);
return (
<CustomThemeContext.Provider value={context}>
<ThemeProvider theme={customTheme} {...props} />
</CustomThemeContext.Provider>
);
};
export const useCustomTheme = () => {
const context = React.useContext(CustomThemeContext);
if (!context) {
throw new Error('useCustomTheme must be used within a CustomThemeProvider');
}
return context;
};
The provider is implemented in the root like so
const Root = () => {
return (
<StrictMode>
<CustomThemeProvider>
<Normalize />
<Global styles={globalStyle} />
<App />
</CustomThemeProvider>
</StrictMode>
);
};
So this code is working, I can get the theme within a function component using the emotion useTheme hook like below:
const theme: Theme = useTheme();
But the question is how to get the theme out of the emotion ThemeProvider and use it in certain situations. Is it possibe to use it in a context like
export const style: Interpolation = {
cssProp: value
};
Or is it usable in a context like below where styled.button is from emotion/styled.
const Button: FC<HTMLProps<HTMLButtonElement> & ButtonProps> = styled.button([]);
and is it usable in the emotion/core method css() like below
const style = css({
cssProp: value
});
I find it very hard to find answers to these questions using google so I hope somebody here can help me out.
So after a while I have finally found an answer to my own question and I like to share it with everyone as it is very hard to find. so here it is.
Instead of Interpolation you can use InterpolationWithTheme like below:
import { InterpolationWithTheme } from '#emotion/core';
export const style: InterpolationWithTheme<Theme> = (theme) => ({
cssProp: theme.value
});
This way you can get the theme out of the ThemeProvider.
With the styled componenents you can implement it like below:
const Button: FC<HTMLProps<HTMLButtonElement> & ButtonProps>
= styled.button(({ theme }: any) => ([
{
cssProp: theme.value
}
]);
And finally when you want to use the css() with the themeProvider you have to replace it with the InterpolationWithTheme to make it work just like in the first example of this answer.
These answers have been found by a combination of looking in the emotionjs docs and inspecting the emotionjs types/interfaces.

CSS import in React Component not working in Electron app

First of all, I want to say sorry because I feel like I lack some theorical background for this problem, but right now I am stuck so here I am.
I want to set up an Electron app with just the usual main.js entry point which handles BrowserWindow creation. I want to create multiple, different windows, so the boilerplates I found on the Internet are no good for me.
So, my main.js creates a window by using an .html file that uses a script tag to add its renderer, that just sets up a React component like this:
const React = require('react');
const ReactDOM = require("react-dom");
const ReactComponent = require('path/to/component').default;
window.onload = () => {
ReactDOM.render(<ReactComponent />, document.getElementById('my-component'));
};
My component is very basic so far:
import React from 'react';
import './ReactComponent.css';
class ReactComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="ReactComponent">
some text
</div>
);
}
}
export default ReactComponent;
And the .css is even simpler:
.ReactComponent {background-color: red;}
I used Gulp + Babel to compile my code, and everything works well until I add that
import './ReactComponent.css';
Which throws this error:
Uncaught SyntaxError: Unexpected token .
Reading the .css file.
Here is my .babelrc:
{
"presets": ["#babel/preset-env", "#babel/preset-react"]
}
And my gulpfile.js:
const spawn = require('child_process').spawn;
const gulp = require('gulp');
const maps = require('gulp-sourcemaps');
const babel = require('gulp-babel');
const css = require('gulp-css');
const path = require('path');
/* Build */
gulp.task('build-css', function () {
return gulp.src('src/**/*.css')
.pipe(maps.init())
.pipe(css())
.pipe(maps.write('.'))
.pipe(gulp.dest('dist/src'));
});
gulp.task('build-js', () => {
return gulp.src(['src/**/*.js', 'src/**/*.jsx', '!src/**/*.test.js'])
.pipe(maps.init())
.pipe(babel())
.pipe(maps.write('.'))
.pipe(gulp.dest('dist/src'));
});
gulp.task('build-main-js', () => {
return gulp.src('main.js')
.pipe(maps.init())
.pipe(babel())
.pipe(maps.write('.'))
.pipe(gulp.dest('dist/'));
});
gulp.task('build', gulp.series('build-css', 'build-js', 'build-main-js'));
/* Copy */
gulp.task('copy-html', () => {
return gulp.src('src/**/*.html')
.pipe(maps.init())
.pipe(maps.write('.'))
.pipe(gulp.dest('dist/src'));
});
gulp.task('copy-assets', () => {
return gulp.src('assets/**/*').pipe(gulp.dest('dist/assets'));
});
gulp.task('copy', gulp.parallel('copy-html', 'copy-assets'));
/* Execute */
const cmd = (name) => path.resolve(__dirname) + '\\node_modules\\.bin\\' + name + '.cmd';
const args = (more) => Array.isArray(more) ? ['.'].concat(more) : ['.'];
const exit = () => process.exit();
gulp.task('start', gulp.series('copy', 'build', () => {
return spawn(cmd('electron'), args(), { stdio: 'inherit' }).on('close', exit);
}));
gulp.task('release', gulp.series('copy', 'build', () => {
return spawn(cmd('electron-builder'), args(), { stdio: 'inherit' }).on('close', exit);
}));
gulp.task('test', gulp.series('copy', 'build', () => {
return spawn(cmd('jest'), args(), { stdio: 'inherit' }).on('close', exit);
}));
At this point I don't even know anymore what the problem is... Please send help!
Many thanks!

Resources