How to make the visited step active? - javascript

I am making a simple react application and included react-stepper-horizontal library and things are fine.
Working Example:
Appropriate Code related to stepper:
const Form = () => {
.
.
.
const [currentPage, setCurrentPage] = useState(1);
const sections = [
{ title: 'Basic Details', onClick: () => setCurrentPage(1) },
{ title: 'Employment Details', onClick: () => setCurrentPage(2) },
{ title: 'Review', onClick: () => setCurrentPage(3) },
];
<Stepper
steps={sections}
activeStep={currentPage}
activeColor="red"
defaultBarColor="red"
completeColor="green"
completeBarColor="green"
/>
.
.
.
}
Steps to reproduce issue:
-> There are totally three steps 1,2,3 and each have different sections as Basic Details, Employment Details and Review respectively.
-> Now if user enter any of the input field in Step 1 and goes to Step 2 and fill some input fields there and goes to Step 3 to review it and if he comes back to the Step 1 again then the active state is lost in Step 3.
-> So now issue is if we want to go to step 3 then we need to again go three steps to reach last Step 3.
Requirement:
-> If user once visited any step then if he comes to any previous step then all the steps that he visited previously needs to be in active.
Eg:
-> If user landed in Step 1, then using next button , he reaches the Step 3 and if he wish to come back to Step 1 to modify some inputs and again if he wants to go to Step 3 for review step then it should be possible by clicking on the Step 3 because he already visited that step.
Kindly help me to achieve the result of making the steps in active state upto which the user visits.. If user visits Step 3 and goes back to step 1 on click of the Step 1 circle then there should be possibility to come back to Step 3 again as he already visited the Step 3..
Any solution without any library also welcomed.
This is a big issue if we have more steps (eg 7 steps). So please kindly help me.. A big thanks in advance..

