Generic Parent ignores Generic children React - reactjs

First, here's my "Parent Component" or "Factory", I'm not quite certain what the terminology is.
export interface ReadOnlyProps {
ignoreReadOnly?: boolean;
disabled?: boolean;
}
export const WithReadOnly = <T,>(Component: React.ComponentType<T>): React.ComponentType<T & ReadOnlyProps> => {
const ReadOnlyComponent = (props: T) => {
const { ...rest } = props as T & ReadOnlyProps;
return <Component {...(rest as T)} disabled={true} />;
};
return ReadOnlyComponent;
};
Here are the components I did for this:
const Dropdown = <T,>(props: { items: T[]; onChange: (data: T) => void }) => {
return (
<ul>
{props.items.map((item, index) => {
<li onClick={() => props.onChange(item)}>{index}</li>;
})}
</ul>
);
};
const DropdownReadOnly = WithReadOnly(Dropdown);
Then I did this two examples.
// Works
<Dropdown items={[{ name: 'Someone' }, { name: 'Someone else' }]} onChange={(item) => alert(item.name)} />;
// Doesn't work
<DropdownReadOnly items={[{ name: 'Someone' }, { name: 'Someone else' }]} onChange={(item) => alert(item.name)} />;
The first one is working, the second one is complaining that item is unknown on the onChange prop.
Anyone know what I am doing wrong here?
Thank you beforehand!

Related

React hooks accordion toggles all blocks instead of one

With a React Accordion I wanted to create a function that will only show the information of the belonging episode. But now it seems that all blocks are doing the same thing. How can I build that function so one block will be toggling instead of all?
Link to CodeSandbox
export const HomePage: React.FC<IProps> = () => {
const [open, setOpen] = useState(false);
return (
<div>{data.characters.results.map((character: { id: number, name: string, image: string; episode: any; }, index: number) => (
<div key={character.id}>
<CharacterHeading>{character.name}</CharacterHeading>
<CharacterImage src={character.image} alt={character.name} />
{character.episode.map((char: { name: string; air_date: string; episode: string; characters: any, id: number; }) => (
<div key={char.id}>
{char.name}
{char.air_date}
{char.episode}
<AccordionButton open={open}
onClick={() => setOpen(!open)}>
Check all characters
</AccordionButton>
<EpisodeListSection open={open}>
{char.characters.map((ep: { id: number, name: string; image: string; }, index: number) => (
<EpisodeInfo key={ep.id}>
<EpisodeInfoBlock>
<EpisodeName>{ep.name}</EpisodeName>
<EpisodeImage src={ep.image} alt={ep.name} />
</EpisodeInfoBlock>
</EpisodeInfo>
))}
</EpisodeListSection>
</div>
))}
</div>
))
}</div>
);
};
You only have one open variable that you are passing to every accordion. Thus, when one accordion changes that value with setOpen, it changes for all of them.
If AccordionButton is a component you built, I would suggest letting that component handle its own open/closed state. Your parent doesn't seem like it needs to control that. Otherwise, you will need open to be an array of values, you will need to only pass the correct open value to each accordion, and you will need to have your onClick function tell the parent which open value it needs to change.
Again, that seems like a lot of work for not a lot of utility. Better to just let the accordions keep track of whether they're open.
Here is the updated code that you can use:
import { useState } from "react";
import { gql, useQuery } from "#apollo/client";
import {
AccordionButton,
CharacterImage,
CharacterHeading,
EpisodeInfo,
EpisodeImage,
EpisodeInfoBlock,
EpisodeListSection,
EpisodeName
} from "./HomePage.styles";
export const GET_CHARACTER = gql`
query {
characters(page: 2, filter: { name: "rick" }) {
results {
name
image
gender
episode {
id
name
air_date
episode
characters {
id
name
image
}
}
}
}
location(id: 1) {
id
}
episodesByIds(ids: [1, 2]) {
id
}
}
`;
export interface Episodes {
name: string;
air_data: string;
episode: string;
characters: Array<any>;
}
export interface IProps {
episodeList?: Episodes[];
}
export const HomePage: React.FC<IProps> = () => {
const { data, loading, error } = useQuery(GET_CHARACTER);
const [inputValue, setInputValue] = useState("");
const [open, setOpen] = useState(false);
const [selectedId, setSelectedId] = useState(0);
const onChangeHandler = (text: string) => {
setInputValue(text);
};
const onSelectItem = (selectedItemId: number) => {
if (selectedId !== selectedItemId) {
setSelectedId(selectedItemId);
} else {
setSelectedId(-1);
}
};
if (loading) return <p>loading</p>;
if (error) return <p>ERROR: {error.message}</p>;
if (!data) return <p>Not found</p>;
return (
<div>
<input
type="text"
name="name"
onChange={(event) => onChangeHandler(event.target.value)}
value={inputValue}
/>
<div>
{data.characters.results.map(
(
character: {
id: number;
name: string;
image: string;
episode: any;
},
index: number
) => (
<div key={character.id}>
<CharacterHeading>{character.name}</CharacterHeading>
<CharacterImage src={character.image} alt={character.name} />
{character.episode.map(
(char: {
name: string;
air_date: string;
episode: string;
characters: any;
id: number;
}) => (
<div key={char.id}>
{char.name}
{char.air_date}
{char.episode}
<AccordionButton
onClick={() => onSelectItem(char.id)}
open={char.id === selectedId ? true : false}
>
Check all characters
</AccordionButton>
<EpisodeListSection
open={char.id === selectedId ? false : true}
>
{char.characters.map(
(
ep: { id: number; name: string; image: string },
index: number
) => (
<EpisodeInfo key={ep.id}>
<EpisodeInfoBlock>
<EpisodeName>{ep.name}</EpisodeName>
<EpisodeImage src={ep.image} alt={ep.name} />
</EpisodeInfoBlock>
</EpisodeInfo>
)
)}
</EpisodeListSection>
</div>
)
)}
</div>
)
)}
</div>
</div>
);
};

