Formik QRcodeReader button submit for itself - reactjs

I created a custom QRcode reader that added as button to formik customTextInout.tsx but when I click on it, it will submit with no value in it.
in this code I created a button that when you click on it, it will show the QrCodeReader.
QrReaderButton.tsx
import { useState } from "react";
import { QrReader } from "react-qr-reader";
import qrcode from "../assets/qr-code.png";
interface Props {
setQrData: any;
}
const QrReaderButton: React.FC<Props> = (props) => {
const [data, setData] = useState<any>();
const [show, setShow] = useState<boolean>(false);
let a;
return (
<div className="flex flex-col w-max items-center">
<button
type="button"
onClick={() => (!show ? setShow(true) : setShow(false))}
>
<img src={qrcode} alt="qr-code" width={40} />
</button>
{show && (
<div className="w-96">
<QrReader
onResult={(result: any, error) => {
if (!!result) {
setData(JSON.parse(result?.text));
props.setQrData(JSON.parse(result?.text));
}
if (!!error) {
// console.info(error);
}
}}
constraints={{ facingMode: "user" }}
/>
<p> {data ? JSON.stringify(data) : "No result"}</p>
</div>
)}
</div>
);
};
export default QrReaderButton;
in this code I created a custom input for formik with help of formik official docs
CustomTextInput.tsx
import {
FormControl,
FormHelperText,
FormLabel,
Input,
} from "#chakra-ui/react";
import QrReaderButton from "../QrReaderButton";
const CustomTextInput = ({
field,
form: { touched, errors },
...props
}: any) => {
return (
<FormControl isInvalid={errors.name && touched.name}>
<FormLabel>{props.label}</FormLabel>
<div className="flex gap-2">
<Input {...field} {...props} />
<div className="w-max">
<QrReaderButton
setQrData={() => {
console.log("test");
}}
/>
</div>
</div>
<FormHelperText>this is helper</FormHelperText>
</FormControl>
);
};
export default CustomTextInput;
in this code I used CustomTextInout component with chakra-ui modal.
FormModal.tsx
import {
useDisclosure,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
FormLabel,
useToast,
ModalFooter,
} from "#chakra-ui/react";
import { Formik, Form, Field } from "formik";
import { useNavigate } from "react-router-dom";
import CustomNumberInput from "./CustomInputs/CustomNumberInput";
import CustomOptionInput from "./CustomInputs/CustomOptionInput";
import CustomTextInput from "./CustomInputs/CustomTextInput";
import TokenButton from "./TokenButton";
interface Props {
key: string;
Title: string;
buttonValue?: string;
image: string;
nav: string;
initialValues: any;
isLoading: boolean;
apiHook: any;
validateName: any;
}
const FormModal: React.FC<Props> = (props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const navigate = useNavigate();
const toast = useToast();
return (
<div className="w-full content-center ">
<TokenButton
nav={props.nav}
image={props.image}
title={props.Title}
onClick={onOpen}
/>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{props.Title}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Formik
initialValues={props.initialValues}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
props
.apiHook(values)
.unwrap()
.catch((e: any) => {
// alert(JSON.stringify(e, null, 2))
toast({
status: "error",
title: "Error",
description: JSON.stringify(e, null, 2),
isClosable: true,
});
if (e.status == 401) {
setTimeout(() => {
navigate("/login");
}, 1000);
localStorage.clear();
}
});
}}
>
{(propsForm) => (
<Form
onSubmit={propsForm.handleSubmit}
onReset={propsForm.handleReset}
className="w-full h-full bg-[#ced8d7ac] shadow-2xl rounded-2xl p-4 items-center content-center text-current"
>
<FormLabel>{props.Title}</FormLabel>
<div className="grid lg:grid-cols-1 md:grid-cols-2 gap-4">
{Object.keys(props.initialValues).map((key) => {
return (
<Field
id={key}
key={key}
name={key}
label={key}
backgroundColor="#ffffffe1"
component={CustomTextInput}
/>
);
})}
</div>
<Button
type="submit"
isLoading={props.isLoading}
className="gre mt-2"
>
{props.Title}
</Button>
</Form>
)}
</Formik>
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={onClose}>
Close
</Button>
<Button variant="ghost">{props.buttonValue}</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
);
};
export default FormModal;

