I'm trying to set up an onChange for a text box input but I can't work out why it isn't working... I've logged the output inside the handler function and the value seems to update. The problem is that when I'm passing this to the input component it shows an empty string still. Not sure why?
A second question I have is that I tried destructuring the input config initial value and the console yields an error saying the value is read-only. Could anyone explain why? This option is currently commented out.
See the component logic below:
import React, { useState } from 'react';
import classes from './BioSection.css';
import Input from '../../UI/Input/Input';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faCameraRetro, faUser } from '#fortawesome/free-solid-svg-icons';
const BioSection = () => {
const [addingBio, setAddingBio] = useState(false);
const [inputConfig, setInputConfig] = useState({
elementType: 'textarea',
elementConfig: {
placeholder: 'Bio',
},
value: '',
length: 0,
validation: {
required: true,
minLength: 10,
maxLength: 100,
},
valid: false,
});
const checkValidity = (value, rules) => {
let isValid = true;
if (rules.minLength) {
isValid = value.length >= rules.minLength && isValid;
}
if (rules.maxLength) {
isValid = value.length <= rules.maxLength && isValid;
}
return isValid;
};
const addBio = () => {
setAddingBio(true);
};
const saveBio = () => {
setAddingBio(!addingBio);
//request POST http
};
const cancelBioUpdate = () => {
setAddingBio(!addingBio);
};
const textAreaChangedHandler = (e) => {
console.log(e.target.value);
const copyOfInputConfig = inputConfig;
copyOfInputConfig.value = e.target.value;
copyOfInputConfig.length = e.target.value.length;
copyOfInputConfig.valid = checkValidity(
copyOfInputConfig.value.trim(),
copyOfInputConfig.validation
);
console.log(copyOfInputConfig);
// const { value, length, valid, validation } = copyOfInputConfig;
// value = e.target.value;
// value = e.target.value;
// length = e.targer.value.length;
// valid = checkValidity(copyOfInputConfig.value.trim(), validation);
let formIsValid = true;
for (let inputIdentifier in copyOfInputConfig) {
formIsValid = copyOfInputConfig.valid && formIsValid;
}
setInputConfig(copyOfInputConfig);
};
return (
<div className={classes.BioSection}>
{addingBio ? (
<div className={classes.UserBio}>
<Input
bioSection
key={inputConfig.elementType}
elementType={inputConfig.elementType}
elementConfig={inputConfig.elementConfig}
value={inputConfig.value}
valueLength={inputConfig.value.length}
invalid={!inputConfig.valid}
shouldValidate={inputConfig.validation}
maxCharacters={inputConfig.validation.maxLength}
changed={(e) => {
textAreaChangedHandler(e);
}}
/>
<div className={classes.BioButtonHolder}>
<button onClick={cancelBioUpdate}>cancel</button>
<button onClick={saveBio}>save</button>
</div>
</div>
) : (
<div>
<span>add travel bio</span>
<button onClick={addBio}>add bio</button>
</div>
)}
</div>
);
};
export default BioSection;
why don't you setState directly like this??
setInputConfig({
...inputConfig,
value: e.target.value,
length: e.target.value.length,
valid: checkValidity(
e.target.value.trim(),
inputConfig.validation
),
});
as for the second question you are trying to assign new value to a constant.
Related
I'm new React developer(mainly with hooks but did not find good example with hooks), here i have antd table with search functionality, my question is when user writes something in search then user gets different result, how to cancel that search by clicking 'Reset' button ?
my code:
https://codesandbox.io/s/antd-table-filter-search-forked-mqhcn?file=/src/EventsSection/EventsSection.js
You can add an id to your input into TitleSearch.js:
<Search
id='IDYOUWANT'
placeholder="Enter Title"
onSearch={onSearch}
onChange={onChange}
style={{ width: 200 }}
/>
And add event into EventsSection.js
ResetInput = () => {
const input = document.getElementById('IDYOUWANT');
input.value = '';
this.handleSearch('');
}
....
<button
onClick={this.ResetInput}
>Reset</button>
Change IDYOUWANT with your id
run this code
Created a new function for reset value and trigger it from reset button.
function:
resetValue = () =>{
this.setState({
eventsData: eventsData
});
}
And trigger from button
<button onClick={this.resetValue}>Reset</button>
all code::
import React, { Component } from "react";
import styles from "./style.module.css";
import { EventsTable } from "../EventsTable";
import { StatusFilter } from "../StatusFilter";
import { TitleSearch } from "../TitleSearch";
const eventsData = [
{
key: 1,
title: "Bulletproof EP1",
fileType: "Atmos",
process: "match media",
performedBy: "Denise Etridge",
operationNote: "-",
updatedAt: "26/09/2018 17:21",
status: "complete"
},
{
key: 2,
title: "Dexter EP2",
fileType: "Video",
process: "Compliance",
performedBy: "Dane Gill",
operationNote: "passed",
updatedAt: "21/09/2018 12:21",
status: "inProgress"
}
];
class EventsSection extends Component {
constructor(props) {
super(props);
this.state = {
eventsData
};
}
handleFilter = (key) => {
const selected = parseInt(key);
if (selected === 3) {
return this.setState({
eventsData
});
}
const statusMap = {
1: "complete",
2: "inProgress"
};
const selectedStatus = statusMap[selected];
const filteredEvents = eventsData.filter(
({ status }) => status === selectedStatus
);
this.setState({
eventsData: filteredEvents
});
};
handleSearch = (searchText) => {
const filteredEvents = eventsData.filter(({ title }) => {
title = title.toLowerCase();
return title.includes(searchText);
});
this.setState({
eventsData: filteredEvents
});
};
handleChange = (e) => {
const searchText = e.target.value;
const filteredEvents = eventsData.filter(({ title }) => {
title = title.toLowerCase();
return title.includes(searchText);
});
this.setState({
eventsData: filteredEvents
});
};
resetValue = () =>{
this.setState({
eventsData: eventsData
});
}
render() {
return (
<section className={styles.container}>
<header className={styles.header}>
<h1 className={styles.title}>Events</h1>
<button onClick={this.resetValue}>Reset</button>
<TitleSearch
onSearch={this.handleSearch}
onChange={this.handleChange}
className={styles.action}
/>
</header>
<EventsTable eventsData={this.state.eventsData} />
</section>
);
}
}
export { EventsSection };
Here is what i did in order to solve it:
i added onClick on the button
<button onClick={this.resetSearch}>Reset</button>
Then in the function i put handleSearch to '', by doing this it reset the table:
resetSearch = () =>{
this.handleSearch('')
}
I have a simple React app that I am going to post entirely (it is not that long). The app doesn't work but doesn't throw any error either. I tried to log the states and it turns out they never change. I am using big things like custom hook and useReducer, but I suspect I lack on understanding basic principles of how react works.
Here's a short summary of how the app should work:
There is a Form component which returns a series of custom Input elements (here only two).
The Input component outsources the validation logic to a custom hook which returns [isTouched, isValid, dispatcherOfTheCustomHookReducer]. When an event occurs, the Input component calls the dispatcher of the custom hook and then styles should be applied to the <input> element based on the state returned by the reducer in the custom hook.
Also since the Form component needs to know if the form as a whole is valid, each Input has an onChangeValidity property used to lift up the isValid state.
In theory the form should appear neutral at the beginning and then, after you focus and blur an input this should become either valid (blue background) or invalid (red background).
I should probably reset the inputs after submission and add something else, but for now I want to make the app work. At the moment the states never changes and the forms appears always neutral (white).
You may prefer look at the files in codesandbox.
App.js
import Form from './components/Form';
function App() {
return (
<div className="app">
<Form />
</div>
);
}
export default App;
Form.js
import { useReducer } from 'react';
import Input from './Input';
// put your inputs' ID here to generate the default state
const defaultState = (inputs = ['username', 'email']) => {
let inputsState = {};
for (const input of inputs) inputsState[input] = false;
return { ...inputsState, isValid: false };
};
const formReducer = (state, action) => {
let newInputsStateList = {...state, [action.id]: action.isValid};
delete newInputsStateList.isValid;
let isValid = true;
for(const key in newInputsStateList) {
if(!newInputsStateList[key]) isValid = false;
break;
}
return { ...newInputsStateList, isValid};
}
const Form = props => {
const [formState, dispatchFormState] = useReducer(formReducer, undefined, defaultState);
const submitHandler = event => {
event.preventDefault();
console.log('You are logged in.');
}
return <form onSubmit={submitHandler}>
<Input
id='username'
label='Username'
type='text'
test={username => username.trim().length > 6}
onChangeValidity={validity => dispatchFormState({id: 'username', isValid: validity})}
/>
<Input
id='email'
label='Email'
type='email'
test={email => email.includes('#')}
onChangeValidity={validity => dispatchFormState({id: 'email', isValid: validity})}
/>
<button type='submit' disabled={!formState.isValid} >Submit</button>
</form>
};
export default Form;
Input.js
import { useEffect } from 'react';
import classes from './Input.module.css';
import useValidation from '../hooks/use-validation';
const Input = props => {
const [isTouched, isValid, checkValidity] = useValidation();
// eslint-disable-next-line
useEffect(() => props.onChangeValidity(isValid), [isValid]);
return <div className={classes.generic_input}>
<label className={classes['generic_input-label']} htmlFor={props.id} >{props.label}</label>
<input
className={classes[`${isTouched ? 'generic_input-input--'+isValid ? 'valid' : 'invalid' : ''}`]}
type={props.type}
name={props.id}
id={props.id}
onChange={event => checkValidity({
type: 'CHANGE',
value: event.target.value,
test: props.test
})}
onBlur={event => checkValidity({
type: 'BLUR',
value: event.target.value,
test: props.test
})}
/>
</div>
};
export default Input;
use-validation.js
import { useReducer } from 'react';
const validationReducer = (state, action) => {
let isTouched = state.isTouched;
let isValid = state.isValid;
if(action.type === 'CHANGE') if (isTouched) isValid = action.test(action.value);
else if(action.type === 'BLUR') {
isValid = action.test(action.value);
if (!isTouched) isTouched = true;
}
else isTouched = isValid = false;
return {isTouched, isValid};
}
const useValidation = () => {
const [validationState, dispatchValidation] = useReducer(validationReducer, {isTouched: false, isValid: false});
return [validationState.isTouched, validationState.isValid, dispatchValidation];
};
export default useValidation;
Input.module.css
.generic_input {
display: flex;
flex-direction: column;
padding: 1rem;
}
.generic_input-label {
font-weight: bold;
}
.generic_input-input--valid {
background-color: lightblue;
}
.generic_input-input--invalid {
border-color: red;
background-color: rgb(250, 195, 187);
}
.submit:disabled {
background-color: #CCC;
color: #292929;
border-color: #CCC;
cursor: not-allowed;
}
I think you need to fix the isTouched logic in your validationReducer. isTouched never gets set to true:
Something like:
const validationReducer = (state, action) => {
let isTouched = state.isTouched;
let isValid = state.isValid;
if (action.type === "CHANGE") {
isTouched = true;
isValid = action.test(action.value)
} else if (action.type === "BLUR") {
isValid = action.test(action.value);
} else {
isTouched = isValid = false;
}
return { isTouched, isValid };
};
... though I'm not sure when you'd want isTouched to be set to false again, so that logic needs some work...
Also, the class on your input is not correct.
Its should look like:
<input
className={
classes[
isTouched
? `generic_input-input--${isValid ? "valid" : "invalid"}`
: ""
]
}
...
>
Take a look at this sandbox
I am trying to make a simple 'Nonogram'/'Picross' game using React to learn UseContext and UseReducer, but am puzzled as to why my top component (App) is not re-rendering when a value it uses changes. Perhaps I am missing something basic, but I've read through documentation and examples online and can't see why it is not re-rendering.
Expectation: User goes on the application, clicks on the squares to change their value (draw a cross by clicking on the squares), and the text underneath the board reads "Congratulations!", as it is based on the value of 'isComplete'
Problem: As above, but 'Keep trying' remains.
I added a button to see the boardState as defined in the UseReducer function, too.
Code is as follows:
App.js
import './App.css';
import { useReducer } from 'react';
import Table from './Table';
import BoardContext from './BoardContext';
import boardReducer from './BoardReducer';
function App() {
//Puzzle layout
const puzzleArray = [
[true, false, true],
[false, true, false],
[true, false, true]
];
//Creating a set of blank arrays to start the game as the userSelection
const generateUserSelection = () => {
const userSelection = [];
puzzleArray.forEach(row => {
let blankRow = [];
row.forEach(square => {
blankRow.push(false)
});
userSelection.push(blankRow);
})
return userSelection;
};
//Initial Context value
const boardInfo = {
puzzleName: "My Puzzle",
puzzleArray: puzzleArray,
userSelection: generateUserSelection(),
isComplete: false
};
const [ boardState, dispatch ] = useReducer(boardReducer, boardInfo)
return (
<BoardContext.Provider value={{board: boardState, dispatch}}>
<div className="App">
<header className="App-header">
<p>
Picross
</p>
<Table />
</header>
<div>
{boardState.isComplete ?
<div>Congratulations!</div>
: <div>Keep trying</div>
}
</div>
<button onClick={() => console.log(boardState)}>boardState</button>
</div>
</BoardContext.Provider>
);
}
export default App;
Table.jsx:
import { useContext, useEffect } from 'react';
import './App.css';
import Square from './Square';
import BoardContext from './BoardContext';
function Table() {
useEffect(() => {console.log('table useEffect')})
const { board } = useContext(BoardContext);
const generateTable = solution => {
const squareLayout = []
for (let i = 0; i < solution.length; i++) {
const squares = []
for (let j = 0; j < solution[i].length; j++) {
squares.push(
<Square
position={{row: i, column: j}}
/>
);
};
squareLayout.push(
<div className="table-row">
{squares}
</div>
);
};
return squareLayout;
};
return (
<div className="grid-container">
{generateTable(board.puzzleArray)}
</div>
);
}
export default Table;
Square.jsx
import { useContext, useState } from 'react';
import './App.css';
import BoardContext from './BoardContext';
function Square(props) {
const { board, dispatch } = useContext(BoardContext)
const [ isSelected, setIsSelected ] = useState(false);
const { position } = props;
const handleToggle = () => {
console.log(board)
board.userSelection[position.row][position.column] = !board.userSelection[position.row][position.column]
dispatch(board);
setIsSelected(!isSelected);
}
return (
<div className={`square ${isSelected ? " selected" : ""}`}
onClick={handleToggle}
>
{position.row}, {position.column}
</div>
);
}
export default Square;
Thanks
Edit: I know for a simple application like this it would be very easy to pass down state through props, but the idea is to practice other hooks, so wanting to avoid it. The ideas I am practicing in this would ideally be extensible to bigger projects in the future.
Edit 2: As requested, here's my BoardReducer.js file:
const boardReducer = (state, updateInfo) => {
let isComplete = false;
if (JSON.stringify(updateInfo.userSelection) === JSON.stringify(state.puzzleArray)) {
isComplete = true;
}
updateInfo.isComplete = isComplete;
return updateInfo;
}
export default boardReducer;
(using JSON.stringify as a cheap way to check matching arrays as it's only a small one for now!)
Issue
You are mutating your state object in a couple places:
const handleToggle = () => {
console.log(board);
board.userSelection[position.row][position.column] = !board.userSelection[position.row][position.column]; // <-- mutation!
dispatch(board);
setIsSelected(!isSelected);
}
And in reducer
const boardReducer = (state, updateInfo) => {
let isComplete = false;
if (JSON.stringify(updateInfo.userSelection) === JSON.stringify(state.puzzleArray)) {
isComplete = true;
}
updateInfo.isComplete = isComplete; // <-- mutation!
return updateInfo; // <-- returning mutated state object
}
Since no new state object is created React doesn't see a state change and doesn't rerender your UI.
Solution
useReducer will typically employ a "redux" pattern where the reducer function consumes the current state and an action to operate on that state, and returns a new state object.
You should dispatch an action that toggles the user selection and checks for a complete board.
Board Reducer
When updating state you should shallow copy any state objects that you are updating into new object references, starting with the entire state object.
const boardReducer = (state, action) => {
if (action.type === "TOGGLE") {
const { position } = action;
const nextState = {
...state,
userSelection: state.userSelection.map((rowEl, row) =>
row === position.row
? rowEl.map((colEl, col) =>
col === position.column ? !colEl : colEl
)
: rowEl
)
};
nextState.isComplete =
JSON.stringify(nextState.userSelection) ===
JSON.stringify(state.puzzleArray);
return nextState;
}
return state;
};
Create an action creator, which is really just a function that returns an action object.
const togglePosition = position => ({
type: "TOGGLE",
position
});
Then the handleToggle should consume/pass the row and column position in a dispatched action.
const handleToggle = () => dispatch(togglePosition(position));
Simple Demo
Demo Code:
const puzzleArray = [
[true, false, true],
[false, true, false],
[true, false, true]
];
const userSelection = Array(3).fill(Array(3).fill(false));
const togglePosition = (row, column) => ({
type: "TOGGLE",
position: { row, column }
});
const boardReducer = (state, action) => {
if (action.type === "TOGGLE") {
const { position } = action;
const nextState = {
...state,
userSelection: state.userSelection.map((rowEl, row) =>
row === position.row
? rowEl.map((colEl, col) =>
col === position.column ? !colEl : colEl
)
: rowEl
)
};
nextState.isComplete =
JSON.stringify(nextState.userSelection) ===
JSON.stringify(state.puzzleArray);
return nextState;
}
return state;
};
export default function App() {
const [boardState, dispatch] = React.useReducer(boardReducer, {
puzzleArray,
userSelection,
isComplete: false
});
const handleClick = (row, column) => () =>
dispatch(togglePosition(row, column));
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>{boardState.isComplete ? "Congratulations!" : "Keep Trying"}</div>
<div>
{boardState.userSelection.map((row, r) => (
<div key={r}>
{row.map((col, c) => (
<span
key={c}
className={classnames("square", { active: col })}
onClick={handleClick(r, c)}
/>
))}
</div>
))}
</div>
</div>
);
}
I hope I'm not the only one who's facing with the problem of processing large amount of data to create JSX elements. The problem with that in my solution, that every time a state or prop changes inside a component, it does the whole processing to create it's children, unnecessarily.
Here's my component:
import React from "react";
import PropTypes from "prop-types";
import Input from "./Input";
import { validate } from "./validations";
class Form extends React.Component {
static propTypes = {
//inputs: array of objects which contains the properties of the inputs
//Required.
inputs: PropTypes.arrayOf(
PropTypes.shape({
//id: identifier of the input.
//Required because the form will return an object where
//the ids will show which value comes from which input.
id: PropTypes.string.isRequired,
//required: if set to false, this field will be accepted empty.
//Initially true, so the field needs to be filled.
//Not required.
required: PropTypes.bool,
//type: type of the input.
//Initially text.
//Not required.
type: PropTypes.string,
//tag: htmlTag
//Initially "input", but can be "select".
//Not required.
tag: PropTypes.oneOf(["input", "select", "radio", "custom-select"]),
//options: options for <select>
//Not required.
options: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.shape({
//value: value of the option
value: PropTypes.string.isRequired,
//displayValue: if defined it
//will be the displayed of the text
displayValue: PropTypes.string,
//element: for tag: custom-select
//must be a JSX element
element: PropTypes.object
}),
//if the value is equal to the display value,
//it can be declared as string
PropTypes.string
])
),
//minLen: minimum length accepted for the field.
//If the input doesn't passes, it will not be valid.
//Initially 0, not required.
minLen: PropTypes.number,
//maxLen: maximum length accepted for the field.
//The characters over the maximum will be cut.
//Initially 20000, not required.
maxLen: PropTypes.number,
//className: class of the container of the input.
//The structure of the input component is:
//<div className={className}>
//[<label>{label}</label>]
//<input>
//<p>{errorMessage}</p>
//</div>
//Not required.
className: PropTypes.string,
//placelholder: placeholder of the input field.
//Not required.
placeholder: PropTypes.string,
//label: label of the input field.
//Not required.
label: PropTypes.string,
//validation: function, which checks
//if the value entered is valid.
//Must return a string of an error message if isn't valid.
//Executes if:
//-the user clicks outside the field if it has focus
//-the user clicks on submit button
//Not required.
validation: PropTypes.func,
//process: function, which processes the input entered
//If the form is ready to submit,
//the field's value will be processed with it.
//Not required.
process: PropTypes.func,
//initValue: initial value of the field.
//Not required.
initValue: PropTypes.string,
//submitOnEnter: if the user presses the "Enter" key,
//it submits the form.
//works only with "input" tags
//Initially true, not required.
submitOnEnter: PropTypes.bool
})
).isRequired,
//onSubmit: function which processes the form.
//Must receive the form as a parameter, which is the shape of:
//{[id]: value, [id]: value}
//Required.
onSubmit: PropTypes.func.isRequired,
//otherElements: addictional elements to the form.
//The function must return JSX elements
//Not required.
otherElements: PropTypes.arrayOf(PropTypes.func),
//className: className of the form.
//Not required.
className: PropTypes.string,
//buttonTitle: the button's title.
//Initially "Submit".
//Not required.
buttonText: PropTypes.string,
//focusOn: puts focus on specified element on specified event.
//Not required.
focusOn: PropTypes.shape({
id: PropTypes.string.isRequired,
event: PropTypes.bool
}),
//collect: collects the specified element id's into one parent.
//Needs to be an object, where the key is the classname of the container,
//and the values are an array of the id's of the items needs to collect
//Not required.
collect: PropTypes.objectOf(PropTypes.array)
}
constructor (props) {
super(props);
this.state = {};
}
componentDidMount () {
const { inputs } = this.props;
const inputProps = inputs.reduce((obj, {id, initValue: value = ""}) => {
obj[id] = { value, error: null};
return obj;
}, {});
this.setState({...inputProps}) //eslint-disable-line
}
//process with max-length checking
completeProcess = (val, process, maxLen) => process(val).substr(0, maxLen)
handleSubmit = () => {
const inputProps = this.state;
const errors = {};
const processedValues = {};
this.props.inputs.forEach(
({
id,
required = true,
validation,
process = v => v,
minLen = 0,
maxLen = 20000
}) => {
const { value } = inputProps[id];
errors[id] = validate(
{value, validation, required, minLen}
);
processedValues[id] = this.completeProcess(
value, process, maxLen
);
}
);
const errorFree = Object.values(errors).every(e => !e);
if (errorFree) {
this.props.onSubmit(processedValues);
} else {
const newState = {};
Object.keys(inputProps).forEach(id => {
const { value } = inputProps[id];
const error = errors[id];
newState[id] = { value, error };
});
this.setState(newState);
}
}
renderInputs = () => {
const { collect } = this.props;
const collectors = { ...collect };
const elements = [];
this.props.inputs.forEach(({
id,
validation,
required = true,
submitOnEnter = true,
label,
initValue = "",
className = "",
placeholder = "",
type = "text",
tag = "input",
options = [],
process = v => v,
minLen = 0,
maxLen = 20000
}) => {
const value = this.state[id] ? this.state[id].value : initValue;
const error = this.state[id] ? this.state[id].error : null;
const onBlur = () => {
const { followChange } = this.state[id];
if (!followChange) {
this.setState({ [id]: {
...this.state[id],
error: validate({value, validation, required, minLen}),
followChange: true
}});
}
};
const onChange = newValue => {
const { followChange } = this.state[id];
const newState = {
...this.state[id],
value: this.completeProcess(newValue, process, maxLen)
};
if (followChange) {
newState.error = validate(
{value: newValue, validation, required, minLen}
);
}
this.setState({ [id]: newState });
};
const onEnterKeyPress = ({ key }) => submitOnEnter && key === "Enter" && this.handleSubmit(); //eslint-disable-line
const focus = () => {
const { focusOn = {} } = this.props;
if (id === focusOn.id && focusOn.event) {
return true;
} else {
return false;
}
};
const input = (
<Input
className={className}
label={label}
placeholder={placeholder}
value={value}
onBlur={onBlur}
onChange={onChange}
type={type}
tag={tag}
options={options}
key={id}
id={id}
error={error}
onEnterKeyPress={onEnterKeyPress}
focusOn={focus()}
/>
);
if (Object.keys(collectors).length) {
let found = false;
Object.keys(collect).forEach(parentId => {
const children = collect[parentId];
children.forEach((childId, i) => {
if (childId === id) {
collectors[parentId][i] = input;
found = true;
}
});
});
if (!found) {
elements.push(input);
}
} else {
elements.push(input);
}
});
const collectorElements = Object.keys(collectors).map(parentId => (
<div className={parentId} key={parentId}>
{collectors[parentId]}
</div>
));
return [
...elements,
...collectorElements
];
}
render () {
const {
className,
buttonText = "Submit",
otherElements
} = this.props;
return (
<div className={`form ${className}`}>
{this.renderInputs()}
{otherElements && otherElements.map((e, i) => e(i))}
<button onClick={this.handleSubmit}>{buttonText}</button>
</div>
);
}
}
export default Form;
Where Input is a Pure Component, but it's not relevant to the question. So as you can see, I tried to make the component as flexible as I can, where I need to define just a few attributes to create almost every type of form. But this flexibility costs much, as it needs to process the properties every time the component renders. As this.props.inputs will not change 100%, it wouldn't cause issues if it would be created just when the component mounts. Is it a good solution to move renderInputs to componentDidMount, and store the elements into this.state instead of returning them inside render? If not, what would be the most efficient solution for those problems?
I have setup a couple Jest tests but they just are not working and returning this error.
TypeError: Cannot read property 'parentNode' of null
44 | console.log(document.getElementById("body"));
45 | var title = document.getElementById("modalTitle"),
> 46 | parentTitle = title.parentNode,
When I run the program though it all works properly, no errors, it's just fine. I added some simulated changes to input some fake values into a journal to check if that would help, but it didn't.
So here is the test code.
DisplayJournal.spec.tsx
import * as React from "react";
import { shallow } from "enzyme";
import { DisplayJournal } from "./DisplayJournal";
import AddJournal from "./AddJournal";
let mock: any = jest.fn();
const DisplayJournal_Mock = shallow(
<DisplayJournal
selectedJournal={{
id: 1,
title: "hello",
notes: "frog",
reference: "Test",
date: "12"
}}
deselectJournal={mock}
onClickEditJournal={mock}
match={mock}
location={mock}
//#ts-ignorecls
history={{ push: mock }}
/>
);
test("DisplayJournal.Render() does not return null.", () => {
expect(DisplayJournal_Mock.type()).not.toBeNull();
expect(DisplayJournal_Mock.type()).not.toEqual(null);
expect(DisplayJournal_Mock.get(0)).not.toBeNull();
expect(DisplayJournal_Mock.get(0)).not.toEqual(null);
});
test("DisplayJournal.tsx contains information", () => {
expect(DisplayJournal_Mock.find(".modalBody")).not.toBeNull();
expect(DisplayJournal_Mock.find("#refPlaceholder")).not.toBeNull();
expect(DisplayJournal_Mock.find(".editButton")).not.toBeNull();
});
test("Checking onClick for edit button can be reached", () => {
const jestFunc = jest.fn();
const AddJournal_Mock = shallow(
<AddJournal handleSubmit={() => this._handleSubmit} />
);
AddJournal_Mock.find("#value").simulate("change", {
target: jestFunc,
value: "testVal",
preventDefault: jestFunc
});
AddJournal_Mock.find("#notes").simulate("change", {
target: jestFunc,
value: "testNotes",
preventDefault: jestFunc
});
AddJournal_Mock.find("#value").simulate("change", {
target: jestFunc,
value: "testName",
preventDefault: jestFunc
});
AddJournal_Mock.find("#button").simulate("click", {
preventDefault: jestFunc
});
const eventProps = {
preventDefault: jestFunc
};
const button = DisplayJournal_Mock.find("button").at(0);
button.simulate("click", eventProps);
expect(jestFunc).toBeCalled();
});
And now here is the file that is being tested against and I will BlockQuote the line that is having the issue.
import * as React from "react";
import { Journal } from "../Models";
import Modal from "../../../global/components/modal/Modal";
import * as css from "../css/journal.scss";
import { connect } from "react-redux";
import { hideJournal, editJournal } from "../Actions";
import { Route, RouteComponentProps, withRouter } from "react-router-dom";
import { getNames } from "../Selectors";
import { State } from "../Reducers";
interface Props extends RouteComponentProps {
selectedJournal: Journal;
deselectJournal: Function;
onClickEditJournal: Function;
}
interface States {
title: string;
body: string;
name: string;
date: string;
names: string[];
}
var edited = false;
export class DisplayJournal extends React.Component<Props, States> {
constructor(props: Props) {
super(props);
this.state = {
title: "",
body: "",
name: "",
date: "",
names: []
};
}
_dismiss(e: React.MouseEvent): void {
e.stopPropagation();
e.preventDefault();
}
handleClick = g => {
console.log(document.getElementById("body"));
var title = document.getElementById("modalTitle"),
parentTitle = title.parentNode,
titleInput = document.createElement("input");
var body = document.getElementById("body"),
parentBody = body.parentNode,
bodyInput = document.createElement("input");
var name = document.getElementById("name"),
parentName = name.parentNode,
nameInput = document.createElement("input");
var date = document.getElementById("date");
var currentDate = new Date();
var day = currentDate.getDate();
var month = currentDate.getMonth() + 1;
var year = currentDate.getFullYear();
var now = day + "/" + month + "/" + year;
titleInput.id = titleInput.name = "title";
bodyInput.id = bodyInput.name = "body";
nameInput.id = nameInput.name = "name";
titleInput.type = "text";
bodyInput.type = "text";
nameInput.type = "text";
titleInput.value = this.state.title;
bodyInput.value = this.state.body;
nameInput.value = this.state.name;
date.innerText = now;
this.setState({ date: now });
parentTitle.replaceChild(titleInput, title);
parentBody.replaceChild(bodyInput, body);
parentName.replaceChild(nameInput, name);
titleInput.addEventListener("blur", this.onBlurEdit, false);
bodyInput.addEventListener("blur", this.onBlurEdit, false);
nameInput.addEventListener("blur", this.onBlurEdit, false);
};
onBlurEdit = e => {
if (e.target.name === "title") {
let titleVal = e.target.value;
this.setState({ title: titleVal });
} else if (e.target.name === "body") {
let bodyVal = e.target.value;
this.setState({ body: bodyVal });
} else if (e.target.name === "name") {
let nameVal = e.target.value;
this.setState({ name: nameVal });
}
edited = true;
//#ts-ignore
let id = this.props.match.params.id;
let title = this.state.title;
let body = this.state.body;
let name = this.state.name;
let date = this.state.date;
this.props.onClickEditJournal(id, title, body, name, date);
};
render() {
const { selectedJournal } = this.props;
const Button = () => (
<Route
render={({ history }) => (
<span
className={css.viewJournalCloseButton}
title="Close the modal dialog"
onClick={() => {
this.props.history.push("/journal");
}}
>
X
</span>
)}
/>
);
if (selectedJournal == null) return null;
if (edited == true) {
selectedJournal.title = this.state.title;
selectedJournal.notes = this.state.body;
selectedJournal.reference = this.state.name;
selectedJournal.date = this.state.date;
}
return (
<Modal title={selectedJournal.title}>
<Button />
<div className={css.modalBody}>
<div>
<div className={css.displayNotes}>
<div id="body" className={css.notesSpan}>
{selectedJournal.notes}
</div>
</div>
<div className={css.row2}>
<div className={css.displayTogether}>
<div className={css.referenceSpan}>
<span id="refPlaceholder">Written by:</span>
<span id="name">{selectedJournal.reference}</span>
</div>
</div>
<div className={css.displayTogether}>
<div className={css.dateSpan}>
Date created:
<span id="date">{selectedJournal.date}</span>
</div>
</div>
</div>
<button className={css.editButton} onClick={this.handleClick}>
Edit
</button>
</div>
</div>
</Modal>
);
}
}
const mapStateToProps = (state: State) => ({
names: getNames(state)
});
const mapDispatchToProps = {
_dismiss: hideJournal,
onClickEditJournal: editJournal
};
export default connect<any, any, any, any>(
mapStateToProps,
mapDispatchToProps
)(withRouter(DisplayJournal));
This issue was that modalTitle was part of the modal component. The way that I fixed this in the JEST testing was by changing shallow to mount I was also able to use shallow(AddJournal_Mock).dive();