How to fix the error type Item[] | undefined is not assignable to Item[] type using react and typescript?

i am new to typescript and i am getting the error
"type Item[] | undefined is not assignable to Item[] type"
below is the code,
function Parent ({Items}: Props) {
return (
<Child subItems={Items.subItems}/> //error here
);
}
function Child({subItems}: Item[]) {
const sortedSubItems = subItems.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt));
return (
<>
{sortedSubItems.map((subItem: Item) => {
return (
<Card key={subItem.id}>
<CardHeader>
{subItem.name}
</CardHeader>
</Card>
);
})}
</>
);
};
here subItems has structure like below,
const subItems = [
{
id: '1',
title: 'subitem-one',
status: 'new',
createdAt: '2020-08-13T16:32:10.000Z',
orders: [
{
id: '1',
title: 'subitem1-order-one',
status: 'new',
},
{
id: '2',
title: 'subitem1-order-two',
status: 'new',
},
]
},
{
id: '2',
title: 'subitem-two',
status: 'new',
createdAt: '2020-08-16T12:02:06.000Z',
orders: [
{
id: '2',
title: 'subitem2-order-one',
status: 'new',
},
],
},
]
and the Item type is like below,
export interface Item {
id: string;
createdAt: string;
name?: string;
orders?: Order[];
subItems?: Item[];
}
could someone help me fix the error using typescript and react. thanks.
Because you have an optional type operator(the question mark) on your subItems property, that indicates to Typescript that that property can be undefined. You will need to type the subItems prop for your Child component to reflect that, and handle it appropriately in the code:
interface ChildProps {
subItems?: Item[]
}
function Child({subItems}: ChildProps) {
const sortedSubItems = subItems ? subItems.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt)) : [];
return (
<>
{sortedSubItems.map((subItem: Item) => {
return (
<Card key={subItem.id}>
<CardHeader>
{subItem.name}
</CardHeader>
</Card>
);
})}
</>
);
};
In your particular case it can be a good idea to provide an empty array as default value to subItems:
interface ChildProps {
subItems: Item[]
}
function Child(props: ChildProps) {
const { subItems = []} = props;
const sortedSubItems = subItems.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt));
return (
<>
{sortedSubItems.map((subItem: Item) => {
return (
<Card key={subItem.id}>
<CardHeader>
{subItem.name}
</CardHeader>
</Card>
);
})}
</>
);
};
You're facing this issue because the Child component is undefined in some cases, in order to fix this you will need to handle these cases.
The sortedSubItems array may be undefined, so you can compute the outside the Child component .ie. in the Parent component and do a && while passing it as a prop in the Child component.
function Parent ({Items}: Props) {
const sortedSubItems = Items?.subItems?.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt));
return (
<>
{sortedSubItems && <Child sortedSubItems={sortedSubItems} />}
</>
);
}
function Child({sortedSubItems}: Item[]) {
return (
<>
{sortedSubItems.map((subItem: Item) => {
return (
<Card key={subItem.id}>
<CardHeader>
{subItem.name}
</CardHeader>
</Card>
);
})}
</>
);
};
You can keep the same code as you have, just change
const sortedSubItems = subItems.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt)) ?? [];
This will conditionally choose the empty array

