React Storybook passing functions in args - reactjs

I currently struggle to finish a story in React story for one of my components : (images below)
My component receives a props from a parent, a boolean and a function to modify this boolean. When I click on a button it should change the value of this boolean (false to true or true to false).
I can't seem to test this behaviour on storybook. I don't know if I do things the right way, but it seems impossible to pass a function from my .Stories filecode to my component to test it.
My question is : Am i doing things the right way and is storybook built for this kind of test ?
story file code :
import React from 'react';
import { ComponentStory, ComponentMeta } from '#storybook/react';
import { ModelCard } from './';
export default {
title: 'ModelCard',
component: ModelCard,
argTypes: {
yearProduct: { control : 'text'},
ecoDesigned: { control: 'boolean'},
titleProduct: {control: 'text'},
photoProduct: {control: 'text'},
setEcoDesigned: {action: 'clicked'}
}
} as ComponentMeta<typeof ModelCard>;
const Template: ComponentStory<typeof ModelCard> = (args) => <ModelCard {...args}/>;
export const ModelCardCompleteControls = Template.bind({});
ModelCardCompleteControls.args = {
yearProduct: '2018',
ecoDesigned: false,
titleProduct: '66180 - W200 S | 1019507 - ATHLLE Watches or Stopwatche 7026 2021 | GEDS',
photoProduct: 'https://picsum.photos/200',
};
My component code :
import React from 'react';
import { useState } from 'react';
import { VtmnButton, VtmnIcon } from '#vtmn/react';
import { EcoDesignedDot } from './EcoDesignedDot';
import './modelcard.scss';
interface ModelCardProps {
photoProduct: string;
yearProduct: string,
titleProduct: string,
ecoDesigned: boolean;
setEcoDesigned: (ecoDesigned: boolean) => void;
}
export const ModelCard = ({ yearProduct, titleProduct, photoProduct, ecoDesigned, setEcoDesigned }: ModelCardProps) => {
const [open, setOpen] = useState(false);
return (
<article className="model-card">
<section className="vtmn-grid vtmn-grid-cols-12 vtmn-items-center vtmn-space-y-5">
<p className="vtmn-col-span-1">{yearProduct}</p>
<img className="vtmn-col-span-1"
style={{ borderRadius: 5 }}
src={photoProduct} width={60}
height={60} />
<p className="vtmn-col-span-6">{titleProduct}</p>
<div className="vtmn-col-span-3">
<EcoDesignedDot ecoDesigned={ecoDesigned}/>
</div>
<div className="vtmn-col-span-1" onClick={() => setOpen(!open)}>
<VtmnIcon value="arrow-up-s-line" className={open ? 'reversed_angle' : 'original_angle'} />
</div>
</section>
<section className="vtmn-grid vtmn-grid-cols-12">
{
open && <div className="vtmn-col-start-3 vtmn-col-span-5">
<p>
Votre produit est-il éco-design ?
</p>
<VtmnButton onClick={() => setEcoDesigned(true)} variant={ecoDesigned ? 'primary' : 'secondary'} size="medium">Oui</VtmnButton> // This is what I'm talking about
<VtmnButton onClick={() => setEcoDesigned(false)} variant={ecoDesigned ? 'secondary' : 'primary'} size="medium">Non</VtmnButton> // This is what I'm talking about
</div>
}
</section>
</article>
);
};

You can add useState in the Template function because it's a react component but, make sure that you send these values to the ModelCard component properly.
const Template: ComponentStory<typeof ModelCard> = (args) => {
const [ecoDesigned, setEcoDesigned] = useState(false);
return <ModelCard {...args} ecoDesigned={ecoDesigned} setEcoDesigned={setEcoDesigned}/>;
};
The args object will have all the default props. So, you should make sure that these are overwritten.

Related

How do you mock data for a provider in Jest? - "Input data should be a String"

