Can you add a component to state with Hooks? - javascript

I have a modal that I'm using to allow editing of individual pieces of a page with a lot of discrete sections. This is a lot more user-friendly than passing them to a form - the form would be enormous.
The sections across the page vary though. Some are simple text and a simple textarea or input will suffice. Some data though can only be edited with a select (or conceivably multiple selects).
For the textareas, I am using the following:
/* inplace-edit dialog */
const [dialog, setDialog] = useState({
open: false, // whether dialog should show
fieldName: null, // reference to field for db update on submit
title: '', // dialog title
content: '', // data to show in dialog content
})
const setDialogState = update => () => {
setDialog({ ...dialog, ...update })
}
As a functional component is essentially a function, is it viable to add that component to the state and then use that component to render the specific form structure when the dialog needs to show?

I've done some more investigation and it seems viable to add a stateless component to state using hooks.
I've modified the state handling code to:
const [dialog, setDialog] = useState({
open: false,
title: '',
formComponent: () => null,
})
const setDialogState = update => () => {
setDialog({ ...dialog, ...update })
}
In the above formComponent is just a default function that returns null.
In a page section that I want to edit, there is a boolean showEditIcons that shows the edit icon if the viewer has appropriate permissions. On clicking the icon, it sets the state. And most importantly, it sets formComponent as a reference to the stateless function:
{showEditIcons && (
<IconButton
onClick={setDialogState({
open: true,
title: 'Project Summary',
formComponent: TextForm,
})}
>
<EditIcon />
</IconButton>
)}
where TextForm is just a function:
const TextForm = ({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field name="content">{({ field }) => <TextArea field={field} />}</Field>
</form>
)
I don't see any issue with assigning a function as an object property. Happens all the time.
The real interesting part of course is how I use TextForm. I pass the dialog values as props to a Dialog component:
<Dialog {...dialog} />
And in the part of the Dialog where I need the form, I use the TextForm to render the form with createElement
<DialogHeader>{title}</DialogHeader>
<DialogContent>
{React.createElement(formComponent)}
</DialogContent>
<DialogFooter ... />

Related

Issue testing functional component with ternary return using React 18 and RTL

I have tried a few ways to do this correctly, but lack the testing experience to catch what I'm missing. I have a LoginForm.tsx component that inside holds a few event handlers and a couple bits of local state using React.useState(). The component returns a ternary statement conditionally rendering two components, and within one of them, that component renders different content based on another boolean condition.
authSuccess: when false, main component returns a <Card /> component; when true, the component returns <Navigate to={...} replace /> to redirect user to account.
isLoading: when false, children of <Card /> is form content, when true, children is a <Spinner /> component.
The problem is, I can't seem to find how to change those useState values in my tests and mock the behavior of this component. I would like to test that errors are rendering correctly as well. I am not using Enzyme since it seems it is dead for anything after React 17, so I have been trying to find a way to do this using just React Testing Library out of the box with Create React App Typescript.
The component code looks like this:
import * as React from 'react'
// ...
export default function LoginForm() {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [errors, setErrors] = React.useState<{ [key: string]: string } | any>({});
const [authSuccess, setAuthSuccess] = React.useState<boolean>(false);
const initialState: LoginFormInitialState = {
email: '',
password: '',
};
// Form Hook
const { values, onChange, onSubmit } = useForm({ callback: handleLogin, initialState });
// ==> HANDLERS
function successCallback(data) {
// ...
return setAuthSuccess(true); // <---- Changes state to return redirect
}
function errorHandler(e: any) {
// ...
setErrors(errors); // <---- // sets errors object to render errors
return setIsLoading(false); // <---- Changes contents of <Card />
}
function handleLogin() {
setIsLoading(true); // <---- Changes content of card
setErrors({}); // <---- Clears errors
// Passes handlers as callbacks into api
return userAccountAPI.login({ data: values, successCallback, errorHandler });
}
return authSuccess ? (
// If authentication was successful, redirect user to account page
<Navigate to={ACCOUNT_OVERVIEW} replace />
) : (
// No auth success yet, keep user on login page
<Card
title={<h1 data-testid="form-header">Login To Account</h1>}
data-testid={'login-card'}
bodyStyle={{
display: isLoading ? 'flex' : 'block',
justifyContent: 'center',
padding: isLoading ? '100px 0 ' : '',
}}>
{isLoading ? (
<Spin size='large' data-testid={'login-spinner'}></Spin>
) : (
<Form name='login_form' data-testid={'login-form'} initialValues={{ remember: true }}>
<Form.Item name='email' rules={[{ required: true, message: 'Please input your email!' }]}>
<Input
name='email'
onChange={onChange}
prefix={<UserOutlined className='site-form-item-icon' />}
placeholder='Email'
/>
</Form.Item>
<Form.Item name='password' rules={[{ required: true, message: 'Please input your Password!' }]}>
<Input
name='password'
onChange={onChange}
prefix={<LockOutlined className='site-form-item-icon' />}
type='password'
placeholder='Password'
/>
</Form.Item>
<Form.Item>
<Button
data-testid='login-button'
onClick={onSubmit}
type='primary'
name='login'
htmlType='submit'
className='login-form-button'
block>
Log in
</Button>
</Form.Item>
</Form>
)}
{/* Render out any form errors from the login attempt */}
{Object.entries(errors).length > 0 && (
<Alert
type='error'
message={
<ul style={{ margin: '0' }}>
{Object.keys(errors).map((er, i) => {
return <li key={i}>{errors[er]}</li>;
})}
</ul>
}
/>
)}
</Card>
);
}
I would like to be able to make an assertion about the card, but not if authSuccess=true, in which case I'd want to assert that we do not have the card and that the redirect has been rendered. I would want to test that the Spinner is a child of the card if isLoading = true, but also that I have the form as a child if it is false.
I have tried some of the approaches I've seen in other issues, but many of them have a button in the UI that directly changes the value and the solution is typically "grab that button and click it" and there you go. But the only button here is the login, and that doesn't directly change the local state values I need to mock.
I have also tried something like this which seems to have worked for some people but not for me here..
import * as React from 'react'
describe('<LoginForm />', () => {
const setup = () => {
const mockStore = configureStore();
render(
<Provider store={mockStore()}>
<LoginForm />
</Provider>
);
};
it('should have a spinner as child of card', () => {
setup();
jest.spyOn(React, 'useState')
.mockImplementationOnce(() => ['isLoading', () => true])
.mockImplementationOnce(() => ['errors', () => {}])
.mockImplementationOnce(() => ['authSuccess', () => false]);
const card = screen.getAllByTestId('login-card');
const spinner = screen.getAllByTestId('login-spinner');
expect(card).toContainElement(spinner);
});
});
It seems like Enzyme provided solutions for accessing and changing state, but as mentioned, I am not using Enzyme since I am using React 18.
How can I test this the way I intend to, or am I making a fundamental mistake with how I am approaching testing this? I am somewhat new to writing tests beyond that basics.
Thanks!
From the test I see that you are using react testing library. In this case you should "interact" with your component inside the test and check if the component reacts properly.
The test for spinner should be like that:
render the component
find the email input field and "type" there an email - use one of getBy* methods and then type with e.g. fireEvent.change(input, {target: {value: 'test#example.com'}})
find the password input field and "type" there a password - same as above
find the submit button and "click" it - use one of getBy* methods to find it and then use fireEvent to click it
this (I assume) should trigger your onSubmit which will call the handleLogin callback which will update the state and that will render the spinner.
check if spinner is in the document.
Most probably you would need some mocking for your userAccountAPI so it calls a mock function and not some real API. In here you can also mock that API to return whatever response you want and check if component displays correct content.

React-hook-form working with multiple array data and conditional fields within a map array

I'm lost in the weeds on this, but I think the issues are straightforward enough, and its just my lack of understanding as to why I cant get this working right. I have a form using react-hook-form that is part of a scheduling/ documentation feature. The initial data is pulled from 1 api endpoint which sets the initial info in the parent level of the form- the standard date/time info and the subsequent conditional goal info if the event has already been interacted with- as an 'event' prop. For the child component within the form (GoalInput), the goal titles are pulled from a separate api endpoint to ensure the available goal fields match the current report. Since the first time a user will interact with any given event, the goal fields should be un-toggled and have no associated user information, however, if they are returning to an event previously interacted with, I want the previously set information (contained in the event initial data mentioned earlier) displayed as the default.
Heres the parent form
/.../
const { register, unregister, handleSubmit, watch, control, setValue, formState: { errors } } = useForm({
defaultValues: {
visitStart: event?.visitStart,
visitEnd: event?.visitEnd,
location: event?.location,
goals: [{
title: '',
marked: false,
note: ''
}]
},
shouldUnregister: true
});
const onSubmit= async (data) => {
/.../
}
return (
<div>
<Button color='primary' variant='contained' onClick={handleClickOpen}>
Edit Visit
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Edit Visit</DialogTitle>
<DialogContent>
<DialogContentText>
Visit for {event.client.fullName}
</DialogContentText>
<form id="editVisit"
onSubmit={(e) =>
handleSubmit(onSubmit, onError)(e).catch((e) => {
console.log("e", e);
})}>
<br></br>
<section>
/... initial fields .../
</section>
{goals && goals.map((goal, index) => (
<GoalInput
key={goal._id}
goal={goal}
index={index}
register={register}
control={control}
errors={errors}
visitGoals={event.goals}
setValue={setValue}
unregister={unregister}
/>
))}
/... end of form/ action buttons .../
And the child component:
function GoalInput({ goal, index, register, unregister, setValue, control, errors, visitGoals }) {
const [toggle, setToggle] = useState(false)
console.log("goal: ", goal)
console.log("visitGoals: ", visitGoals)
const goalData = visitGoals?.filter((value)=> {
if ( value.marked === true) {
return value
}
})
console.log("goalData: ", goalData)
useEffect(() => {
if(!toggle) {
unregister(`goals.${index}.title`)
unregister(`goals.${index}.marked`)
unregister(`goals.${index}.note`)
}
}, [unregister, toggle])
return (
<>
<FormControlLabel
{...register(`goals.${index}.title`, goal.title)}
value={goal.title}
name={`goals.${index}.title`}
control={
<Switch
key={index}
{...register(`goals.${index}.marked`)}
checked={goalData.marked || toggle}
// checked={toggle}
name={`goals.${index}.marked`}
value={goalData.marked || toggle}
onClick={console.log("marked? ", goalData.marked, "toggle ", toggle)}
// value={toggle}
onChange={
() => {
setToggle(!toggle);
setValue(`goals.${index}.title`, goal.title)
}}
/>
}
label={goal.title}
/>
<br />
{toggle ? (
<>
<Controller
control={control}
name={`goals.${index}.note`}
id={`goals.${index}.note`}
render={({field}) => (
<TextField
index={index}
error={!!errors.note}
value={goalData.note || field.value}
// value={field.value}
onChange={(e)=>field.onChange(e)}
label="Progress Note"
/>
)}
/>
<br />
</>
) : <></>}
</>
)
}
The visitGoals prop is passing down the event information if it already contains existing goals. Currently the log is showing that the component is correctly filtering out if the goals had been marked: true previously, however, the actual Switch component is not registering the goalData value. I tried setting it as state and having a useEffect set the state, but I was getting just an empty array. I'm sure theres something simple I'm missing to get the input fields to recognize the values, but I cant figure it.
As an extra question if I may, I'd also like to unregister any fields if the Switch input is not toggled, so that its false, so that I'm not storing a bunch of empty objects. Following the docs and video, I thought I've set it up correctly, even trying shouldUnregister: true in the parent form, but I can't seem to navigate that either. The submission data shows the fields are being registered fine by RHF, so I figured the unregister by the same syntax should have worked.
React Hook Form's unregister docs: https://react-hook-form.com/api/useform/unregister
Any direction or guidance would be greatly appreciated.

ReactJS manage lots of select state

Given I have this code (I removed a lot of the select items so it wasn't a massive list but there would be an extra 20 or so):
import * as React from "react";
import { Form, Card, Grid } from "tabler-react";
import { Button } from "semantic-ui-react";
class Skills extends React.Component {
constructor(props) {
super(props);
this.state = {
showSaveButton: false,
showCancelButton: false,
};
}
onChange = (event) => {
this.setState(
{
showSaveButton: true,
showCancelButton: true,
});
}
cancelChanges = () => {
this.setState(
{
showSaveButton: false,
showCancelButton: false,
});
}
render() {
return (
<div className="card" name="skills">
<Card.Body>
<Card.Title>Top skills</Card.Title>
<Grid.Row>
<Grid.Col offset={1} md={10}>
<Form.Group name="softskills" label="Soft Skills">
<Form.SelectGroup canSelectMultiple pills onChange={this.onChange}>
<Form.SelectGroupItem
label="Communication"
name="communication"
value="Communication"
/>
<Form.SelectGroupItem
label="Teamwork"
name="teamwork"
value="Teamwork"
/>
</Form.SelectGroup>
</Form.Group>
</Grid.Col>
</Grid.Row>
<Button content='Cancel changes' floated='left' color='red' basic hidden={this.state.showCancelButton ? '' : 'hidden'} onClick={this.cancelChanges}/>
<Button content='Save changes' floated='right' color='green' basic hidden={this.state.showSaveButton ? '' : 'hidden'}/>
</Card.Body>
</div>
);
}
}
export default Skills;
The current functionality is that on change, 2 buttons will appear that are cancel or accept.
I need the below functionality but I can't work out how to do it unless I have like 60+ states (an initial and a working state for each option) which seems ridiculous:
The initial state is pulled from a database in a JSON array whereby everything that appears in that array should start out as selected (checked=true). For example, if the array is ["communication", "timemanagement"] I need to set the Communication and Time Management options to checked=true.
The initial state needs to be saved so that if anything changes and then the user clicks cancel, the checked boolean for each option is reset to what it was originally
If accept is clicked, the information needs to be sent to the database and so it needs to know what options have checked=true and be able to grab their names
So is there a way to do this without having a massive amount of states?
What you can do is create a mapping in state for all 60. When you get the results from the database, store them in state with fields to track checked and changed statuses:
// isDirty is a flag to say there are pending changes on the option
const options = arrayFromDatabase.map(arrayElement => ({ name: arrayElement, checked: true, isDirty: false })
then store that array in your state, e.g.,
this.setState({...this.state, options })
When a change is made, mark the specific option as dirty -> isDirty = true. If it's cancelled, flip the flag back to false -> isDirty = false.
Should look something like,
this.setState({
...state,
options: this.state.map(
option => option.name === valueToChange ? {
...option,
isDirty: true | false } :
option
)
})
Handle your check-changed in the same way.

Using React, JSX: how to retrieve input value, post it as an element, and then clear it to repeat

This is an assignment for school. It involves using React and JSX to create an input field and a submit button. When the button's clicked, the input value should render as an element to the body. I was able to create it for the first click, but don't know how to repeat it.
If you look at the code below, you'll see that when user types, handleChange changes state of input and when the button's clicked, handleClick changes the boolean state of the button (called 'post'). If post is true, the input along with a timestamp is rendered as a heading.
The problem is that after the render, the input isn't cleared. If the user changes input and clicks button again, it updates the heading with a new timestamp and new input instead of adding another heading.
I've tried changing back state for input and post in handleClick, handleChange, componentDidMount, and componentDidUpdate. But that repeatedly calls setState and I get an error message 'maximum update depth exceeded.'
So, what I want it to do is post a new heading of the input value every time the user clicks the post button. I also want it to clear the input/placeholder text.
import React, { Component } from 'react';
import './App.css';
import Firstposts from './firstposts.jsx';
class App extends Component {
constructor(props) {
super(props)
this.state = {
input: "",
post: false
}
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(event) {
this.setState({ input: event.target.value });
}
handleClick() {
this.setState({
post: true
})
}
render() {
let timestamp = new Date();
return (
<div className="container">
<div className="panel">
<img height="100 px" src="https://marketing.twitter.com/content/dam/marketing-twitter/brand/logo.png" alt=""></img>
<h1>Chirper</h1>
</div>
<div className="body">
<input
placeholder="type your message here"
onChange={this.handleChange}
/>
<button
onClick={this.handleClick}
>Post</button>
<h2>Log</h2>
{<Firstposts />}
{this.state.post ?
<div>
<h3>{timestamp.toString()}</h3>
<h4>{this.state.input}</h4>
</div>
:
<div />
}
</div>
</div >
);
}
}
export default App;
Update your handleClick method to set posts to an array of posts, instead of a boolean:
handleClick() {
this.setState({
posts: [
...this.state.posts,
this.state.input
]
})
}
This will add the value of this.state.input to the end of this.state.posts, preserving all previous posts.
You can update this further to clear the value of the input field:
handleClick() {
this.setState({
posts: [
...this.state.posts,
this.state.input
],
input: '' // add this line to clear your input field when a new post is submitted
})
}
Also, make sure to give your <input> element a value of this.state.input:
<input
value={this.state.input}
placeholder="type your message here"
onChange={this.handleChange}
/>
Without this, you will not be able to programmatically update the value of the <input> field by using setState. You can read more on uncontrolled components in React.
Then, update your render method to map over this.state.posts and render each one:
{this.state.posts.map(post => (
<h4>{post}</h4>
))}
You can read more on rendering lists in React.

Using React portals to show a modal component(?) when a table row is clicked

While becoming acquainted with React I stumbled upon the concept of portals in the developer documentation. However, I'm having difficulty understanding how this portal component actually renders on-demand, and how I can pass data to it to populate a modal.
Currently, I have two components that interact with each other: View.js and DataTable.js.
View.js:
const Example = (props) => {
console.log(props);
return (
<div>
<TopBar />
<DeploymentsHeader env={props.match.params.env} />
<PendingDataTable env={props.match.params.env} />
<DataTable env={props.match.params.env} />
</div>
);
}
Now for the DataTable component, a react-table is being rendered. When a user clicks on an individual row, my goal is to have a modal pop up (still unclear to me whether or not this needs to have its own separate component if I'm using React portals) and have it be populated with data that is already bound to the individual row (which I tested and have access too).
The code looks something like this:
<ReactTable
data={tableData}
filterable={true}
getTrProps={this.onRowClick}
columns={[
{
Header: "Header",
accessor: "service_name"
},
...
]}
/>
Now this is the function that gets passed to the table row props and executes on click:
onRowClick = (state, rowInfo) => {
return {
onClick: e => {
console.log('A Tr Element was clicked!');
console.log(rowInfo.original);
}
}
}
The data that I need is readily available to me in the object rowInfo.original. Now my question is: what is considered the 'correct' or 'best-practice' way to load a modal using a portal when an event such as this onClick trigger executes?
Do I need to have a separate Modal.js component that is actually a portal?
How do I get the data from this onRowClick function transported to this modal portal?
Thanks everyone.
You can conditionally render a portal as if it was just another React component. To start, you should separate the modal out into it's own component. Then, you can store the item id or item in state and toggle to let the modal know when to show or not.
onRowClick = (state, rowInfo) => {
return {
onClick: e => {
console.log('A Tr Element was clicked!');
console.log(rowInfo.original);
this.setState({
data: rowInfo.original,
showModal: true
});
}
}
}
render() {
return (
<ReactTable
data={tableData}
filterable={true}
getTrProps={this.onRowClick}
columns={[
{
Header: "Header",
accessor: "service_name"
},
...
]}
/>
{this.state.showModal && React.createPortal( <Modal data={this.state.data}>Your Data Goes Here</Modal>, document.getElementById('modal-portal')) }
)
}
EDIT:
They have a Modal example in their Portal docs that you should check out.
EDIT 2:
this.state.showModal is a piece of state that you need to add. You will use this to conditionally render the <Modal /> component (that you create). What I've done here is shorthand for:
if(this.state.showModal) {
return React.createPortal(...);
} else {
return null;
}
As for the actual <Modal /> component, you can make that however you want, you can use a react modal package, bootstrap modals or just build your own.
Example custom Modal.js:
const Modal = ({ children, data }) => (
<div className="my-modal">
{children}
// Here you can do stuff with data if you want
</div>
);
CSS:
.my-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
Note ReactDOM.createPortal its a function from react-dom not react
import {createPortal} from 'react-dom'

Categories

Resources