I am striving to make customized rating component. I know there are other libraries for it but I wanted to do it myself so i can understand the underlying process. However, i am struggling on custom component part. For default case, it's working fine. For custom component what I tried is allow developer to pass svg component of their desired icon and then show that icon as a rating component. Up to this, it's working but I have no idea on handling mouseover, mouseout functionality.
here is what I have tried
import React from "react";
const DefaultComponent = ({
ratingRef,
currentRating,
handleMouseOut,
handleMouseOver,
handleStarClick,
totalRating
}) => {
return (
<div
className="rating"
ref={ratingRef}
data-rating={currentRating}
onMouseOut={handleMouseOut}
>
{[...Array(+totalRating).keys()].map(n => {
return (
<span
className="star"
key={n + 1}
data-value={n + 1}
onMouseOver={handleMouseOver}
onClick={handleStarClick}
>
★
</span>
);
})}
</div>
);
};
const CustomComponent = ({
ratingRef,
currentRating,
handleMouseOut,
handleMouseOver,
handleStarClick,
totalRating,
ratingComponent
}) => {
return (
<>
{[...Array(+totalRating).keys()].map(n => {
return ratingComponent({ key: n });
})}
</>
);
};
const Rating = props => {
const { totalRating = 5, onClick, ratingComponent } = props;
const [currentRating, setCurrentRating] = React.useState(0);
React.useEffect(() => {
handleMouseOut();
}, []);
const handleMouseOver = ev => {
const stars = ev.target.parentElement.getElementsByClassName("star");
const hoverValue = ev.target.dataset.value;
Array.from(stars).forEach(star => {
star.style.color = hoverValue >= star.dataset.value ? "#FDC60A" : "#444";
});
};
const handleMouseOut = ev => {
const stars = ratingRef?.current?.getElementsByClassName("star");
stars &&
Array.from(stars).forEach(star => {
star.style.color =
currentRating >= star.dataset.value ? "#FDC60A" : "#444";
});
};
const handleStarClick = ev => {
let rating = ev.target.dataset.value;
setCurrentRating(rating); // set state so the rating stays highlighted
if (onClick) {
onClick(rating); // emit the event up to the parent
}
};
const ratingRef = React.useRef();
console.log("ratingComponent", ratingComponent);
return (
<>
{ratingComponent ? (
<CustomComponent
ratingRef={ratingRef}
currentRating={currentRating}
handleMouseOut={handleMouseOut}
handleMouseOver={handleMouseOver}
handleStarClick={handleStarClick}
totalRating={totalRating}
ratingComponent={ratingComponent}
/>
) : (
<DefaultComponent
ratingRef={ratingRef}
currentRating={currentRating}
handleMouseOut={handleMouseOut}
handleMouseOver={handleMouseOver}
handleStarClick={handleStarClick}
totalRating={totalRating}
/>
)}
</>
);
};
export default Rating;
I have created a sandbox as well.
https://codesandbox.io/s/stoic-almeida-yz7ju?file=/src/components/Rating/Rating.js:0-2731
Can anyone give me idea on how to make reusable rating component which can support custom icons for rating like in my case i needed exactly that gem and star icon?
I'd do it this way:
reuse your DefaultComponent to take in the icon
use classnames instead of selecting DOM elements; one for default and one for hover. Add fill or color properties for the hover/selected classname
use a hoverIndex state to track which element I'm hovering on
Improvement: colocate the hover events within the DefaultComponent since the are relevant to only the icon, and aren't needed in the parent container
I had trouble styling the star svg. Essentially, you need to override the fill color for it - but it seems to be a complicated one. If you can simplify it, or pass down the fill as prop into the svg and use it internally - either would work!
Sandbox: https://codesandbox.io/s/epic-pascal-j1d0c
Hope this helps!
const DefaultIcon = _ => "a"; // = '★'
const DefaultComponent = ({
ratingComponent = DefaultIcon,
ratingRef,
currentRating,
handleStarClick,
totalRating
}) => {
const RatingIconComponent = ratingComponent;
const [hoverIndex, setHoverIndex] = React.useState(-Infinity);
const handleMouseOver = index => {
setHoverIndex(index);
};
const handleMouseOut = ev => {
setHoverIndex(-Infinity);
};
return (
<div
className="rating"
ref={ratingRef}
data-rating={currentRating}
onMouseOut={handleMouseOut}
>
{[...Array(+totalRating).keys()].map(n => {
const isFilled = n < currentRating || n <= hoverIndex;
return (
<span
className={`ratingIcon ${isFilled && "fill"}`}
key={n + 1}
data-value={n + 1}
onMouseOver={_ => handleMouseOver(n)}
onClick={_ => handleStarClick(n + 1)}
>
<RatingIconComponent />
</span>
);
})}
</div>
);
};
const Rating = props => {
const { totalRating = 5, onClick, ratingComponent } = props;
const [currentRating, setCurrentRating] = React.useState(0);
const handleStarClick = rating => {
setCurrentRating(rating); // set state so the rating stays highlighted
if (onClick) {
onClick(rating); // emit the event up to the parent
}
};
const ratingRef = React.useRef();
return (
<DefaultComponent
ratingComponent={ratingComponent}
ratingRef={ratingRef}
currentRating={currentRating}
handleStarClick={handleStarClick}
totalRating={totalRating}
/>
);
};
<Rating ratingComponent={Gem} />
/* for normal text */
.ratingIcon {
color: "gray";
}
.ratingIcon.fill {
color: yellow;
}
/* for svg */
.ratingIcon svg path {
fill: gray !important;
}
.ratingIcon.fill svg path {
fill: yellow !important;
}
Related
I'm currently trying to implement a chess variant, it is played on a 10x8 board instead of the normal 8x8. I'm doing it in React and using the DnD library, but I don't know how to implement the drop functionality. I'm not using the react chess library for this project. I can drag my piece as of now but it returns to its original position after I let go and doesn't drop.
My approach is to have a component for a chessboard, tile, and piece component.
So I am able to drag and piece and my console log shows the id and position that I created in my piece component useDrag hook. I've already started implement my useDrop hook in my tile class but my big issue is figuring out where the coordinates are when I drop my piece. How can I get that info? Once I have that I assume I can pass this it down as props to my piece and update the position like that. I've tried reading the docs but honestly I don't really understand it.
chess board component
import './Chessboard.css';
import Tile from '../Tile/Tile';
const verticalAxis = ["1","2","3","4","5","6","7","8"];
const horizonalAxis = ["a","b","c","d","e","f","g","h","i","j"];
const ChessBoard = ()=>{
const getPosition = () => {
//Don't know what to put here
}
let board = [];
let pieces = {1:"white_pawn", 6: "black_pawn", P03: "white_queen", P04: "white_empress", P05: "white_king", P06: "white_princess",
P73: "black_queen", P74: "black_empress", P75: "black_king", P76: "black_princess"};
pieces.P00 = pieces.P09 = "white_rook";
pieces.P01 = pieces.P08 = "white_knight";
pieces.P02 = pieces.P07 = "white_bishop";
pieces.P70 = pieces.P79 = "black_rook";
pieces.P71 = pieces.P78 = "black_knight";
pieces.P72 = pieces.P77 = "black_bishop";
for(let j = verticalAxis.length - 1; j >= 0; j--){
let row = [];
for(let i = 0; i < horizonalAxis.length; i++){
let type = undefined;
if(j === 1 || j === 6){
type = pieces[j];
}else{
type = pieces[`P${j}${i}`];
}
let color = (i + j + 2) % 2 === 0 ? "tile white-tile" : "tile black-tile";
row.push(<Tile key={`${j},${i}`} id={`${j},${i}`} type={type} color={color} position={`${j}${i}`}/>);
}
board.push(row);
}
return(
<div id="chessboard">{board}</div>
)
}
export default ChessBoard;
Tile Component
import './Tile.css';
import Piece from '../Piece/Piece';
import { useDrop } from 'react-dnd';
const Tile = ({color, type, id, position})=>{
const [{ isOver }, drop] = useDrop({
accept: "piece",
drop: (item) => console.log(item),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
});
const movePiece = (from, to) => {
}
return(
<div className={color} ref={drop}>
{type ? <Piece type={type} id={id} position={position}/> : ""}
</div>
)
};
export default Tile;
Piece component
import './Piece.css';
import { useDrag } from 'react-dnd';
const Piece = ({type, id, position}) =>{
const pieceImage = require(`../../assets/${type}.png`);
const [{isDragging}, drag] = useDrag(() => ({
type: "piece",
item: {id: type, position: position},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
})
}));
const handleClick = e => {
console.log(e.currentTarget.id);
}
return(
<div className="chess-piece" id={id} ref={drag} onClick={handleClick} style={{opacity: isDragging ? 0 : 1}}>
<img src={pieceImage}/>
</div>
)
};
export default Piece;
I'm building an application using react-leaflet and I recently updated all of my dependencies which brought on an error that I can't solve.
The error:
React has detected a change in the order of Hooks called by ForwardRef(ContainerComponent)
Removing the <MapContainer> component from my app fixes the error, but I cannot seem to figure out where the ForwardRef component is being rendered or why the order of hooks changes within it between renders.
This is my component:
const Map = ({ openModal }) => {
const [homeCoords, setHomeCoords] = useState([49.2, -123]);
const [bounds, setBounds] = useState({
lat_lower: 48.9,
lat_upper: 49.5,
lon_left: -123.8,
lon_right: -122.2
});
// Get the user's location with navigator.geolocation
useEffect(() => {
if(!window.navigator.geolocation) {
return;
}
window.navigator.geolocation.getCurrentPosition(
// success
(res) => {
setHomeCoords([res.coords.latitude, res.coords.longitude]);
},
// failure
() => {
console.error('Must allow pestlocations.com to access your location to use this feature.')
}
)
}, []);
// Helper function for BoundTracker component
const collectBounds = (e) => {
const bounds = e.target.getBounds();
const currLatLower = bounds._southWest.lat;
const currLatUpper = bounds._northEast.lat;
const currLonLeft = bounds._southWest.lng;
const currLonRight = bounds._northEast.lng;
setBounds({
lat_lower: currLatLower,
lat_upper: currLatUpper,
lon_left: currLonLeft,
lon_right: currLonRight
})
}
// Listen for dragging or zooming on map and update bounds
const BoundTracker = () => {
useMapEvents({
// Drag map
dragend: (e) => {
collectBounds(e);
},
// Zoom map
zoomend: (e) => {
collectBounds(e);
}
})
}
const HomeButton = () => {
const map = useMap();
return (
<div className="btn home" aria-disabled="false" onClick={() => {
map.panTo(homeCoords);
const bounds = map.getBounds();
setBounds({
lat_lower: bounds._southWest.lat,
lat_upper: bounds._northEast.lat,
lon_left: bounds._southWest.lng,
lon_right: bounds._northEast.lng
})
}}>
<AiFillHome />
</div>
)
}
return (
<>
<MapContainer className="Map" position={homeCoords} zoom={10}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<HomeButton />
<div className="btn info" onClick={() => openModal("Welcome")}><AiOutlineInfoCircle /></div>
<div className="btn legend" onClick={() => openModal("Legend")}><BsMap /></div>
<div className="search-btn" onClick={() => return}>Search This Area</div>
<PointClusters />
<BoundTracker />
</MapContainer>
</>
)
}
EDIT
Here is my PointClusters component:
import { useState } from 'react';
import MarkerClusterGroup from 'react-leaflet-cluster';
import { Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import './PointClusters.css';
// For testing purposes only - - - - - - - - - - - - - - - - - -
const testPoints = require('../../../testPoints.json').features;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const PointClusters = () => {
// Initialize points as testpoints. ** CHANGE FOR PRODUCTION **
const [points, setPoints] = useState(testPoints);
const newicon = new L.divIcon({
className: "custom-marker",
html: "<span class='arrow'></span>"
});
const createClusterCustomIcon = function (cluster) {
return L.divIcon({
html: `<span>${cluster.getChildCount()}</span>`,
className: 'custom-marker-cluster',
iconSize: L.point(33, 33, true),
})
}
return (
<MarkerClusterGroup
iconCreateFunction={createClusterCustomIcon}
>
{
points.map((point, i) => {
const { name, address, type, numOfReports, url } = point.properties;
const coords = [point.geometry.coordinates[1], point.geometry.coordinates[0]];
return (
<Marker position={coords} icon={newicon} key={i}>
<Popup>
{
name === "None" ? null :
<>
<b>{name}</b>
<br />
</>
}
<strong>Address</strong>: {address}
<br />
<strong>Type</strong>: {type}
<hr />
At least <a href={url}>{numOfReports} reports</a> on bedbugregistry.com
</Popup>
</Marker>
)
})
}
</MarkerClusterGroup>
)
}
Since you only have the useState hook in PointClusters I assume the issue here is react-leaflet-cluster package. I know it did not have support för react-leaflet 4 when I wanted to use it. It now have a version 2.0.0 that should be compatible, however looking into the code they use hooks in the solution.
Since they did not support react-leaflet 4 when I needed it I decided to adapt the actual code and modify to work and to fit my needs. Below is that adaption:
import { createPathComponent } from "#react-leaflet/core";
import L, { LeafletMouseEventHandlerFn } from "leaflet";
import "leaflet.markercluster";
import { ReactElement, useMemo } from "react";
import { Building, BuildingStore, Circle } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
import styles from "./LeafletMarkerCluster.module.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
type ClusterType = { [key in string]: any };
type ClusterEvents = {
onClick?: LeafletMouseEventHandlerFn;
onDblClick?: LeafletMouseEventHandlerFn;
onMouseDown?: LeafletMouseEventHandlerFn;
onMouseUp?: LeafletMouseEventHandlerFn;
onMouseOver?: LeafletMouseEventHandlerFn;
onMouseOut?: LeafletMouseEventHandlerFn;
onContextMenu?: LeafletMouseEventHandlerFn;
};
// Leaflet is badly typed, if more props needed add them to the interface.
// Look in this file to see what is available.
// node_modules/#types/leaflet.markercluster/index.d.ts
// MarkerClusterGroupOptions
export interface LeafletMarkerClusterProps {
spiderfyOnMaxZoom?: boolean;
children: React.ReactNode;
size?: number;
icon?: ReactElement;
}
const createMarkerCluster = (
{
children: _c,
size = 30,
icon = <Circle size={size} />,
...props
}: LeafletMarkerClusterProps,
context: any
) => {
const markerIcons = {
default: <Circle size={size} />,
property: <Building size={size} />,
business: <BuildingStore size={size} />,
} as { [key in string]: ReactElement };
const clusterProps: ClusterType = {
iconCreateFunction: (cluster: any) => {
const markers = cluster.getAllChildMarkers();
const types = markers.reduce(
(
acc: { [x: string]: number },
marker: {
key: string;
options: { icon: { options: { className: string } } };
}
) => {
const key = marker?.key || "";
const type =
marker.options.icon.options.className || key.split("-")[0];
const increment = (key.split("-")[1] as unknown as number) || 1;
if (type in markerIcons) {
return { ...acc, [type]: (acc[type] || 0) + increment };
}
return { ...acc, default: (acc.default || 0) + increment };
},
{}
) as { [key in string]: number };
const typeIcons = Object.entries(types).map(([type, count], index) => {
if (count > 0) {
const typeIcon = markerIcons[type];
return (
<div key={`${type}-${count}`} style={{ display: "flex" }}>
<span>{typeIcon}</span>
<span style={{ width: "max-content" }}>{count}</span>
</div>
);
}
});
const iconWidth = typeIcons.length * size;
return createLeafletIcon(
<div style={{ display: "flex" }} className={"cluster-marker"}>
{typeIcons}
</div>,
iconWidth,
undefined,
iconWidth,
30
);
},
showCoverageOnHover: false,
animate: true,
animateAddingMarkers: false,
removeOutsideVisibleBounds: false,
};
const clusterEvents: ClusterType = {};
// Splitting props and events to different objects
Object.entries(props).forEach(([propName, prop]) =>
propName.startsWith("on")
? (clusterEvents[propName] = prop)
: (clusterProps[propName] = prop)
);
const instance = new (L as any).MarkerClusterGroup(clusterProps);
instance.on("spiderfied", (e: any) => {
e.cluster._icon?.classList.add(styles.spiderfied);
});
instance.on("unspiderfied", (e: any) => {
e.cluster._icon?.classList.remove(styles.spiderfied);
});
// This is not used at the moment, but could be used to add events to the cluster.
// Initializing event listeners
Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
instance.on(clusterEvent, callback);
});
return {
instance,
context: {
...context,
layerContainer: instance,
},
};
};
const updateMarkerCluster = (instance: any, props: any, prevProps: any) => {};
const LeafletMarkerCluster = createPathComponent(
createMarkerCluster,
updateMarkerCluster
);
const LeafletMarkerClusterWrapper: React.FC<LeafletMarkerClusterProps> = ({
children,
...props
}) => {
const markerCluster = useMemo(() => {
return <LeafletMarkerCluster>{children}</LeafletMarkerCluster>;
}, [children]);
return <>{markerCluster}</>;
};
export default LeafletMarkerClusterWrapper;
I combine different types of markers and show each icon with the number in the cluster. You should be able to replace iconCreateFunction to fit your needs.
The createLeafletIcon look like this:
import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";
export const createLeafletIcon = (
icon: ReactElement,
size: number,
className?: string,
width: number = size,
height: number = size
) => {
return divIcon({
html: renderToString(icon),
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
className: className ? className : "",
});
};
Another tip is to looking into using useMemo on props, that the system might otherwise see as "new", but the first order of action should make it work, then you can try to find props that cause rerenders. Best of luck and let me know if you have any questions regarding the implementation
I created a simple logic: when you click on a certain block, the classname changes, but the problem is that when you click on a certain block, the classname changes and the rest of the blocks looks like this
I need to change only the name of the class that I clicked, I think I need to use the index, but I don't quite understand how to reolize it
export default function SelectGradientTheme(props) {
const resultsRender = [];
const [borderColor, setBorderColor] = useState(false);
const setBorder = () => {
setBorderColor(!borderColor)
}
const borderColorClassName = borderColor ? "selectBorder" : null;
for (var i = 0; i < GradientThemes.length; i += 3) {
resultsRender.push(
<div className={"SelectThemePictures_Separator"}>
{
GradientThemes.slice(i, i + 3).map((col, index) => {
return (
<div key={index} className={borderColorClassName} onClick={() => props.SideBarPageContent(col)|| setBorder()}>
</div>
);
})
}
</div>
)
}
return (
<div className="SelectThemeWrapper">
{resultsRender}
</div>
);
};
You can remember the selected index
Please reference the following code:
export default function SelectGradientTheme(props) {
const resultsRender = [];
const [selectedIndex, setSelectedIndex] = useState(false);
const setBorder = (index) => {
setSelectedIndex(index);
};
for (var i = 0; i < GradientThemes.length; i += 3) {
resultsRender.push(
<div className={"SelectThemePictures_Separator"}>
{
GradientThemes.slice(i, i + 3).map((col, index) => {
return (
<div key={index}
className={index === selectedIndex ? 'selectBorder' : null}
onClick={() => props.SideBarPageContent(col)|| setBorder(index)}>
</div>
);
})
}
</div>
)
}
return (
<div className="SelectThemeWrapper">
{resultsRender}
</div>
);
};
I am trying to make each individual list item clickable.
Here I have a color change state so that the color changes to red. But everytime I click one list item, it makes all of the boxes red. Again I want to select which ones to turn red, not turn all of them red. a hint in the right direction or a link would do fine.
import React, { Component } from 'react';
import './App.css';
import MainTool from './components/Layout/MainTool/MainTool.js';
import Aux from './hoc/Aux.js';
class App extends Component {
state = {
MainList: [
"GamePlay",
"Visuals",
"Audio",
"Story"
],
color: "white"
}
changeEle = () =>{
this.setState({color : "red"});
}
render() {
return (
<Aux>
<MainTool MainList = {this.state.MainList}
change = {this.changeEle}
color = {this.state.color}/>
</Aux>
);
}
}
export default App;
This MainTool just shoots my state arguments to Checkblock. Just for reference. Its unlikely the error is here although I have been wrong plenty times before.
import React from 'react';
import './MainTool.css';
import CheckBlock from '../../CheckBlock/CheckBlock';
const MainTool = props => {
return (
<div className = "mtborder">
<CheckBlock MainList = {props.MainList}
change = {props.change}
color = {props.color}/>
</div>
);
};
export default MainTool;
And here is my best guess at where the problem is. I used a loop to iterate through my state object array and print out the list and divs next to each list item. the divs being the elements I want to make clickable individually.
import React from 'react';
import './CheckBlock.css';
import Aux from '../../hoc/Aux';
const CheckBlock = props => {
console.log(props.change);
console.log(props.color);
let mainList = [];
for(let i = 0; i <= 3; i++)
{
mainList[i] = <li key = {i}
className = "nameBox">{props.MainList[i]}
<div onClick = {props.change}
style = {{backgroundColor: props.color}}
className = "clickBox"></div></li>
}
//console.log(dubi());
return (
<Aux>
<ul className = "mainList">{mainList}</ul>
<button>Enter</button>
</Aux>
);
};
export default CheckBlock;
You need a state based ListItem component with internal color state. No need to pass function as prop to change color. Use internal method
class ListItem extends Component {
state = { color : 'white' };
onClick = () => {
this.setState({ color: 'red' });
}
render () {
return (
<li key={i} className="nameBox">
{this.props.value}
<div onClick={this.onClick} style={{backgroundColor: props.color}}
className="clickBox">
</div>
</li>
);
}
}
const CheckBlock = props => {
console.log(props.change);
console.log(props.color);
let mainList = [];
for(let i = 0; i <= 3; i++)
{
mainList[i] = <ListItem key={i} value={props.MainList[i]} />
}
return (
<Aux>
<ul className = "mainList">{mainList}</ul>
<button>Enter</button>
</Aux>
);
};
I put jsfiddle together:
Let me know if there is a better practice to toggle element in React;
Cheers!
List item toggle jsfiddle
// ----------------APP-------------------
class App extends React.Component {
state = {
mainList: [
{
label: 'GamePlay',
id: 1,
},
{
label: 'Visuals',
id: 2,
},
{
label: 'Audio',
id: 3,
},
{
label: 'Story',
id: 4,
},
],
}
render() {
const { mainList } = this.state;
return (
<div>
<List mainList={mainList} />
</div>
);
}
}
// ----------------LIST-------------------
const List = ({ mainList }) => (
<div>
{mainList.map((listItem) => {
const { label, id } = listItem;
return (
<ListItem key={id} id={id} label={label} />
);
})}
</div>
);
// ----------------LIST-ITEM-------------------
class ListItem extends React.Component{
state = {
selected: false,
}
changeColor = () => {
const { selected } = this.state;
this.setState({selected: !selected})
}
render(){
const { label, id } = this.props;
const { selected } = this.state;
return console.log(selected ,' id - ', id ) || (
<button
className={selected ? 'green' : 'red'}
onClick= {() => this.changeColor()}
>
{label}
</button>
)
}
}
I can't seem to pass this handler correctly. TabItem ends up with undefined for onClick.
SearchTabs
export default class SearchTabs extends Component {
constructor(props) {
super(props)
const breakpoints = {
[SITE_PLATFORM_WEB]: {
displayGrid: true,
autoFocus: true,
},
[SITE_PLATFORM_MOBILE]: {
displayGrid: false,
autoFocus: false,
},
};
this.state = {
breakpoints,
filters: null,
filter: null,
isDropdownOpen: false,
selectedFilter: null,
tabs: null,
};
this.tabChanged = this.tabChanged.bind(this);
this.closeDropdown = this.closeDropdown.bind(this);
}
... more code
createTabs(panels) {
if(!panels) return;
const tabs = panels.member.map((panel, idx) => {
const { selectedTab } = this.props;
const { id: panelId, headline } = panel;
const url = getHeaderLogo(panel, 50);
const item = url ? <img src={url} alt={headline} /> : headline;
const classname = classNames([
searchResultsTheme.tabItem,
(idx === selectedTab) ? searchResultsTheme.active : null,
]);
this.renderFilters(panel, idx, selectedTab);
return (
<TabItem
key={panelId}
classname={classname}
idx={idx}
content={item}
onClick={this.tabChanged(idx, headline)}
/>
);
});
return tabs;
}
tabChanged(idx, headline) {
const { selectedTab } = this.props;
const { selectedFilter } = this.state;
const selectedFilterIdx = _.get(selectedFilter, 'idx', null);
if (selectedTab !== idx) {
this.props.resetNextPage();
this.props.setTab(idx, selectedFilterIdx, headline);
this.closeDropdown();
}
}
render() {
// const { panels, selectedTab } = this.props;
// if (!panels || panels.length === 0) return null;
//
//
// const { tabs, selectedTab } = this.props;
return (
<div>
<ul>{this.state.tabs}</ul>
</div>
);
}
}
export const TabItem = ({ classname, content, onClick, key }) => (
<li key={key} className={`${classname} tab-item`} onClick={onClick} >{content}</li>
);
so in TabItem onClick={onClick} ends up with undefined for onClick.
More info
here's how this used to work, when this was a function in the parent Container:
// renderDefaultTabs() {
// const { panels, selectedTab } = this.props;
//
// if (!panels || panels.length === 0) return;
//
// let filter = null;
//
// const tabs = panels.member.map((panel, idx) => {
// const { id: panelId, headline } = panel;
// const url = getHeaderLogo(panel, 50);
// const item = url ?
// <img src={url} alt={headline} /> : headline;
// const classname = classNames([
// searchResultsTheme.tabItem,
// (idx === selectedTab) ? searchResultsTheme.active : null,
// ]);
//
// filter = (idx === selectedTab) ? this.renderFilters(panel) : filter;
//
// return (
// <li
// key={panelId}
// className={classname}
// onClick={() => {
// this.tabChanged(idx, headline);
// }}
// >
// {item}
// </li>
// );
// });
So I extracted that out to that SearchTabs including moving the tabChange d method to my new SearchTabs component. And now in the container the above now does this:
renderDefaultTabs() {
const {
onFilterClick,
panels,
resetNextPage,
selectedTab,
selectedFilter,
isDropdownOpen,
} = this.props;
return (<SearchTabs
panels={panels}
...
/>);
}
Note: renderDefaultTabs() is sent as a prop to in the render() of the container and the Search calls it back thus rendering it in the Search's render():
Container
render() {
return (
<Search
request={{
headers: searchHeaders,
route: searchRoute,
}}
renderTabs={this.renderDefaultTabs}
renderSearchResults={this.renderSearchResults}
handleInputChange={({ input }) => {
this.setState({ searchInput: input });
}}
renderAltResults={true}
/>
);
}
Search is a shared component our apps use.
Update
So I mentioned that the Container's render() passes the renderDefaultTabs function as a prop to <Search />. Inside <Search /> it ultimately does this: render() { <div>{renderTabs({searchResults})}</div>} which calls the container's renderDefaultTabs function which as you can see above, ultimately renders
So it is passing it as a function. It's just strange when I click a TabItem, it doesn't hit my tabChanged function whatsoever
Update
Christ, it's hitting my tabChanged. Errr..I think I'm good. Thanks all!
onClick={this.tabChanged(idx, headline)}
This is not a proper way to pass a function to child component's props. Do it like (though it is not recommended)
onClick={() => this.tabChanged(idx, headline)}
UPDATE
I want to add more explanation. By onClick={this.tabChanged(idx, headline)}, you are executing tabChanged and pass its returned value to onClick.
With your previous implementation: onClick={() => { this.tabChanged(idx, headline); }}, now onClick will be a function similar to:
onClick = {(function() {
this.tabChanged(idx, headline);
})}
So it works with your previous implementation.
With your new implementation, onClick={() => this.tabChanged(idx, headline)} should work