Programmatically adding components with React - javascript

I'm fairly new to React and I'm trying to add new components to already exsisting, but I am not sure how to do that.
So here I have the list of my Seances and the button to add more:
SeanceManager.js
return (
<MDBRow>
<MDBCol md="6">
<MDBCard>
<MDBCardBody>
<form>
<p className="h4 text-center py-4">Sign up</p>
<div className="grey-text">
<MDBInput
label="Type your email"
icon="envelope"
group
type="email"
validate
error="wrong"
success="right"
/>
<MDBInput
label="Type your password"
icon="lock"
group
type="password"
validate
/>
</div>
<div className="text-center py-4 mt-3">
<MDBBtn color="cyan" type="submit" onClick={props.submitLogin}>
Log in
</MDBBtn>
</div>
</form>
</MDBCardBody>
</MDBCard>
</MDBCol>
</MDBRow>
);
By pressing the button to add more, a modal view pops up and at the end it has a submit button that should add the Seance.
AddSeanceModal.js
return (
<Modal
{...this.props}
size="lg"
aria-labelledby="contained-modal-title-vcenter"
centered
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter">Add Seance</Modal.Title>
</Modal.Header>
<Modal.Body>
<div>
<form>
{/*First row*/}
<MDBRow>
<MDBCol md="4">
<div className="custom-file">
<input
type="file"
className="custom-file-input"
id="inputGroupFile01"
aria-describedby="inputGroupFileAddon01"
/>
<label className="custom-file-label" htmlFor="inputGroupFile01">
Choose file
</label>
</div>
</MDBCol>
</MDBRow>
{/*Second row*/}
<MDBRow>
<MDBCol md="4">
<MDBInput
onChange={this.changeHandler}
type="text"
id="materialFormRegisterPasswordEx4"
name="algus_aeg"
label="Algus Aeg"
required
/>
</MDBCol>
<MDBCol md="4">
<MDBInput
onChange={this.changeHandler}
type="text"
id="materialFormRegisterPasswordEx4"
name="lopp_aeg"
label="Lõpp Aeg"
required
/>
</MDBCol>
</MDBRow>
{/*Third row*/}
<MDBRow>
<MDBCol md="4">
<MDBInput
onChange={this.changeHandler}
type="text"
id="materialFormRegisterPasswordEx4"
name="aja_samm"
label="Aja Samm"
required
/>
</MDBCol>
</MDBRow>
<Button variant="secondary" onClick={this.props.onHide}>
Close
</Button>
<MDBBtn color="success" type="submit" className="float-right">
Add Seance
</MDBBtn>
</form>
</div>
</Modal.Body>
</Modal>
);
And finally the Seance itself:
Seance.js
return (
<div
className="card"
style={{ marginBottom: "7px" }}
onClick={() => this.setState({ modalShow: true })}
>
<div className="card-body">
<h5 className="card-title">Seance nr: {this.props.id}</h5>
<p className="card-text">Start aeg: {this.props.startDate}</p>
<p className="card-text">End aeg: {this.props.endDate}</p>
<button type="button" className="close float-right" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<ResultModal id={1} show={this.state.modalShow} onHide={modalClose} />
</div>
</div>
);
I also made a fiddle on sandbox:
https://codesandbox.io/s/qloo1vqr7j?fontsize=14
At the moment I have 4 static Seances, but it should start with 0 and add more once you add them.
Also the Xon the Seance should remove it.
I have tried creating a list on state in SeanceManager.js, but I have not understoond how to add components to the list from another component AddSeanceModal.