React & TypeScript how pass data from child to parent at onClick via props

I have parent and child component. And i wan't to pass from child to parent component data at onClick via props. How can i do that? i tried:
Parent.tsx
const items = [
{
title: "A"
},
{
title: "B"
}
]
const Parent = () => {
const [someState, setSomeState] = useState("");
const toggleState = (e: React.MouseEvent, title: string) => {
setSomeState(title);
}
return (
<Child items={items} toggleState={(e) => toggleState(e, item.title)} />
)
}
Child.tsx
type ChildProps = {
items: Item[];
toggleState: (e: React.MouseEvent) => void;
}
const Child: React.FC<ChildProps> = (props) => {
return (
{props.items.map((item) => (
<button onClick={props.toggleState}> pass clicked item title to parent </button>
))}
)
}
but in my parent component omn toggleState prop item.title showing error:
any
Cannot find name 'item'.
As with any other function call, you have to pass the information as an argument:
Parent.tsx:
const Parent = () => {
const [someState, setSomeState] = useState("");
const toggleState = (e: React.MouseEvent, title: string) => {
setSomeState(title);
}
return (
<Child toggleState={(e, title) => toggleState(e, title)} items={items}/>
)
// −−−−−−−−−−−−−−−−−−−−−^^^^^^^−−−−−−−−−−−−−−−−−−−−^^^^^
}
Child.tsx:
type ChildProps = {
items: Item[];
toggleState: (e: React.MouseEvent, title: string) => void;
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^
}
const Child: React.FC<ChildProps> = (props) => {
return (
{props.items.map((item) => (
<button onClick={(e) => props.toggleState(e, item.title)}> pass clicked item title to parent </button>
))}
)
// −−−−−−−−−−−−−−−−−−^^^^^^^−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^
}
Note the changes to toggleState in Parent.tsx and onClick in Child.tsx.
Live Example:
const { useState } = React;
/*
interface Item {
title: string;
}
*/
const items = [
{title: "Item 1"},
{title: "Item 2"},
{title: "Item 3"},
{title: "Item 4"},
];
const Parent = () => {
const [someState, setSomeState] = useState("");
const toggleState = (e/*: React.MouseEvent*/, title/*: string*/) => {
setSomeState(title);
}
return (
<div>
<div>someState = {someState}</div>
<Child items={items} toggleState={(e, title) => toggleState(e, title)} />
</div>
)
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^−−−−−−−−−−−−−−−−−−−−^^^^^
}
/*
type ChildProps = {
items: Item[];
toggleState: (e: React.MouseEvent, title: string) => void;
}
*/
const Child/*: React.FC<ChildProps>*/ = (props) => {
return <React.Fragment>
{props.items.map((item) => (
<button onClick={(e) => props.toggleState(e, item.title)}> pass clicked item title to parent </button>
))}
</React.Fragment>
// −−−−−−−−−−−−−−−−−−^^^^^^^−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^
}
ReactDOM.render(<Parent />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
On the playground to show the types work.

Delete an element by key from Array

I used library react-sortable-hoc for drag and drop element, but the library documentation does not have any actions for delete items. I want to delete, drag and drop item when click on close button. Which method is right for removing elements by key from object?
React
const SortableItem = SortableElement(({ value }: { value: string }, onRemove: any) =>
<div className="dragItems" style={{ background: 'gray' }}>
<img src={value} alt="" />
<button className="dragCloseBtn" onClick={() => onRemove(any)} />
</div>
);
const SortableList = SortableContainer(({ items }: { items: string[] }) => {
return (
<div className="dragAndDrop">
{items.map((value, index) => (
<SortableItem key={'item-${index}'} index={index} value={value} />
))}
</div>
);
});
constructor(props: any) {
super(props);
this.state = {
items: [
{
"id": 0,
"link": "https://via.placeholder.com/150"
},
{
"id": 1,
"link": "https://via.placeholder.com/150"
}
],
};
}
public onSortEnd = ({ oldIndex, newIndex }: { oldIndex: number, newIndex: number }) => {
this.setState({
items: arrayMove(this.state.items, oldIndex, newIndex),
});
};
public onRemove(e: { target: { value: any; }; }) {
const array = [...this.state.items];
const index = array.indexOf(e.target.value)
if (index !== -1) {
array.splice(index, 1);
this.setState({items: array});
}
}
<SortableList items={this.state.items}
onSortEnd={this.onSortEnd}
lockAxis="xy"
axis="xy" />
UPDATED:
Hi there, I figured out what went wrong and made a successful remove event on your application.
Everything is illustrated with comments at this codesandbox.
=========
I modified this one, it should do the required using Array's filter method.
public onRemove(e: { target: { value: any; }; }) {
let array = [...this.state.items];
const intId = parseInt(e.target.value, 10);
array = array.filter(item => item.id !== intId);
this.setState({items: array});
}
So there were few problems in your code! You seemed to be confuse how react works with passing down props. You have to pass down the method required for remove. And you should bind it inside the class that you will be calling it.
onRemove should be bound to current context
onRemove should be passed down across the component tree
Check my //[NOTE]====> comments for additional explanation
Working code sandbox is here
import * as React from "react";
import * as ReactDOM from "react-dom";
import {
arrayMove,
SortableContainer,
SortableElement
} from "react-sortable-hoc";
//[NOTE]====>value contains the relevent object containing the callback. Onclick call it with the relevant id
const SortableItem = SortableElement(
({ value }: { value: any }, onRemove: any) => (
<div className="dragItems" style={{ background: "gray", margin: "20px" }}>
<img src={value.link} alt="" />
<button className="dragCloseBtn" onClick={() => value.onRemove(value.id)}>
{" "}
X{" "}
</button>
</div>
)
);
const SortableList = SortableContainer(({ items }: { items: string[] }) => {
return (
<div className="dragAndDrop">
{items.map((value, index) => (
<SortableItem key={`item-${index}`} index={index} value={value} />
))}
</div>
);
});
class SortableComponent extends React.Component<{}, { items: string[] }> {
constructor(props: {}) {
super(props);
//[NOTE]====>Send the callback on each element bound to the current context
//This is like telling the function from where exactly the function will be called
this.state = {
items: [
{
id: 0,
link: "https://via.placeholder.com/150",
onRemove: this.onRemove.bind(this)
},
{
id: 1,
link: "https://via.placeholder.com/150",
onRemove: this.onRemove.bind(this)
}
]
};
}
public render() {
return <SortableList items={this.state.items} onSortEnd={this.onSortEnd} />;
}
public onSortEnd = ({
oldIndex,
newIndex
}: {
oldIndex: number;
newIndex: number;
}) => {
this.setState({
items: arrayMove(this.state.items, oldIndex, newIndex)
});
};
//[NOTE]====>Use the id to filter out and set the new set of items
public onRemove(id) {
console.log(id);
let array = [...this.state.items];
const intId = parseInt(id, 10);
array = array.filter((item: any) => item.id !== intId);
this.setState({ items: array });
}
}
ReactDOM.render(<SortableComponent />, document.getElementById("root"));

How to add right click menu to react table row, and access its properties?

I've added react-table package to my project and everything is fine, but I also wanted to have a possibility to right click on a row and perform some actions on it (cancel, pause etc). I'm using React with Typescript but I hope it doesn't add any complexity.
My initial idea was to use react-contextify, however I can't find any working examples that would combine react-table and react-contextify together.
The only "working" example I have found is this one:
React Context Menu on react table using react-contexify
I ended up not using react-contextify and it "kind of works" but I'm not totally certain about this one as I sometimes keep getting exceptions like this:
Uncaught TypeError: Cannot read property 'original' of undefined
The code I have now is this:
const columns = [
{
Header: "Name",
accessor: "name"
},
{
Header: "Age",
accessor: "age",
Cell: (props: { value: React.ReactNode }) => (
<span className="number">{props.value}</span>
)
},
{
id: "friendName", // Required because our accessor is not a string
Header: "Friend Name",
accessor: (d: { friend: { name: any } }) => d.friend.name // Custom value accessors!
},
{
Header: (props: any) => <span>Friend Age</span>, // Custom header components!
accessor: "friend.age"
}
];
return (
<div>
<ContextMenuTrigger id="menu_id">
<ReactTable
data={data}
columns={columns}
showPagination={false}
getTdProps={(
state: any,
rowInfo: any,
column: any,
instance: any
) => {
return {
onClick: (e: any, handleOriginal: any) => {
const activeItem = rowInfo.original;
console.log(activeItem);
},
onContextMenu: () => {
console.log("contextMenu", rowInfo);
this.setState({
showContextMenu: true,
rowClickedData: rowInfo.original
});
}
};
}}
/>
</ContextMenuTrigger>
{this.state.showContextMenu ? (
<MyAwesomeMenu clickedData={this.state.rowClickedData} />
) : null}
</div>
);
}
}
const MyAwesomeMenu = (props: { clickedData: any }) => (
<ContextMenu id="menu_id">
<MenuItem
data={props.clickedData}
onClick={(e, props) => onClick({ e, props })}
>
<div className="green">ContextMenu Item 1 - {props.clickedData.id}</div>
</MenuItem>
</ContextMenu>
);
const onClick = (props: {
e:
| React.TouchEvent<HTMLDivElement>
| React.MouseEvent<HTMLDivElement, MouseEvent>;
props: Object;
}) => console.log("-------------->", props);
What is the best (and simplest) way to add a context menu to react-table so I can use clicked row's props? I really like react-contextify but haven't found any examples.
Thanks
React Hooks exmaple on dev.to
Class Based Compnent example on codepen
class App extends React.Component {
constructor() {
super();
this.state = {
value: ''
};
}
render() {
return(
<div>
{
['row1', 'row2', 'row3'].map((row) => {
return (
<ContextMenu
key={row}
buttons={[
{ label: 'Editovat', onClick: (e) => alert(`Editace ${row}`) },
{ label: 'Smazat', onClick: (e) => alert(`Mažu ${row}`) }
]}
>
<div className="row">{row}</div>
</ContextMenu>
);
})
}
</div>
);
}
}
class ContextMenu extends React.Component {
static defaultProps = {
buttons: []
};
constructor() {
super();
this.state = {
open: false
};
}
componentDidMount() {
document.addEventListener('click', this.handleClickOutside);
document.addEventListener('contextmenu', this.handleRightClickOutside);
}
handleClickOutside = (e) => {
if (!this.state.open) {
return;
}
const root = ReactDOM.findDOMNode(this.div);
const context = ReactDOM.findDOMNode(this.context);
const isInRow = (!root.contains(e.target) || root.contains(e.target));
const isInContext = !context.contains(e.target);
if (isInRow && isInContext) {
this.setState({
open: false
});
}
}
handleRightClickOutside = (e) => {
if (!this.state.open) {
return;
}
const root = ReactDOM.findDOMNode(this.div);
const isInRow = !root.contains(e.target);
if (isInRow) {
this.setState({
open: false
});
}
}
handleRightClick = (e) => {
e.preventDefault();
console.log(e.nativeEvent, window.scrollY);
this.setState({
open: true,
top: window.scrollY + e.nativeEvent.clientY,
left: e.nativeEvent.clientX,
});
}
render() {
return (
<div
onContextMenu={this.handleRightClick}
ref={(node) => this.div = node}
>
{this.props.children}
{
!this.state.open
? null
: <div
className="context"
ref={(div) => this.context = div}
style={{ top: this.state.top, left: this.state.left }}
>
<ul>
{
// button - name, onClick, label
this.props.buttons.length > 0 &&
this.props.buttons.map((button) => {
return <li key={button.label}>
<a href="#" onClick={button.onClick}>
{button.label}
</a>
</li>
})
}
</ul>
</div>
}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));

Resources