i have a question . I have to build an app where people can search for music from "lastFm" . So far so good , i already made few things to works normal , but i have a problem with if/else in map function , i've try to show user "no result found" if there are any , but with no luck .If there is 1+ results , will be displayed on the screen , but if there are any , nothing happen . Here is my code .
import React, { Component } from 'react';
import AppBar from '#material-ui/core/AppBar';
import Toolbar from '#material-ui/core/Toolbar';
import Typography from '#material-ui/core/Typography';
import {
TextField,
Button,
List,
ListItem,
ListItemAvatar,
ListItemText,
Avatar,
Card,
CardContent
} from '#material-ui/core';
import axios from 'axios';
import './App.css';
const API_URL = 'http://ws.audioscrobbler.com/2.0/?limit=5&format=json&method=artist.search&api_key=' + process.env.REACT_APP_LASTFM_APPKEY;
const isEmpty = (str) => str.length === 0;
class App extends Component {
state = {
searchTerm: '',
savedArtists: []
}
componentDidMount() {
const existing = localStorage.getItem('savedArtists')
if (existing) {
this.setState({ savedArtists: JSON.parse(existing) })
}
}
onTextChange = (event) => {
const value = event.target.value;
this.setState({ searchTerm: value });
}
search = (terms) => {
const request = API_URL + '&artist=' + terms;
axios.get(request).then((response) => {
const results = response.data.results;
const artists = results.artistmatches.artist.map((artist) => {
const avatarImage = artist.image.find(image => image.size === 'medium');
const imageUrl = avatarImage['#text'];
return { ...artist, avatar: imageUrl }
});
this.setState({ artists });
})
}
onSearchClick = () => {
this.search(this.state.searchTerm);
}
clearSearch = () => {
this.setState({
searchTerm: '',
artists: []
})
}
onResultClick = (artist) => {
this.clearSearch();
const savedArtists = this.state.savedArtists;
savedArtists.push(artist)
this.setState({ savedArtists: savedArtists })
localStorage.setItem('savedArtists', JSON.stringify(savedArtists));
}
render() {
const results = this.state.artists || [];
return (
<div className="App">
<header className="App-header">
<AppBar position="static" color="primary">
<Toolbar className="search-bar">
<Typography variant="h6" color="inherit">
Photos
</Typography>
<TextField
placeholder="Search on Last.fm"
className="search-input"
onChange={this.onTextChange}
value={this.state.searchTerm}
/>
<Button
onClick={this.onSearchClick}
variant="contained"
color="secondary"
disabled={isEmpty(this.state.searchTerm)}
>
Search
</Button>
{!isEmpty(this.state.searchTerm) && (
<Button
onClick={this.clearSearch}
variant="contained"
>
Clear
</Button>)
}
</Toolbar>
</AppBar>
</header>
//****Here is where i've try to use if/else
<List className="search-results">
{
results.map((artist ,results) => {
if(results.length === 0)
return (<ListItem> Not Found</ListItem>
); else {
return ( <ListItem
button
key={artist.name}
className="result"
onClick={() => this.onResultClick(artist)}
>
<ListItemAvatar>
<Avatar src={artist.avatar} alt={artist.name} />
</ListItemAvatar>
<ListItemText primary={artist.name} />
<Button
variant="outlined"
color="secondary"
size="small"
className="add-button"
>
Add to favorites
</Button>
</ListItem>);
}
})
}
</List>
<div className="artist-container">
{
this.state.savedArtists.map((artist, i) => {
return (
<Card className="artist-card"
key={i}
>
<CardContent>
{artist.name}
</CardContent>
</Card>
)
})
}
</div>
</div>
);
}
}
export default App;
You're having an error there. It's .map(result: any, index: number, original: []), so you're referring to an index number with argument results:
results.map((artist, results) => {
if(results.length === 0) { ... }
});
So fix it just by not referring to results as a argument of .map
The problem is that you're trying to do an if/else in the map of the array. But if the array has no items, then there is nothing to map.
What to do is to use a ternary to check if the array has any results:
{ results && result.length ?
<List className="search-results">
{
results.map((artist) => {
return (
<ListItem button key={artist.name} className="result" onClick={() => this.onResultClick(artist)} >
<ListItemAvatar>
<Avatar src={artist.avatar} alt={artist.name} />
</ListItemAvatar>
<ListItemText primary={artist.name} />
<Button
variant="outlined"
color="secondary"
size="small"
className="add-button"
>
Add to favorites
</Button>
</ListItem>
);
})
}
</List>
: <div>No Results</div>
}
Here, we're checking if results.length is considered truthy or not, if it's 1 or higher, then it will render your list, otherwise it will render our div informing the user there is no results, which you can change out to be whatever you want.
Related
I'm working on a simple todos app and I'm stuck at a point. Basically, my array filtering is not working. It's not doing anything in fact and I couldn't find out why. I'm using Material UI in the app and I'm suspecting there is something related to that but couldn't figure out entirely.
I'm trying to delete one todo by clicking the trash icon which triggers "deleteTodo" function. But it's not deleting it from the todos. Actually, as I said it's doing nothing. I'm keeeping my todos in the localStorage.
Here is my delete one todo function:
function deleteTodo(id) {
setTodos(todos.filter((todo,i,arr) => {
console.log("id:", id)
console.log("todo.id:",todo.id)
console.log("are equal:", todo.id === id)
console.log(i, arr)
return (todo.id !== id)
}))
}
console output:
[Log] id: – "37dbcd88d5a"
[Log] todo.id: – "37dbcd88d5a"
[Log] are equal: – true
[Log] 1 – [{text: "two", done: false, id: "7dbcd88d5a3"}, {text: "one", done: false, id: "37dbcd88d5a"}] (2)
my component as a whole:
import { uid } from 'uid'
import { useState , useEffect, useLayoutEffect, useRef } from "react"
import { Card, CardContent, Modal, List, ListItem, Box, Button, IconButton, TextField, Typography } from "#mui/material"
import styles from "../styles/Todos.module.css"
import { TransitionGroup } from 'react-transition-group';
import ReportProblemIcon from '#mui/icons-material/ReportProblem';
import DeleteIcon from '#mui/icons-material/Delete';
import EditIcon from '#mui/icons-material/Edit';
export default function Todos () {
const [ text, setText ] = useState("")
const [ todos, setTodos ] = useState([])
const [ showClearTodosModal, setShowClearTodosModal] = useState(false)
const inputRef = useRef()
useEffect(() => {
// localStorage.todos && console.log('b1:',JSON.parse(localStorage.todos))
if (localStorage.todos) {
// localStorage.todos && console.log(JSON.parse(localStorage.todos))
setTodos(JSON.parse(localStorage.todos))
} else {
localStorage.todos = []
}
// localStorage.todos && console.log('a1:',JSON.parse(localStorage.todos))
}, [])
useEffect(() => {
if (todos && todos.length > 0) {
localStorage.todos && console.log('b2:',JSON.parse(localStorage.todos))
localStorage.setItem("todos", JSON.stringify(todos))
localStorage.todos && console.log('a2:',JSON.parse(localStorage.todos))
}
}, [todos])
function handleSubmit(e) {
e.preventDefault()
const id = uid()
setTodos([{text, done:false, id:id}, ...todos])
setText("")
}
function markDone(e) {
setTodos(todos.map(todo => {
if (e.target.innerText === todo.text) {
return {...todo, done:!todo.done}
} else {
return todo
}
}))
}
function deleteTodo(id) {
setTodos(todos.filter((todo,i,arr) => {
console.log("id:", id)
console.log("todo.id:",todo.id)
console.log("are equal:", todo.id === id)
console.log(i, arr)
return (todo.id !== id)
}))
}
function deleteAll() {
setTodos([])
setShowClearTodosModal(false)
localStorage.removeItem("todos")
}
return (
<Box>
<form
onSubmit={handleSubmit}
>
<TextField
className={styles.entryfield}
label="Add a todo"
autoFocus
value={text}
onChange={(e) => setText(e.target.value)}
ref={inputRef}
/>
</form>
<Button
color="primary"
aria-label="upload picture"
component="span"
className={styles.clearall}
onClick={() => setShowClearTodosModal(true)}
startIcon={<ReportProblemIcon />}
>
Delete All Todos
</Button>
<Modal
open={showClearTodosModal}
onClose={() => setShowClearTodosModal(false)}
aria-labelledby="delete all todos"
aria-describedby="delete all todos"
className={styles.deleteallmodal}
>
<Box className={styles.modalbox}>
<Typography id="modal-modal-title" variant="h6" component="h2">
Delete all todos?
</Typography>
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
Are you sure you want to delete all todos? This action is irreversable and you will lose all of your todos.
</Typography>
<Button
onClick={() => setShowClearTodosModal(false)}
variant="contained"
sx={{m:1}}
>
Nah, don't delete my todos
</Button>
<Button
onClick={deleteAll}
variant="contained"
startIcon={<ReportProblemIcon />}
sx={{
m:1,
color: "maroon",
}}
>
Yes I'm sure delete all of them
</Button>
</Box>
</Modal>
<List>
{todos && todos.map(todo =>(
<ListItem
key={todo.id}
onClick={markDone}
>
<Card className={styles.card}
>
<IconButton
className={styles.icons}
onClick={() => {deleteTodo(todo.id)}}
aria-label="delete"
>
<DeleteIcon fontSize="small"/>
</IconButton>
<IconButton
className={styles.icons}
aria-label="edit"
>
<EditIcon fontSize="small"/>
</IconButton>
<Typography
variant="body1"
style= {{
color: todo.done ? "#555" : "",
margin: 10,
}}
>
{todo.text}
</Typography>
</Card>
</ListItem>
)
)}
</List>
</Box>
)
}
Here is the working codesandbox of your component. Please align your code with my example and be sure that you do not have any other wrong implementations in your code
https://codesandbox.io/s/heuristic-shadow-7lz1o0?file=/src/App.js
import { useState } from "react";
import "./styles.css";
export default function App() {
const [todos, setTodos] = useState([
{ text: "two", done: false, id: "7dbcd88d5a3" },
{ text: "one", done: false, id: "37dbcd88d5a" },
{ text: "one", done: false, id: "643dbcd88d5a" }
]);
function deleteTodo(id) {
setTodos(
todos.filter((todo, i, arr) => {
console.log("id:", id);
console.log("todo.id:", todo.id);
console.log("are equal:", todo.id === id);
console.log(i, arr);
return todo.id !== id;
})
);
}
return (
<div className="App">
{todos &&
todos.map((todo) => (
<div key={todo.id}>
<div>
<button
onClick={() => {
deleteTodo(todo.id);
}}
aria-label="delete"
>
{todo.id}
</button>
</div>
</div>
))}
</div>
);
}
let testArray = [{text: "two", done: false, id: "7dbcd88d5a3"}, {text: "one", done: false, id: "37dbcd88d5a"},
{text: "one", done: false, id: "643dbcd88d5a"}]
function deleteTodo(id) {
console.log(testArray.filter((todo,i,arr) => {
return (todo.id !== id)}))
}
deleteTodo('7dbcd88d5a3');
Thanks for sharing your full code! It was helpful to understand what was happening.
The culprit line that's causing you the headache is here:
<ListItem
key={todo.id}
onClick={markDone}
>
<Card className={styles.card}
>
<IconButton
className={styles.icons}
onClick={() => {deleteTodo(todo.id)}}
aria-label="delete"
>
Specifically, onClick={markDone} on ListItem. You put the markDone function on the whole list item. So clicking on the IconButton causes the ListItem event to be triggered too.
I know you have the e.preventDefault() but unfortunately the order is reversed here with event bubbling (starts inside out and you need stopPropagation instead of preventDefault): I put some logs in deleteTodo and markDone to indicate the start and end of each function and this is the order. This was the output:
Todos.tsx:82 deleteTodo start
Todos.tsx:86 deleteTodo end
Todos.tsx:66 markdone start
Todos.tsx:76 markdone end
So what's likely happening is react is either doing the setTodos sequentially or batching them, but either way it looks like the setTodos from markDone is taking precedence, and the setTodo there is just a map of the same values again. Hence why clicking the delete button leads to no changes and the same todo items remaining.
Possible solutions could include
a) Add the event as a parameter to deleteTodo and run e.stopPropagation() at the top of the function to prevent event bubbling further.
function deleteTodo(e, id) {
e.stopPropagation();
console.log('deleteTodo start');
setTodos(
todos.filter((todo, i, arr) => {
return todo.id !== id;
})
);
console.log('deleteTodo end');
}
and setup your IconButton:
<IconButton
onClick={(e) => {
deleteTodo(e, todo.id);
}}
This should help stop the event from bubbling further up (I confirmed this to be working locally).
b) You could remove the onClick={markDone} and add a separate button to do the mark done action (removing the onClick is confirmed to make your delete method work)
c) You keep the markDone but do some additional conditional checking on the e event target to see if the delete button was hit - if it was, return early in the function.
I hope you could help me with this part, so I have no problem with the stepper and no problem with the forms too when it comes to getting data input and validating fields with yup .
but I have a problem with the checkboxes and autocomplete. when I go back to a previous step, I lose the values I have already entered and validation no longer works with checkbox and autocomplete field.
There is an example of a hook that I made for the checkboxes that I will use later in a form :
const CheckBoxField = (props) => {
const { label, ...rest } = props;
const [field, meta, helper] = useField(props);
const { setValue } = helper;
const [touched, error] = at(meta, "touched", "error");
const isError = error && touched && true;
function __renderHelperText() {
if (isError) {
return <FormHelperText>{error}</FormHelperText>;
}
}
function _onChange(event) {
setValue(event.target.checked);
}
const configFormControl = {
...field,
...rest,
onChange: _onChange,
};
return (
<FormControl
component="fieldset"
{...configFormControl}
error={Boolean(isError)}
>
<FormControlLabel
// value={field.checked}
checked={field.checked}
label={label}
onChange={_onChange}
control={
<BpCheckbox
{...configFormControl}
// checked={field.checked}
color="primary"
/>
}
color="primary"
/>
{__renderHelperText()}
</FormControl>
);
};
And there is the code for validation :
import * as yup from "yup";
import signUpFormModel from "../signUpFormModel";
const {
formField: {
terms //Boolean , used in checkboxes, should be true
},
} = signUpFormModel;
const signUpValidationSchema = [
yup.object().shape({
[terms.name]: yup.boolean().oneOf([true], `${terms.requiredErrMsg}`),
//other fields ...
}),
//other forms ...
];
export default signUpValidationSchema;
My Initial value :
import signUpFormModel from "../signUpFormModel";
const {
formField: {
terms,
},
} = signUpFormModel;
const formInitialValue = {
[terms.name]: false,
};
export default formInitialValue;
and some props helper (used as model for my forms)
import React, { Fragment } from "react";
import { Link } from "#mui/material";
const signUpFormModel = {
formId: "registration",
formField: {
terms: {
type: "checkbox",
name: "terms",
id: "terms",
label: () => (
<Fragment>
J'accepte les{" "}
<Link href="#" variant="body2">
termes et conditions générales d'utilisation
</Link>{" "}
et la{" "}
<Link href="#" variant="body2">
politique de confidentialité
</Link>
.
</Fragment>
),
requiredErrMsg: "Vous devez accepter les termes et conditions",
},
},
};
export default signUpFormModel;
Finaly the form itself :
import React from "react";
import { Grid } from "#mui/material";
import CheckBoxField from "../CheckBoxField";
const PrimarySignUpForm = (props) => {
const {
formField: {
terms,
},
} = props;
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<CheckBoxField name={terms.name} label={terms.label()} />
</Grid>
</Grid>
);
};
export default PrimarySignUpForm;
And this is how I made the stepper :
import React, { Fragment, useState } from "react";
import SignUpFormLogs from "../forms/SignUpFormLogs";
import { Formik, Form } from "formik";
import formInitialValuefrom from "../formModel/formInitialValue";
import signUpFormModel from "../formModel/signUpFormModel";
import signUpValidationSchema from "../formModel/signUpValidationSchema";
import {
Button,
Link,
Step,
StepLabel,
Stepper,
Typography,
} from "#mui/material";
import { Box } from "#mui/system";
const steps = ["step1"];
const { formId, formField } = signUpFormModel;
function _renderSteps(step) {
switch (step) {
case "step1":
return <SignUpFormLogs formField={formField} />;
default:
return <div>Not Found</div>;
}
}
function _sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const SignUp = () => {
const [activeStep, setActiveStep] = useState(0);
const currentValidationSchema = signUpValidationSchema[activeStep]; //activeStep
const isLastStep = activeStep === steps.length - 1;
async function _submitForm(values, actions) {
await _sleep(1000);
console.log(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
setActiveStep(activeStep + 1);
}
function _handleSubmit(values, actions) {
if (isLastStep) {
_submitForm(values, actions);
} else {
setActiveStep(activeStep + 1);
actions.setTouched({});
actions.setSubmitting(false);
}
}
function _handleBack() {
setActiveStep(activeStep - 1);
}
return (
<Fragment>
<Stepper activeStep={activeStep} sx={{ pt: 3, pb: 5 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{activeStep === steps.length ? (
<Typography variant="h6">Last step</Typography>
) : (
<Formik
initialValues={formInitialValuefrom}
validationSchema={currentValidationSchema}
onSubmit={_handleSubmit}
>
{({ isSubmitting }) => (
<Form id={formId}>
{_renderSteps(steps[activeStep])}
{activeStep !== 0 && (
<Button
onClick={_handleBack}
disabled={isSubmitting}
variant="outlined"
color="primary"
sx={{ mt: 4, mb: 2 }}
>
Back
</Button>
)}
<Button
type="submit"
disabled={isSubmitting}
variant="contained"
color="primary"
sx={{ mt: 4, mb: 2 }}
>
{isLastStep ? "Enregistrer" : "Suivant"}
</Button>
{isSubmitting && (
<Typography variant="h6">submitting...</Typography>
)}
</Form>
)}
</Formik>
)}
</Fragment>
);
};
export default SignUp;
I am trying to do a next button which will find the next vehicle in an array and display it.
However I cannot figure out why when I click on the next button the first time my console.log(nextVin) shows blank. On the second click it shows fine.
What is my best way to resolve this?
Please see my code below:
import React, {useState} from "react";
import { useParams } from "react-router-dom";
import { Link } from 'react-router-dom';
import {Typography, Container, Paper, Button} from "#material-ui/core/";
import ArrowBackIosIcon from '#material-ui/icons/ArrowBackIos';
import ArrowForwardIosIcon from '#material-ui/icons/ArrowForwardIos';
const CarDetail = (props) => {
let {id} = useParams();
const [nextVin, setNextVin] = useState("");
const nextCar = () => {
const carIndex = props.cars.findIndex(car => car.vin===id)
props.cars.map((car, index) => {
if(index === carIndex +1){
setNextVin(car.vin);
}
})
console.log(nextVin);
}
return (
<div>
<div>
<Typography color="primary" variant ="h3">Car details</Typography>
{props.cars.map((car, index) => {
if(car.vin === id){
return(
<div key = {car.vin}>
<Container maxWidth="sm" >
<Paper elevation={3} spacing={14} >
<Typography color="primary" variant ="h5">Car model</Typography>
<span>{car.model_variant}</span>
<Typography color="primary" variant ="h5">Fuel type</Typography>
<span>{car.fuel_type}</span>
<Typography color="primary" variant ="h5">Engine Size</Typography>
<span>{car.engine_size}</span>
<Typography color="primary" variant ="h5">Body type</Typography>
<span>{car.body_type}</span>
<Typography color="primary" variant ="h5">Reg number</Typography>
<span>{car.regno}</span>
<Typography color="primary" variant ="h5">Doors</Typography>
<span>{car.doors}</span>
<Typography color="primary" variant ="h5">Transmission type</Typography>
<span>{car.transmission_type}</span>
<Typography color="primary" variant ="h5">VIN</Typography>
<span>{car.vin}</span>
</Paper>
</Container>
{index!==0 ? <Button><ArrowBackIosIcon/> Previous car</Button> : null}
<Link
to={`/joes-garage/`}>
<Button variant ="contained" color="primary">Return to car list</Button>
</Link>
{index !== props.cars.length-1 ?
<Button onClick={nextCar}>Next car <ArrowForwardIosIcon/></Button>
:null }
</div>
);
}else return false;
})}
</div>
</div>
);
};
export default CarDetail;
try this
const nextCar = (props) => {
let {id} = useParams();
const [nextVin, setNextVin] = useState("");
const [lock , setLock] = useState("true")
const carIndex = props.cars.findIndex(car => car.vin===id)
props.cars.map((car, index) => {
if(index === carIndex +1)
if(!lock){
setNextVin(car.vin);
}else{
//set blanck at first render
setNextVin('');
setLock(false);
}
})
console.log(nextVin);
}
Currently, using default alert which is alert(response.data.result). To make my site more beautiful, I want to use Material-ui Alert. https://material-ui.com/components/alert/
My issue is I have no idea on how to call it from const.
Here's my code.
function Test(){
const saveData=async() => {
await axios.post('/API', passedParams)
.then(response => {
if(response.data.success === true)
{
alert(response.data.result)
}
else
{
alert(response.data.result)
//<Alert severity='error'>{response.data.result}</Alert> tried to use this but nothing displayed
}
}).catch(error=>{
alert(error)
})
//content of modal
cosnt bodyInsert = (
<div>
...fields
<Button onClick={()=>saveData()}>Save</Button>
</div>
)
return(
<div>
<Modal
open = {modalInsert}
onClose = {openCloseModalInsert}>
{bodyInsert}
</Modal>
</div>
)
}
export default Test;
Hoping for your consideration. thank you.
function Test(){
const saveData=async() => {
const [alert, setAlert] = useState(false);
const [alertContent, setAlertContent] = useState('');
await axios.post('/API', passedParams)
.then(response => {
if(response.data.success === true)
{
setAlertContent(response.data.result);
setAlert(true);
}
else
{
setAlertContent(response.data.result);
setAlert(true);
}
}).catch(error=>{
alert(error)
})
//content of modal
cosnt bodyInsert = (
<div>
...fields
<Button onClick={()=>saveData()}>Save</Button>
</div>
)
return(
<div>
{alert ? <Alert severity='error'>{alertContent}</Alert> : <></> }
<Modal
open = {modalInsert}
onClose = {openCloseModalInsert}>
{bodyInsert}
</Modal>
</div>
)
}
I think you can not do it that way. Use a state for it.
const [showAlert, setShowAlert] = useState(null);
const saveData=async() => {
await axios.post('/API', passedParams)
.then(response => {
if(response.data.success === true)
{
alert(response.data.result)
}
else
{
alert(response.data.result)
setShowAlert(response.data.result);
}
}).catch(error=>{
alert(error)
})
// Your return
return showAlert && <Alert severity='error' onClose={() => setShowAlert(null)} > { showAlert } </Alert>
You can use Collapse component collapse doc
const alertComponent = (<Alert severity='error'>{alertContent}</Alert>);
<Collapse in={alert}>{ alertComponent }</Collapse>
We can use <Alert> and <Dialog> components to create an alert in the react application
Example:
import React, { useCallback } from 'react'
import { makeStyles } from '#material-ui/core/styles'
import Alert from '#material-ui/lab/Alert'
import IconButton from '#material-ui/core/IconButton'
import CloseIcon from '#material-ui/icons/Close'
import Dialog from '#material-ui/core/Dialog'
const useStyles = makeStyles(theme => ({
root: {
'& > * + *': {
marginTop: theme.spacing(2),
},
width: '100%',
},
}))
export default function TransitionAlerts(props) {
const classes = useStyles()
return (
<div className={classes.root}>
<Dialog open={props.open}>
<Alert
action={
<IconButton
aria-label='close'
color='inherit'
size='small'
onClick={
useCallback(() => props.closeAlert({msg: '', open: false }))
}
>
<CloseIcon fontSize='inherit' />
</IconButton>
}
>
{props.msg}
</Alert>
</Dialog>
</div>
)
}
In some of the material-ui examples they have internal functions inside the functional component. Those are unaccessible for unit testing, am I losing something?
What I've could done is just either convert it into a class component or extract those functions outside and bind the params inside...
IE: https://material-ui.com/components/steppers/
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Stepper from '#material-ui/core/Stepper';
import Step from '#material-ui/core/Step';
import StepLabel from '#material-ui/core/StepLabel';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
const useStyles = makeStyles(theme => ({
...
}));
function getSteps() {
return ['Select campaign settings', 'Create an ad group', 'Create an ad'];
}
function getStepContent(step) {
switch (step) {
case 0:
return 'Select campaign settings...';
case 1:
return 'What is an ad group anyways?';
case 2:
return 'This is the bit I really care about!';
default:
return 'Unknown step';
}
}
export default function HorizontalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [skipped, setSkipped] = React.useState(new Set());
const steps = getSteps();
function isStepOptional(step) {
return step === 1;
}
function isStepSkipped(step) {
return skipped.has(step);
}
function handleNext() {
let newSkipped = skipped;
if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}
setActiveStep(prevActiveStep => prevActiveStep + 1);
setSkipped(newSkipped);
}
...
return (
<div className={classes.root}>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (isStepOptional(index)) {
labelProps.optional = <Typography variant="caption">Optional</Typography>;
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
<div>
{activeStep === steps.length ? (
<div>
<Typography className={classes.instructions}>
All steps completed - you're finished
</Typography>
<Button onClick={handleReset} className={classes.button}>
Reset
</Button>
</div>
) : (
<div>
<Typography className={classes.instructions}>{getStepContent(activeStep)}</Typography>
<div>
<Button disabled={activeStep === 0} onClick={handleBack} className={classes.button}>
Back
</Button>
{isStepOptional(activeStep) && (
<Button
variant="contained"
color="primary"
onClick={handleSkip}
className={classes.button}
>
Skip
</Button>
)}
<Button
variant="contained"
color="primary"
onClick={handleNext}
className={classes.button}
>
{activeStep === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
)}
</div>
</div>
);
}
in this example functions like isStepOptional, isStepSkipped,
handleNext are not accessible from the outside of the component
from a test file would be necessary to access those functions in order to unit-test them.
And another good question would be if those functions are not re-defining on every prop change of that component...
Isn't it a little bit anti-pattern?