I am testing a React component using Jest and need to mock data to override the default value of the provider. The issue I am having is that I cannot seem to structure the data properly to pass to the provider in my test.
Here is the test:
// mocked props
const commentId = 'commentId'
const page = '/user'
// mocked data for provider
const mockData = {
commentsById: {
commentId: 'string',
},
}
test('should render CommentBlock correctly with props', () => {
render(
<PostContext.Provider value={mockData}>
<CommentBlock commentId={commentId} page={page} />
</PostContext.Provider>
)
screen.debug()
})
I believe the mockData value in the test has to be changed. The error is thrown when I run the test and line 28 and 29 in the component get an undefined value the data overriding the provider is structured improperly.
Here is the component I am testing:
// import BlockButton from './blockButton';
import { useState, useContext } from 'react'
import { useHistory } from 'react-router-dom'
import dateformat from 'dateformat'
import { observer } from 'mobx-react-lite'
import UnirepContext from '../../context/Unirep'
import PostContext from '../../context/Post'
import { EXPLORER_URL } from '../../config'
import { Page, ButtonType } from '../../constants'
import BlockButton from './blockButton'
import MarkdownIt from 'markdown-it'
const markdown = new MarkdownIt({
breaks: true,
html: false,
linkify: true,
})
type Props = {
commentId: string
page: Page
}
const CommentBlock = ({ commentId, page }: Props) => {
const postContext = useContext(PostContext)
const comment = postContext.commentsById[commentId]
const commentHtml = markdown.render(comment.content)
const unirepConfig = useContext(UnirepContext)
const date = dateformat(new Date(comment.post_time), 'dd/mm/yyyy hh:MM TT')
const history = useHistory()
const [isEpkHovered, setEpkHovered] = useState<boolean>(false)
const gotoPost = () => {
if (page === Page.User) {
history.push(`/post/${comment.post_id}`, { commentId: comment.id })
}
}
return (
<div className="comment-block">
<div className="block-header comment-block-header no-padding">
<div className="info">
<span className="date">{date} |</span>
<span
className="user"
onMouseEnter={() => setEpkHovered(true)}
onMouseLeave={() => setEpkHovered(false)}
>
Post by {comment.epoch_key}{' '}
<img
src={require('../../../public/images/lighting.svg')}
/>
{isEpkHovered ? (
<span className="show-off-rep">
{comment.reputation ===
unirepConfig.commentReputation
? `This person is very modest, showing off only ${unirepConfig.commentReputation} Rep.`
: `This person is showing off ${comment.reputation} Rep.`}
</span>
) : (
<span></span>
)}
</span>
</div>
<a
className="etherscan"
target="_blank"
href={`${EXPLORER_URL}/tx/${comment.id}`}
>
<span>Etherscan</span>
<img
src={require('../../../public/images/etherscan.svg')}
/>
</a>
</div>
<div
className="block-content no-padding-horizontal"
onClick={gotoPost}
>
<div
style={{
maxHeight: page == Page.Home ? '300px' : undefined,
overflow: 'hidden',
}}
dangerouslySetInnerHTML={{
__html: commentHtml,
}}
/>
</div>
<div className="block-buttons no-padding">
<BlockButton
type={ButtonType.Boost}
count={comment.upvote}
data={comment}
/>
<BlockButton
type={ButtonType.Squash}
count={comment.downvote}
data={comment}
/>
<BlockButton type={ButtonType.Share} count={0} data={comment} />
</div>
</div>
)
}
export default observer(CommentBlock)
This was solved by properly mocking the data with this structure:
const postData = {
commentsById: {
commentId: {
id: 'commentId',
content: 'string',
post_time: '00',
reputation: 30,
epoch_key: 'epoch_key test',
},
},
}

what's the difference between 'onClick' and 'onMouseEnter' in React?