Default behavior of button in form is submit. so you need to make sure that you specified a type except the submit.
See reference in here
Your code will be like below
QrReaderButton
import { useState } from "react";
import { QrReader } from "react-qr-reader";
import qrcode from "../assets/qr-code.png";
interface Props {
setQrData: any;
}
const QrReaderButton: React.FC<Props> = (props) => {
const [data, setData] = useState<any>();
const [show, setShow] = useState<boolean>(false);
let a;
return (
<div className="flex flex-col w-max items-center">
<button
type="button" //make sure you add this
onClick={() => (!show ? setShow(true) : setShow(false))}
>
<img src={qrcode} alt="qr-code" width={40} />
</button>
{show && (
<div className="w-96">
<QrReader
onResult={(result: any, error) => {
if (!!result) {
setData(JSON.parse(result?.text));
props.setQrData(JSON.parse(result?.text));
}
if (!!error) {
// console.info(error);
}
}}
constraints={{ facingMode: "user" }}
/>
<p> {data ? JSON.stringify(data) : "No result"}</p>
</div>
)}
</div>
);
};
export default QrReaderButton;

Related

How can I use forwardRef in React Component?

Can someone help me? I'm creating a component inside React, and I want to make it more accessible using forwardRef. In my case, I'm making a button and I'm using the button's properties, and a few more I've done to make it more dynamic.
This is a summary of my code.
export interface ButtonProps
extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
children?: React.ReactNode;
loading?: boolean;
}
class Button extends React.Component<ButtonProps> {
render() {
const {
...otherProps
} = this.props;
return (
<button {...(otherProps)}></button>
)
}
}
export default Button;
I tried to start something, but right away it gave an error
const ForwardedElement = React.forwardRef<ButtonProps, HTMLButtonElement> (
(props: ButtonProps, ref) => <Button {...props}/>
)
export default ForwardedElement;
I suggest you to use useImperativeHandle hook
useImperativeHandle customizes the instance value that is exposed to parent components when using ref. Let's visualize that with an example.
Here's a component as search bar
import React, {forwardRef, useImperativeHandle, useRef} from "react";
import {Form} from "reactstrap";
const SearchBar = (props, ref) => {
const buttonRef = useRef<any>();
useImperativeHandle(ref, () => ({
getCurrentValue: () => {
return buttonRef.current ? buttonRef.current["value"] : '';
},
setCurrentValue: (value) => {
if (buttonRef.current) {
buttonRef.current["value"] = value;
}
}
}));
return (
<Form className="p-3 w-100" onSubmit={(e) => props.onSubmitHandler(e)}>
<div className="form-group m-0">
<div className="input-group">
<input
type="text"
className="form-control"
placeholder="Search ..."
aria-label="Word to be searched"
ref={buttonRef}
/>
<div className="input-group-append">
<button className="btn btn-primary" type="submit">
<i className="mdi mdi-magnify" />
</button>
</div>
</div>
</div>
</Form>
);
}
export default forwardRef(SearchBar);
This is the header component in which we call our search bar component
import React, {useEffect, useRef, useState} from 'react';
import SearchBar from '../Form/Search/SearchBar';
import Router from 'next/router';
const Header = () => {
const mobileSearchRef = useRef<any>();
const [search, setSearch] = useState<any>(false);
const codeSearchHandler = (e) => {
e.preventDefault();
setSearch(!search);
if (mobileSearchRef.current) {
if (mobileSearchRef.current.getCurrentValue() == '') {
return;
}
}
Router.push({
pathname: '/search',
query: {
searchTerm: mobileSearchRef.current
? mobileSearchRef.current.getCurrentValue()
: ''
},
});
mobileSearchRef.current.setCurrentValue('');
};
return (
<React.Fragment>
<header id="page-topbar">
<div className="navbar-header">
<div className="d-flex">
<div className="dropdown d-inline-block d-lg-none ms-2">
<button
onClick={() => {
setSearch(!search);
}}
type="button"
className="btn header-item noti-icon mt-2"
id="page-header-search-dropdown"
>
<i className="mdi mdi-magnify" />
</button>
<div
className={
search
? 'dropdown-menu dropdown-menu-lg dropdown-menu-end p-0 show'
: 'dropdown-menu dropdown-menu-lg dropdown-menu-end p-0'
}
aria-labelledby="page-header-search-dropdown"
>
<SearchBar
id="headerSearchBar"
ref={mobileSearchRef}
onSubmitHandler={(e) => codeSearchHandler(e)}
/>
</div>
</div>
</div>
</div>
</header>
</React.Fragment>
);
};
export default Header;
If we look at the header component, we can see that we get the input value of search bar component using mobileSearchRef and getCurrentValue method. We can also set its value using setCurrentValue method.
You have to pass the ref aside the spread props:
export interface ButtonProps
extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
children?: React.ReactNode;
loading?: boolean;
ref?: React.RefObject<HTMLButtonElement>
}
class Button extends React.Component<ButtonProps> {
render() {
const {
...otherProps,
ref
} = this.props;
return (
<button {...otherProps} ref={ref}></button>
)
}
}
export default Button;
const ForwardedElement = React.forwardRef<ButtonProps, HTMLButtonElement> (
(props: ButtonProps, ref) => <Button {...props} ref={ref}/>
)
export default ForwardedElement;
now it should work, see this question

