To create a dropdown in React, I envisioned a component as follows.
/** #jsxImportSource #emotion/react */
import React, { useEffect, useState } from "react";
interface DropProps {
label?: string;
value: string;
children: React.ReactNode;
}
export const TouchDropdown = ({label, value, children}: DropProps ) => {
return (
<div
css={{
flexDirection: 'column',
display: 'flex',
padding: '10px',
backgroundColor: 'gray',
}}
>
<div>{label}</div>
{children}
</div>
)
}
TouchDropdown.Header = () => {
return (
<div></div>
)
}
TouchDropdown.Trigger = ({as, onClick}: {as: React.ReactNode; onClick: () => void}) => {
return (
<div onClick={onClick}>
{as}
</div>
)
}
TouchDropdown.Menu = ({children, value}: {children: React.ReactNode; value: any}) => {
return (
<div>
{children}
</div>
)
}
TouchDropdown.View = ({children}: {children: React.ReactNode}) => {
return (
<div>
{children}
</div>
)
}
However, I want to be able to see the hidden View when I click the Menu.
but, menu do not support useState().
How can I interact with Menu and View?
Also, is there a way to pass an event from TouchDropdown to TouchDropdown.Menu?
I want to be able to see the hidden View when I click the menu component
but.. TouchDropdown.Menu component do not support useState()..
also, is there a way to pass an event from TouchDropdown to TouchDropdown.Menu? or TouchDropdown.Menu to TouchDropdown.View..
Related
I have a simple component with a few nested bits of mark-up:
import React, { ReactElement } from "react";
type MenuItem = {
title: string;
subItems?: Array<string>;
};
type MenuConfig = Array<MenuItem>;
function Item({ item }: { item: MenuItem }): ReactElement {
const [showItem, setShowItem] = React.useState(false);
const handleShowItem = (): void => {
setShowItem(!showItem);
};
return (
<>
{item.subItems && (
<button onClick={() => handleShowItem()}>
{showItem ? "Hide" : "Expand"}
</button>
)}
{showItem && <SubItem item={item} />}
</>
);
}
function SubItem({ item }: { item: MenuItem }): ReactElement {
const { title } = item;
return (
<ul>
{item?.subItems?.map((subitem: string, i: number) => (
<li key={i}>
{subitem}
</li>
))}
</ul>
);
}
function Solution({ menuConfig }: { menuConfig: MenuConfig }): ReactElement {
return (
<>
{menuConfig.map((item: MenuItem, i: number) => (
<div key={i}>
<span>{item.title}</span>
<Item item={item} />
</div>
))}
</>
);
}
export default Solution;
This is what I am passing in:
menuConfig={[
{
title: "About",
},
{
title: "Prices",
subItems: ["Hosting", "Services"],
},
{
title: "Contact",
subItems: ["Email", "Mobile"],
},
]}
Now, it functions as expected, if an item contains subItems then an Expand button will be show which, if clicked, will only expand the relevant list.
How should I go about making sure only one list would be open at a time, given my implementation?
So if the user clicks Expand on a button, the other previously expanded lists should close.
I can't mess with the data that's coming in, so can't add ids to the object, but titles are unique.
I've searched and whilst there are a few examples, they don't help me, I just can't wrap my head around this.
This is a perfect use case for React context. You essentially want a shared state among your menu. You can achieve this with prop drilling however contexts are a much cleaner solution.
Code sandbox
const MenuContext = createContext<{
expandedState?: [number, React.Dispatch<React.SetStateAction<number>>];
}>({});
function Solution(): ReactElement {
const expandedState = useState<number>(null);
const value = { expandedState };
return (
<>
<MenuContext.Provider value={value}>
{menuConfig.map((item: MenuItem, i: number) => (
<div key={i}>
<span>{item.title}</span>
<Item i={i} item={item} />
</div>
))}
</MenuContext.Provider>
</>
);
}
function Item({ item, i }: { item: MenuItem; i: number }): ReactElement {
const [expanded, setExpanded] = useContext(MenuContext)?.expandedState ?? [];
const shown = expanded === i;
return (
<>
{item.subItems && (
<button onClick={() => setExpanded?.(i)}>
{shown ? "Hide" : "Expand"}
</button>
)}
{shown && <SubItem item={item} />}
</>
);
}
function Foo({
children,
...rest
}: {
children: React.ReactNode;
/*rest: how do I type `rest`? */
}) {
return (
<span style={{ background: "red" }} {...rest}>
{children}
</span>
);
}
export default function App() {
return (
<div className="App">
<Foo style={{ background: "blue" }}>Hello CodeSandbox</Foo>
</div>
);
}
Here is the demo: https://codesandbox.io/s/how-to-type-rest-huv2z?file=/src/App.tsx:50-412
Foo is used to override the props that span accepts. How do I type rest in Foo?
You can't type the spread portion of the object directly, but you can add the necessary properties with the union type (&).
In this case, the extra properties you want to allow are of type React.HTMLAttributes<HTMLSpanElement>. So you end up with:
function Foo({
children,
...rest
}: {
children: React.ReactNode;
} & React.HTMLAttributes<HTMLSpanElement>) {
return (
<span style={{ background: "red" }} {...rest}>
{children}
</span>
);
}
As an aside, you can avoid manually typing the children property using the React.FunctionComponent type. This actually avoids the union definition and is a bit more idiomatic in React code.
const Foo: React.FunctionComponent<React.HTMLAttributes<HTMLSpanElement>> = ({
children,
...rest
}) => {
// ...
}
You can just use [x:string]: any; like
function Foo({
children,
...rest
}: {
children: React.ReactNode;
[rest:string]: any;
}) {
return (
<span style={{ background: "red" }} {...rest}>
{children}
</span>
);
}
export default function App() {
return (
<div className="App">
<Foo style={{ background: "blue" }} id="example">Hello CodeSandbox</Foo>
</div>
);
}
You can use something like this as a type:
{
children: React.ReactNode;
/*rest: how do I type `rest`? */
} & HTMLProps<HTMLSpanElement>
I have a small React app that makes calls to a JSON file and returns the text of a card.
I have successfully made this work by calling the onClick on a button, but what I really want to do is add a onClick as an event to my Cards module I've created. I've looked at binding at this level but with no success;
<Cards name={card} onClick={() => setCard(Math.floor(Math.random() * 57) + 1)}/>
Do I need to write a custom handler in my cards.js to handle the onClick? Or can I append it within this file (App.js)?
The full code is below.
import React, { useState } from 'react';
import './App.css';
import Cards from '../cards';
function App() {
const [card, setCard] = useState('1');
return (
<div className="wrapper">
<h1>Card text:</h1>
<button onClick={() => setCard(Math.floor(Math.random() * 57) + 1)}>Nile</button>
<Cards name={card} />
</div>
);
}
export default App;
My card.js component code is;
export default function Cards({ name }) {
const [cardInformation, setCards] = useState({});
useEffect(() => {
getCards(name)
.then(data =>
setCards(data)
);
}, [name])
const FlippyOnHover = ({ flipDirection = 'vertical' }) => (
<Flippy flipOnHover={true} flipDirection={flipDirection}>
</Flippy>
);
return(
<div>
<Flippy flipOnHover={false} flipOnClick={true} flipDirection="vertical" style={{ width: '200px', height: '200px' }} >
<FrontSide style={{backgroundColor: '#41669d',}}>
</FrontSide>
<BackSide style={{ backgroundColor: '#175852'}}>
{cardInformation.continent}
</BackSide>
</Flippy>
<h2>Card text</h2>
<ul>
<li>{cardInformation.continent}</li>
</ul>
</div>
)
}
Cards.propTypes = {
name: PropTypes.string.isRequired
}
We can create an event. So let's call it onSetCard.
App.js file:
class App extends Component {
handleSetCard= () => {
// set state of card here
};
<Cards name={card}
card=card
onSetCard={this.handleSetCard}
/>
}
Cards.js file:
class Cards extends Component {
render() {
console.log("Cards props", this.props);
return (
<button
onClick={() => this.props.onSetCard(this.props.card)}
className="btn btn-secondary btn-sm m-2"
>
FooButton
</button>
)
}
}
I'm creating a simple SPFX extension to manage a task list. I have created a list which gets items via REST through pnpjs.
I want to be able to delete said items by clicking on a button. When I click the delete button, a modal (WarningModal) opens, asking for confirmation. I have managed to create a working example, but I'm having a hard time understanding why it works. When I try to follow the guides in the official docs(https://reactjs.org/docs/faq-functions.html), I can't get my code to work.
Render method in parent component:
public render = (): React.ReactElement<ITakenbeheerProps> => {
const {
taken,
showErrorMessage,
errorMessage,
showSuccessMessage,
successMessage,
isDataLoaded,
showTryAgainButton,
showDeleteModal,
deleteModalTitle,
deleteModalContent,
deleteModalOkCallback,
deleteModalCancelCallback,
} = this.state;
const { ShimmerCollection } = this;
let AssignTaskModal = this.AssignTaskModal;
let addTaskIcon: IIconProps = { iconName: "AddToShoppingList" };
return (
<Stack
tokens={this.verticalGapStackTokens}
className={styles.takenBeheer}
>
<ActionButton iconProps={addTaskIcon}>
{strings.NieuweTaakButton}
</ActionButton>
{showErrorMessage ? (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={false}
onDismiss={this.onResetAllMessageBars}
>
{errorMessage}
</MessageBar>
) : null}
{showSuccessMessage ? (
<MessageBar
messageBarType={MessageBarType.success}
isMultiline={false}
onDismiss={this.onResetAllMessageBars}
>
{successMessage}
</MessageBar>
) : null}
<Stack horizontalAlign={"center"}>
{showTryAgainButton && (
<PrimaryButton
text={strings.TryAgain}
onClick={this._populateList}
/>
)}
</Stack>
<ShimmerCollection />
{isDataLoaded ? (
<div>
{taken.findIndex((t) => t.toegewezenAan.name == null) > -1 ? (
<List
items={taken.filter((t) => {
return t.toegewezenAan.name == null;
})}
onRenderCell={this.onRenderCell}
/>
) : (
<div className={styles.noNewTasks}>{strings.NoNewTasks}</div>
)}
</div>
) : null}
<AssignTaskModal />
<WarningModal
isModalOpen={showDeleteModal}
title={deleteModalTitle}
content={deleteModalContent}
okCallback={deleteModalOkCallback}
cancelCallback={deleteModalCancelCallback}
/>
</Stack>
);};
The warning modal
Screenshot
import * as React from "react";
import * as strings from "CicOvlTakenlijstWebPartStrings";
import {
Modal,
DialogFooter,
DefaultButton,
Button,
getTheme,
mergeStyleSets,
FontWeights,
} from "office-ui-fabric-react";
/* Prop definition */
export interface IWarningModalProps {
isModalOpen: boolean;
title: string;
content: string;
okCallback: any;
cancelCallback: any;
}
/* State definition */
export interface IWarningModalState {}
export default class WarningModal extends React.Component<
IWarningModalProps,
IWarningModalState
> {
public constructor(props: IWarningModalProps) {
super(props);
this.state = {
/* State initialization */
isModalOpen: false,
};
}
public render(): React.ReactElement<IWarningModalProps> {
const {
isModalOpen,
okCallback,
cancelCallback,
title,
content,
} = this.props;
return (
<Modal
isOpen={isModalOpen}
isBlocking={false}
containerClassName={contentStyles.container}
>
<div className={contentStyles.header}>
{title}
</div>
<div className={contentStyles.body}>
{content}
<DialogFooter>
<DefaultButton onClick={okCallback} text={strings.OkMessage} />
<Button onClick={cancelCallback} text={strings.CancelMessage} />
</DialogFooter>
</div>
</Modal>
);
}
}
Why does this work? I would expect the code in the warning modal to be as following, with an arrow function:
<DialogFooter>
<DefaultButton onClick={() => okCallback()} text={strings.OkMessage} />
<Button onClick={() => cancelCallback()} text={strings.CancelMessage} />
</DialogFooter>
But when I click the buttons, nothing happens. The debugger also doesn't show any error message.
Hello i have a child component and a parent component. In the child component there is a state. The state has to toggle between classNames in the parent component. How can i do that?
export function Parent({ children, darkMode }) {
return (
<div className={cx(styles.component, darkMode && styles.darkMode)}>
{ children }
</div>
)
}
export function Child() {
const [darkMode, setDarkMode] = React.useState(false)
return (
<header>
<div className={styles.component}>
<div className={styles.content}>
<button onClick={colorSwith} className={styles.toggle}>Toggle</button>
</div>
</div>
</header>
)
function colorSwith() {
setDarkMode(true)
}
}
With state it's 1 directional
It's not possible to pass state up the tree. In the solution below you might need to bind the function. You can mod props of children via the clone element React method.
export function Parent({ children, darkMode }) {
const [darkMode, setDarkMode] = React.useState(false)
return (
<div className={cx(styles.component, darkMode && styles.darkMode)}>
{React.cloneElement(children, { setDarkMode })}
</div>
)
}
export function Child(props) {
return (
<header>
<div className={styles.component}>
<div className={styles.content}>
<button onClick={colorSwith} className={styles.toggle}>Toggle</button>
</div>
</div>
</header>
)
function colorSwith() {
props.setDarkMode(true)
}
}
Use the context api
You can also use the context api to access state anywhere in the tree. This way any component that has access to the context will rerender on change and data is passable and changable to any point in the tree.
Check out this example from the react docs
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
See how the context is set on the `App` level with a `Provider` and then changed in the `ThemeButton` with the `useContext` hook. This is a simple use case that seems simmilar to yours.