I am trying to add side toggle menu box in main page which is positioning on right side of document when the button is clicked.
I made a function for toggling action(you can check the function below) with some of react hooks and added by using onClick() method. I clicked the btn to check if it works, but it doesn't. I changed onClick() method to onMouseEnter() and it worked. I added callback(onClick(()=>{function()})) but it still doesn't work.
I think toggling function doesn't have any problem(because it worked properly when it's on onMouseEnter). Some mechanisms of them makes the difference. I checked the docs of javascript but it was not helpful.
I wish somebody provide me a demonstration of this.
Here is my codes.
import React, { useState } from "react";
import "../css/side-toggle.css";
import hbgBtn from "../image/hamburgerBtn.png";
const SideBar = ({ wid = 380, children }) => {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => {
setIsOpen((isOpen) => !isOpen);
console.log('1')
};
const closeMenu = () => {
setIsOpen((isOpen) => !isOpen);
}
return (
<div className="container" wid={wid}>
<img onMouseEnter={(e) => toggleMenu(e)}
className={!isOpen ? "show-btn" : "hide"}
src={hbgBtn}
alt=""
/>
<div onMouseEnter={closeMenu} className={isOpen? "dimmer" : 'hide'}>{children}</div>
<div className={isOpen ? "side-column" : "hide"}></div>
</div>
);
};
export default SideBar;
(and i will appreciate when you understand my wierd English. I'm ESL)
There is a possiblity that you may have made some error while styling. I have tried my best to replicate your code and it works for me fine.
import React, { useState } from 'react';
import './style.css';
const SideBar = ({ wid = 380, children }) => {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => {
setIsOpen((isOpen) => !isOpen);
console.log('1');
};
const closeMenu = () => {
setIsOpen((isOpen) => !isOpen);
};
return (
<div className="container">
<div onClick={(e) => toggleMenu(e)}>
<img
className={!isOpen ? 'show-btn' : 'hide'}
src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Hamburger_icon.svg/1024px-Hamburger_icon.svg.png"
alt=""
/>
</div>
<div onMouseEnter={closeMenu} className={isOpen ? 'dimmer' : 'hide'}>
{children}
</div>
<div className={isOpen ? 'side-column' : 'hide'}></div>
</div>
);
};
export default SideBar;
Here is the stackblitz code link
https://stackblitz.com/edit/react-xtwxzc?file=src/App.js

A method triggers a console.log from another component

When I click on bet now the function triggers a console.log from another component. betNow should group all the inputs from stake in one common array but when I click on it it renders the console log from stake and includes all the values that I typed into one array. Everything works but not as I wish. The parent component should display the common array with all the values. I do not understand why it is happening.Could anyone explain me why is reacting like that? Thanks in advance
Parent Component
import React, { useState } from 'react';
import Button from '#material-ui/core/Button';
import FilterMenu from "./selectButton";
import FetchRandomBet from "./fetchRandomBets";
function Betslip() {
const data = [
{
value: 0,
label: "No Filter"
},
{
value: 1,
label: "Less than two"
},
{
value: 2,
label: "More than two"
},
]
const [selectedValue, setSelectedValue] = useState(0);
const [allStakes, setAllStakes] = useState([]);
const handleChange = obj => {
setSelectedValue(obj.value);
}
const betNow = () => {
const stakes = localStorage.getItem("stakes");
const jsnStake = JSON.parse(stakes) || [];
setAllStakes([...allStakes, jsnStake]);
}
return (
<div className="betslip">
<div className="betslip-top">
<h1 className="text">BETSLIP</h1>
<p className="text-two">BET WITH US!</p>
<div>
<FilterMenu
optionsProp={data}
valueProp={selectedValue}
onChangeProp={handleChange}
/>
</div>
</div>
<div>
<FetchRandomBet
valueProp={selectedValue}
/>
</div>
<Button
onClick={betNow}
className="betnow"
variant="contained"
>
Bet Now!
</Button>
</div>
);
}
export default Betslip;
Child Component
import React, { useState, useEffect } from 'react';
function Stake() {
const [stakes, setStakes] = useState([]);
const addStake = (e) => {
e.preventDefault();
const newStake = e.target.stake.value;
setStakes([newStake]);
};
useEffect(() => {
const json = JSON.stringify(stakes);
localStorage.setItem("stakes", json);
}, [stakes]);
console.log(stakes)
return (
<div>
<form onSubmit={addStake}>
<input
style={{
marginLeft: "40px",
width: "50px"
}}
type="text"
name="stake"
required
/>
</form>
</div>
);
}
export default Stake;
You have this console.log in you function that will run every time the component is rendered, since it´s outside of any function:

Ionic popover with React - How to make it sticky to the button

Following the Ionic documentation, I am trying to get the popover sticky to the button (like on their own example).
Unfortunately I do not know how to achieve this...
Thanks
import React, { useState } from 'react';
import { IonPopover, IonButton } from '#ionic/react';
export const PopoverExample: React.FC = () => {
const [showPopover, setShowPopover] = useState(false);
return (
<>
<IonPopover
isOpen={showPopover}
onDidDismiss={e => setShowPopover(false)}
>
<p>This is popover content</p>
</IonPopover>
<IonButton onClick={() => setShowPopover(true)}>Show Popover</IonButton>
</>
);
};
You also need to include an event in the showPopover hook -
const [showPopover, setShowPopover] = useState<{open: boolean, event: Event | undefined}>({
open: false,
event: undefined,
});
<IonPopover
isOpen={showPopover.open}
event={showPopover.event}
onDidDismiss={e => setShowPopover({open: false, event: undefined})}
>
<p>This is popover content</p>
</IonPopover>
<IonButton onClick={(e) => setShowPopover({open: true, event: e.nativeEvent})}>Click</IonButton>

Refactoring a ReactJS class to a Hook issue

I have the following class that works fine (I know that DOMSubtreeModified is depreacted, I will update this too). It's a very basic WYIWYG I'm trying to refactor to a hook:
export class TextEditorClass extends React.Component {
constructor(props) {
super(props)
this.state ={
// this props contains the html content of the contentEditable
content: this.props.content,
}
}
componentDidMount() {
// here I add a listener to the contentEditable div that calls updateContent
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
rangy.init();
}
setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
updateContent() {
this.props.setContent('content', document.getElementById('editor').innerHTML);
this.setState({
content: document.getElementById('editor').innerHTML,
})
}
render() {
return (
<div className='editor-content dashed'>
<input id="myInput" type="file" ref={(ref) => this.upload = ref} style={{ display: 'none' }} />
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
}
This is what I wrote so far:
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
function updateContent() {
value.dispatch({type: 'content', value: document.getElementById('editor').innerHTML});
}
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
But it doesn't work:
Warning: A component is changing a controlled input of type text to be
uncontrolled. Input elements should not switch from controlled to
uncontrolled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component.
Second warning:
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn't have a dependency array, or one of the dependencies changes on
every render.
The reason behind this I suppose is the fact that the typing triggers a loop of rerenders. Why it doesn't happen in the class?
EDIT: I believe the key to fix the issue is in useEffect(); I suppose it behaves differently from componentDidMount, in fact in the hook version I had to swap these two lines to prevent another error from occurring from this:
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
to this:
document.getElementById("editor").innerHTML = this.props.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
EDIT 2:
Adding [] as a parameter for useEffect() I got rid of the first error:
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
}, []);
More info about it here: https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
Your issue does not happen in React class component because componentDidMount is run once and you put the code of componentDidMount in useEffect without any conditions - which means it will run on each render.
Problems with your useEffect:
1) You are adding an event listender but you are not doing a clean-up afterwards.
2)You are mutating the DOM directly which is an anti pattern
3)You are applying any depedencies on your useEffect, which execut the useEffect on each render and will cause a memory leak.
Here is how your code should look like: (will be writing code shortly)
...
const [editorValue, setEditorValue] = React.useState('')
// If you want your hook to run once
React.useEffect( () => {
setEditorValue(value.state.content)
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
// The returning funnction will be executed on unmounting
() => document.getElementById("editor").removeEventListener("DOMSubtreeModified", () => updateContent(), false);
// note the `[]`] as second argument. That means to run only once.
}, [])
And the content of your #editor is then the controllable state editorValue.
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
{editorValue}
</div>
Matthew's answer pointed me to the right direction, and after a bit of tweaking I came up with this code that works fine:
import React from 'react'
import { Icon, IconButton, ButtonGroup, ButtonToolbar } from 'rsuite'
import rangy from 'rangy';
import 'rangy/lib/rangy-classapplier';
import dompurify from 'dompurify';
import './TextEditor.css';
import { ManagerContext } from './LessonManager.js';
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
// here I initialize the editor content with the content I receive from the provider
const [editorValue, setEditorValue] = React.useState(value.state.content);
React.useEffect( () => {
rangy.init();
const editor = document.getElementById('editor');
// here I replaced the deprecated DOMSubtreeModified with MutationObserver
// basically when you change the dom of the contentEditable I call dispatch
// and update the content
let mutationObserver = new MutationObserver(function(mutations) {
value.dispatch({type: 'content', value: editor.innerHTML});
});
mutationObserver.observe(editor, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
// the cleanup can be done with .disconnect();
return () => mutationObserver.disconnect();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
// the createMarkup() function is used to generate the markup that I will put inside
// my contentEditable, I installed an additional npm package (dompurify) to clean up
// the markup to prevent XSS attacks
// https://dev.to/jam3/how-to-prevent-xss-attacks-when-using-dangerouslysetinnerhtml-in-react-1464)
function createMarkup() {
const sanitizer = dompurify.sanitize;
return {__html: sanitizer(editorValue)}
};
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
// here I insert the html with the purified html
dangerouslySetInnerHTML={createMarkup()}
>
</div>
</div>
)
}
To give a complete answer I will add also the parent components. This is the LessonContents component:
import React, { useEffect } from 'react';
import { Input } from 'rsuite';
// TextEditorHook is the WYSIWYG component
import { TextEditorHook } from './TextEditor.js';
// this context contain the value of the lesson manager
// we will use the context for the lesson's title, desc, content and location
import { ManagerContext } from './LessonManager.js';
// truncate is a function to shorten text in order to make it fit
// maxLenght is the default max text length
import { truncate, maxLength } from '../Common.js';
export default function Menu() {
const value = React.useContext(ManagerContext);
useEffect(() => {
// if not editing inject the html in the display div on load
if (!value.editing) document.querySelector('.lesson-content').innerHTML = value.state.content;
console.log(value);
});
const editorContent = () => {
return (
<React.Fragment>
<Input
className='rsuite-input dashed'
placeholder='Lesson title'
value={value.state.title}
onChange={(v) => value.dispatch({type: 'title', value: v})}
/>
<Input
className='rsuite-input dashed'
componentClass="textarea"
rows={1}
style={{ width: '100%' }}
placeholder='Lesson description'
value={value.state.desc}
onChange={(v) => value.dispatch({type: 'desc', value: v})}
/>
<TextEditorHook/>
</React.Fragment>
)
}
const viewerContent = () => {
return (
<React.Fragment>
<div className='content'>
<span>{truncate(value.state.title, maxLength, false)}</span>
</div>
<div className='content'>
<span>{value.state.desc}</span>
</div>
<div className='content'>
<span>{truncate(value.state.location, maxLength, false)}</span>
</div>
<div className='content justify-left lesson-content'>
</div>
</React.Fragment>
)
}
return (
<React.Fragment>
{value.editing ? editorContent() : viewerContent()}
</React.Fragment>
)
}
And finally the LessonManager component:
import React from 'react';
import LessonMenu from './LessonMenu.js';
// this context will be used from child components to access the lesson status
import LessonContents from './LessonContents.js';
export const ManagerContext = React.createContext(null);
const initialState = {
title: 'Lesson title',
desc: 'Lesson description',
content: 'Lesson content',
location: 'home / english'
};
function reducer(state, action) {
console.log(action.type, action.value, state)
switch (action.type) {
case 'title':
return {...state, title: action.value};
case 'desc':
return {...state, desc: action.value};
case 'content':
return {...state, content: action.value};
default:
throw new Error();
}
}
export default function LessonManager() {
const [editing, toggleEditor] = React.useState(true);
const [state, dispatch] = React.useReducer(reducer, initialState);
const value = React.useMemo(() => {
return {
state,
dispatch,
editing,
toggleEditor,
}
}, [state, editing]);
return (
<ManagerContext.Provider value={value}>
<div className='box-default expand'>
<div className='handle' style={{display: 'flex', justifyContent: 'center', width: '100%', cursor: 'grab'}}>
<LessonMenu />
</div>
<LessonContents />
</div>
</ManagerContext.Provider>
)
}
I am not completely sure this is the best approach to solve this problem, but it works fine.

Resources