There are quite a few code choices that are holding your application back from being dynamic.
First, you'll need to utilize a container component that handles all things related to Seance state. This includes, adding, removing and viewing. All of this needs to be handled by a parent that updates its children according to what current in its state. Please follow this guide to understand containers vs components.
Second, you need to refactor how you're utilizing Seances. They need to be stored as an array within the parent's state. As you have it now, it's hard-coded with 4 <Seances /> that can't be removed/updated.
Instead, you should create an array of objects like so within state:
seances: [
{ id: "1", startDate: "2019-04-10 10:28:05.926-07", endDate: "2019-05-10 10:28:05.924-07" },
{ id: "2", startDate: "2019-04-11 11:28:05.926-07", endDate: "2019-05-11 11:28:05.924-07" },
{ id: "3", startDate: "2019-04-12 12:28:05.926-07", endDate: "2019-05-12 12:28:05.924-07" },
...etc
];
You'll utilize Array.map to map over this seances array and implicitly return these seances dynamically (to make easier to read, I'm writing your <Seance id={id} /> component inline):
<div>
{seances.map(({ id, startDate, endDate }) => (
<div key={id}>
<h1>Seance nr: {id} </h1>
<p>Start date: {startDate}</p>
<p>End date: {endDate}</p>
<button type="button" onClick={() => handleDeleteSeance(id)}>X</button>
</div>
))}
</div>
Now you'll remove these items from the seances array by utilizing Array.filter by a seance's id property. When a user clicks the "X" button it calls this.handleDeleteSeance with the specific seance id:
handleDeleteSeance = id => {
this.setState(prevState => ({
...prevState, // spread out any previous state not related to "seances"
seances: prevState.seances.filter(seance => seance.id !== id) // this translates to: compare each "seance" within "seances" and implicitly return any that do NOT match the "id" that was clicked
})
};
To add an item to the seances array, you'll utilize the spread operator, and add an object with properties that were collected from a form:
handleSubmit = e => {
e.preventDefault();
const { id, startDate, endDate } = this.state;
if (!id || !startDate || !endDate } return null; // if any properties are missing, don't add an item yet
this.setState(prevState => ({
...prevState, // spread out any previous state not related to "seances"
seances: [...prevState.seances, { id, startDate, endDate } ] // this translates to: spread out any previous seances and add a new object with "id", "startDate" and "endDate"
});
}
Working with arrays example (since this a client-only implementation, I decided to filter by array position so I don't have to have to deal with generating unique ids; however, you can utilize uuid to generate these dynamic ids if you wish):
Other notes:
One of your components contains a let modalClose = () =>
this.setState({ modalShow: false }); within the render method.
This is invalid and should be defined as a class method.
Structure your components to be reusable. You have some components that are, but then you have some that reside within another component (like FileInput). As your application grows, you may need to use FileInput elsewhere for another form.
Utilize ES6 syntax throughout your project to simplify your code. For example you can deconstruct props for your Login component like so: const Login = ({ submitLogin }) => ( <MDBRow>...etc</MDBRow>);.

Related

Hide icon on each last item

I have a nested list with some items
Currently I'm displaying an arrow for each item, which results in a problem - if item has no sub-items, it still shows an arrow.
ListItem code (classNames are excluded for a better reading):
function CodeItem({ props }: CodeItemProps) {
const cpvs = Object.keys(tree) as Array<CpvLabelKey>;
const [open, setOpen] = React.useState(cpvs.some(c => codes.includes(c)));
return (
<div>
<fieldset>
<div >
<button
onClick={() => setOpen(!open)}
aria-expanded={open}
>
// this icon should be displayed only if Item has sub-elements
<UilAngleRight/>
</button>
<input
type="checkbox"
name={`cpv-${code}`}
id={`cpv-${code}`}
checked={checked}
onChange={e => onChange(e.target.checked, code)}
/>
</div>
<label htmlFor={`cpv-${code}`}>
{code} - {tCPV(code)}
</label>
</fieldset>
// where I map each Item
{open && (
<div>
{cpvs.map(code => (
<CodeItem
key={code}
code={code}
onChange={onChange}
tree={tree[code]?.children || {}}
checked={codes.includes(code)}
codes={codes}
/>
))}
</div>
)}
</div>
);
}
How could I hide arrow on last elements, which has no over sub-elements?
You can check if the array is not empty before rendering the icon
{!!cpvs.length && <UilAngleRight/>}

Handle an aray of inputs in react

Im trying to handle an aray of inputs in react which are viewed by ".map" function.
The problem is I couldn't give the field a specific value. As a result i couldn't handle the input in onChange function.
I have list of card each card will have a Admin description input and two buttons each one will send a different request.
Here A made the function that present the cards. The porplem is with the input
function createrequestcard(prop){
return(
<Card className="text-center" key={prop._id}>
<div class="wrapper">
<div id="requestspart1" class="left">
<Card.Body>
<Card.Title>Admin Description</Card.Title>
<Card.Text>
<textarea
// --> Value of the index in aray
// --> Handle Change of input
/>
</Card.Text>
</Card.Body>
</div>
<div id="requestspart3" class="left">
<Card.Body>
<Card.Title>CREATE</Card.Title>
<Button variant="outline-success" className="AdminRequestButton">APPROVE</Button>
<Button variant="outline-danger" className="AdminRequestButton">DENY</Button>
</Card.Body>
</div>
</div>
</Card>
)
}
In initialising values on class
this.state = {
requests: [],
description: '',
}
}
The request aray is updated from the backend:
componentDidMount(){
this.checkloginstatus();
axios.get('http://localhost:3000/request', {withCredentials: true})
.then(resp => {
this.setState({requests: resp.data})
}).catch(err => console.log(err))
}
And in render function:
<div>
{this.state.requests.map(createrequestcard)}
</div>
Thank you very much for helping me out.
You can pass index in map method like the below,
<div>
{this.state.requests.map((req,index) => createrequestcard(req, inex))}
</div>
function createrequestcard(prop, index){
The structure of map method is like below
map((element) => { ... } )
map((element, index) => { ... } )
map((element, index, array) => { ... } )
your card component should defined as follow, here i have named it RequestCard (just to make more readable),
this component will get handleOnChange as argument , we will pass updated value to it,when onchange event occurs in
textarea.
function RequestCard(props){
return(
<Card className="text-center" key={prop._id}>
<div class="wrapper">
<div id={props.id} class="left">
<Card.Body>
<Card.Title>Admin Description</Card.Title>
<Card.Text>
<textarea
onChange={(e)=>props.handleOnChange(e.target.value)}
// --> Value of the index in aray
// --> Handle Change of input
/>
</Card.Text>
</Card.Body>
</div>
<div id="requestspart3" class="left">
<Card.Body>
<Card.Title>CREATE</Card.Title>
<Button variant="outline-success" className="AdminRequestButton">APPROVE</Button>
<Button variant="outline-danger" className="AdminRequestButton">DENY</Button>
</Card.Body>
</div>
</div>
</Card>
)}
now you should render it as follow
<div>
{this.state.requests.map((request,index)=>{
return <RequestCard id={index} handleOnChange={(updatedValue)=>this.handleOnChange(index,updatedValue)}
})}
finally your handleOnChange of parent component should be like this,
handleOnChange=(index,updatedValue)=>{
let curState=this.state;
curState.requests[index].value=updatedValue;//note that i am not aware what member of request you want to update
//so i assume value is the member of request object,you can change whatever you want
this.setState(curState);
}

Can I filter elements based on searchbox input in React?

I'm incredibly new to react and web development in general and I wanted to start by making a very simple shopping cart app. I found a nice tutorial on youtube for it, and am now trying to expand on it a bit. I wanted to add a search bar that filters out elements on the home-page as you type in it.
const Home = (props) => {
function handleChange(e){
console.log(e.target.value);
}
return (
<div>
<Row align="middle" className='title'> **search bar**
<Input placeholder="Search for a school" onChange={handleChange} />
</Row>
<Row align="middle" className='title'>
<Divider orientation="left"><b>Category E Schools</b></Divider>
</Row>
<Row align="middle" className='container'>
**Every item for sale is in a Col element**
<Col className = "Princeton University" xs={0} md={11} align="left">
<div className='image'>
<img src={Princeton} alt="Princeton University" />
<h3>Princeton</h3>
<h3>$1100.00</h3>
<button onClick={() => props.addBasket('princeton')} className='addToCart cart1' href='#'>Add to Cart</button>
</div>
</Col>
I'm using AntDesign Row-Col components, and my thought was to define a className for each Col. I was hoping that with the className I could implement the handleChange function to directly remove elements whose classNames don't contain the letters typed into the input bar. Sorry for the beginner-level work going on here.
Pat of the genius of react is that you can abstract your components and render them dynamically. to do this I would create a reusable component for all items and then render them dynamically.
Example:
first create an item component
const Item = (props) => (
<Col className = "Princeton University" xs={0} md={11} align="left">
<div className='image'>
<img src={props.image} alt={props.title} />
<h3>{props.title}</h3>
<h3>{props.cost}</h3>
<button onClick={() => props.addBasket('princeton')} className='addToCart cart1' href='#'>Add to Cart</button>
</div>
</Col>
)
}
add your items to state and render your state dynamically
also here I added state to search and set up two way binding between my input and my state value
i also added the filter method to my dynamic rendering of the items
i would recommend adding a library like loadash and using the debounce function so that the results re-render once 350ms after the user finishes typing instead of re-rendering on every single keystroke from the user
const Home = (props) => {
const [items, setItems] = useState([
{name: Princeton, price: 140000.00, img: http:princetonimages.com/f313b},
])
const [search, setSearch] = useState('')
const renderItems = (items) => (items.map(item => <Item {...item} />))
return (
<div>
<Row align="middle" className='title'> **search bar**
<Input
placeholder="Search for a school"
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
</Row>
<Row align="middle" className='title'>
<Divider orientation="left"><b>Category E Schools</b></Divider>
</Row>
<Row align="middle" className='container'>
{renderItems(state.items.filter(search))}
</Row>
)
}

Invalid hook call when extracting react hooks in nested forms

I'm trying to use formik <FieldArray /> where I can use the push arrayHelper to fill in the array data. In the example, they are creating the form straightaway from the button. However I want to extract it to use useCallback
Here's the initiative that I did
import React, { useCallback } from "react";
import { useField, FieldArray } from "formik";
import { Button } from "#material-ui/core";
import { FormikTextField, formStyles } from "./Utils";
function WitnessChild({ remove, push }) {
const classes = formStyles();
const [field] = useField("witnesses");
const handleAdd = useCallback(
() =>
push({
FirstName: "",
LastName: "",
AddressStreet: "",
AddressSuburb: "",
AddressPostcode: "",
Mobile: ""
}),
[field.value]
);
return (
<>
{field.value.length > 0 &&
field.value.map((witness, index) => (
<div>
<div className={`${classes.margin} row`}>
<Button
variant="contained"
component="label"
className={`${classes.instructions}`}
onClick={handleAdd}
>
Add New
</Button>
</div>
<div className={`${classes.margin} row`}>
<div className="col-6">
<FormikTextField
className={`${classes.instructions}`}
id={`witnesses.${index}.FirstName`}
name={`witnesses.${index}.FirstName`}
label="First Name"
/>
</div>
<div className="col-6">
<FormikTextField
className={`${classes.instructions}`}
id="witnessLastName"
name="witnessLastName"
label="Last Name"
/>
</div>
</div>
<div className={`${classes.margin} row`}>
<div className="col-6">
<FormikTextField
className={`${classes.instructions}`}
id="witnessAddressStreet"
name="witnessAddressStreet"
label="Street"
/>
</div>
<div className="col-6">
<FormikTextField
className={`${classes.instructions}`}
id="witnessAddressSuburb"
name="witnessAddressSuburb"
label="Suburb"
/>
</div>
</div>
<div className={`${classes.margin} row`}>
<div className="col-6">
<FormikTextField
className={`${classes.instructions}`}
id="witnessAddressPostcode"
name="witnessAddressPostcode"
label="Postcode"
/>
</div>
<div className="col-6">
<FormikTextField
className={`${classes.instructions}`}
id="witnessMobile"
name="witnessMobile"
label="Mobile"
/>
</div>
</div>
</div>
))}
</>
);
}
export default function WitnessDetails() {
return <FieldArray name="witnesses" render={WitnessChild} />;
}
I'm not sure why it returns:
×
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See link for tips about how to debug and fix this problem.
I can actually remove this error by creating the handleAdd func inside the button () => {}, however I don't think that's a good practice because it will be initiated every time. Can anyone tell me what is the best practice in extracting hooks like this?
Thanks

React - map using multiple arrays

I'm new to React, sorry if this is too basic.
I have an array:
const previews = this.state.previews;
structured like so:
previews: [
"https://p.scdn.co/mp3-preview/86319eaef2091a7f89e53f133ad8d76025e50c4b?cid=d3b2f7a12362468daa393cf457185973",
"https://p.scdn.co/mp3-preview/e5a194941e4268f3861439567b1b5700b6060732?cid=d3b2f7a12362468daa393cf457185973",
]
which I'm mapping in order to render each one of its values, like so:
return (
<div id="parent">
{previews.map((preview, index) =>
<span key={index}>
<Media>
<div className="media">
<div className="media-player">
<Player src={preview} />
</div>
<div className="media-controls">
<PlayPause />
<CurrentTime />
<Progress />
<Duration />
<MuteUnmute />
</div>
</div>
</Media>
</span>)
}
</div>
);
The above works. But lets say I have another array (of corresponding artists for each preview):
const artists = this.state.artists;
structured like so:
artists: [
"The Milk Carton Kids",
"Yo La Tengo",
]
and I want to include this array in the mapping, in order to render each artist as well, like so:
return (
(...)
<td class="number">{ artist }</td>
(...)
);
How do map this second array using map() in the code above?
Do I have to create another object with nested arrays?
What is the best way to go about this?
One approach would be to use the second index parameter passed to your map callback, to access the item of the previews array that corresponds to the current artist item being mapped as follows:
render() {
const { artists, previews } = this.state;
return (<>
{
artists.map((artist, index) => {
/*
Obtain preview from state.previews for current artist index
*/
const preview = previews[index];
/*
Render current artist data, and corresponding preview data
for the current artist
*/
return (<span key={index}>
<span class="number">{ artist }</span>
<Media>
<div className="media">
<div className="media-player">
<Player src={preview} />
</div>
<div className="media-controls">
<PlayPause />
<CurrentTime />
<Progress />
<Duration />
<MuteUnmute />
</div>
</div>
</Media>
</span>)
})
}
</>)
}

Categories