Here's a simple implementation of the <Stepper /> component in question. The key is to have a tracker that tracks the visited steps internally, persist that information across re-renders.
Demoboard Playground
const { useState, useEffect, useMemo } = React;
const cx = classNames;
function range(a, b) {
return new Array(Math.abs(a - b) + 1).fill(a).map((v, i) => v + i);
}
function Stepper({ steps, activeStep, children }) {
const count = steps.length;
const listOfNum = useMemo(() => range(1, count), [count]);
const tracker = useMemo(() => {
let highestStep = 0;
function hasVisited(step) {
return highestStep >= step;
}
function addToBackLog(step) {
if (step > highestStep) highestStep = step;
}
return {
hasVisited,
addToBackLog,
getHighestStep() {
return highestStep;
},
};
}, []);
tracker.addToBackLog(activeStep);
const noop = () => {};
const prevStep = steps[activeStep - 2];
const currentStep = steps[activeStep - 1];
const nextStep = steps[activeStep];
return (
<div>
<div>
{" "}
{listOfNum.map((num, i) => {
const isActive = activeStep == num;
const isVisited = tracker.hasVisited(num);
const isClickable = num <= tracker.getHighestStep() + 1 || isVisited;
return (
<div
key={num}
className={cx("circle", {
active: isActive,
visited: isVisited,
clickable: isClickable,
})}
onClick={isClickable ? steps[i].onClick : noop}
>
{num}{" "}
</div>
);
})}{" "}
</div>{" "}
<h2> {currentStep && currentStep.title} </h2> <div> {children} </div>{" "}
<div className="footer">
{" "}
{prevStep ? (
<button onClick={prevStep.onClick}> prev </button>
) : null}{" "}
{nextStep ? <button onClick={nextStep.onClick}> next </button> : null}{" "}
</div>{" "}
</div>
);
}
function App() {
const [currentPage, setCurrentPage] = useState(1);
const sections = [
{
title: "Un",
onClick: () => setCurrentPage(1),
},
{
title: "Deux",
onClick: () => setCurrentPage(2),
},
{
title: "Trois",
onClick: () => setCurrentPage(3),
},
{
title: "Quatre",
onClick: () => setCurrentPage(4),
},
{
title: "Cinq",
onClick: () => setCurrentPage(5),
},
];
return (
<Stepper steps={sections} activeStep={currentPage}>
I 'm page {currentPage}{" "}
</Stepper>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
body {
color: #0f0035;
padding-bottom: 2rem;
}
.circle {
display: inline-flex;
height: 2em;
width: 2em;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: lightgrey;
margin: 0 0.5em;
color: white;
cursor: not-allowed;
}
.active {
background-color: rgba(50, 50, 250) !important;
}
.visited {
background-color: rgba(50, 50, 250, 0.5);
}
.clickable {
cursor: pointer;
}
.footer {
margin-top: 1em;
display: flex;
justify-content: space-between;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.6/index.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

Unable to move indicator to active link with NavLink in React Router 6.4.3

So, there are 5 NavLink's in my navbar, I'm trying to make indicator move when link is clicked and it works, but only before page reload.
Down are my js and scss files for you to see.
scss for indicator
#marker {
position: absolute;
top: 0;
width: 41px;
height: 41px;
transition: 0.5s;
z-index: 1;
}
#marker::before {
content: '';
position: absolute;
top: -10px;
left: 20%;
transform: translateX(-50%);
width: 40px;
height: 35px;
border-radius: 8px;
background: rgb(255, 255, 255);
box-shadow: 0 0 10px rgb(58, 134, 255), 0 0 20px rgb(255, 255, 255),
0 0 40px #fff, 0 0 80px #fff, 0 0 160px rgb(58, 134, 255);
}
Part of js file with NavLink, this code loops through li and deletes .active or gives it if item was clicked
const Sidebar = () => {
const marker = document.getElementById('marker')
const items = document.querySelectorAll('ul li')
function indicator(e) {
marker.style.left = e.offsetLeft + 'px'
marker.style.width = e.offsetWidth + 'px'
}
items.forEach((link) => {
link.addEventListener('click', (e) => {
indicator(e.target)
})
})
function activeLink() {
const current = document.querySelector('.active')
indicator(current)
}
items.forEach((link) => {
link.addEventListener('mouseover', (e) => {
activeLink()
})
})
const isActive = ({ isActive }) => (isActive ? 'active' : '')
<div className="navigation">
<ul>
<li className="list">
<NavLink to="/about" className={isActive}>
<FontAwesomeIcon
className="icon"
icon={faInfo}
></FontAwesomeIcon>
</NavLink>
</li>
<div id="marker"></div>
</ul>
</div>
The NavLink component already has and handles an active state and conditionally applies an "active" class by default. Querying the DOM directly is also considered an anti-pattern in React. You should instead use React refs to query underlying DOMNodes or directly attach event handlers to elements.
The following snippet pair also adds "click" and "mouseover" event handlers each render cycle by they are never removed.
const items = document.querySelectorAll('ul li');
...
items.forEach((link) => {
link.addEventListener('click', (e) => {
indicator(e.target);
});
});
items.forEach((link) => {
link.addEventListener('mouseover', (e) => {
activeLink();
});
});
Relying on querying the DOM and mouseover events can be problematic since these all exist outside React.
I suggest the following refactor:
Define the link data in an array that can be easily mapped to menu items.
const links = [
{
to: "/about",
icon: faInfo,
label: "About"
},
{
to: "/portfolio",
icon: faSuitcase,
label: "Works"
},
{
to: "/",
icon: faUser,
label: "Home"
},
{
to: "/contact",
icon: faContactBook,
label: "Contact"
},
{
to: "/links",
icon: faLink,
label: "Links"
}
];
Use React refs to create, attach, and hold references to the link item DOMNodes. The links array will be mapped to list items rendering the NavLink component, and the NavLink component will use the children render function so it's passing the isActive prop. Abstract the "children" into a React component that can call a function to set which menu item's DOMNode is the active node in the parent.
const SidebarItem = ({ isActive, link, setIsActive }) => {
useEffect(() => {
if (isActive) setIsActive();
}, [isActive, setIsActive]);
return (
<>
<FontAwesomeIcon className="icon" icon={link.icon} />
<span className="text">{link.label}</span>
</>
);
};
const Sidebar = () => {
const markerRef = useRef();
const linkRefs = useRef([]);
const [anchorEl, setAnchorEl] = useState(null);
linkRefs.current = links.map((_, i) => linkRefs.current[i] ?? createRef());
useEffect(() => {
if (anchorEl) {
markerRef.current.style.left = anchorEl.offsetLeft + "px";
markerRef.current.style.width = anchorEl.offsetWidth + "px";
}
}, [anchorEl]);
return (
<>
<div className="navigation">
<ul>
{links.map((link, index) => (
<li key={link.to}>
<NavLink ref={linkRefs.current[index]} to={link.to}>
{({ isActive }) => (
<SidebarItem
{...{
isActive,
link,
setIsActive: () =>
setAnchorEl(linkRefs.current[index].current)
}}
/>
)}
</NavLink>
</li>
))}
<div ref={markerRef} id="marker"></div>
</ul>
</div>
</>
);
};

How to implement server-side pagination using react-table?

I am new to react-table. I am trying to implement server-side pagination but I am not getting the logic on how to detect the page change in the new react-table version. I am using fetch data proper I am unable to detect the change. Each time I click on the Next button I should be able to change the offset value in the API endpoint in the increments of 20 to fetch new data. I am unable to perform this operation. Kindly help.
import React, { useEffect, useState, useMemo } from 'react'
import { URLs } from "../../../Config/url";
import cookie from 'react-cookies';
import "./OrderManagementScreen.css"
import { useTable, usePagination, useSortBy } from 'react-table';
import styled from 'styled-components';
const Styles = styled.div`
padding: 1rem;
table {
border-spacing: 0;
border: 1px solid lightgray;
width: 100%;
text-align: "center" !important;
tr {
:last-child {
td {
border-bottom: 0;
text-align: "center" !important;
}
}
}
th {
padding: 3px;
box-shadow: 0px 5px 7px 2px lightgrey;
}
td {
padding: 5px;
}
th,
td {
margin: 0;
text-align: "center";
border-bottom: 1px solid #73737361;
border-right: 1px solid #73737361;
:last-child {
border-right: 0;
}
}
}
.pagination {
}
`;
const WrapperTable = styled.div`
background: #ffffff;
box-shadow: 3px 3px 2px 0px rgb(162 161 161 / 75%) !important;
border-radius: 5px;
`
const Table = ({ columns, data }) => {
// Use the state and functions returned from useTable to build your UI
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
page, // Instead of using 'rows', we'll use page,
// which has only the rows for the active page
// The rest of these things are super handy, too ;)
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize, sortBy },
} = useTable(
{
columns,
data,
initialState: { pageIndex: 0 },
},
useSortBy,
usePagination
);
// const sorted = column.isSorted ? (column.isSortedDesc ? " 🔽" : " 🔼") : "";
// const sorted = column.isSorted ? (column.isSortedDesc ? {borderTop:"1px solid "} :{borderTop:"1px solid "}) : "";
// Render the UI for your table
return (
<>
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render("Header")}
{/* Add a sort direction indicator */}
<span>
{column.isSorted
? column.isSortedDesc
? " 🔽"
: " 🔼"
: ""}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})}
</tbody>
</table>
{/*
Pagination can be built however you'd like.
This is just a very basic UI implementation:
*/}
<div className="pagination">
{/* <button
className="pagination-btn"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
>
First
</button> */}
<button
className="pagination-btn"
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
Previous
</button>
<span className="pagination-btn text-center">
Page{" "}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{" "}
</span>
<button
className="pagination-btn"
onClick={() => nextPage()}
disabled={!canNextPage}
>
Next
</button>
{/* <button
className="pagination-btn"
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
>
Last
</button> */}
{/* <span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
/>
</span> */}
{/* <select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select> */}
</div>
</>
);
};
const OrderManagementScreen = () => {
const token = cookie.load("userObj").data.token;
//orderid, outletname, area, distributor, ordervalue, outlet type, discount group, salesofficer,order
const [tableData, SetData] = useState([]);
const [loading, setLoading] = React.useState(false);
const fetchIdRef = React.useRef(0);
const sortIdRef = React.useRef(0);
const columns = React.useMemo(
() => [
{
Header: "Order Id",
accessor: "id",
},
{
Header: "Outlet Name",
id: "outlet_detail",
accessor: data => {
let output = [];
data.outlet_detail.map(item => {
return output.push(item.name);
});
return output.join(', ');
}
},
{
Header: "Area",
id: "area",
accessor: data => {
let output = [];
data.outlet_detail.map(item => {
return output.push(item.area__name);
});
return output.join(', ');
}
},
{
Header: "Distributor",
id: "distributor",
accessor: data => {
let output = [];
data.outlet_detail.map(item => {
return output.push(item.distributor_name);
});
return output.join(', ');
}
},
{
Header: "Order Value",
accessor: "total_price",
},
{
Header: "Outlet Type",
id: "outlet_type__name",
accessor: data => {
let output = [];
data.outlet_detail.map(item => {
return output.push(item.final_value);
});
return output.join(', ');
}
},
{
Header: "Discount Group",
id: "discount__name",
accessor: data => {
let output = [];
data.outlet_detail.map(item => {
return output.push(item.discount__name);
});
return output.join(', ');
}
},
{
Header: "Sales Officer",
id: "sales_officer",
accessor: data => {
let output = [];
data.outlet_detail.map(item => {
return output.push(item.by_user__username);
});
return output.join(', ');
}
}
],
[]
);
const listdata = async () => {
const response = await fetch(`${URLs.orderUrl}?limit=20&offset=0`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${token}`
}
})
const data = await response.json();
SetData(data);
}
const fetchData = React.useCallback(({ pageSize, pageIndex, sortBy }) => {
// This will get called when the table needs new data
// You could fetch your data from literally anywhere,
// even a server. But for this example, we'll just fake it.
// Give this fetch an ID
console.log(pageIndex);
console.log(pageSize);
const fetchId = ++fetchIdRef.current;
// Set the loading state
setLoading(true);
// We'll even set a delay to simulate a server here
setTimeout(() => {
// Only update the data if this is the latest fetch
if (fetchId === fetchIdRef.current) {
const startRow = pageSize * pageIndex;
const endRow = startRow + pageSize;
if (sortBy.length === 0) {
SetData(tableData.sort().slice(startRow, endRow));
} else {
SetData(
tableData
.sort((a, b) => {
const field = sortBy[0].id;
const desc = sortBy[0].desc;
if (a[field] < b[field]) {
return desc ? -1 : 1;
}
if (a[field] > b[field]) {
return desc ? 1 : -1;
}
return 0;
})
.slice(startRow, endRow)
);
}
// Your server could send back total page count.
// For now we'll just fake it, too
// setPageCount(Math.ceil(serverData.length / pageSize));
setLoading(false);
}
}, 1000);
}, []);
useEffect(() => {
listdata();
}, [])
return (
<div className="p-3 text-center">
<h4>Order Management</h4>
<WrapperTable>
<Styles>
<Table columns={columns} fetchData={fetchData} data={tableData} />
</Styles>
</WrapperTable>
</div>
)
}
export default OrderManagementScreen;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
First, you need to understand the basic difference between client-side pagination and server-side pagination.
In client-side pagination, we already have all the data for all the pages which we need to display in the table that means we know the total count as well (totalcount=pagesize*number of pages).
Now compare this with server-side pagination. We shall be getting the slice of data which we request means if we are keeping page size as 10 and we have 100 data at our server but since we requested 10 so we'll only get 10 items. Then how will the pagination component know what will be the total number of pages which he needs to display?
That's why we need a total count from the server as well when we are fetching the data.
But wait, do we need it every time? Well, it depends on your use-case. In general, we need the total count either for the first time or in case we are doing any find or filter.
Now coming to your solution-
In react-table if we did not explicitly set the flag manualPagination as true then it will process the number of pages based of your supplied data and the pagesize so it will auto handle the pagination. So we need to make this manualPagination as true in options we passed to useTable and also we need to supply the total number of pages that is pageCount. So this will be something like
useTable(
{
columns,
data,
initialState: { pageIndex: 0 },
},
useSortBy,
usePagination,
manualPagination: true,
pageCount: (totalcount/pagesize),//you can handle this calculation in your fetchdata method
);
and then add your fetch data call inside a new useEffect with your pageindex and the
pagesize as dependencies
React.useEffect(() => {
fetchData({ pageIndex, pageSize })
}, [fetchData, pageIndex, pageSize])
I hope this will solve your issue. This is also well explained in the react-table documentation with proper codeshare example. Check here

fill up a bar with incremental value and timeout

I'm trying to do a loading bar with fixed timeout, says within 5 seconds, the bar should all filled up. I'm able to write the html and css but stuck in the js logic.
function App() {
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setTick(tick => tick + 10); //some calculation is missing
}, 1000);
setTimeout(() => {
clearInterval(id);
}, 5000);
return () => clearInterval(id);
}, []);
return (
<div className="App">
<div
style={{
width: "100%",
background: "yellow",
border: "1px solid"
}}
>
<div
style={{
height: "10px",
background: "black",
width: tick + "%"
}}
/>
</div>
</div>
);
}
https://codesandbox.io/s/proud-architecture-fuwcw
I refactored your code a little.
I created 3 constants:
maxLoad: Is the percentage to cover, in your case a 100%.
fulfillInterval: It's the interval to fill a step in the bar.
step: It's the calculation of the width to fill in the present iteration.
Then I changed a while the code adding 1 milisecond to the clearTimeout to ensure that it's going to work and... it's working. :)
Hope this helps.
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [tick, setTick] = useState(0);
const maxLoad = 100; // total percentage to cover
const fulfillInterval = 5000; // clear interval timeout
const step = maxLoad/(fulfillInterval/1000); // % filled every step
useEffect(() => {
const id = setInterval(() => {
setTick(tick => tick + step); // No dependency anymore
}, 1000);
setTimeout(() => {
clearInterval(id);
}, fulfillInterval+1);
return () => clearInterval(id);
}, []);
return (
<div className="App">
<div
style={{
width: "100%",
background: "yellow",
border: "1px solid"
}}
>
<div
style={{
height: "10px",
background: "black",
width: tick + "%"
}}
/>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useEffect(() => {
const id = setInterval(() => {
if(tick !==100)
setTick(tick => tick + 10); // No dependency anymore
}, 1000);
setTimeout(() => {
clearInterval(id);
}, 5000);
return () => clearInterval(id);
}, [tick])
Replace your useEffect function like this.

How do you call a function by default in the render method?

For some reason, its not actually accessing the exercises method. Can someone tell me what I'm doing wrong here. Appreciate it!
class StartWorkout extends Component {
state = {
step: 0
};
exercises = () => {
const { exerciselist } = this.props.selectedWorkout;
const { step } = this.state.step;
while (step < exerciselist.length) {
return <StartExercise exercise={exerciselist[step]} />;
}
};
render() {
const { name } = this.props.selectedWorkout;
const { step } = this.state;
return (
<>
<ClientMenuBar title={name} />
<Container maxWidth="xl">
{this.exercises()}
<div style={styles.buttonWrapper}>
<Button
color="inherit"
variant="contained"
style={styles.button}
size="large"
>
Back
</Button>
<Button
color="primary"
variant="contained"
type="submit"
style={styles.button}
size="large"
onClick={() => {
this.setState({ step: step + 1 });
}}
>
Next
</Button>
</div>
</Container>
</>
);
}
}
I know its not actually going into the method because I am console logging right before i access it and right before the while loop in the function and its only calling the first.
The issue is with the exercises method. Here you are trying to accessstep from this.state.step. Replace const { step } = this.state.step; . by const { step } = this.state;.
Notice the extra step property which you are trying to access which comes as undefined and undefined < any number is false, and hence the code inside while loop never gets executed.
A few problems here:
// this line is double-dereferencing 'step'
const { step } = this.state.step;
// step will be undefined here
while (step < exerciselist.length) {
// if you're going to return this, what's the loop for?
// (and if you don't return, this loop will never exit
// because 'step' never changes)
return <StartExercise exercise={exerciselist[step]} />;
}
Here's an updated example of how you might go about it now that I better understand what you're trying to do.
class Exercises extends React.Component {
state = {step: 0}
render() {
const {step} = this.state;
const {workout: {exercises}} = this.props;
return (
<div>
<div> {exercises[step]} </div>
<button
disabled={step < 1}
onClick = {() => this.setState({step: step - 1})}
>
Previous
</button>
<button
disabled={step >= exercises.length - 1}
onClick = {() => this.setState({step: step + 1})}
>
Next
</button>
</div>
)
}
}
const workout = {
exercises: Array.from({length: 5}, (_, i) => `Exercise ${i + 1}`)
};
ReactDOM.render(
<Exercises workout={workout} />,
document.querySelector("#app")
);
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.done {
color: rgba(0, 0, 0, 0.3);
text-decoration: line-through;
}
input {
margin-right: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
You need to use constructor. Update your code like this:
class StartWorkout extends Component {
constructor(props){
super(props);
this.state = {
step: 0,
};
}
exercises = () => {
const { exerciselist } = this.props.selectedWorkout;
// The way your destructuring the state is wrong. It should be like this.
const { step } = this.state; // not required anymore
return exerciselist.map(item => {
return <StartExercise exercise={item} />;
}
};
// Use your render function as it is.
}
Hope this helps!!

changing state of individual react component

Im new to React and started working on a memory game where you flip cards and compare two cards. Im having trouble understanding how to change state of individual component. now when I click a component the state of all components change and all my cards turn red instead of one. later I was thinking to add photos but for now just testing with background color. Also I know I have to add some logic/features but cant get past state problem.
App.js
import React, {Component} from 'react';
import './App.css';
import Grid from './grid/grid';
import Header from './Header/header';
import Footer from './Footer/footer';
class App extends Component {
cards = [{id:1, name: 'dog'},{id:2, name: 'dog'},{id:3, name: 'cat'},{id:4, name: 'cat'},{id:5, name: 'mouse'},{id:6, name: 'mouse'},{id:7, name: 'horse'},{id:8, name: 'horse'},
{id:9, name: 'pig'},{id:10, name: 'pig'},{id:11, name: 'chicken'},{id:12, name: 'chicken'},{id:13, name: 'cow'},{id:14, name: 'cow'},{id:15, name: 'fox'},{id:16, name: 'fox'}]
.sort( () => Math.random() - 0.5);
clicks = [];
state = {
current: 0,
}
clickHandler = (click) => {
this.clicks.push(click.name);
this.setState({
current: 1
})
console.log(this.clicks);
if (this.clicks.length > 1) {
this.compare(click.name);
}
}
compare = (name) => {
if (name === this.clicks[0]) {
console.log('pair')
} else {
console.log('nope');
}
}
render() {
return (
<div className="App">
<Header />
<div className='Grid-container'>
<div className='wrapper'>
{this.cards.map(child =>
<Grid click={() => this.clickHandler(child)}
active={this.state.current === 0}
id={child.id}
/>)}
</div>
</div>
<Footer />
</div>
);
}
}
export default App;
grid.js
import React from 'react';
import './grid.css';
const Grid = (props) => {
return (
<div className={'Child' + (props.active ? '': ' active')}
onClick={props.click}
>
{props.id}
</div>
);
}
export default Grid;
App.css
.Grid-container {
display: flex;
background-color: black;
justify-content: center;
align-items: center;
}
.wrapper {
display: grid;
width: 700px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
grid-gap: 10px;
background-color: black;
justify-content: center;
align-items: center;
}
**grid.css**
.Child {
width: auto;
height: 120px;
background-color: azure;
border-radius: 10px;
}
.Child.active {
width: auto;
height: 120px;
background-color: red;
border-radius: 10px;
}
You have to use index for this. You have index as second argument in map,
{this.cards.map( ( child, index ) =>
<Grid
click={() => this.clickHandler(child,index)}
active={this.state.current === index}
id={child.id}
key ={child.id} //Provide unique key here
/>
)}
Your click hander should be,
clickHandler = (click,index) => {
this.clicks.push(click.name);
this.setState({
current: index //Set index in a state
})
console.log(this.clicks);
if (this.clicks.length > 1) {
this.compare(click.name);
}
}
You need to initialize current state as empty, otherwise you will get first grid by default active
state = {
current: '',
}
Your Grid component will be this,
const Grid = (props) => {
return (
<div
className={`Child ${props.active ? 'active': ''}`} //Your condition should be this
onClick={props.click}
>
{props.id}
</div>
);
}
Demo
Updating state section
state = {
current: -1, // nothing is selected it contains id of selected card
clicks: [],
}
clickHandler = (selectedId) => {
const { clicks, current } = this.state;
if (current === -1) { // no card selected to check
this.setState({
current: selectedId,
clicks: clicks.includes(selectedId) ? clicks : [...clicks, selectedId],
});
return; // no more in this funtion
}
if (selectedId === current) { // already selected card(totally same card)
this.setState({
current: -1, // unselect last selected card :(
clicks: clicks.slice(0, clicks.length - 1), // remove last selected click
});
} else { // different card. here check if they have same name
if (this.cards[selectedId].name === this.cards[current].name) {
// couple cards
console.log('Bingo They are Same!!!');
this.setState({
current: -1,
clicks: [...cliks, selectedId], // add just selected card in cliks
});
} else {
// Oh, you failed!
this.setState({
current: -1, // unselect last selected card :(
clicks: clicks.slice(0, clicks.length - 1),
});
}
}
}
Render
...
<Grid
click={() => this.clickHandler(child.id)}
active={this.state.clicks.includes(child.id)} // check if it is included in clicks
id={child.id}
/>
...
Then you can see cool cards game. :)

Categories

Resources