I have a swipeable component with an open and close function that I would like to trigger from a child of the component. I don't think using state to do it in this instance would be correct since I want to avoid a re-render while the swipeable component is animating (please correct me if I'm wrong).
I'm using react-native-gesture-handler/swipeable and following their example here
SwipeCard component
import React, { useRef } from 'react';
import { RectButton } from 'react-native-gesture-handler';
import Swipeable from 'react-native-gesture-handler/Swipeable';
import Animated from 'react-native-reanimated';
const AnimatedView = Animated.createAnimatedComponent(View);
export const SwipeCard = ({ children }) => {
let swipeableRow = useRef(null);
let renderRightActions = (_progress, dragX) => {
let scale = dragX.interpolate({
inputRange: [-80, 0],
outputRange: [1, 0],
extrapolate: 'clamp',
});
return (
<RectButton style={styles.rightAction} onPress={close}>
{/* Change it to some icons */}
<AnimatedView style={[styles.actionIcon]} />
</RectButton>
);
};
let close = () => {
swipeableRow?.close();
};
let open = () => {
swipeableRow?.openRight();
};
return (
<Swipeable
ref={swipeableRow}
renderRightActions={renderRightActions}
friction={2}
rightThreshold={40}
>
{children}
</Swipeable>
);
};
Below is the component where I'm using SwipeCard and the Toggle is the event I want to use to fire the open() method in the SwipeCard component.
<Row>
{arr.map((item) => {
return (
<SwipeCard key={item.id}>
<CardContainer>
<CleaningCard
cleaningData={item}
/>
<Toggle onPress={() => {}}>
<Icon name="dots" />
</Toggle>
</CardContainer>
</SwipeCard>
);
})}
</Row>
You can use the render prop pattern and pass close, open as arguments.
Parent component where SwipeCard is used:
<Row>
{arr.map((item) => {
return (
<SwipeCard key={item.id}>
{({ close, open }) => (
<CardContainer>
<CleaningCard cleaningData={item} />
<Toggle
onPress={() => {
// close()/open()
}}
>
<Icon name="dots" />
</Toggle>
</CardContainer>
)}
</SwipeCard>
);
})}
</Row>;
SwipeCard component:
<Swipeable
ref={swipeableRow}
renderRightActions={renderRightActions}
friction={2}
rightThreshold={40}
>
{children({close, open})}
</Swipeable>
We're simply making the children a function that takes an object and returns the JSX. The required object is passed as an argument (children({close, open})).
Related
I've got a simple setup for some MUI tabs that I'm working to implement some drag and drop functionality to. The issue I'm running into is that when the SortableContext is nested inside of the TabList component, drag and drop works but the values no longer work for the respective tabs. When I move the SortableContext outside of the TabList component the values work again, but the drag and drop doesn't. If anybody has any guidance here that would be greatly appreciated!
Here is a link to a CodeSandbox using the code below: https://codesandbox.io/s/material-ui-tabs-with-drag-n-drop-functionality-05ktf3
Below is my code snippet:
import { Box } from "#mui/material";
import { useState } from "react";
import { DndContext, closestCenter } from "#dnd-kit/core";
import { arrayMove, SortableContext, rectSortingStrategy } from "#dnd-kit/sortable";
import SortableTab from "./SortableTab";
import { TabContext, TabList } from "#mui/lab";
const App = () => {
const [items, setItems] = useState(["Item One", "Item Two", "Item Three", "Item Four", "Item Five"]);
const [activeTab, setActiveTab] = useState("0");
const handleDragEnd = (event) => {
const { active, over } = event;
console.log("Drag End Called");
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
const handleChange = (event, newValue) => {
setActiveTab(newValue);
};
return (
<div>
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabContext value={activeTab}>
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={items} strategy={rectSortingStrategy}>
<TabList onChange={handleChange} aria-label="basic tabs example">
{items.map((item, index) => (
<SortableTab value={index.toString()} key={item} id={item} index={index} label={item} />
))}
</TabList>
</SortableContext>
</DndContext>
</TabContext>
</Box>
</Box>
</div>
);
};
export default App;
import { useSortable } from "#dnd-kit/sortable";
import { CSS } from "#dnd-kit/utilities";
import { Tab, IconButton } from "#mui/material";
import DragIndicatorIcon from "#mui/icons-material/DragIndicator";
const SortableTab = (props) => {
const { attributes, listeners, setNodeRef, transform, transition, setActivatorNodeRef } = useSortable({
id: props.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} {...attributes} style={style}>
<Tab {...props} />
<IconButton ref={setActivatorNodeRef} {...listeners} size="small">
<DragIndicatorIcon fontSize="5px" />
</IconButton>
</div>
);
};
export default SortableTab;
You can make drag and drop tabs work by moving <SortableContext> inside the <TabList> component something like the below. Then change sorting started to horizontalListSortingStrategy.
In this case, your Dnd will work, but you will lose all MUI transitions/animations. Because now DndContext is overriding MuiContext. The best solution to create something like this is to create custom Tabs components where your context does not depend on TabContext.
I hope it helps.
<TabContext value={activeTab}>
<DndContext
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<TabList onChange={handleChange} aria-label="basic tabs example">
<SortableContext
items={items}
strategy={horizontalListSortingStrategy}
>
{items.map((item, index) => (
<SortableTab
value={index.toString()}
key={item}
id={item}
index={index}
label={item}
/>
))}
</SortableContext>
</TabList>
</DndContext>
</TabContext>
So in order for this to work I ended up figuring out that without a draghandle on the Tab the click event would only either fire for the drag event or setting the value depending on where the SortableContext was placed. This was my solution:
SortableTab
import { useSortable } from "#dnd-kit/sortable";
import { CSS } from "#dnd-kit/utilities";
import { Tab, IconButton } from "#mui/material";
import MoreVertRoundedIcon from "#mui/icons-material/MoreVertRounded";
const SortableTab = (props) => {
const { attributes, listeners, setNodeRef, transform, transition, setActivatorNodeRef } = useSortable({
id: props.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} {...attributes} style={style}>
<Tab {...props} />
<IconButton ref={setActivatorNodeRef} {...listeners} size="small">
<MoreVertRoundedIcon fontSize="5px" />
</IconButton>
</div>
);
};
export default SortableTab;
DndContext code chunk:
const renderedTab = selectedScenario ? (
scenarioModels.map((item, index) => <SortableTab key={item} label={models.data[item].model} id={item} index={index} value={index + 1} onClick={() => handleModelClick(item)} />)
) : (
<Tab label="Pick a Scenario" value={0} />
);
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={scenarioModels} strategy={horizontalListSortingStrategy}>
<Tabs value={selectedTab} onChange={handleChange} centered>
{selectedScenario ? <Tab label="Source Data" /> : ""}
{renderedTab}
</Tabs>
</SortableContext>
</DndContext>
How to pass active state, when a button is clicked in below React component?
I have a component:
<MyLinks links={links}/>
Where I pass this array: const links: CopyType[] = ['link', 'embed'];
// MyLinks component:
const [copyLinkType, setCopyLinkType] = useState<CopyType>();
return (
<React.Fragment>
<ul>
{links.map((linkType) => (
<TabIcon
type={linkType}
onClick={() => {
setCopyLinkType(linkType);
}}
/>
))}
</ul>
{copyLinkType && <TabPanel type={copyLinkType} />}
</React.Fragment>
);
In the frontend you get 2 buttons which show/hide the <TabPanel /> with it's associated content.
How to get/pass an active state when a button is clicked?
I tried passing isActive state as a prop through <TabIcon isActive={isActive} /> and then on the onClick handler setIsActive(true); but this will pass active state to both buttons in the same time?
Try to use refs. Something like this (w/o typescript codepen):
const TabIcon = (props) => {
const {activeRefs, onClick, type, index} = props
return (
<a onClick={()=>{
// On each click change value to the opposite. Or add your
// logic here
activeRefs.current[index] = !activeRefs.current[index]
onClick()
}}>
{type}
</a>
)
}
const MyLinks = (props) => {
const [copyLinkType, setCopyLinkType] = React.useState();
// Array which holds clicked link state like activeRefs.current[index]
const activeRefs = React.useRef([]);
const links = ['link1', 'link2'];
console.log({activeRefs})
return (
<ul>
{links.map((linkType, index) => (
<TabIcon
index={index}
activeRefs={activeRefs}
type={linkType}
onClick={() => {
setCopyLinkType(linkType);
}}
/>
))}
</ul>
);
}
ReactDOM.render(
<MyLinks />,
document.getElementById('root')
);
Hi I have been using this package react-to-print to print document and it works really well. Passing value to child component works and I can print the dynamic data too. However, I am facing problem to pass dynamic data of array list. It always gets the last item of array. I wrote an example, please take a look at it
import * as React from "react";
import { useRef } from "react";
import ReactToPrint from "react-to-print";
const ComponentToPrint = React.forwardRef((props, ref) => {
const { value } = props;
return (
<div className="print-source" ref={ref}>
Number {value}
</div>
);
});
export default function App() {
const componentRef = useRef();
const numbers = [1, 2, 3, 4, 5];
return (
<>
{numbers.map(function (item, index) {
return (
<div style={{ display: "flex" }}>
<li key={index}>{item}</li>
<ReactToPrint
trigger={() => <button type="primary">Print</button>}
content={() => componentRef.current}
/>
<ComponentToPrint ref={componentRef} value={item} />
</div>
);
})}
</>
);
}
Live Demo
Whenever I click the print button, I expect to send the unique value of number to child component but every time I am getting the last value of array. What am I doing wrong?
Because there's just one componentRef instance, which on the order of rendering will have the last rendered value.
Instead each returned component from App needs to have its own instance of componentRef.
This can be achieved if you
make the returned html from App a component too (say ComponentToPrintWrapper)
have this component its own componentRef.
const ComponentToPrintWrapper = ({ item }) => { // 1.
const componentRef = useRef(); // 2.
return (
<div style={{ display: "flex" }}>
<li>{item}</li>
<ReactToPrint
trigger={() => <button type="primary">Print</button>}
content={() => componentRef.current}
/>
<ComponentToPrint ref={componentRef} value={item} />
</div>
);
};
Use ComponentToPrintWrapper on your App instead
...
export default function App() {
const numbers = [1, 2, 3, 4, 5];
return (
<>
{numbers.map(function (item, index) {
return <ComponentToPrintWrapper key={index} item={item} />;
})}
</>
);
...
}
This will ensure each return element has its own componentRef instead.
CodeSandbox
I have struggled with the pass props issue for few days but haven't figure out how to achieve this.
In my Project, I have a rechart page index.js and the inherit relationship looks like: index.js => RechartsComponent => LineChartComponent But I can't prop the list from index.js to its grandchild LineChartComponent to replace the data source of SimpleLineChart. I have tried different ways but nothing works. Any advice will be appreciated.
index.js
render() {
const { rechartdata } = this.props
const { list } = rechartdata
return (
<Page inner>
<RadioGroup
options={chartList}
defaultValue="lineChart"
onChange={this.handleRadioGroupChange}
/>
<div className={styles.chart}>
<ReChartsComponent type={this.state.type} list={list} />
</div>
</Page>
)
}
RechartsComponent.js
const ReChartsComponent = ({ type,list}) => {
if (type === 'areaChart') return <AreaChartComponent />
if (type === 'barChart') return <BarChartComponent />
return <LineChartComponent list={list}/>
}
LineChartComponent.js
const SimpleLineChart = ({list}) => (
<Container>
<LineChart
// data={data}
data={list}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<XAxis dataKey="name" />
<YAxis />
<CartesianGrid strokeDasharray="3 3" />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="id"
stroke="#8884d8"
activeDot={{
r: 8,
}}
/>
</LineChart>
</Container>
)
const LineChartPage = () => (
<div className="content-inner">
<Button
type="primary"
style={{
position: 'absolute',
right: 0,
top: -48,
}}
>
<a
href="http://recharts.org/#/en-US/examples/TinyBarChart"
target="blank"
>
Show More
</a>
</Button>
<Row gutter={32}>
<Col {...colProps}>
<Card title="Simple Chart">
<SimpleLineChart />
</Card>
</Col>
<Col {...colProps}>
<Card title="DashedLineChart">
<DashedLineChart />
</Card>
</Col>
<Col {...colProps}>
<Card title="CustomizedDotLineChart">
<CustomizedDotLineChart />
</Card>
</Col>
<Col {...colProps}>
<Card title="VerticalLineChart">
<VerticalLineChart />
</Card>
</Col>
</Row>
</div>
)
export default LineChartPage
Reason is simply what you're exporting for LineChartComponent.js is export default LineChartPage
Hence when you define LineChartPage, you need to have prop for that
const LineChartPage = ({list}) => (
...
)
Something good to have but not mandatory is that having the same file name as component will avoid careless mistake like what you've made, such as below
LineChartComponent.js
const LineChartComponent = ({list})=> (
...
)
export default LineChartComponent;
Keep the name of your component and the file name consistent
First off, in your example, the SimpleLineChart isn't receiving a list from LineChartPage. You would need to add a list prop to LineChartPage and pass that into SimpleLineChart
That said, when I need to pass props past more than one level of depth, I generally switch to the Context API.
I would create a context provider for the state you want to pass down:
import { createContext, FC, useReducer } from "react";
// ...
const InitialState : GrandparentStateI = {
// As you wish...
}
export const GrandparentContext = createContext(InitialState);
// ...
grandparentContextReducer = (state : GrandparentStateI, action : {
type : string,
payload : any
})=>{
switch(action.type){
// ...
}
}
export const GrandparentContextProvider : FC<{}> = ({children})=>{
const [state, dispatch] = useReducer(grandparentContextReducer, InitialState);
return (
<GrandparentContext.Provider value={state}>{children}</GrandparentContext.Provider>
)
}
Define a hook which you will use to access the context:
export const useGrandparentContext = () : GrandparentContextProps =>{
const context = useContext(GrandparentContext);
if(!context._inProvider){ // I usually have an _inProvider member
throw new Error("useGrandparentContext must be called within a GrandparentProvider.");
}
return {
...context,
};
}
Wrap the Grandparent's children in the provider;
const Grandparent : FC<{...}> = ({children})=>{
return (
<div>
// ...
<GrandparentContextProvider>{children}</GrandparentContextProvider>
</div>
)
}
Call the hook from the grandchildren at whatever depth:
const Grandchild : FC<{...}> = ()=>{
const {
...
} = useGrandparentContext();
return (
// ...
)
}
The Context API generally makes refactoring or adding layers of depth much easier than other patterns.
I created a sandbox: https://codesandbox.io/s/happy-rgb-vks06?file=/src/App.js
I am trying to pass props to the SlotSettings component, but I get this error:
Warning: Function components cannot be given refs. Attempts to access
this ref will fail. Did you mean to use React.forwardRef()?
I tried to read both Bootstrap docs and React docs but I could not understand how this should work.
This is the code I'm using:
const SlotSettings = props => {
console.log(props.hello); // this works
return <Popover {...props} id="popover-basic">
<Popover.Title as="h3">Popover right</Popover.Title>
<Popover.Content>
And here's some <strong>amazing</strong> content. It's very engaging.
right?
</Popover.Content>
</Popover>
}
const getDaySlots = slots => {
if (slots.length >= 1) {
return slots.map(slot => {
const variant = slot.status === "free" ? "success" : "secondary";
const buttonRef = createRef();
return (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={<SlotSettings hello="hello" />}
rootClose
>
<Button size="sm" ref={buttonRef} variant={variant} style={{ margin: "8px"}}>{slot.start}</Button>
</OverlayTrigger>
)
});
}
return "No lessons available."
}
I accomplished this by utilizing the popperConfig property of OverlayTrigger.
PopperConfig is used to pass and object to the the underlying popper instance.
link to docs
Simple example:
function renderTooltip(props) {
let message = ""
//Sometimes, props.popper.state is undefined.
//It runs this function enough times that state gets a value
if (props.popper.state) {
message = props.popper.state.options.testObj
}
return (
<Tooltip id="button-tooltip" {...props}>
{message}
</Tooltip>
);
}
function getDaySlots(slots) {
//Other stuff
return (
<OverlayTrigger
placement="right"
delay={{ show: 250, hide: 400 }}
overlay={renderTooltip}
popperConfig={{testObj:"hello there"}}
>
<Button variant="success">Click here</Button>
</OverlayTrigger >
);
}
I messed with your codesandbox, but couldn't get popper to get a state value for some reason. What I posted above works for my project, hope this helps you get started.
import React, { useState ,useRef} from 'react';
import { Popover, Overlay } from 'react-bootstrap';
const DemoComponent = () => {
const [show, setShow] = useState(false);
const target = useRef(null);
return (
<div className="">
<span ref={target} onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
open tooltip
</span>
<Overlay target={target.current} show={show} placement="top">
{(props) => (
<Popover id="popover-basic" className="customize-tooltip" {...props}>
<Popover.Body>
put here dyanamic content
</Popover.Body>
</Popover>
)}
</Overlay>
</div>
);}
export default DemoComponent;