This may sound strange, maybe I completely get it wrong in the first place. But as I read some articles and react docs related to get the children and identify specific child via React.Component.map()
however when I try this with my custom components, the child returns a stringify function as type
. (I know that react does the stringify thing to prevent script injection). But I basically need to identify specific children that pass into the component and place them in the correct positions in another custom component. (material_ui style).
<Card>
<CardTitle>
</CardTitle>
<CardContent>
</CardContent>
</Card>
The problem is I can't map passed children since the type
has a string.
my environment uses
"react": "^17.0.2",
"@types/react": "^17.0.0",
"react-dom": "^17.0.2",
"@types/react-dom": "^17.0.0",
"typescript": "^4.1.2"
and this is what I have so far
type ExpandableCardProps = {
children: ReactElement<any> | ReactElement<any>[],
}
const ExpandableCard = ({children}: ExpandableCardProps) => {
React.Children.map(children, (child) => {
concole.log(child); // just can't map the child elements as described in some articales
})
// note that I need to identify the correct child to be render in correct position
render (
<div>
<div className="title-container">
// I want to render <ExpandableTitle> here
</div>
<div className="content-container">
// I want to render <ExpandableContent> here
</div>
<div className="content-other">
// may be some other elements here
</div>
</div>
);
}
export default ExpandableCardProps;
type CommonType = {
children: ReactNode;
}
export const ExpandableTitle ({children}:CommonType) => {
<div>
{children}
</div>
}
export const ExpandableContent ({children}:CommonType) => {
<div>
{children}
</div>
}
// usage
<ExpandableCard>
<ExpandableTitle>
/*{ some jsx here }*/
</ExpandableTitle>
<ExpandableContent>
/*{ some jsx here }*/
</ExpandableContent>
</ExpandableCard>
Here's what it looks like in the console
Here's an article I was referring to and which explained most closely what I need, but Can't use the pattern it explained since the type
stringify thing, wonder it's with the React version or maybe as I mentioned earlier it's completely misunderstood by myself. I need some insight into this. How can I achieve something like this?
After a few workarounds with the Neal Burns answer, I concluded with a typescript
compatible solution.
I Will post it here since for someone it may be come in handy someday.
import React, { Children, ReactElement, ReactNode, useEffect, useRef, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './expandableCard.scss';
import { v4 as uuidv4 } from 'uuid'
const types = {
EXPANDABLE_CARD_HEADER: 'expandableCardTitle',
EXPANDABLE_CARD_CONTENT: 'expandableCardContent',
EXPANDABLE_CARD_FOOTER: 'expandableCardFooter',
EXPANDABLE_ITEMS: 'expandableItems',
}
type ExpandableCardProps = {
id?: string;
select?: boolean;
onSelect?: (id: string) => void;
children: ReactElement<ExpandableCardContentProps> | ReactElement<ExpandableCardContentProps>[];
}
const ExpandableCard = ({ id = uuidv4(), select = false, children, onSelect = () => { } }: ExpandableCardProps) => {
const transitionRef = useRef(null);
const [selected, setSelected] = useState(select);
const [expand, setExpand] = useState(false);
const headerElement = useRef<any>(null);
const contentElement = useRef<any>(null);
const expandableFooter = useRef<any>(null);
const expandableItems = useRef<any>(null);
const handleSelected = () => {
setSelected(!selected);
}
useEffect(() => {
if (selected) {
onSelect(id);
}
}, [id, onSelect, selected])
const handleExpand = () => {
setExpand(!expand);
}
Children.forEach(children, (child) => {
switch (child.props.__TYPE) {
case types.EXPANDABLE_CARD_HEADER:
headerElement.current = child;
break;
case types.EXPANDABLE_CARD_CONTENT:
contentElement.current = child;
break;
case types.EXPANDABLE_ITEMS:
expandableItems.current = child;
break;
case types.EXPANDABLE_CARD_FOOTER:
expandableFooter.current = child;
break;
default:
return <div></div>;
}
});
return (
<div className={`expandable-card ${selected ? 'expandable-card-selected' : ''}`}>
<div className={`expandable-card--content ${expand ? 'expandable-card--content-active' : ''}`}>
<div className="expandable-card--expand-button">
<button type="button" onClick={handleExpand}>expand</button>
</div>
{headerElement.current &&
<div className="expandable-card--header">
{headerElement.current}
</div>
}
{contentElement.current}
<div className="d-flex align-items-center mt-3">
<button
type="button"
className={`btn expandable-card--button ${selected ? 'expandable-card--button-active' : ''}`}
onClick={handleSelected}>
{selected && !}
</button>
{expandableFooter.current}
</div>
</div>
<CSSTransition
nodeRef={transitionRef}
in={expand}
timeout={500}
classNames={`expandable-card--drawer`}
mountOnEnter
unmountOnExit>
<div ref={transitionRef} className="expandable-card--drawer">
{expandableItems.current}
</div>
</CSSTransition>
</div >
);
}
type ExpandableCardContentProps = {
children: ReactNode,
__TYPE: string;
}
export const ExpandableCardHeader = ({ children }: ExpandableCardContentProps) => {
return (
<>
{children}
</>
);
}
ExpandableCardHeader.defaultProps = {
__TYPE: types.EXPANDABLE_CARD_HEADER,
}
export const ExpandableCardContent = ({ children }: ExpandableCardContentProps) => (
<>
{children}
</>
);
ExpandableCardContent.defaultProps = {
__TYPE: types.EXPANDABLE_CARD_CONTENT,
}
export const ExpandableCardFooter = ({ children }: ExpandableCardContentProps) => (
<>
{children}
</>
);
ExpandableCardFooter.defaultProps = {
__TYPE: types.EXPANDABLE_CARD_FOOTER,
}
export const ExpandableItems = ({ children }: ExpandableCardContentProps) => (
<>
{children}
</>
);
ExpandableItems.defaultProps = {
__TYPE: types.EXPANDABLE_ITEMS,
}
export default ExpandableCard;
Please note that this is the complete expandable component with animations in it I'll put up the SCSS code also with this to be complete
.expandable-card {
display: flex;
flex-direction: column;
box-shadow: 0 0px 25px 0px rgba(0, 0, 0, 0.2);
width: 100%;
background-color: #fff;
border-radius: 14px;
position: relative;
&--expand-button {
position: absolute;
top: 10px;
right: 15px;
}
&-selected {
border-bottom: 15px solid yellow;
border-radius: 14px;
}
&--content {
padding: 18px 15px;
border-radius: 14px 14px 0 0;
transition: all 500ms ease-out;
&-active {
z-index: 1;
box-shadow: 0 7px 7px 0 rgba(0, 0, 0, 0.2);
}
}
&--drawer {
display: flex;
flex-direction: column;
width: 100%;
max-height: 0;
background-color: #fff;
padding: 18px 20px;
border-radius: 0 0 14px 14px;
overflow-x: hidden;
overflow-y: auto;
transition: all 500ms ease-out;
/* .classes for help dropdown animations */
&-enter-active {
max-height: 320px;
padding: 18px 20px;
}
&-enter-done {
max-height: 320px;
padding: 18px 20px;
}
&-exit-active {
max-height: 0;
padding: 0 20px;
}
&-exit-done {
max-height: 0;
padding: 0 20px;
}
}
&--header {
display: flex;
align-items: center;
}
&--button {
min-width: 43px;
height: 43px;
background: transparent;
border: 2px solid aqua;
box-sizing: border-box;
border-radius: 10px;
&:focus {
box-shadow: none;
}
&-active {
background-color: blue;
border: none;
}
}
}