I'm trying to follow https://storybook.js.org/docs/react/writing-stories/introduction and hitting a snag when trying to do the Template example. My code is:
import { ComponentMeta, ComponentStory } from '#storybook/react';
import React, { FC } from 'react';
interface FooProps {
myArg: string;
}
const Foo: FC<FooProps> = ({ myArg }) => <p>{myArg}</p>;
Foo.displayName = 'Foo';
export default {
component: Foo,
title: 'Foo',
} as ComponentMeta<typeof Foo>;
const Template: ComponentStory<typeof Foo> = (args) => {
console.log(args);
return <Foo {...args} />;
};
export const Default = Template.bind({});
Default.args = { myArg: 'Foo' };
However, the args argument that's passed to Template is a complex object that describes the story and has nested under it args.args which is what I'd want to pass to my component. Trying to use that throws a TS error though, but also from looking at the docs and GH issues, it seems like people are successfully using this paradigm, so I'm not sure why it's failing for me.
I'm using the latest storybook version that's been released (6.5.13), and my configuration is:
module.exports = {
stories: [
'./**/*.stories.#(js|jsx|ts|tsx)',
],
addons: [
{
name: '#storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
framework: '#storybook/react',
};
Related
I have a below query
export const GET_ALL_ADVERT_CUSTOM_FIELD = gql(`
query advertCustomFields {
advertCustomFields {
nodes {
slug
valueType
displayName
description
canRead
}
}
}
`)
And I would like to get the list filtered of nodes like this
import { Props as SelectProps } from 'react-select'
import React, { FC, useState } from 'react'
import ObjectSelector from 'components/Common/ObjectSelector'
import OptionWithDescription from './OptionWithDescription'
import { useQuery } from '#apollo/client'
import { AdvertCustomFieldsDocument } from '__generated__/graphql'
export const AdvertCustomFieldSelector: FC<SelectProps> = (props) => {
const [data, setData] = useState<NodeType[]>()
useQuery(AdvertCustomFieldsDocument, {
onCompleted: (res) => {
const filterData = res.advertCustomFields?.nodes?.filter((e) => e.canRead)
setData(filterData)
},
})
return (
<ObjectSelector<Node>
name={props.name}
onChange={props.onChange}
options={data as any}
getOptionLabel={(option) => option?.displayName as string}
getOptionValue={(option) => `${option.slug}`}
components={{ Option: OptionWithDescription }}
/>
)
}
The thing is #graphql-codegen/cli does not export type for the NodeType.
This is my codegen config
import { CodegenConfig } from '#graphql-codegen/cli'
const config: CodegenConfig = {
schema: './app/graphql/schema.graphql',
documents: ['./facerberus/components/**/*.ts'],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
'./facerberus/__generated__/': {
preset: 'client',
plugins: [],
presetConfig: {
gqlTagName: 'gql',
},
},
},
}
export default config
which config to make codegen export type of NodeType or how to achieve it via ts
Codegen doesn't generate a standalone type for every part of the operaation.
But you can easily extract the needed part from the operation type. Should be something like this:
type NodeType = AdvertCustomFieldsQuery['advertCustomFields']['nodes'][number]
I hope you're doing fine 👋.
Playing around with the new internalization NextJS feature, I found out an error I was not expecting at all.
If I pass a plain string as prop from the getStaticProps to the Page Component everything works fine in the default locale, but if I pass down an object instead of a plain string, it breaks.
I leave both codes here.
The following code is the one that works fine 👇:
it passes down a string value
works fine in both locales /en and /es even without it (it picks the default locale)
import { useRouter } from "next/dist/client/router";
export async function getStaticPaths() {
return {
paths: [
{ params: { name: `victor`, locale: "es" } },
{ params: { name: `victor`, locale: "en" } },
],
fallback: true,
};
}
export async function getStaticProps(context) {
const { params } = context;
const { name } = params;
return {
props: {
name,
},
};
}
/*
* ✅ The following URLs work
* - localhost:3000/victor
* - localhost:3000/en/victor
* - localhost:3000/es/victor
*
* TypeError: Cannot destructure property 'name' of 'person' as it is undefined.
*/
export default function PageComponent({ name }) {
const router = useRouter();
return (
<>
<div>The name is: {name}</div>
<div>Locale used /{router.locale}/</div>
</>
);
}
The following code is the one that DOESN'T WORK 👇:
It passes down an object
Works fine in '/en' and without explicit locale (default locale) but doesn't work with /es
import { useRouter } from "next/dist/client/router";
export async function getStaticPaths() {
return {
paths: [
{ params: { name: `victor`, locale: "es" } },
{ params: { name: `victor`, locale: "en" } },
],
fallback: true,
};
}
export async function getStaticProps(context) {
const { params } = context;
const { name } = params;
return {
props: {
person: {
name,
},
},
};
}
/*
* ✅ The following URLs work
* - localhost:3000/victor
* - localhost:3000/en/victor
*
* 👎 The following URLs DOESN'T work
* - localhost:3000/es/victor
*
* TypeError: Cannot destructure property 'name' of 'person' as it is undefined.
*/
export default function PageComponent(props) {
const router = useRouter();
const { person } = props;
const { name } = person;
return (
<>
<div>The name is: {name}</div>
<div>Locale used /{router.locale}/</div>
</>
);
}
It is because you are using fallback: true.
The page is rendered before the data is ready, you need to use router.isFallback flag to handle this situation inside your component:
if (router.isFallback) {
return <div>Loading...</div>
}
Or you can use fallback: 'blocking' to make Next.js wait for the data before render the page.
More info in the docs
How to write a generic so that the Result takes on the argument of another type supplied to the generic?
This is my example code.
import { Story } from '#storybook/react/types-6-0';
type TKeyAny = {
[key: string]: string; // | 'other args values here...';
};
// this fails
export const bindargs = <A extends TKeyAny, T extends Story<A>>(args: A, Template: T): T => {
const Comp = Template.bind({});
Comp.args = args;
return Comp;
};
export default bindargs;
This would work but its not specific to the arguments being passed into it, which is why I'd like a generic:
// This works but I'd like this instead to be in a generic
// export const bindargs = (args: TKeyAny, Template: Story<TKeyAny>): Story<TKeyAny> => {
// const Comp = Template.bind({});
// Comp.args = args;
// return Comp;
// };
We use generic like this in our Storybook:
Maybe you can do a similar thing
import React from 'react'
import { Meta, Story } from '#storybook/react/types-6-0'
import SelectDropdown, { ISelectDropdownProps, SelectOption } from './SelectDropdown'
export default {
title: 'Shared/Components/SelectDropdown',
component: SelectDropdown,
argTypes: {
options: {
control: {
type: 'object',
},
},
(...)
} as Meta
const Template = <T extends {}>(): Story<ISelectDropdownProps<T>> => args => (
<div
style={{
width: '300px',
}}
>
<SelectDropdown<T> {...args} />
</div>
)
export const Normal = Template<number>().bind({})
Normal.args = {
options: createOptions(5),
}
I liked the idea of a helper function so I went playing around. It does cast to any inside the function but it doesn't matter for it's usage.
import React from "react";
import { Meta, Story } from "#storybook/react";
import { Box, BoxProps } from "./Box";
type BindTemplate = <T extends Story<any>>(template: T, args: T["args"]) => T;
const bindTemplate: BindTemplate = (template, args) => {
const boundTemplate = (template as any).bind({});
boundTemplate.args = args;
return boundTemplate;
};
const config: Meta = {
title: "Box",
component: Box,
};
export default config;
const Template: Story<BoxProps> = (args) => <Box {...args}>lol</Box>;
export const Primary = bindTemplate(Template, {
className: "valid",
someInvalidProps: "invalid",
});
Tracker.autorun not working inside componentDidMount of react when I specify the projection (fields) for output. But the same works when I dont have any projection on mongo query.
This works:
Meteor.subscribe('quotes');
this.quotesTracker = Tracker.autorun(() => {
const quotes = Quotes.find(
{instrument_token: 12374274},
{
sort: {timestamp: 1},
limit: 5000
}
).fetch();
This doesnt work
Meteor.subscribe('quotes');
this.quotesTracker =Tracker.autorun(() => {
const quotes = Quotes.find(
{instrument_token: 12374274},
{
fields: {
last_price: 1,
timestamp: 1,
},
sort: {timestamp: 1},
limit: 5000
}
).fetch();
What am I missing here?
I don't think Meteor's tracker works well with ReactJS, as their mechanism of re-render is different.
You might want to use this package.
https://github.com/meteor/react-packages/tree/devel/packages/react-meteor-data
You can use it like so.
import { Meteor } from 'meteor/meteor';
import { mount } from 'react-mounter';
import { withTracker } from 'meteor/react-meteor-data';
import { IndexPage } from "./index-page";
FlowRouter.route('/', {
action: () => {
mount(IndexPageContainer, {});
}
});
export const IndexPageContainer = withTracker(() => {
Meteor.subscribe('whatever');
return {
Meteor: {
collection: {
whatever: Whatever.find().fetch()
},
user: Meteor.user(),
userId: Meteor.userId(),
status: Meteor.status(),
loggingIn: Meteor.loggingIn()
}
};
})(IndexPage);
Where IndexPage is your actual component.
You can then access the db by this.props.Meteor.collection.whatever.find()
I use react-native-code-push. which is:
This plugin provides client-side integration for the CodePush service,
allowing you to easily add a dynamic update experience to your React
Native app(s).
but In some of native implementations of navigation like react-native-navigation there isn't any root component.
the app will start calling a function like this:
// index.js
import { Navigation } from 'react-native-navigation';
Navigation.startTabBasedApp({
tabs: [
{
label: 'One',
screen: 'example.FirstTabScreen', // this is a registered name for a screen
icon: require('../img/one.png'),
selectedIcon: require('../img/one_selected.png'), // iOS only
title: 'Screen One'
},
{
label: 'Two',
screen: 'example.SecondTabScreen',
icon: require('../img/two.png'),
selectedIcon: require('../img/two_selected.png'), // iOS only
title: 'Screen Two'
}
]
});
// or a single screen app like:
Navigation.registerComponent('example.MainApplication', () => MainComponent);
Navigation.startSingleScreenApp({
screen: {
screen: 'example.MainApplication',
navigatorButtons: {},
navigatorStyle: {
navBarHidden: true
}
},
})
since there is no root component, It's not clear where should I call CodePush, since normally I should wrap my whole root component with CodePush like a higher order component.
what I used to do was:
// index.js
class MyRootComponent extends Component {
render () {
return <MainNavigator/> // a navigator using react-navigation
}
}
let codePushOptions = {
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
installMode: CodePush.InstallMode.ON_NEXT_RESUME
}
export default CodePush(codePushOptions)(MyRootComponent)
Is there a proper way to solve this problem!?
I know I could do this:
Navigation.registerComponent('example.MainApplication', () => CodePush(codePushOptions)(RootComponent));
Navigation.startSingleScreenApp({
screen: {
screen: 'example.MainApplication',
navigatorButtons: {},
navigatorStyle: {
navBarHidden: true
}
},
})
but then I should use a Navigator only for projecting my root component, and It doesn't look like a good idea. I think this problem probably has a best-practice that I'm looking for.
UPDATE
I think there are some complications registering a tab navigator inside a stacknavigator in react-native-navigation at least I couldn't overcome this problem. example tabBasedApp in react-native-navigation with react-native-code-push, will be all that I need.
Thanks for the previous code snippets. I was able to get code push check on app resume and update immediately with react-native-navigation V2 with the below code without requiring wrapper component for codePush. This is the relevant part of the app startup logic.
Navigation.events().registerAppLaunchedListener(() => {
console.log('Navigation: registerAppLaunchedListener ')
start()
})
function checkCodePushUpdate () {
return codePush.sync({
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
installMode: codePush.InstallMode.IMMEDIATE,
deploymentKey: CODEPUSH_KEY,
})
}
function start () {
checkCodePushUpdate ()
.then(syncStatus => {
console.log('Start: codePush.sync completed with status: ', syncStatus)
// wait for the initial code sync to complete else we get flicker
// in the app when it updates after it has started up and is
// on the Home screen
startApp()
return null
})
.catch(() => {
// this could happen if the app doesn't have connectivity
// just go ahead and start up as normal
startApp()
})
}
function startApp() {
AppState.addEventListener('change', onAppStateChange)
startNavigation()
}
function onAppStateChange (currentAppState) {
console.log('Start: onAppStateChange: currentAppState: ' + currentAppState)
if (currentAppState === 'active') {
checkCodePushUpdate()
}
}
function startNavigation (registered) {
console.log('Start: startNavigation')
registerScreens()
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'FirstScreen,
},
}],
},
},
})
}
I got it working this way, although this is for RNN v2
// index.js
import App from './App';
const app = new App();
// App.js
import CodePush from 'react-native-code-push';
import { Component } from 'react';
import { AppState } from 'react-native';
import { Navigation } from 'react-native-navigation';
import configureStore from './app/store/configureStore';
import { registerScreens } from './app/screens';
const appStore = configureStore();
registerScreens(appStore, Provider);
const codePushOptions = {
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
updateDialog: true,
installMode: CodePush.InstallMode.IMMEDIATE
};
export default class App extends Component {
constructor(props) {
super(props);
// Set app state and listen for state changes
this.appState = AppState.currentState;
AppState.addEventListener('change', this.handleAppStateChange);
this.codePushSync();
Navigation.events().registerAppLaunchedListener(() => {
this.startApp();
});
}
handleAppStateChange = nextAppState => {
if (this.appState.match(/inactive|background/) && nextAppState === 'active') {
this.handleOnResume();
}
this.appState = AppState.currentState;
};
codePushSync() {
CodePush.sync(codePushOptions);
}
handleOnResume() {
this.codePushSync();
...
}
startApp() {
Navigation.setRoot({
root: {
stack: {
children: [
{
component: {
name: 'MyApp.Login'
}
}
]
}
}
});
}
}
// app/screens/index.js
import CodePush from 'react-native-code-push';
import { Navigation } from 'react-native-navigation';
import Login from './Login';
function Wrap(WrappedComponent) {
return CodePush(WrappedComponent);
}
export function registerScreens(store, Provider) {
Navigation.registerComponentWithRedux(
'MyApp.Login',
() => Wrap(Login, store),
Provider,
store.store
);
...
}
I found the answer myself.
Look at this example project structure:
.
├── index.js
├── src
| └── app.js
└── screens
├── tab1.html
└── tab2.html
you can register you code-push in index.js.
//index.js
import { AppRegistry } from 'react-native';
import App from './src/app';
import CodePush from 'react-native-code-push'
let codePushOptions = {
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
installMode: CodePush.InstallMode.ON_NEXT_RESUME
}
AppRegistry.registerComponent('YourAppName', () => CodePush(codePushOptions)(App));
now you can start react-native-navigation in app.js like this:
import {Navigation} from 'react-native-navigation';
import {registerScreens, registerScreenVisibilityListener} from './screens';
registerScreens();
registerScreenVisibilityListener();
const tabs = [{
label: 'Navigation',
screen: 'example.Types',
icon: require('../img/list.png'),
title: 'Navigation Types',
}, {
label: 'Actions',
screen: 'example.Actions',
icon: require('../img/swap.png'),
title: 'Navigation Actions',
}];
Navigation.startTabBasedApp({
tabs,
tabsStyle: {
tabBarBackgroundColor: '#003a66',
tabBarButtonColor: '#ffffff',
tabBarSelectedButtonColor: '#ff505c',
tabFontFamily: 'BioRhyme-Bold',
},
appStyle: {
tabBarBackgroundColor: '#003a66',
navBarButtonColor: '#ffffff',
tabBarButtonColor: '#ffffff',
navBarTextColor: '#ffffff',
tabBarSelectedButtonColor: '#ff505c',
navigationBarColor: '#003a66',
navBarBackgroundColor: '#003a66',
statusBarColor: '#002b4c',
tabFontFamily: 'BioRhyme-Bold',
}
});