Related
as far as now my code is working fine : The tab indicator is moving accordingly to the url of my tab.
But there's a strange behaviour happening when the back button of the browser is getting pressed, the url is changing but the indicator stay on the same tab as before.
Here's the code :
import * as React from 'react';
import { Tabs, Tab, Box } from '#mui/material';
import { useNavigate } from 'react-router-dom';
import { HOME_PAGE } from '../../../router/customRouter';
const navigationsListTabs = [
{
id: 0,
path: '/dev',
label: 'Dev',
isDisable: false,
},
{
id: 1,
path: '/images',
label: 'Images',
isDisable: false,
},
{
id: 2,
path: '/services',
label: 'Services',
isDisable: false,
},
{
id: 3,
path: '/users',
label: 'users',
isDisable: true,
},
];
export const Header = () => {
const [value, setValue] = React.useState(0);
const navigate = useNavigate();
React.useEffect(() => {
navigate('/dev');
}, []);
function handleChange(event, newValue) {
const selectedTab = navigationsListTabs.find((tab) => tab.id === newValue);
navigate(HOME_PAGE + selectedTab.path);
setValue(newValue);
}
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={value}
onChange={handleChange}
data-testid="tabs-menu-component"
>
{navigationsListTabs.map((item) => (
<Tab
key={item.id}
value={item.id}
label={item.label}
aria-label={item.label}
disabled={item.isDisable}
/>
))}
</Tabs>
</Box>
</Box>
);
};
Tried so far :
Using multiple conditions comparing url (not working) :
let url = window.location.pathname;
console.log(url);
const valueGetter = () => {
if (url.includes('dev')) {
return 0;
} else if (url.includes('images')) {
return 1;
} else if (url.includes('services')) {
return 2;
} else {
return 3;
}
};
console.log(valueGetter());
const [value, setValue] = React.useState(valueGetter);
Thanks to anyone who can help :)
If value is fully dependent on path, perhaps consider to always get value from pathname, instead of saving it as a state and handle both.
This example handles pathname with useLocation, so it gets updated by the hook when path changes. value is generated based on pathname.
Example:
import * as React from "react";
import { Tabs, Tab, Box } from "#mui/material";
import { useNavigate, useLocation } from "react-router-dom";
import { HOME_PAGE } from "../../../router/customRouter";
const navigationsListTabs = [
{
id: 0,
path: "/dev",
label: "Dev",
isDisable: false,
},
{
id: 1,
path: "/images",
label: "Images",
isDisable: false,
},
{
id: 2,
path: "/services",
label: "Services",
isDisable: false,
},
{
id: 3,
path: "/users",
label: "users",
isDisable: true,
},
];
export const Header = () => {
const navigate = useNavigate();
// 👇 Get value from pathname instead of saving it as state
const { pathname } = useLocation();
const currentTab = navigationsListTabs.find(
(tab) => tab.path === `/${pathname.split("/")?.pop()}`
);
const value = currentTab ? currentTab?.id : 0;
React.useEffect(() => {
navigate("/dev");
}, []);
function handleChange(event, newValue) {
const selectedTab = navigationsListTabs.find((tab) => tab.id === newValue);
navigate(HOME_PAGE + selectedTab.path);
}
return (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}
data-testid="tabs-menu-component"
>
{navigationsListTabs.map((item) => (
<Tab
key={item.id}
value={item.id}
label={item.label}
aria-label={item.label}
disabled={item.isDisable}
/>
))}
</Tabs>
</Box>
</Box>
);
};
This is my initial state:
const [inputList, setInputList] = useState([
{ answer: "Answer 1", isCorrect: false, points: 0 },
]);
I am adding input field dynamically and initially there will be one field. There is an Add button through which I can add more fields and remove button with that I can remove fields. But the problem I'm facing, after submitting the value I want to clear all the fields and there should be only one field as it was initially.Suppose if I add 4 fields and after submitting there should be only one. But when I'm trying to do that its not working but add, remove and even setting empty the state is working but setting only one field is not working.I'm mapping depending on inputList value but still after submitting I'm getting all the fields. If I have added 4 fields then after submitting also its 4. The state value is not being changed at all.
ADD more function:
const handleAddClick = () => {
let count = inputList.length;
let newfield = {
answer: `Answer ${count + 1}`,
isCorrect: false,
points: 0,
};
setInputList([...inputList, newfield]);
};
remove function:
const handleRemoveClick = (index) => {
let list = [...inputList];
let newList = list.filter((item, ind) => ind !== index);
setInputList(newList);
};
submit function: This should reset the state to initial state that is only one field should be there But not working
const Save = () => {
setInputList([{ answer: "Answer 1", isCorrect: false, points: 0 }])
}
I tried to do loop also for deleting every index but it only removes one no matter how many times the loop runs
Whole Component:
import DeleteIcon from "#mui/icons-material/Delete";
import React from "react";
import Checkbox from "#mui/material/Checkbox";
import Radio from "#mui/material/Radio";
import FormControl from "#mui/material/FormControl";
import IconButton from "#mui/material/IconButton";
import Switch from "#mui/material/Switch";
import TextField from "#mui/material/TextField";
import { useState } from "react";
import PlusSign from "../../../../Files/img/plus.png";
import Header from "../../../Header/Header";
import Topbar from "../../../Topbar/Topbar";
import styles from "../MultipleResponse/MultipleResponse.module.scss";
import QuestionEditor from "../../Editor/QuestionEditor/QuestionEditor";
import ReactTag from "../../TagInput/ReactTag";
import FormControlLabel from "#mui/material/FormControlLabel";
import { styled } from "#mui/material/styles";
import axios from "axios";
import "react-quill/dist/quill.snow.css";
import AnswerEditor from "../../Editor/AnswerEditor/AnswerEditor";
import SelectComp from "../../SelectComp/SelectComp";
import {
timeOptions,
categoryOptions,
difficultyLevelOptions,
} from "../../../../utils/constants";
import { RadioGroup } from "#mui/material";
import Snackbars from "../../../Assesment/MyAssesment/Snackbar";
import { useEffect } from "react";
export default function MultipleChoice() {
const baseURL = "http://localhost:5000/api/question";
const [distributePoints, setDistributePoints] = useState(false);
const [inputList, setInputList] = useState([
{ answer: "Answer 1", isCorrect: false, points: 0 },
]);
const [question, setQuestion] = useState("");
const [tags, setTag] = useState([]);
const [title, setTitle] = useState("");
const [time, setTime] = useState("");
const [difficultyLevel, setdifficultyLevel] = useState("");
const [category, setCategory] = useState("");
const [whatToLook, setwhatToLook] = useState("");
const [questionRelevent, setQuestionRelevent] = useState("");
const [shuffle, setShuffle] = useState(false);
const [value, setValue] = React.useState("");
const [loading, setLoading] = useState(false);
const [loading2, setLoading2] = useState(false);
const [snackText, setSnackText] = useState("");
const [severity,setSeverity]=useState("")
useEffect(() => {
const items = JSON.parse(localStorage.getItem("tempData"));
if (items) {
setTitle(items[0].title);
setdifficultyLevel(items[0].level);
setwhatToLook(items[0].whatToLookFor);
setQuestionRelevent(items[0].whyQuestionIsRelevant);
setTag(items[0].tags);
setTime(items[0].time);
setInputList(items[0].answers);
setCategory(items[0].category);
setQuestion(items[0].detail);
setShuffle(items[0].shuffleAnswers);
setDistributePoints(items[0].distributePoints);
setValue(items[0].radioValue);
}
}, []);
const [snack, setSnack] = React.useState(false);
const closeSnackbar = (event, reason) => {
if (reason === "clickaway") {
return;
}
setSnack(false);
};
const IOSSwitch = styled((props) => (
<Switch
focusVisibleClassName=".Mui-focusVisible"
disableRipple
{...props}
/>
))(({ theme }) => ({
width: 40,
height: 24,
padding: 0,
marginRight: 10,
"& .MuiSwitch-switchBase": {
padding: 0,
margin: 2,
transitionDuration: "300ms",
"&.Mui-checked": {
transform: "translateX(16px)",
color: "#fff",
"& + .MuiSwitch-track": {
backgroundColor:
theme.palette.mode === "dark" ? "#3699FF" : "#3699FF",
opacity: 1,
border: 0,
},
"&.Mui-disabled + .MuiSwitch-track": {
opacity: 0.5,
},
},
"&.Mui-focusVisible .MuiSwitch-thumb": {
color: "#33cf4d",
border: "6px solid #fff",
},
"&.Mui-disabled .MuiSwitch-thumb": {
color:
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[600],
},
"&.Mui-disabled + .MuiSwitch-track": {
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
},
},
"& .MuiSwitch-thumb": {
boxSizing: "border-box",
width: 20,
height: 20,
backgroundColor: "#FFFFFF",
},
"& .MuiSwitch-track": {
borderRadius: 26 / 2,
backgroundColor: theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
opacity: 1,
transition: theme.transitions.create(["background-color"], {
duration: 500,
}),
},
}));
const switchHandler = (event) => {
setDistributePoints(event.target.checked);
};
const handleInputChange = (e, index) => {
const list = [...inputList];
list[index]["answer"] = e;
setInputList(list);
};
const handlePointChange = (e, index) => {
const { name, value } = e.target;
const list = [...inputList];
list[index][name] = value;
setInputList(list);
};
const handleRemoveClick = (index) => {
let list = [...inputList];
let newList = list.filter((item, ind) => ind !== index);
setInputList(newList);
};
const handleAddClick = () => {
let count = inputList.length;
let newfield = {
answer: `Answer ${count + 1}`,
isCorrect: false,
points: 0,
};
setInputList([...inputList, newfield]);
};
const SaveAndAddAnother = () => {
setLoading2(true);
const newInputList = inputList.map(({ points, ...rest }) => {
return rest;
});
try {
axios
.post(baseURL, {
questionType: "MULTIPLE_CHOICE",
time: time.toString(),
title: title,
level: difficultyLevel,
detail: question,
category: category,
tags: tags,
whatToLookFor: whatToLook,
whyQuestionIsRelevant: questionRelevent,
answers: distributePoints ? inputList : newInputList,
distributePoints: distributePoints,
shuffleAnswers: shuffle,
})
.then((response) => {
setLoading(false);
if (response.data.result === 1) {
setSnack(true);
setSeverity('success')
setTimeout(() => {
setLoading2(false);
}, 1000);
setSnackText("Question added successfully");
setTime("");
setTitle("");
setdifficultyLevel("");
setQuestion("");
setCategory("");
setTag([]);
setwhatToLook("");
setQuestionRelevent("");
setDistributePoints(false);
setShuffle(false);
setValue("A");
let newList = [{ answer: "Answer 1", isCorrect: false, points: 0 }];
setInputList(newList);
localStorage.removeItem("tempData");
} else {
setLoading(false);
}
})
.catch((err) => {
// Handle error
setLoading2(false);
setSnack(true)
setSnackText("Error Adding question")
setSeverity('error')
});
} catch (e) {
setLoading(false);
}
};
const Save = () => {
setInputList([{ answer: "Answer 1", isCorrect: false, points: 0 }])
// setSnack(true);
// setSnackText("Question Saved as draft");
// setSeverity('success')
// setLoading(true);
// const localData = [
// {
// questionType: "MULTIPLE_CHOICE",
// time: time.toString(),
// title: title,
// level: difficultyLevel,
// detail: question,
// category: category,
// tags: tags,
// whatToLookFor: whatToLook,
// whyQuestionIsRelevant: questionRelevent,
// answers: inputList,
// distributePoints: distributePoints,
// shuffleAnswers: shuffle,
// radioValue: value,
// },
// ];
// localStorage.setItem("tempData", JSON.stringify(localData));
// setTimeout(() => {
// setLoading(false);
// }, 2000);
};
const questionText = (value) => {
setQuestion(value);
};
const selectedTags = (tags) => {
setTag(tags);
};
const handleTitle = (e) => {
setTitle(e.target.value);
};
const handleTime = (e) => {
setTime(e.target.value);
};
const handleDifficulty = (e) => {
setdifficultyLevel(e.target.value);
};
const handleCategory = (e) => {
setCategory(e.target.value);
};
const handleWhatToLookFor = (e) => {
setwhatToLook(e.target.value);
};
const handleQuestionRelevent = (e) => {
setQuestionRelevent(e.target.value);
};
const handleShuffle = (event) => {
setShuffle(event.target.checked);
};
function handleRadioButton(event, i) {
if (event.target.value === value) {
setValue("");
} else {
setValue(event.target.value);
}
const list = [...inputList];
Object.keys(list).forEach((key) => {
list[key]["isCorrect"] = false;
});
list[i]["isCorrect"] = true;
setInputList(list);
}
return (
<div className={styles.mainContainer}>
<Header />
<Topbar
questionType="Multiple-response"
loading={loading}
loading2={loading2}
SaveAndAddAnother={SaveAndAddAnother}
save={Save}
/>
<div className={styles.Container}>
<div className={styles.questionContainer}>
<div className={styles.left}>
<p className={styles.question}>Question</p>
<div className={styles.input}>
<TextField
variant="standard"
InputProps={{
disableUnderline: true,
}}
className={styles.title}
placeholder="Title.."
value={title}
onChange={(e) => handleTitle(e)}
/>
<QuestionEditor
question={question}
questionText={questionText}
theme="snow"
/>
</div>
<div className={styles.category}>
<ReactTag tags={tags} selectedTags={selectedTags} />
</div>
</div>
<div className={styles.right}>
<div className={styles.rightText}>
<span className={styles.heading}>Select the right answer</span>
<div>
<IOSSwitch
checked={distributePoints}
onChange={switchHandler}
/>
<span className={styles.instructinHeading}>
Distribute points across answers
</span>
</div>
</div>
<div className={styles.answers}>
<div className={styles.radio}>
<FormControl style={{ display: "flex", flex: 1 }}>
<RadioGroup
aria-labelledby="demo-controlled-radio-buttons-group"
name="controlled-radio-buttons-group"
value={value}
>
{inputList.map((x, i) => {
return (
<div key={i}>
<div key={i} className={styles.options}>
{!distributePoints ? (
<FormControlLabel
key={i}
value={x.answer}
control={
<Radio
onClick={(e) => handleRadioButton(e, i)}
/>
}
defaultChecked={false}
/>
) : (
""
)}
<div className="editor">
<AnswerEditor
handleAnswer={handleInputChange}
index={i}
theme="bubble"
val={x.answer}
/>
</div>
{distributePoints ? (
<div className={styles.inputCounter}>
<TextField
variant="standard"
name="points"
InputProps={{
disableUnderline: true,
type: "number",
}}
inputProps={{
min: 0,
style: { textAlign: "center" },
}}
value={x.points}
onChange={(e) => handlePointChange(e, i)}
/>
</div>
) : (
""
)}
{inputList.length > 1 && (
<IconButton
onClick={() => handleRemoveClick(i)}
className={styles.icon}
>
<DeleteIcon
sx={{ fontSize: 24, color: "#3699FF" }}
/>
</IconButton>
)}
</div>
{inputList.length - 1 === i && (
<div className={styles.bottomItemContainer}>
{distributePoints ? (
<div className={styles.pointsInfoText}>
Allocate points across answers. Give the best
answer 5 points
</div>
) : (
""
)}
{!inputList.some((el) => el.isCorrect === true) &&
!distributePoints ? (
<div className={styles.pointsInfoText}>
Select the correct answer
</div>
) : (
""
)}
<div
style={{
display: "flex",
justifyContent: "space-between",
}}
>
<button
onClick={handleAddClick}
type="button"
className={styles.addButton}
>
<img src={PlusSign} alt="" /> Add another
answer
</button>
<div className={styles.label}>
<Checkbox
checked={shuffle}
onChange={handleShuffle}
style={{ color: "#00A3FF" }}
/>
Shuffle answer
</div>
</div>
</div>
)}
</div>
);
})}
</RadioGroup>
</FormControl>
</div>
</div>
</div>
</div>
<div className={styles.itemContainer}>
<div className={styles.timeContainer}>
<SelectComp
handleChange={handleTime}
title="Time to answer the question"
val={time}
optionData={timeOptions}
/>
</div>
<div className={styles.timeContainer}>
<SelectComp
handleChange={handleCategory}
title="Question Category"
val={category}
optionData={categoryOptions}
/>
</div>
</div>
<div className={styles.itemContainer}>
<div style={{ flex: 1 }}>What to look for in the answer?</div>
<div style={{ flex: 1 }}>Why is this question relevant?</div>
</div>
<div className={styles.itemContainer}>
<div className={styles.releventText}>
<TextField
variant="standard"
InputProps={{
disableUnderline: true,
}}
className={styles.text}
placeholder="What to look for in the answer..."
value={whatToLook}
onChange={(e) => handleWhatToLookFor(e)}
/>
</div>
<div className={styles.releventText}>
<TextField
variant="standard"
InputProps={{
disableUnderline: true,
}}
className={styles.text}
placeholder="Why is this question relevant.."
value={questionRelevent}
onChange={(e) => handleQuestionRelevent(e)}
/>
</div>
</div>
<div className={styles.itemContainer}>
<div className={styles.difficultyLevel}>
<SelectComp
handleChange={handleDifficulty}
title="Difficulty Level"
val={difficultyLevel}
optionData={difficultyLevelOptions}
/>
</div>
{/* <div style={{ flex: 1 }}>Why is this question relevant?</div> */}
</div>
<div className={styles.bottomInfo}>
<div className={styles.element}>
<div className={styles.circle}></div>
<div className={styles.text}>
How should I formate my questions?
</div>
</div>
<div className={styles.element}>
<div className={styles.circle2}></div>
<div className={styles.text}>
{" "}
How do I use the question editor?
</div>
</div>
<div className={styles.element}>
<div className={styles.circle3}></div>
<div className={styles.text}>How do I use the formula editor?</div>
</div>
</div>
</div>
{snack && (
<Snackbars
text={snackText}
snack={snack}
closeSnackbar={closeSnackbar}
severity={severity}
/>
)}
{/* <div style={{ marginTop: 20 }}>{JSON.stringify(inputList)}</div> */}
</div>
);
}
I have a table component where many of the table columns are handled similarly.
Is there any way to optimize this code, maybe in a separate function?
import useTableStyles from 'admin/components/table/AdminTable.styles';
import useStyles from 'portal/pages/wasOperators/views/ViewEditOperators.style';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { Form } from 'shared/components/Form';
import WssoService from 'shared/services/WssoService';
import DateFnsUtils from '#date-io/date-fns';
import { Button, Grid, TextField } from '#material-ui/core';
import { DatePicker, MuiPickersUtilsProvider } from '#material-ui/pickers';
interface Props {
isEditing?: boolean;
}
const BenchmarkingTable: React.FC<Props> = ({ isEditing }) => {
const tableClasses = useTableStyles();
const classes = useStyles();
const { t } = useTranslation();
const { operatorsId }: any = useParams();
const valuesForInverseCalculation = [
'continuityWaterSupply',
'totalLossesWaterSupplySystems',
'pressureWaterSupplySystem',
'sewerNetworkAccidents',
'floodsThirdPartyCausedBySewage',
];
const benchmarkingDetailsInit = {
levelWaterSupplyServices: {
target: '',
result: '',
status: '',
},
qualityDrinkWaterLargeAreas: {
target: '',
result: '',
status: '',
},
qualityDrinkWaterSmallAreas: {
target: '',
result: '',
status: '',
},
monitorQualityDrinkWater: {
target: '',
result: '',
status: '',
},
continuityWaterSupply: {
target: '',
result: '',
status: '',
},
totalLossesWaterSupplySystems: {
target: '',
result: '',
status: '',
},
pressureWaterSupplySystem: {
target: '',
result: '',
status: '',
},
levelCoverageServiceDisposalOfWastewater: {
target: '',
result: '',
status: '',
},
levelCoverageServiceTreatmentOfWastewater: {
target: '',
result: '',
status: '',
},
wastewaterQuality: {
target: '',
result: '',
status: '',
},
sewerNetworkAccidents: {
target: '',
result: '',
status: '',
},
floodsThirdPartyCausedBySewage: {
target: '',
result: '',
status: '',
},
};
const [data, setData] = useState<any>(benchmarkingDetailsInit);
const [KEY, setKEY] = useState<any>(new Date());
useEffect(() => {
(async () => {
const result: any = await WssoService.getBenchmarking(
operatorsId,
KEY.getFullYear()
);
if (result && result.data && result.data.json) {
setData(JSON.parse(result.data.json));
} else {
setData(null);
}
})();
/* eslint-disable react-hooks/exhaustive-deps */
}, [KEY, operatorsId]);
const HandleKEYChange = () => {
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<DatePicker
variant="inline"
inputVariant="outlined"
format={'yyyy'}
views={['year']}
onChange={setKEY}
value={KEY}
/>
</MuiPickersUtilsProvider>
);
};
const updateBenchmarking = async (editValues: any) => {
const request = {
json: JSON.stringify(editValues),
};
const result: any = await WssoService.editAddBenchmarking(
operatorsId,
KEY.getFullYear(),
request
);
if (result && result.data && result.json) {
setData(JSON.parse(result.data.json));
}
};
const onUpdateSuccess = () => {
toast.success(t('itemUpdateSuccessfully'));
};
const handleSaveData = async () => {
if (data) {
await updateBenchmarking(data);
onUpdateSuccess();
}
};
const handleOnTextChange = (
key: string,
valueKey: string,
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
const newData = Object.assign({}, data, {
[key]: Object.assign({}, data[key], {
[valueKey]: value,
}),
});
setData(newData);
};
const handleColorChange = (name: any, target: any, result: any) => {
const removePercentFromTarget = target.includes('%')
? target.slice(0, -1)
: target;
const convertedTarget = Number(removePercentFromTarget);
const removePercentFromResult = result.includes('%')
? result.slice(0, -1)
: result;
const convertedResult = Number(removePercentFromResult);
target = convertedTarget;
result = convertedResult;
let final: any;
const arrOfNames = [];
arrOfNames.push(name);
if (arrOfNames.some(x => valuesForInverseCalculation.includes(x))) {
if (target < result) {
Object.keys(data).map(k => {
return (final = data[k].status = 'red');
});
} else if (target > result) {
Object.keys(data).map(k => {
return (final = data[k].status = 'green');
});
} else if (target === result) {
Object.keys(data).map(k => {
return (final = data[k].status = 'yellow');
});
}
} else {
if (target > result) {
Object.keys(data).map(k => {
return (final = data[k].status = 'red');
});
} else if (target < result || (target === result && result === 100)) {
Object.keys(data).map(k => {
return (final = data[k].status = 'green');
});
} else if (target === result) {
Object.keys(data).map(k => {
return (final = data[k].status = 'yellow');
});
}
}
return final;
};
const updateData = (newData: any[]) => {
setData(newData);
};
const renderBenchmarkDetails = () => {
if (isEditing) {
return data ? (
Object.keys(data).map(k => {
return (
<tr>
<td>{t(k)}</td>
<td className={classes.benchTextfieldAlign}>
<TextField
type="text"
onChange={(e: any) => handleOnTextChange(k, 'target', e)}
value={data[k].target}
inputProps={{ min: 0, style: { textAlign: 'center' } }}
required
/>
</td>
<td className={classes.benchTextfieldAlign}>
<TextField
type="text"
onChange={(e: any) => handleOnTextChange(k, 'result', e)}
value={data[k].result}
inputProps={{ min: 0, style: { textAlign: 'center' } }}
required
/>
</td>
</tr>
);
})
) : (
<tr>
<td>{t('noDetailsToDisplay')}</td>
<td>{t('noDetailsToDisplay')}</td>
<td>{t('noDetailsToDisplay')}</td>
<td>{t('noDetailsToDisplay')}</td>
</tr>
);
} else {
return data ? (
Object.keys(data).map((rowName, i) => {
return (
<tr key={i}>
<td>{t(rowName)}</td>
<td className={classes.benchTextfieldAlign}>
{data[rowName].target}
</td>
<td className={classes.benchTextfieldAlign}>
{data[rowName].result}
</td>
<td className={classes.benchTextfieldAlign}>
<div
style={{
width: '15px',
height: '15px',
borderRadius: '50%',
backgroundColor: handleColorChange(
rowName,
data[rowName].target,
data[rowName].result
),
}}
/>
</td>
</tr>
);
})
) : (
<tr>
<td>{t('noDetailsToDisplay')}</td>
<td>{t('noDetailsToDisplay')}</td>
<td>{t('noDetailsToDisplay')}</td>
<td>{t('noDetailsToDisplay')}</td>
</tr>
);
}
};
const handleExport = useCallback(async () => {
const dataRows: any = [];
if (data) {
Object.keys(data).map(rowName => {
dataRows.push([t(rowName), data[rowName].target, data[rowName].result]);
return null;
});
}
let dataToCSV: any[][] = [];
if (dataRows && dataRows.length > 0) {
dataToCSV = [[t('criteria'), t('targets'), t('results')], ...dataRows];
const csvContent: string =
'data:text/csv;charset=utf-8,\uFEFF' +
dataToCSV
.map(e =>
e
.map(r =>
r instanceof Array
? `"${(r ?? '').toString().replace(',', ', ')}"`
: (r ?? '').toString().replace(',', ' ')
)
.join(',')
)
.join('\n');
const encodedUri: string = encodeURI(csvContent);
const link: HTMLAnchorElement = document.createElement('a');
link.setAttribute('href', encodedUri);
link.setAttribute('download', `${t('exportedTableData')}.csv`);
link.click();
}
}, [data, t]);
console.log(operatorsId);
console.log(benchmarkingDetailsInit);
console.log(updateBenchmarking);
return (
<Grid>
<Form
onSubmit={(formData, { resetForm }) => {
const tempData = !!operatorsId
? {
...formData,
}
: {
...formData,
...(!!operatorsId && {
operatorsAreaEntity: {
id: parseInt(operatorsId, 0),
},
}),
id: operatorsId ? undefined : Date.now(),
};
updateData([tempData, ...data]);
resetForm();
}}
initialValues={benchmarkingDetailsInit}
enableReinitialize={true}
>
<div className={classes.tableContainer}>
<Grid item xs={12}>
<div className={classes.containerLegendBench}>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
}}
>
<div className={classes.yearBoxBenchmarketType}>
<HandleKEYChange />
</div>
<div className={classes.legendBoxBenchmarketType}>
{t('reachedResult')}
<div
style={{
marginLeft: '3px',
width: '11px',
height: '9px',
borderRadius: '50%',
backgroundColor: '#006400',
display: 'inline-block',
}}
/>
</div>
<div className={classes.legendBoxBenchmarketType}>
{t('almostReachedResult')}
<div
style={{
marginLeft: '3px',
width: '11px',
height: '9px',
borderRadius: '50%',
backgroundColor: '#FFDF00',
display: 'inline-block',
}}
/>
</div>
<div className={classes.legendBoxBenchmarketType}>
{t('notReachedResult')}
<tr
style={{
marginLeft: '3px',
width: '11px',
height: '9px',
borderRadius: '50%',
backgroundColor: '#FF0000',
display: 'inline-block',
}}
/>
</div>
</div>
</div>
</Grid>
<table className={tableClasses.table}>
<thead>
<tr>
<th>{t('criteria')}</th>
<th>{t('targets')}</th>
<th>{t('results')}</th>
<th />
</tr>
</thead>
<tbody>{renderBenchmarkDetails()}</tbody>
</table>
</div>
<Grid item xs={12}>
<Button
style={{ margin: '5px 0px 10px 10px' }}
type="button"
color="primary"
variant="contained"
onClick={handleExport}
disabled={!data}
>
{t('export')}
</Button>
</Grid>
{isEditing && (
<Button onClick={handleSaveData} className={classes.submitButton}>
{t('save')} <i className={`fas fa-save ${classes.submitIcon}`} />
</Button>
)}
</Form>
</Grid>
);
};
export default BenchmarkingTable;
You've got 60 lines of code just to initialize an empty object! It seems like each key of this object is a row of your table and each value shares the same format:
interface Benchmark {
target: string;
result: string;
status: string;
}
So the data can be described as Record<string, Benchmark>. Though it might make more sense to make the name be a property and let data be an array.
Your condition with the <td>{t('noDetailsToDisplay')}</td> will never be hit because you've initialized data to your empty object so data is always true.
If you want to show something different when there is not data then you should do any one of these:
Use an undefined initial state.
Have a separate isLoading state that you set with your useEffect hooks.
Check Object.keys(data).length > 0 instead of checking !! data.
Another part of your code that seems majorly illogical is the handleColorChange function. Despite the misleading name, it seems like this function is designed to compute a color string based on the result and target values. This color is the status property and you mutate state to get it there. Instead you should compute the color and add it as status after you fetch the data and before you set it to state.
If you want a row with 4 empty cells, you can do this:
<tr>
{Array.from({ length: 4 }).map((_, i) => (
<td key={i}>{t("noDetailsToDisplay")}</td>
))}
</tr>
You have that same JSX block in two places so you want to fix that.
I haven't fixed everything, but I have fixed quite a lot.
import useTableStyles from 'admin/components/table/AdminTable.styles';
import useStyles from 'portal/pages/wasOperators/views/ViewEditOperators.style';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { Form } from 'shared/components/Form';
import WssoService from 'shared/services/WssoService';
import DateFnsUtils from '#date-io/date-fns';
import { Button, Grid, TextField } from '#material-ui/core';
import { DatePicker, MuiPickersUtilsProvider } from '#material-ui/pickers';
interface BenchmarkFromService {
target: string;
result: string;
}
interface BenchmarkRow extends BenchmarkFromService {
name: string;
status: string;
}
const BenchmarkingTable: React.FC<Props> = ({ isEditing }) => {
const tableClasses = useTableStyles();
const classes = useStyles();
const { t } = useTranslation();
const { operatorsId } = useParams<{ operatorsId: string }>();
/**
* each row of the table is a Benchmark
*/
const [data, setData] = useState<BenchmarkRow[]>([]);
const [KEY, setKEY] = useState(new Date());
// extract shared logic from "get" and "update" responses
// memoized so it's safe as a useEffect dependency
const handleResult = useCallback(
// is this really any? what is it when it's not { data: { json: string } }
(result: any) => {
/**
* regular colors and inverted colors have the same logic, just swap the target and result
*/
const computeColor = (a: number, b: number): string => {
if (a > b) {
return "red";
} else if (a < b || (a === b && b === 100)) {
return "green";
} else if (a === b) {
return "yellow";
}
};
const valuesForInverseCalculation = [
"continuityWaterSupply",
"totalLossesWaterSupplySystems",
"pressureWaterSupplySystem",
"sewerNetworkAccidents",
"floodsThirdPartyCausedBySewage"
];
const toNum = (value: string): number =>
parseFloat(value.replace("%", ""));
try {
const raw: Record<string, BenchmarkFromService> = JSON.parse(
result.data.json
);
const formatted = Object.entries(raw).map(
([name, { target, result }]) => ({
name,
target,
result,
status: valuesForInverseCalculation.includes(name)
? computeColor(toNum(result), toNum(target))
: computeColor(toNum(target), toNum(result))
})
);
setData(formatted);
} catch (e) {
// catch errors from malformatted response or unparsable JSON
setData([]);
}
},
[]
);
useEffect(() => {
(async () => {
const result: any = await WssoService.getBenchmarking(
operatorsId,
KEY.getFullYear()
);
handleResult(result);
})();
}, [KEY, operatorsId, handleResult]);
const updateBenchmarking = async (editValues: any) => {
const request = {
json: JSON.stringify(editValues)
};
const result: any = await WssoService.editAddBenchmarking(
operatorsId,
KEY.getFullYear(),
request
);
handleResult(result);
};
const onUpdateSuccess = () => {
toast.success(t("itemUpdateSuccessfully"));
};
const handleSaveData = async () => {
if (data) {
await updateBenchmarking(data);
onUpdateSuccess();
}
};
const renderCell = (property: keyof BenchmarkRow, row: BenchmarkRow) => {
const onChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setData((previous) =>
previous.map((obj) =>
obj.name === row.name
? {
...obj,
[property]: e.target.value
}
: obj
)
);
};
return (
<td className={classes.benchTextfieldAlign}>
{isEditing ? (
<TextField
type="text"
onChange={onChange}
value={row[property]}
inputProps={{ min: 0, style: { textAlign: "center" } }}
required
/>
) : (
row[property]
)}
</td>
);
};
const renderBenchmarkDetails = () => {
if (data.length === 0) {
return (
<tr>
{Array.from({ length: 4 }).map((_, i) => (
<td key={i}>{t("noDetailsToDisplay")}</td>
))}
</tr>
);
}
return data.map((row) => (
<tr key={row.name}>
<th scope="row">{row.name}</th>
{renderCell("target", row)}
{renderCell("result", row)}
<td className={classes.benchTextfieldAlign}>
<div
style={{
width: "15px",
height: "15px",
borderRadius: "50%",
backgroundColor: row.status
}}
/>
</td>
</tr>
));
};
const handleExport = useCallback(async () => {
if (data.length === 0) {
return;
}
const dataRows = data.map((row) => [t(row.name), row.target, row.result]);
const dataToCSV = [
[t("criteria"), t("targets"), t("results")],
...dataRows
];
const csvContent: string =
"data:text/csv;charset=utf-8,\uFEFF" +
dataToCSV
.map((row) =>
row
.map(
(cell) => cell.replace(",", "\\,") // I think it's ok to escape the comma like this?
)
.join(",")
)
.join("\n");
const encodedUri: string = encodeURI(csvContent);
const link: HTMLAnchorElement = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", `${t("exportedTableData")}.csv`);
link.click();
}, [data, t]);
return (
<Grid>
<Form
onSubmit={(formData, { resetForm }) => {
const tempData = !!operatorsId
? {
...formData
}
: {
...formData,
...(!!operatorsId && {
operatorsAreaEntity: {
id: parseInt(operatorsId, 0)
}
}),
id: operatorsId ? undefined : Date.now()
};
setData([tempData, ...data]);
resetForm();
}}
initialValues={{}}
enableReinitialize={true}
>
<div className={classes.tableContainer}>
<Grid item xs={12}>
<div className={classes.containerLegendBench}>
<div
style={{
display: "flex",
justifyContent: "flex-end"
}}
>
<div className={classes.yearBoxBenchmarketType}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<DatePicker
variant="inline"
inputVariant="outlined"
format={"yyyy"}
views={["year"]}
onChange={setKEY}
value={KEY}
/>
</MuiPickersUtilsProvider>
</div>
<div className={classes.legendBoxBenchmarketType}>
{t("reachedResult")}
<div
style={{
marginLeft: "3px",
width: "11px",
height: "9px",
borderRadius: "50%",
backgroundColor: "#006400",
display: "inline-block"
}}
/>
</div>
<div className={classes.legendBoxBenchmarketType}>
{t("almostReachedResult")}
<div
style={{
marginLeft: "3px",
width: "11px",
height: "9px",
borderRadius: "50%",
backgroundColor: "#FFDF00",
display: "inline-block"
}}
/>
</div>
<div className={classes.legendBoxBenchmarketType}>
{t("notReachedResult")}
<tr
style={{
marginLeft: "3px",
width: "11px",
height: "9px",
borderRadius: "50%",
backgroundColor: "#FF0000",
display: "inline-block"
}}
/>
</div>
</div>
</div>
</Grid>
<table className={tableClasses.table}>
<thead>
<tr>
<th>{t("criteria")}</th>
<th>{t("targets")}</th>
<th>{t("results")}</th>
<th />
</tr>
</thead>
<tbody>{renderBenchmarkDetails()}</tbody>
</table>
</div>
<Grid item xs={12}>
<Button
style={{ margin: "5px 0px 10px 10px" }}
type="button"
color="primary"
variant="contained"
onClick={handleExport}
disabled={!data}
>
{t("export")}
</Button>
</Grid>
{isEditing && (
<Button onClick={handleSaveData} className={classes.submitButton}>
{t("save")} <i className={`fas fa-save ${classes.submitIcon}`} />
</Button>
)}
</Form>
</Grid>
);
};
export default BenchmarkingTable;
I wrote a component that is supposed to list out a bunch of checkboxes with corresponding textfields. When you click on the checkboxes, or type in the fields it's meant to update state.
The textbox is working ok, but when I type in the fields, it updates state ok, but I lose focus whenever I tap the keyboard.
I realized this is probably due to not having keys set, so I added keys to everything but it still is losing focus. At one point I tried adding in stopPropegation on my events because I thought maybe that was causing an issue?? I'm not sure.. still learning...didn't seem to work so I removed that part too.
Still can't seem to figure out what is causing it to lose focus... does anyone have any advice/solves for this issue?
I consolidated my code and cut out the unnecessary bits to make it easier to read. There are three relevant JS files.. please see below:
I'm still a beginner/learning so if you have useful advice related to any part of this code, feel free to offer. Thanks!
App.js
import React, { Component } from 'react';
import Form from './Form'
class App extends Component {
constructor() {
super();
this.state = {
mediaDeliverables: [
{label: 'badf', checked: false, quantity:''},
{label: 'adfadf', checked: false, quantity:''},
{label: 'adadf', checked: false, quantity:''},
{label: 'addadf', checked: false, quantity:''},
{label: 'adfdes', checked: false, quantity:''},
{label: 'hghdgs', checked: false, quantity:''},
{label: 'srtnf', checked: false, quantity:''},
{label: 'xfthd', checked: false, quantity:''},
{label: 'sbnhrr', checked: false, quantity:''},
{label: 'sfghhh', checked: false, quantity:''},
{label: 'sssddrr', checked: false, quantity:''}
]
}
}
setMediaDeliverable = (value, index) => {
let currentState = this.getStateCopy();
currentState.mediaDeliverables[index] = value;
this.setState(currentState);
}
getStateCopy = () => Object.assign({}, this.state);
render() {
return (
<div className="App">
<Form
key="mainForm"
mediaDeliverablesOptions={this.state.mediaDeliverables}
setMediaDeliverable={this.setMediaDeliverable}
/>
</div>
);
}
}
export default App;
Form.js
import React from 'react';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import FormControl from '#material-ui/core/FormControl';
import FormLabel from '#material-ui/core/FormLabel';
import FormGroup from '#material-ui/core/FormGroup';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import Checkbox from '#material-ui/core/Checkbox';
import MediaDeliverablesCheckBox from './MediaDeliverablesCheckBox';
const useStyles = makeStyles(theme => ({
container: {
display: 'inline-block',
flexWrap: 'wrap',
},
root: {
display: 'inline-block',
flexWrap: 'wrap',
maxWidth: 600,
textAlign: 'left',
},
extendedIcon: {
marginRight: theme.spacing(1),
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
maxWidth: 300,
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 370,
},
dense: {
marginTop: 19,
},
chips: {
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: 2,
},
noLabel: {
marginTop: theme.spacing(3),
},
}));
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};
function getStyles(name, accountName, theme) {
// console.log('>> [form.js] (getStyles) ',accountName)
return {
fontWeight:
accountName.indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium,
};
}
export default function Form(props) {
const mediaDeliverablesOptions = props.mediaDeliverablesOptions;
const classes = useStyles();
const theme = useTheme();
const CheckboxGroup = ({ values, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={value.checked}
onChange={onChange(index)}
/>
}
label={value.label}
/>
))}
</FormGroup>
</FormControl>
);
const MediaDeliverableCheckBoxList = ({values, label}) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<MediaDeliverablesCheckBox
key={index}
mediaDeliverablesOptions={value}
onMediaDeliverableChange={onMediaDeliverableChange(index)}
/>
))}
</FormGroup>
</FormControl>
);
const onCheckBoxChange = index => ({ target: { checked } }) => {
const newValues = [...values];
const value = values[index];
newValues[index] = { ...value, checked };
props.setDesignOrDigital(newValues);
};
const onMediaDeliverableChange = index => (deliverableData, e) => {
props.setMediaDeliverable(deliverableData, index);
}
return (
<div className={classes.root}>
<MediaDeliverableCheckBoxList
label="Please Choose Deliverables:"
values={mediaDeliverablesOptions}
key="media-deliverable-checkbox-list"
/>
</div>
);
}
MediaDeliverablesCheckbox.js
import React from 'react';
import Checkbox from '#material-ui/core/Checkbox';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import FormControl from '#material-ui/core/FormControl';
import FormLabel from '#material-ui/core/FormLabel';
import FormGroup from '#material-ui/core/FormGroup';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import TextField from '#material-ui/core/TextField';
export default function MediaDeliverablesCheckBox(props) {
let deliverableData = Object.assign({}, props.mediaDeliverablesOptions);
const onCheckBoxChange = (e) => {
deliverableData.checked = e.target.checked;
props.onMediaDeliverableChange(deliverableData, e);
}
const onQuantityChange = (e) => {
deliverableData.quantity = e.target.value;
props.onMediaDeliverableChange(deliverableData, e);
}
const CheckboxGroup = ({ value, label }) => (
<FormControl component="fieldset">
<FormGroup>
<FormControlLabel
control={
<Checkbox
key={props.index}
checked={value.checked}
onChange={onCheckBoxChange}
/>
}
label={label}
/>
</FormGroup>
</FormControl>
);
return(
<div className="MediaDeliverablesCheckBox">
<CheckboxGroup
key={props.index}
label={props.mediaDeliverablesOptions.label}
value={props.mediaDeliverablesOptions}
/>
<TextField
key={'tf'+props.index}
id={'quantity-'+props.index}
label="Quantity"
placeholder="How many do you need?"
multiline
variant="outlined"
value={props.mediaDeliverablesOptions.quantity}
onChange={onQuantityChange}
fullWidth
/>
</div>
);
}
Updated Form.js based on recommended edits by Ryan C.
import React from 'react';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import FormControl from '#material-ui/core/FormControl';
import FormLabel from '#material-ui/core/FormLabel';
import FormGroup from '#material-ui/core/FormGroup';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import Checkbox from '#material-ui/core/Checkbox';
import MediaDeliverablesCheckBox from './MediaDeliverablesCheckBox';
const useStyles = makeStyles(theme => ({
container: {
display: 'inline-block',
flexWrap: 'wrap',
},
root: {
display: 'inline-block',
flexWrap: 'wrap',
maxWidth: 600,
textAlign: 'left',
},
extendedIcon: {
marginRight: theme.spacing(1),
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
maxWidth: 300,
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 370,
},
dense: {
marginTop: 19,
},
chips: {
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: 2,
},
noLabel: {
marginTop: theme.spacing(3),
},
}));
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};
function getStyles(name, accountName, theme) {
return {
fontWeight:
accountName.indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium,
};
}
// Failed to compile
// ./src/Form.js
// Line 86: Parsing error: Unexpected token, expected ","
// 84 |
// 85 | const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
// > 86 | {values.map((value, index) => (
// | ^
// 87 | <MediaDeliverablesCheckBox
// 88 | key={index}
// 89 | index={index}
// This error occurred during the build time and cannot be dismissed.
const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
{values.map((value, index) => (
<MediaDeliverablesCheckBox
key={index}
index={index}
mediaDeliverablesOptions={value}
onMediaDeliverableChange={onMediaDeliverableChange(index)}
/>
))}
);
export default function Form(props) {
const mediaDeliverablesOptions = props.mediaDeliverablesOptions;
const classes = useStyles();
const theme = useTheme();
const CheckboxGroup = ({ values, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={value.checked}
onChange={onChange(index)}
/>
}
label={value.label}
/>
))}
</FormGroup>
</FormControl>
);
const onCheckBoxChange = index => ({ target: { checked } }) => {
const newValues = [...values];
const value = values[index];
newValues[index] = { ...value, checked };
props.setDesignOrDigital(newValues);
};
const onMediaDeliverableChange = index => (deliverableData, e) => {
props.setMediaDeliverable(deliverableData, index);
}
return (
<div className={classes.root}>
<MediaDeliverableCheckBoxList
onMediaDeliverableChange={onMediaDeliverableChange}
/>
</div>
);
}
I see two main issues:
How you are defining your different components (nesting component types)
Not passing the index prop through to components that are expecting it
You have the following structure (leaving out details that are not directly related to my point):
export default function Form(props) {
const onMediaDeliverableChange = index => (deliverableData, e) => {
props.setMediaDeliverable(deliverableData, index);
}
const MediaDeliverableCheckBoxList = ({values, label}) => (
<FormGroup>
{values.map((value, index) => (
<MediaDeliverablesCheckBox key={index} onMediaDeliverableChange={onMediaDeliverableChange(index)}/>
))}
</FormGroup>
);
return (
<MediaDeliverableCheckBoxList/>
);
}
The function MediaDeliverableCheckBoxList represents the component type used to render the <MediaDeliverableCheckBoxList/> element. Whenever Form is re-rendered due to props or state changing, React will re-render its children. If the component type of a particular child is the same (plus some other criteria such as key being the same if specified), then it will update the existing DOM node(s). If the component type of a particular child is different, then the corresponding DOM nodes will be removed and new ones added to the DOM.
By defining the MediaDeliverableCheckBoxList component type within the Form function, you are causing that component type to be different on every render. This will cause all of the DOM nodes to be replaced rather than just updated and this will cause the focus to go away when the DOM node that previously had focus gets removed. It will also cause performance to be considerably worse.
You can fix this by moving this component type outside of the Form function and then adding any additional props that are needed (e.g. onMediaDeliverableChange) to convey the context known inside of Form. You also need to pass index as a prop to MediaDeliverablesCheckBox since it is using it.
const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
<FormGroup>
{values.map((value, index) => (
<MediaDeliverablesCheckBox key={index} index={index} onMediaDeliverableChange={onMediaDeliverableChange(index)}/>
))}
</FormGroup>
);
export default function Form(props) {
const onMediaDeliverableChange = index => (deliverableData, e) => {
props.setMediaDeliverable(deliverableData, index);
}
return (
<MediaDeliverableCheckBoxList onMediaDeliverableChange={onMediaDeliverableChange}/>
);
}
You have this same issue with CheckboxGroup and possibly other components as well.
This issue is solely because of your key in the TextField. You must ensure that the key remains the same on every update. Otherwise you would the face the current issue.
Anyone know how to change the fontSize of the TableHeaderRow in a DevExtreme React Grid?
Here's an example of code from the website (https://devexpress.github.io/devextreme-reactive/react/grid/demos/featured/data-editing/) that I have been working with
import * as React from 'react';
import {
SortingState, EditingState, PagingState,
IntegratedPaging, IntegratedSorting,
} from '#devexpress/dx-react-grid';
import {
Grid,
Table, TableHeaderRow, TableEditRow, TableEditColumn,
PagingPanel, DragDropProvider, TableColumnReordering,
} from '#devexpress/dx-react-grid-material-ui';
import Paper from '#material-ui/core/Paper';
import Dialog from '#material-ui/core/Dialog';
import DialogActions from '#material-ui/core/DialogActions';
import DialogContent from '#material-ui/core/DialogContent';
import DialogContentText from '#material-ui/core/DialogContentText';
import DialogTitle from '#material-ui/core/DialogTitle';
import Button from '#material-ui/core/Button';
import IconButton from '#material-ui/core/IconButton';
import Input from '#material-ui/core/Input';
import Select from '#material-ui/core/Select';
import MenuItem from '#material-ui/core/MenuItem';
import TableCell from '#material-ui/core/TableCell';
import DeleteIcon from '#material-ui/icons/Delete';
import EditIcon from '#material-ui/icons/Edit';
import SaveIcon from '#material-ui/icons/Save';
import CancelIcon from '#material-ui/icons/Cancel';
import { withStyles } from '#material-ui/core/styles';
import { ProgressBarCell } from '../../../theme-sources/material-ui/components/progress-bar-cell';
import { HighlightedCell } from '../../../theme-sources/material-ui/components/highlighted-cell';
import { CurrencyTypeProvider } from '../../../theme-sources/material-ui/components/currency-type-provider';
import { PercentTypeProvider } from '../../../theme-sources/material-ui/components/percent-type-provider';
import {
generateRows,
globalSalesValues,
} from '../../../demo-data/generator';
const styles = theme => ({
lookupEditCell: {
paddingTop: theme.spacing.unit * 0.875,
paddingRight: theme.spacing.unit,
paddingLeft: theme.spacing.unit,
},
dialog: {
width: 'calc(100% - 16px)',
},
inputRoot: {
width: '100%',
},
});
const AddButton = ({ onExecute }) => (
<div style={{ textAlign: 'center' }}>
<Button
color="primary"
onClick={onExecute}
title="Create new row"
>
New
</Button>
</div>
);
const EditButton = ({ onExecute }) => (
<IconButton onClick={onExecute} title="Edit row">
<EditIcon />
</IconButton>
);
const DeleteButton = ({ onExecute }) => (
<IconButton onClick={onExecute} title="Delete row">
<DeleteIcon />
</IconButton>
);
const CommitButton = ({ onExecute }) => (
<IconButton onClick={onExecute} title="Save changes">
<SaveIcon />
</IconButton>
);
const CancelButton = ({ onExecute }) => (
<IconButton color="secondary" onClick={onExecute} title="Cancel changes">
<CancelIcon />
</IconButton>
);
const commandComponents = {
add: AddButton,
edit: EditButton,
delete: DeleteButton,
commit: CommitButton,
cancel: CancelButton,
};
const Command = ({ id, onExecute }) => {
const CommandButton = commandComponents[id];
return (
<CommandButton
onExecute={onExecute}
/>
);
};
const availableValues = {
product: globalSalesValues.product,
region: globalSalesValues.region,
customer: globalSalesValues.customer,
};
const LookupEditCellBase = ({
availableColumnValues, value, onValueChange, classes,
}) => (
<TableCell
className={classes.lookupEditCell}
>
<Select
value={value}
onChange={event => onValueChange(event.target.value)}
input={(
<Input
classes={{ root: classes.inputRoot }}
/>
)}
>
{availableColumnValues.map(item => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</Select>
</TableCell>
);
export const LookupEditCell = withStyles(styles, { name: 'ControlledModeDemo' })(LookupEditCellBase);
const Cell = (props) => {
const { column } = props;
if (column.name === 'discount') {
return <ProgressBarCell {...props} />;
}
if (column.name === 'amount') {
return <HighlightedCell {...props} />;
}
return <Table.Cell {...props} />;
};
const EditCell = (props) => {
const { column } = props;
const availableColumnValues = availableValues[column.name];
if (availableColumnValues) {
return <LookupEditCell {...props} availableColumnValues={availableColumnValues} />;
}
return <TableEditRow.Cell {...props} />;
};
const getRowId = row => row.id;
class DemoBase extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
columns: [
{ name: 'product', title: 'Product' },
{ name: 'region', title: 'Region' },
{ name: 'amount', title: 'Sale Amount' },
{ name: 'discount', title: 'Discount' },
{ name: 'saleDate', title: 'Sale Date' },
{ name: 'customer', title: 'Customer' },
],
tableColumnExtensions: [
{ columnName: 'amount', align: 'right' },
],
rows: generateRows({
columnValues: { id: ({ index }) => index, ...globalSalesValues },
length: 12,
}),
sorting: [],
editingRowIds: [],
addedRows: [],
rowChanges: {},
currentPage: 0,
deletingRows: [],
pageSize: 0,
pageSizes: [5, 10, 0],
columnOrder: ['product', 'region', 'amount', 'discount', 'saleDate', 'customer'],
currencyColumns: ['amount'],
percentColumns: ['discount'],
};
const getStateDeletingRows = () => {
const { deletingRows } = this.state;
return deletingRows;
};
const getStateRows = () => {
const { rows } = this.state;
return rows;
};
this.changeSorting = sorting => this.setState({ sorting });
this.changeEditingRowIds = editingRowIds => this.setState({ editingRowIds });
this.changeAddedRows = addedRows => this.setState({
addedRows: addedRows.map(row => (Object.keys(row).length ? row : {
amount: 0,
discount: 0,
saleDate: new Date().toISOString().split('T')[0],
product: availableValues.product[0],
region: availableValues.region[0],
customer: availableValues.customer[0],
})),
});
this.changeRowChanges = rowChanges => this.setState({ rowChanges });
this.changeCurrentPage = currentPage => this.setState({ currentPage });
this.changePageSize = pageSize => this.setState({ pageSize });
this.commitChanges = ({ added, changed, deleted }) => {
let { rows } = this.state;
if (added) {
const startingAddedId = rows.length > 0 ? rows[rows.length - 1].id + 1 : 0;
rows = [
...rows,
...added.map((row, index) => ({
id: startingAddedId + index,
...row,
})),
];
}
if (changed) {
rows = rows.map(row => (changed[row.id] ? { ...row, ...changed[row.id] } : row));
}
this.setState({ rows, deletingRows: deleted || getStateDeletingRows() });
};
this.cancelDelete = () => this.setState({ deletingRows: [] });
this.deleteRows = () => {
const rows = getStateRows().slice();
getStateDeletingRows().forEach((rowId) => {
const index = rows.findIndex(row => row.id === rowId);
if (index > -1) {
rows.splice(index, 1);
}
});
this.setState({ rows, deletingRows: [] });
};
this.changeColumnOrder = (order) => {
this.setState({ columnOrder: order });
};
}
render() {
const {
classes,
} = this.props;
const {
rows,
columns,
tableColumnExtensions,
sorting,
editingRowIds,
addedRows,
rowChanges,
currentPage,
deletingRows,
pageSize,
pageSizes,
columnOrder,
currencyColumns,
percentColumns,
} = this.state;
return (
<Paper>
<Grid
rows={rows}
columns={columns}
getRowId={getRowId}
>
<SortingState
sorting={sorting}
onSortingChange={this.changeSorting}
/>
<PagingState
currentPage={currentPage}
onCurrentPageChange={this.changeCurrentPage}
pageSize={pageSize}
onPageSizeChange={this.changePageSize}
/>
<IntegratedSorting />
<IntegratedPaging />
<CurrencyTypeProvider for={currencyColumns} />
<PercentTypeProvider for={percentColumns} />
<EditingState
editingRowIds={editingRowIds}
onEditingRowIdsChange={this.changeEditingRowIds}
rowChanges={rowChanges}
onRowChangesChange={this.changeRowChanges}
addedRows={addedRows}
onAddedRowsChange={this.changeAddedRows}
onCommitChanges={this.commitChanges}
/>
<DragDropProvider />
<Table
columnExtensions={tableColumnExtensions}
cellComponent={Cell}
/>
<TableColumnReordering
order={columnOrder}
onOrderChange={this.changeColumnOrder}
/>
<TableHeaderRow showSortingControls />
<TableEditRow
cellComponent={EditCell}
/>
<TableEditColumn
width={120}
showAddCommand={!addedRows.length}
showEditCommand
showDeleteCommand
commandComponent={Command}
/>
<PagingPanel
pageSizes={pageSizes}
/>
</Grid>
<Dialog
open={!!deletingRows.length}
onClose={this.cancelDelete}
classes={{ paper: classes.dialog }}
>
<DialogTitle>
Delete Row
</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure to delete the following row?
</DialogContentText>
<Paper>
<Grid
rows={rows.filter(row => deletingRows.indexOf(row.id) > -1)}
columns={columns}
>
<CurrencyTypeProvider for={currencyColumns} />
<PercentTypeProvider for={percentColumns} />
<Table
columnExtensions={tableColumnExtensions}
cellComponent={Cell}
/>
<TableHeaderRow />
</Grid>
</Paper>
</DialogContent>
<DialogActions>
<Button onClick={this.cancelDelete} color="primary">
Cancel
</Button>
<Button onClick={this.deleteRows} color="secondary">
Delete
</Button>
</DialogActions>
</Dialog>
</Paper>
);
}
}
export default withStyles(styles, { name: 'ControlledModeDemo' })(DemoBase);
The font size of the text labelling the columns (e.g. product, region, amount) is fixed, and I see no parameters that can change it. Any ideas?
I think there are a few ways around this, the way I have used is having a fully controlled component.
Looks a little like this
<TableHeaderRow cellComponent={this.ExampleHeaderCell} />
Where ExampleHeaderCell is a component that would look something like this
ExampleHeaderCell = (props: any) => (<TableHeaderRow.Cell
className={exampleClass}
{...props}
key={column.name}
getMessage={() => column.title}
/>)
From there you can pass it a class as shown with exampleClass
You can take this further and have it customised for a particular column.
ExampleHeaderCells = (props: any) => {
const exampleClass = css({ backgroundColor: "blue" })
const { column } = props
if (column.name === "name") {
return (
<TableHeaderRow.Cell
className={exampleClass}
{...props}
key={column.name}
getMessage={() => column.title}
/>
)
}
return <TableHeaderRow.Cell {...props} key={column.name} getMessage={() => column.title} />
}
The example above is returning a specific cell with the exampleClass if the column name is equal to "name". Otherwise it just returns the regular TableHeaderRow.Cell