Rendering different youtube videos on button click

I created a react app where I display different video games and the app decides which game to play. I also have a file where I stored the data of video games. The goal I'm trying to achieve is to render the youtube video trailer of the corresponding video game when clicking on a button while using React Hooks. I've been using the npm package react-player. If someone could help, I'd appreciate it.
This is the code for the Video Game component:
import React from 'react';
import { Button, message } from 'antd';
import { Row, Col } from 'antd';
import GameEntry from '../GameEntry';
import Games from '../Games';
function VideoGameSection() {
const chooseGame = () => {
var randomGameTitle = [
'Gears 5',
'Halo',
'Hellblade',
'It takes two',
'A Plague Tale',
'Psychonauts',
'Twelve Minutes',
'Ori',
'Streets of Rage',
'Last of Us',
'Boodborne',
'Geenshin Impact',
'Dragon Ball Z:KAKAROT',
'Ghost Tsushima',
'Naruto',
'Overcooked',
'Horizon',
'Tomb Raider',
'Uncharted',
'Person 5 Royal',
'Ratchet',
'Spider-Man',
];
var randomIndex = Math.floor(Math.random() * randomGameTitle.length);
return message.info(
'The game you will play is: ' + randomGameTitle[randomIndex] + '.',
);
};
return (
<div id="video-game" className="block bgGray">
<div className="container-fluid">
<div className="titleHolder">
<h2>Video Games</h2>
<p>A list of current games</p>
<div className="site-button-ghost-wrapper">
<Button
className="gameButton"
type="primary"
danger
ghost
onClick={chooseGame}
>
Pick for me
</Button>
</div>
</div>
<Row gutter={[16, 24]}>
{Games.map((videogame, i) => (
<Col span={8}>
<GameEntry
id={i}
key={i}
title={videogame.title}
imgURL={videogame.imgURL}
description={videogame.console}
/>
</Col>
))}
</Row>
</div>
</div>
);
}
export default VideoGameSection;
This is the code for my game entry component:
import React, { useState } from 'react';
import { Card, Button, Modal } from 'antd';
import YoutubeSection from './Home/YoutubeSection';
const { Meta } = Card;
function GameEntry(props) {
const [isModalVisible, setIsModalVisible] = useState(false);
const showModal = () => {
setIsModalVisible(true);
};
const handleClose = () => {
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
};
return (
<div>
<Card
className="gameCard"
hoverable
cover={<img className="cardImg" alt={props.title} src={props.imgURL} />}
>
<div className="cardTitle">
<Meta title={props.title} description={props.description} />
</div>
<>
<Button
className="trailerButton"
type="primary"
block
style={{
color: '#fff',
borderColor: '#fff',
backgroundColor: '#e6544f',
}}
onClick={showModal}
>
Click for trailer
</Button>
<Modal
title={props.title}
width={'725px'}
visible={isModalVisible}
onOk={handleClose}
onCancel={handleCancel}
>
<YoutubeSection />
</Modal>
</>
</Card>
</div>
);
}
export default GameEntry;
This is the code for the youtube component:
import React, { useState } from 'react';
import ReactPlayer from 'react-player';
function YoutubeSection(props) {
return (
<div className="container-fluid">
<ReactPlayer
// url={videoTrailer}
muted={false}
playing={true}
controls={true}
/>
</div>
);
}
export default YoutubeSection;
example of data file:
const Games = [
{
id: 1,
title: 'Gears 5',
imgURL: '../Images/gears-5.jpeg',
console: 'Xbox',
videoID: 'SEpWlFfpEkU&t=7s',
},
You can keep a single Modal component and use it for that.
ModalView.js
import React, { useState } from "react";
import YoutubeSection from "./YoutubeSection";
import { Modal } from "antd";
const ModalView = ({
title,
isModalVisible,
handleClose,
handleCancel,
videoID
}) => {
return (
<Modal
title={title}
width={"725px"}
visible={isModalVisible}
onOk={handleClose}
onCancel={handleCancel}
>
<YoutubeSection videoID={videoID} />
</Modal>
);
};
export default ModalView;
Move the ModalView and its state, control functions to the VideoGameSection.
VideoGameSection.js
import React, { useState } from "react";
import { Button, message } from "antd";
import { Row, Col } from "antd";
import GameEntry from "./GameEntry";
import Games from "./Games";
import ModalView from "./ModalView";
function VideoGameSection() {
const [isModalVisible, setIsModalVisible] = useState(false);
const [currentVideoID, setCurrentVideoID] = useState("");
const showModal = () => {
setIsModalVisible(true);
};
const handleClose = () => {
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
};
const chooseGame = () => {
var randomGameTitle = [
"Gears 5",
"Halo",
"Hellblade",
"It takes two",
"A Plague Tale",
"Psychonauts",
"Twelve Minutes",
"Ori",
"Streets of Rage",
"Last of Us",
"Boodborne",
"Geenshin Impact",
"Dragon Ball Z:KAKAROT",
"Ghost Tsushima",
"Naruto",
"Overcooked",
"Horizon",
"Tomb Raider",
"Uncharted",
"Person 5 Royal",
"Ratchet",
"Spider-Man"
];
var randomIndex = Math.floor(Math.random() * randomGameTitle.length);
return message.info(
"The game you will play is: " + randomGameTitle[randomIndex] + "."
);
};
return (
<div id="video-game" className="block bgGray">
<div className="container-fluid">
<div className="titleHolder">
<h2>Video Games</h2>
<p>A list of current games</p>
<div className="site-button-ghost-wrapper">
<Button
className="gameButton"
type="primary"
danger
ghost
onClick={chooseGame}
>
Pick for me
</Button>
</div>
</div>
<Row gutter={[16, 24]}>
{Games.map((videogame, i) => (
<Col span={8}>
<GameEntry
id={i}
key={i}
title={videogame.title}
imgURL={videogame.imgURL}
description={videogame.console}
videoID={videogame.videoID}
setCurrentVideoID={setCurrentVideoID}
showModal={showModal}
/>
</Col>
))}
<ModalView
videoID={currentVideoID}
handleClose={handleClose}
isModalVisible={isModalVisible}
/>
</Row>
</div>
</div>
);
}
export default VideoGameSection;
Access the videoID passed via ModalView. You can save gameId instead of videoID to get any other info of the game Ex:title.
YoutubeSection.js
import React, { useState } from "react";
import ReactPlayer from "react-player";
function YoutubeSection(props) {
return (
<div className="container-fluid">
<ReactPlayer
url={`https://www.youtube.com/watch?v=${props.videoID}`}
muted={false}
playing={true}
controls={true}
/>
</div>
);
}
export default YoutubeSection;
GameEntry.js
import React, { useState } from "react";
import { Card, Button, Modal } from "antd";
import YoutubeSection from "./YoutubeSection";
const { Meta } = Card;
function GameEntry(props) {
return (
<div>
<Card
className="gameCard"
hoverable
cover={<img className="cardImg" alt={props.title} src={props.imgURL} />}
>
<div className="cardTitle">
<Meta title={props.title} description={props.description} />
</div>
<>
<Button
className="trailerButton"
type="primary"
block
style={{
color: "#fff",
borderColor: "#fff",
backgroundColor: "#e6544f"
}}
onClick={() => {
props.setCurrentVideoID(props.videoID);
props.showModal();
}}
>
Click for trailer
</Button>
</>
</Card>
</div>
);
}
export default GameEntry;
Code sandbox => https://codesandbox.io/s/flamboyant-dan-z0kc0?file=/src/ModalView.js

Build search functionality in Next.js app

I want to integrate a search functionality in my Next.js app which should look like this:
User types a keyword in searchBar which is inside of Header
After clicking on the search button the keyword should be passed on the SearchResult page, in which I load it dynamically into the youtube-search-api link
The structure looks like this:
Header.js (contains SearchBar component)
_app.js (contains Header and components)
SearchResults.js (inside pages folder, should fetch typed keyword from Header)
I tried it with zustand and it looks like this currently:
Header.js:
import SearchBar from "./SearchBar";
const Header = () => {
return (
<div className={styles.header}>
...
<SearchBar />
...
</div>
);
};
export default Header;
SearchBar.js:
import create from 'zustand';
import { useRouter } from 'next/router';
import Image from 'next/image';
import styles from '../styles/Header.module.scss';
import loupe from '../public/images/loupe.png';
const useStore = create((set) => ({
keyword: '',
setKeyword: (keyword) =>
set((state) => ({
...state,
keyword,
})),
}));
const SearchBar = () => {
const router = useRouter();
const keyword = useStore((state) => state.keyword);
const setKeyword = useStore((state) => state.setKeyword);
const handleClick = (e) => {
e.preventDefault();
router.push('/searchResults');
};
return (
<div className={styles.searchBar}>
<input
type='text'
value={keyword}
placeholder='Suche...'
onChange={(e) => setKeyword(e.target.value)}
/>
<button className={styles.searchBtn} type='submit' onClick={handleClick}>
<Image src={loupe} alt='' />
</button>
</div>
);
};
export default SearchBar;
_app.js:
import Header from '../components/Header';
function MyApp({ Component, pageProps }) {
return (
<>
<Header />
<Component {...pageProps} />
</>
);
}
export default MyApp;
and SearchResults.js:
import { fetchData } from '../lib/utils';
import Moment from 'react-moment';
import { Modal } from 'react-responsive-modal';
import ReactPlayer from 'react-player/youtube';
export default function SearchResults({ videos }) {
console.log(videos);
const [modalIsOpen, setModalIsOpen] = useState(false);
const [modalData, setModalData] = useState(null);
const videoURL = 'https://www.youtube.com/watch?v=' + modalData;
const sortedVids = videos
.sort((a, b) =>
Number(
new Date(b.snippet.videoPublishedAt) -
Number(new Date(a.snippet.videoPublishedAt))
)
)
.filter((vid) => vid.snippet.title.toLowerCase());
return (
<>
<div className={`${styles.playlist_container} ${styles.search}`}>
<div className={styles.main_container}>
<h1>Search results</h1>
{sortedVids
.filter((v) => v.snippet.title !== 'Private video')
.map((vid, id) => {
return (
<div className={styles.item_container}
key={id}>
<div className={styles.clip_container}>
<Image
className={styles.thumbnails}
src={vid.snippet.thumbnails.medium.url}
layout='fill'
objectFit='cover'
alt={vid.snippet.title}
/>
<button
className={styles.playBtn}
onClick={() => {
setModalData(vid.snippet.resourceId.videoId);
console.log(modalData);
setModalIsOpen(true);
}}
>
<Image src='/images/play.svg' width='60' height='60' />
</button>
</div>
<div className={styles.details_container}>
<h3>{vid.snippet.title}</h3>
</div>
</div>
);
})}
</div>
</div>
<div>
<Modal
open={modalIsOpen}
onClose={() => setModalIsOpen(false)}
center
classNames={{
overlay: 'customOverlay',
modal: 'customModal',
overlayAnimationIn: 'customEnterOverlayAnimation',
overlayAnimationOut: 'customLeaveOverlayAnimation',
modalAnimationIn: 'customEnterModalAnimation',
modalAnimationOut: 'customLeaveModalAnimation',
}}
animationDuration={800}
>
<ReactPlayer
playing={true}
url={videoURL}
width='100%'
height='calc(100vh - 100px)'
config={{
youtube: {
playerVars: {
autoplay: 1,
controls: 1,
},
},
}}
/>
</Modal>
</div>
<Footer />
</>
);
}
export async function getStaticProps() {
const keyword = useStore((state) => state.keyword);
const { YOUTUBE_KEY } = process.env;
const uploadsURL = `https://youtube.googleapis.com/youtube/v3/search?part=snippet&channelId=UCbqKKcML7P4b4BDhaqdh_DA&maxResults=50&key=${YOUTUBE_KEY}&q=${keyword}`;
async function getData() {
const uploadsData = fetchData(uploadsURL);
return {
videos: await uploadsData,
};
}
const { videos } = await getData();
return {
revalidate: 86400,
props: {
videos: videos.items,
},
};
}
Would someone please help me out by telling me what I did wrong and how it works in the right way? Thank you guys!!

How to call child submit method from another child component

Im trying to solve this for 2 days but cant.
How to call onSubmit method in CreateProject component when onApply function called in ModalContent component with Typescript and react-hook-form.
The idea: when I clicked button in ModalContent it should call react-hook-form onSumbit method in CreateProject. Then CreateProject calls onSubmit in parent component of these two childs.
Or maybe the idea of such component structure is wrong?
Thanks everyone for any answers.
Parent component:
import { ModalBody } from '../../components/modals'
import useCreateProject from '../../api/hooks/createProject'
export default function New(): JSX.Element {
const [modalVisible, setModalVisible] = React.useState(true)
const onSubmit = useCreateProject((data) => {
const { createProject } = data.data.data
console.log(data)
})
const onApply = () => {
console.log(123)
}
return (
<ModalContent
name={'Create project'}
visible={modalVisible}
onApply={onApply}
onCancel={() => setModalVisible(false)}
>
<ModalBody>
<CreateProject onSubmit={onSubmit.mutate} />
</ModalBody>
</ModalContent>
)
}
Child component
import React from 'react'
import Input from '../Inputs/Input'
import Textarea from '../Inputs/Textarea'
import FileUploader from '../uploader/FileUploader'
import Tasks from '../forms/Tasks'
import { useForm } from 'react-hook-form'
import {
draftBudgetMaxLength,
projectNameMaxLength,
descriptionMaxLength,
} from '../../constants/createProjectModal'
import ButtonSecondary from '../buttons/ButtonSecondary'
export interface IModalInputs {
create_project_name: string
}
export interface ICreateProjectProps {
onSubmit: (values: IModalInputs) => void
}
const СreateProject: React.FC<ICreateProjectProps> = ({
onSubmit,
}): React.ReactElement => {
const {
register,
getValues,
control,
handleSubmit,
formState: { errors },
} = useForm<IModalInputs>()
return (
<>
<form onSubmit={handleSubmit(() => onSubmit(getValues()))}>
<div className="p-4">
<div className="flex flex-col -m-1.5">
<div className="m-1.5">
<Input
type="text"
label="Project name"
name="create_project_name"
register={register}
error={errors.create_project_name}
options={{
required: true,
maxLength: projectNameMaxLength,
}}
/>
</div>
</div>
</div>
</form>
</>
)
}
export default СreateProject
Child component with click event
import React from 'react'
import Image from 'next/image'
import close from '../../assets/close.svg'
import ButtonSecondary from '../buttons/ButtonSecondary'
interface IModalContentProps {
children: React.ReactElement
onApply?: () => void
visible: boolean
buttonName?: string
}
const ModalContent: React.FC<IModalContentProps> = ({
name,
children,
visible,
onCancel,
onApply,
buttonName,
}) => {
return (
<>
{visible && (
<div className="flex p-4 space-x-2 justify-end">
{children}
<ButtonSecondary
click={onApply}
type={'submit'}
label={buttonName || 'Ok'}
id={buttonName}
shown={true}
styleClass="styleClass"
paddingClass="py-2 py-2 pr-4 pl-2"
/>
</div>
)}
</>
)
}
export default ModalContent
The problem was resolved with useRef and useImperativeHandle hooks.
Parent:
export default function New(): JSX.Element {
const [modalVisible, setModalVisible] = React.useState(true)
const childRef = React.useRef<any>()
const onSubmit = useCreateProject((data) => {
const { createProject } = data.data.data
console.log(data)
})
return (
<ModalContent
name={'Create project'}
visible={modalVisible}
onApply={() => childRef.current.SubmitForm()}
onCancel={() => setModalVisible(false)}
>
<ModalBody>
<CreateProject onSubmit={onSubmit.mutate} ref={childRef} />
</ModalBody>
</ModalContent>
)
}
Child:
export interface IModalInputs {
create_project_name: string
}
export interface ICreateProjectProps {
onSubmit: (values: IModalInputs) => void
}
function CreateProject(props: ICreateProjectProps, ref) {
const {
register,
getValues,
control,
handleSubmit,
formState: { errors },
} = useForm<IModalInputs>()
useImperativeHandle(ref, () => ({
SubmitForm() {
handleSubmit(() => props.onSubmit(getValues()))()
},
}))
return (
<>
<form ref={ref}>
<div className="p-4">
<div className="flex flex-col -m-1.5">
<div className="m-1.5">
<Input
type="text"
label="Project name"
name="create_project_name"
register={register}
error={errors.create_project_name}
options={{
required: true,
maxLength: projectNameMaxLength,
}}
/>
</div>
</div>
</div>
</form>
</>
)
}
export default React.forwardRef(CreateProject)

react-bootstrap accordion defaultActiveKey is not opening default active key

UPDATE: After some experimentation, I have determined that the problem is that I have some code (see the useEffect() section below) which updates the accordion when the menu object changes. On. the first render, defaultActiveKey works, but on subsequent re-renders, it does not.
I am using the Accordion control from react-bootstrap and can get the basic example from the docs working, but after customizing the code quite a bit, the default accordion no longer opens by... default. Here's probably the most interesting part of the code:
return (
<>
<MenuMobile menuItems={menuItems} open={open} setOpen={setOpen} />
<Navbar bg="white" variant="light" expand="lg" fixed="left">
<Navbar.Brand href="/home">
<img src={logo} width="113" height="40" alt={siteTitle + " Logo"} />
</Navbar.Brand>
<NavbarToggler open={open} setOpen={setOpen} />
<Accordion
defaultActiveKey={menu.defaultActiveKey}
className="sidebar-menu"
data-active={menu.defaultActiveKey}
>
{menu.cards.map((card, index) => {
return (
<Card key={index}>
<CustomToggle title={card.title} eventKey={card.eventKey} anchors={card.anchors} />
<Accordion.Collapse eventKey={card.eventKey}>
<Card.Body>
{card.anchors.map((anchor) => (
<a href={`#${anchor.href}`} key={anchor.href}>
{anchor.text}
</a>
))}
</Card.Body>
</Accordion.Collapse>
</Card>
);
})}
</Accordion>
</Navbar>
</>
);
I've outputted the menu.defaultActiveKey in a data attribute just to make sure it's getting it right and it is. I suspect the problem has to do with the fact that I'm generating the child <Card> components dynamically but I'm not sure what the fix is?
The entire source code is below if you're interested:
import React, { useState, useContext, useEffect } from "react";
import PropTypes from "prop-types";
import Navbar from "react-bootstrap/Navbar";
import AccordionContext from "react-bootstrap/AccordionContext";
import Accordion from "react-bootstrap/Accordion";
import Card from "react-bootstrap/Card";
import { useAccordionToggle } from "react-bootstrap/AccordionToggle";
import classNames from "classnames";
import queryString from "query-string";
import MenuMobile from "./menuMobile";
import NavbarToggler from "./navbarToggler";
import EduMenus from "../utility/educationMenus";
import logo from "../images/logo-white.svg";
const CustomToggle = ({ title, eventKey, anchors, callback }) => {
const currentEventKey = useContext(AccordionContext);
const onClickToggle = useAccordionToggle(eventKey, () => {
callback(eventKey);
});
const isOpen = currentEventKey === eventKey;
return (
<div className={classNames("card-header", { open: isOpen })} onClick={onClickToggle}>
<h2>{title}</h2>
{!!anchors.length && <i className={classNames("fa", { "fa-angle-down": !isOpen }, { "fa-angle-up": isOpen })} />}
</div>
);
};
CustomToggle.propTypes = {
title: PropTypes.string.isRequired,
eventKey: PropTypes.string.isRequired,
anchors: PropTypes.array,
callback: PropTypes.func,
};
CustomToggle.defaultProps = {
anchors: [],
callback: () => null,
};
const DocsNavbar = ({ siteTitle, location }) => {
const [open, setOpen] = useState(false);
const [menu, setMenu] = useState(EduMenus.default);
const menuItems = [
{
href: "/education/overview",
text: "Education",
},
{
href: "/home",
text: "Business",
},
{
href: "/home",
text: "Travel",
},
{
href: "/home",
text: "Healthcare",
},
];
useEffect(() => {
if (!!location && location.search !== "") {
const params = queryString.parse(location.search);
if (params.menu) {
if (Object.prototype.hasOwnProperty.call(EduMenus, params.menu)) {
setMenu(EduMenus[params.menu]);
} else {
console.error(`Menu named '${params.menu}' does not exist`);
}
}
}
});
return (
<>
<MenuMobile menuItems={menuItems} open={open} setOpen={setOpen} />
<Navbar bg="white" variant="light" expand="lg" fixed="left">
<Navbar.Brand href="/home">
<img src={logo} width="113" height="40" alt={siteTitle + " Logo"} />
</Navbar.Brand>
<NavbarToggler open={open} setOpen={setOpen} />
<Accordion
defaultActiveKey={menu.defaultActiveKey}
className="sidebar-menu"
data-active={menu.defaultActiveKey}
>
{menu.cards.map((card, index) => {
return (
<Card key={index}>
<CustomToggle title={card.title} eventKey={card.eventKey} anchors={card.anchors} />
<Accordion.Collapse eventKey={card.eventKey}>
<Card.Body>
{card.anchors.map((anchor) => (
<a href={`#${anchor.href}`} key={anchor.href}>
{anchor.text}
</a>
))}
</Card.Body>
</Accordion.Collapse>
</Card>
);
})}
</Accordion>
</Navbar>
</>
);
};
DocsNavbar.propTypes = {
siteTitle: PropTypes.string,
location: PropTypes.object,
};
DocsNavbar.defaultProps = {
siteTitle: ``,
};
export default DocsNavbar;
Ok, so I came up with a workaround. Not sure if this is the best way but it works so I'll put it out there but if someone has a better solution I'm all ears.
Basically, I removed defaultActiveKey because it seems like it only works on initial render, and explicitly set the active accordion with activeKey and maintain that in state and set that state whenever the menu changes.
import React, { useState, useContext, useEffect } from "react";
import PropTypes from "prop-types";
import Navbar from "react-bootstrap/Navbar";
import AccordionContext from "react-bootstrap/AccordionContext";
import Accordion from "react-bootstrap/Accordion";
import Card from "react-bootstrap/Card";
import { useAccordionToggle } from "react-bootstrap/AccordionToggle";
import classNames from "classnames";
import queryString from "query-string";
import MenuMobile from "./menuMobile";
import NavbarToggler from "./navbarToggler";
import EduMenus from "../utility/educationMenus";
import logo from "../images/logo-white.svg";
const CustomToggle = ({ title, eventKey, anchors, callback }) => {
const currentEventKey = useContext(AccordionContext);
const onClickToggle = useAccordionToggle(eventKey, () => {
callback(eventKey);
});
const isOpen = currentEventKey === eventKey;
return (
<div className={classNames("card-header", { open: isOpen })} onClick={onClickToggle}>
<h2>{title}</h2>
{!!anchors.length && <i className={classNames("fa", { "fa-angle-down": !isOpen }, { "fa-angle-up": isOpen })} />}
</div>
);
};
CustomToggle.propTypes = {
title: PropTypes.string.isRequired,
eventKey: PropTypes.string.isRequired,
anchors: PropTypes.array,
callback: PropTypes.func,
};
CustomToggle.defaultProps = {
anchors: [],
callback: () => null,
};
const DocsNavbar = ({ siteTitle, location }) => {
const [open, setOpen] = useState(false);
const [menu, setMenu] = useState(EduMenus.default);
const [active, setActive] = useState(EduMenus.default.defaultActiveKey);
const menuItems = [
{
href: "/education/overview",
text: "Education",
},
{
href: "/home",
text: "Business",
},
{
href: "/home",
text: "Travel",
},
{
href: "/home",
text: "Healthcare",
},
];
useEffect(() => {
if (!!location && location.search !== "") {
const params = queryString.parse(location.search);
if (params.menu) {
if (Object.prototype.hasOwnProperty.call(EduMenus, params.menu)) {
setMenu(EduMenus[params.menu]);
setActive(EduMenus[params.menu].defaultActiveKey);
} else {
console.error(`Menu named '${params.menu}' does not exist`);
}
}
}
});
return (
<>
<MenuMobile menuItems={menuItems} open={open} setOpen={setOpen} />
<Navbar bg="white" variant="light" expand="lg" fixed="left">
<Navbar.Brand href="/home">
<img src={logo} width="113" height="40" alt={siteTitle + " Logo"} />
</Navbar.Brand>
<NavbarToggler open={open} setOpen={setOpen} />
<Accordion activeKey={active} className="sidebar-menu" onSelect={(e) => setActive(e)}>
{menu.cards.map((card, index) => {
return (
<Card key={index}>
<CustomToggle title={card.title} eventKey={card.eventKey} anchors={card.anchors} />
<Accordion.Collapse eventKey={card.eventKey}>
<Card.Body>
{card.anchors.map((anchor) => (
<a href={`#${anchor.href}`} key={anchor.href}>
{anchor.text}
</a>
))}
</Card.Body>
</Accordion.Collapse>
</Card>
);
})}
</Accordion>
</Navbar>
</>
);
};
DocsNavbar.propTypes = {
siteTitle: PropTypes.string,
location: PropTypes.object,
};
DocsNavbar.defaultProps = {
siteTitle: ``,
};
export default DocsNavbar;

Resources