How to pass props to a state in a React component - javascript

I have this component that I've created which uses an array of objects and populates results that are located in the state. It's a Carousel that displays arrows based on how much data is present, so that's why I'm doing this through state. (i'm including the entire code so you can see).
Now I want this Carousel to be used on different pages and the data to be pulled from each page dynamically. I understand that I can do this through props, but I'm not sure how to reorganize the current component to be able to do that.
How do I pass data into props on a different page and add it inside the state here?
class Carousel extends Component {
constructor(props) {
super(props);
this.state = {
items: [
{name: 'First Name', city: 'City'},
{name: 'First Name2', city: 'City2'},
{name: 'First Name3', city: 'City3'}
],
active: 1,
direction: ''
};
this.rightClick = this.moveRight.bind(this);
this.leftClick = this.moveLeft.bind(this);
this.setActive = this.setActive.bind(this);
}
renderSlides(item, index) {
let position;
const {active} = this.state;
if (active === index) {
position = `${styles.active}`;
} else if (active === index + 1) {
position = `${styles.prev}`;
} else if (active === index - 1) {
position = `${styles.next}`;
} else if (active > index) {
position = `${styles.left}`;
} else if (active < index) {
position = `${styles.right}`;
}
return (
<div>
<div className={styles.SliderItem__name}>
- {item.name}, {item.city} {item.state}
</div>
</div>
);
}
renderSliderNav() {
const slides = this.state.items;
const active = this.state.active;
return slides.map((slide, index) => (
<a onClick={() => this.setActive(index)}
key={index}
className={classnames({[styles.active]: active === index})}>
<li className={classnames(styles.LessonSlide__navDot, {[styles.active]: active === index})}></li>
</a>
));
};
setActive(selected) {
const direction = selected > this.state.active ? 'right' : 'left';
this.setState({
active: selected,
direction: direction
});
}
moveLeft() {
const newActive = this.state.active - 1;
if (this.state.active + 1 === 1) {
return;
}
this.setState({
active: newActive < 0 ? this.state.items.length - 1 : newActive,
direction: 'left'
});
}
moveRight() {
const newActive = this.state.active;
if (this.state.active + 1 === this.state.items.length) {
return;
}
this.setState({
active: (newActive + 1) % this.state.items.length,
direction: 'right'
});
}
render() {
const items = this.state.items;
if (!items || items.length === 0) {
return null;
}
return (
<div className={styles.LessonSlider}>
<div className={styles.SliderItem__wrapper}>
<div className={styles.SliderItem__container}>
<h3>Results</h3>
<div className={`${styles.LessonSlider__container} ${styles.noselect}`}>
{this.state.active + 1 !== 1
&& <button className={`${styles.LessonSlider__button} ${styles.LessonSlider__button__prev}`} onClick={this.leftClick} />}
{this.state.items.map((item, index) => this.renderSlides(item, index))}
{this.state.active + 1 !== this.state.items.length
&& <div className={`${styles.LessonSlider__button} ${styles.LessonSlider__button__next}`}
onClick={this.rightClick} />}
</div>
<div>
<ul className={`${styles.LessonSlide__nav} ${styles.noselect} ${this.props.colourScheme}`}>
{this.renderSliderNav()}
</ul>
</div>
</div>
</div>
</div>
);
}
}
export default Carousel;

It sounds to me like you want to be able to call Carousel from other pages like this
<Carousel items=[{name: 'First Name', city: 'City'}] />
Which you can access in Carousel with props.items
Solution
constructor(props) {
super(props);
this.state = {
items: props.items
}
}
You could also put ALL your props into state like this; however, that can have unexpected consequences and is not recommended.
constructor(props) {
super(props);
this.state = {
...props
}
}
Documentation
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

Related

React app not re-rendering child component on setState

I'm trying to make the search function work on this simple react app. I can see that it is getting the new data properly when I log it to the console, but it doesn't seem like the child component that builds the "page" to display the data with pagination is ever being called after the initial rendering. Codepen & code below:
https://codepen.io/eacres/pen/LYmwmmZ
const propTypes = {
books: React.PropTypes.array.isRequired,
onChangePage: React.PropTypes.func.isRequired,
initialPage: React.PropTypes.number
}
const defaultProps = {
initialPage: 1
}
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = { pager: {} };
}
componentWillMount() {
// set page if books array isn't empty
if (this.props.books && this.props.books.length) {
this.setPage(this.props.initialPage);
}
}
componentDidUpdate(prevProps, prevState) {
// reset page if books array has changed
if (this.props.books !== prevProps.books) {
this.setPage(this.props.initialPage);
}
}
setPage(page) {
var books = this.props.books;
var pager = this.state.pager;
if (page < 1 || page > pager.totalPages) {
return;
}
// get new pager object for specified page
pager = this.getPager(books.length, page);
// get new page of books from books array
var pageofBooks = books.slice(pager.startIndex, pager.endIndex + 1);
// update state
this.setState({ pager: pager });
// call change page function in parent component
this.props.onChangePage(pageofBooks);
}
getPager(totalbooks, currentPage, pageSize) {
// default to first page
currentPage = currentPage || 1;
// default page size is 8
pageSize = pageSize || 8;
// calculate total pages
var totalPages = Math.ceil(totalbooks / pageSize);
var startPage, endPage;
if (totalPages <= 10) {
// less than 10 total pages so show all
startPage = 1;
endPage = totalPages;
} else {
// more than 10 total pages so calculate start and end pages
if (currentPage <= 6) {
startPage = 1;
endPage = 10;
} else if (currentPage + 4 >= totalPages) {
startPage = totalPages - 9;
endPage = totalPages;
} else {
startPage = currentPage - 5;
endPage = currentPage + 4;
}
}
// calculate start and end item indexes
var startIndex = (currentPage - 1) * pageSize;
var endIndex = Math.min(startIndex + pageSize - 1, totalbooks - 1);
// create an array of pages to ng-repeat in the pager control
var pages = [...Array((endPage + 1) - startPage).keys()].map(i => startPage + i);
// return object with all pager properties required by the view
return {
totalbooks: totalbooks,
currentPage: currentPage,
pageSize: pageSize,
totalPages: totalPages,
startPage: startPage,
endPage: endPage,
startIndex: startIndex,
endIndex: endIndex,
pages: pages
};
}
render() {
var pager = this.state.pager;
if (!pager.pages || pager.pages.length <= 1) {
// don't display pager if there is only 1 page
return null;
}
return (
<ul className="pagination">
<li className={pager.currentPage === 1 ? 'disabled' : ''}>
<a onClick={() => this.setPage(1)}>First</a>
</li>
<li className={pager.currentPage === 1 ? 'disabled' : ''}>
<a onClick={() => this.setPage(pager.currentPage - 1)}>Previous</a>
</li>
{pager.pages.map((page, index) =>
<li key={index} className={pager.currentPage === page ? 'active' : ''}>
<a onClick={() => this.setPage(page)}>{page}</a>
</li>
)}
<li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
<a onClick={() => this.setPage(pager.currentPage + 1)}>Next</a>
</li>
<li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
<a onClick={() => this.setPage(pager.totalPages)}>Last</a>
</li>
</ul>
);
}
}
Pagination.propTypes = propTypes;
Pagination.defaultProps = defaultProps;
/* App Component
-------------------------------------------------*/
class App extends React.Component {
constructor() {
super();
// an example array of books to be paged
axios.get(`https://goodreads-server-express--dotdash.repl.co/search/name`)
.then(response => {
this.setState({bookList: response.data.list}) ;
})
.catch(error => {
// edge case
// alert("Yikes! Looks like we don't have anything for that search. Please edit your search and try again.");
console.log(error);
});
this.state = {
bookList: [],
pageofBooks: []
};
this.onChangePage = this.onChangePage.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
onChangePage(pageofBooks) {
// update state with new page of books
this.setState({ pageofBooks: pageofBooks }, () => {
//console.log(this.state.pageofBooks)
});
}
handleChange (e) {
e.preventDefault();
this.setState({searchString: e.target.value})
}
handleSubmit (e) {
e.preventDefault();
this.setState({ bookList : [] });
// edge case
if (!this.state.searchString) {
alert('Oops! Please enter your search in the box below.')
} else {
axios.get(`https://goodreads-server-express--dotdash.repl.co/search/${this.state.searchString}`)
.then(response => {
this.setState({ bookList: response.data.list });
})
.catch(error => {
// edge case
alert("Yikes! Looks like we don't have anything for that search. Please edit your search and try again.");
console.log(error);
});
}
}
render() {
return (
<div>
<div className="container">
<div className="text-center">
<form className="search-bar">
<label htmlFor="search">Find me a book</label>
<input id="search" onChange={this.handleChange} />
<button onClick={this.handleSubmit}>Search</button>
</form>
<div className="search-results">
{this.state.pageofBooks.map( (item, i) =>
<BookCard book={item} key={item.title} />
)}
</div>
<Pagination books={this.state.bookList} onChangePage={this.onChangePage} />
</div>
</div>
<hr />
</div>
);
}
}
class BookCard extends React.Component {
constructor(props) {
super(props);
this.state = {
author: props.book.authorName,
title: props.book.title,
image: props.book.imageUrl
}
}
render() {
return (
<div className="book-card">
<div className="image__container">
<img src={this.state.image} />
</div>
<div className="book-card__header">
<h3>{this.state.author}</h3>
<h2>{this.state.title.length > 40 ? this.state.title.slice(0, 40) + '...' : this.state.title}</h2>
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
Thanks in advance!
The issue might be
var pager = this.stata.pager;
Replaced it by
var pager = this.getPager(books.length, page);
Hope this is it
const propTypes = {
books: React.PropTypes.array.isRequired,
onChangePage: React.PropTypes.func.isRequired,
initialPage: React.PropTypes.number
}
const defaultProps = {
initialPage: 1
}
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = { pager: {} };
}
componentDidUpdate(prevProps, prevState) {
// reset page if books array has changed
console.log('1', this.props.books?.[0], prevProps.books?.[0])
if (this.props.books !== prevProps.books) {
console.log('this.props.books 2', this.props.books);
this.setPage(this.props.initialPage);
}
}
setPage(page) {
var books = this.props.books;
var pager = this.getPager(books.length, page);
console.log('pager', pager)
if (page < 1 || page > pager.totalPages) {
return;
}
// get new pager object for specified page
// get new page of books from books array
var pageofBooks = books.slice(pager.startIndex, pager.endIndex + 1);
// update state
this.setState({ pager: pager });
// call change page function in parent component
console.log('pageofBooks', pageofBooks)
console.log('books', books)
this.props.onChangePage(pageofBooks);
}
getPager(totalbooks, currentPage, pageSize) {
// default to first page
currentPage = currentPage || 1;
// default page size is 8
pageSize = pageSize || 8;
// calculate total pages
var totalPages = Math.ceil(totalbooks / pageSize);
var startPage, endPage;
if (totalPages <= 10) {
// less than 10 total pages so show all
startPage = 1;
endPage = totalPages;
} else {
// more than 10 total pages so calculate start and end pages
if (currentPage <= 6) {
startPage = 1;
endPage = 10;
} else if (currentPage + 4 >= totalPages) {
startPage = totalPages - 9;
endPage = totalPages;
} else {
startPage = currentPage - 5;
endPage = currentPage + 4;
}
}
// calculate start and end item indexes
var startIndex = (currentPage - 1) * pageSize;
var endIndex = Math.min(startIndex + pageSize - 1, totalbooks - 1);
// create an array of pages to ng-repeat in the pager control
var pages = [...Array((endPage + 1) - startPage).keys()].map(i => startPage + i);
// return object with all pager properties required by the view
return {
totalbooks: totalbooks,
currentPage: currentPage,
pageSize: pageSize,
totalPages: totalPages,
startPage: startPage,
endPage: endPage,
startIndex: startIndex,
endIndex: endIndex,
pages: pages
};
}
render() {
var pager = this.state.pager;
if (!pager.pages || pager.pages.length <= 1) {
// don't display pager if there is only 1 page
return null;
}
return (
<ul className="pagination">
<li className={pager.currentPage === 1 ? 'disabled' : ''}>
<a onClick={() => this.setPage(1)}>First</a>
</li>
<li className={pager.currentPage === 1 ? 'disabled' : ''}>
<a onClick={() => this.setPage(pager.currentPage - 1)}>Previous</a>
</li>
{pager.pages.map((page, index) =>
<li key={index} className={pager.currentPage === page ? 'active' : ''}>
<a onClick={() => this.setPage(page)}>{page}</a>
</li>
)}
<li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
<a onClick={() => this.setPage(pager.currentPage + 1)}>Next</a>
</li>
<li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
<a onClick={() => this.setPage(pager.totalPages)}>Last</a>
</li>
</ul>
);
}
}
Pagination.propTypes = propTypes;
Pagination.defaultProps = defaultProps;
/* App Component
-------------------------------------------------*/
class App extends React.Component {
constructor() {
super();
// an example array of books to be paged
axios.get(`https://goodreads-server-express--dotdash.repl.co/search/name`)
.then(response => {
this.setState({bookList: response.data.list, page: 1}) ;
})
.catch(error => {
// edge case
// alert("Yikes! Looks like we don't have anything for that search. Please edit your search and try again.");
console.log(error);
});
this.state = {
bookList: [],
pageofBooks: []
};
this.onChangePage = this.onChangePage.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
onChangePage(pageofBooks) {
// update state with new page of books
this.setState({ pageofBooks: pageofBooks }, () => {
//console.log(this.state.pageofBooks)
});
}
handleChange (e) {
e.preventDefault();
this.setState({searchString: e.target.value})
}
handleSubmit (e) {
e.preventDefault();
this.setState({ bookList : [] });
// edge case
if (!this.state.searchString) {
alert('Oops! Please enter your search in the box below.')
} else {
axios.get(`https://goodreads-server-express--dotdash.repl.co/search/${this.state.searchString}`)
.then(response => {
console.log('response.data.list', response.data.list)
this.setState({ bookList: response.data.list });
})
.catch(error => {
// edge case
alert("Yikes! Looks like we don't have anything for that search. Please edit your search and try again.");
console.log(error);
});
}
}
render() {
console.log('this.state.bookList', this.state.bookList)
return (
<div>
<div className="container">
<div className="text-center">
<form className="search-bar">
<label htmlFor="search">Find me a book</label>
<input id="search" onChange={this.handleChange} />
<button onClick={this.handleSubmit}>Search</button>
</form>
<div className="search-results">
{this.state.pageofBooks.map( (item, i) =>
<BookCard book={item} key={item.title} />
)}
</div>
<Pagination books={this.state.bookList} onChangePage={this.onChangePage} />
</div>
</div>
<hr />
</div>
);
}
}
class BookCard extends React.Component {
constructor(props) {
super(props);
this.state = {
author: props.book.authorName,
title: props.book.title,
image: props.book.imageUrl
}
}
render() {
return (
<div className="book-card">
<div className="image__container">
<img src={this.state.image} />
</div>
<div className="book-card__header">
<h3>{this.state.author}</h3>
<h2>{this.state.title.length > 40 ? this.state.title.slice(0, 40) + '...' : this.state.title}</h2>
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
Props shouldn't be stored in the state
Notice that constructor is only called once so the state never had a chance to update
class BookCard extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="book-card">
<div className="image__container">
<img src={this.props.imageUrl} />
</div>
<div className="book-card__header">
<h3>{this.props.authorName}</h3>
<h2>{this.props.title.length > 40 ? this.props.title.slice(0, 40) + '...' : this.props.title}</h2>
</div>
</div>
);
}
}

Why is array.findIndex returning not a fucntion. (mozilla says it basically is?)

changing state.findIndex to state.ids.findIndex resolved this issue.
However when calling the function onClick it automatically updates ALL of the active values to true. rather than waiting for the click event.
I am trying to set up an onClick method/function that will find the active: true and set it to false and then find the object with the matching id: xxxxxx and set that object's active: to true.
According to developer.mozilla.org and several other sites, my method should theoretically work, or, more likely I'm misinterpreting it.
This is the error it's returning
This is the function I'm trying to write - why does it return that findindex is not a function?
function changeActiveField(im) {
const i = state.findIndex((obj) => obj.active === true);
state[i].active = false;
const index = state.findIndex((obj) => obj.id === im);
state[index].active = true;
}
Here the const i finds the index of the obj{ active: true} and changes it to obj{ active: false}
then const index finds the obj with the obj{id: value} that matches the im which is passed in from the clicked component...
<div className="thumbs">
{state.ids.map((i) => (
<Image
className="carouselitem"
rounded
fluid
onClick={changeActiveField(i.id)}
src={"http://img.youtube.com/vi/" + i.id + "/hqdefault.jpg"}
size="small"
/>
))}
</div>
Go to https://test.ghostrez.net and navigate to the Services page to see the video menu. The small thumbnails are the "clickable" items that should trigger the changeActiveField function in turn setting the clicked id to active: true and changing the activevid div
here is the full page code.
import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import { Embed, Image, Loader } from "semantic-ui-react";
import "./Player.css";
var state = {
ids: [
{
id: "iCBvfW08jlo",
active: true,
},
{
id: "qvOcCQXZVg0",
active: false,
},
{
id: "YXNC3GKmjgk",
active: false,
},
],
};
function firstActiveId(ids) {
for (var i = 0; i < ids.length; i++) {
if (ids[i].active) {
return ids[i].id;
}
}
}
function changeActiveField(im) {
const i = state.findIndex((obj) => obj.active === true);
state[i].active = false;
const index = state.findIndex((obj) => obj.id === im);
state[index].active = true;
}
export default class Player extends React.Component {
constructor(props) {
super(props);
this.state = {
state: false,
};
}
render() {
return (
<div className="carouselwrap">
<div className="activevid">
<Embed
active
autoplay={false}
color="white"
hd={false}
id={firstActiveId(state.ids)}
iframe={{
allowFullScreen: true,
style: {
padding: 10,
},
}}
placeholder={
"http://img.youtube.com/vi/" +
firstActiveId(state.ids) +
"/hqdefault.jpg"
}
source="youtube"
/>
</div>
<div className="thumbs">
{state.ids.map((i) => (
<>
<Image
className="carouselitem"
rounded
fluid
onClick={changeActiveField(i.id)}
src={"http://img.youtube.com/vi/" + i.id + "/hqdefault.jpg"}
size="small"
/>
<h2>
{/*this is for testing purposes only*/}
{i.id} {i.active ? "true" : "false"}
</h2>
</>
))}
</div>
</div>
);
}
}
the problem was that the state was not the array, rather ids i the array... so changing my statements to state.ids.findIndex solved that problem
function changeActiveField(im) {
const i = state.findIndex((obj) => obj.active === true);
state[i].active = false;
const index = state.findIndex((obj) => obj.id === im);
state[index].active = true;
}

React - Render Component between two existing ones

I am pretty new on using React, what i'm trying to build is a dynamic form in which user can add/remove fields, the problems come when rendering after a row (field) is added
Here is my Row Component, which I use as a template to fill by props
class Row extends React.Component {
constructor(props){
super(props);
this.addLine = this.addLine.bind(this)
this.handleTitleChange = this.handleTitleChange.bind(this)
this.handleInquisitionChange = this.handleInquisitionChange.bind(this)
}
state = {
pos: this.props.pos,
title: this.props.title,
inquisition: this.props.inquisition
}
addLine() {
this.props.addLine(this.state.pos);
}
handleTitleChange = async (event) => {
await this.setState({title: event.target.value});
this.props.updateRowState(this.state.pos, this.state.title, "title")
}
handleInquisitionChange = async (event) => {
await this.setState({inquisition: event.target.value});
this.props.updateRowState(this.state.pos, this.state.inquisition, "inquisition")
}
render(){
return(
<div className="w3-row odg-line">
<div className="w3-col m2" style={{paddingRight: "8px"}}>
<input type="text" name="titolo[]" placeholder="Argomento" style={{width:"100%"}} onChange={this.handleTitleChange} required/>
</div>
<div className="w3-col m4" style={{paddingRight: "8px"}}>
<textarea form="convocazione" name="istruttoria[]" placeholder="Istruttoria" style={{width:"100%"}} onChange={this.handleInquisitionChange} required></textarea>
</div>
<div className="w3-col m1">
<button type="button" style={{padding:0, height: "24px", width: "24px"}} className="w3-red w3-button w3-hover-white" onClick={() => this.addLine()}>+</button>
</div>
</div>
)
}
}
And this is its parent Convoca, as you can see by its addLine method whenever a "plus" button is pressed it pushes a row after that and updates component state, as far as I know this should cause the component to render again but when it comes it just adds the new one after the already rendered ones
class Convoca extends React.Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this);
this.addLine = this.addLine.bind(this);
}
state = {
rows: [
{pos:0, title: "", inquisition: ""},
{pos:1, title: "", inquisition: ""}
]
}
async addLine(position){
let buffer = this.state.rows;
buffer.splice(position+1, 0, {pos: position+1, title: "", inquisition: ""})
for(let i = position+2; i<buffer.length; i++){
buffer[i].pos++;
}
await this.setState({rows: buffer})
}
handleChangeState = async (pos, val, field) => {
let buffer = this.state.rows;
if(field === "title") buffer[pos].title = (field === "title" ? val : null);
if(field === "inquisition") buffer[pos].inquisition = (field === "inquisition" ? val : null);
await this.setState({rows: buffer})
}
handleSubmit(){
console.log("submitted")
}
render() {
return(
<div className="w3-main" style={{marginLeft:"340px", marginRight:"40px"}}>
<form action="/convocazione" id="convocazione">
{ this.state.rows.map((row) => (
<Row updateRowState={(pos, val, field) => this.handleChangeState(pos, val, field)} addLine={(pos)=>this.addLine(pos)} pos={row.pos} title={row.title} inquisition={row.inquisition}></Row>)
) }
<input className="w3-red w3-button w3-hover-white" type="submit" value="Convoca"/>
</form>
</div>
);
}
}
I would implement addLine function another way
Take a look at the snippet.
const createElement = React.createElement;
class Row extends React.Component {
render() {
const {
position
} = this.props;
return createElement('div', null, [
createElement('input', {
type: "text",
value: this.props.title
}),
createElement('button', {
type: "button",
onClick: () => this.props.onAdd(position)
}, '+')
]);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
rows: [{
id: 0,
position: 0,
title: 'id 0 position 0'
},
{
id: 1,
position: 1,
title: 'id 1 position 1'
},
]
};
this.addLine = this.addLine.bind(this);
}
addLine(position) {
const {
rows
} = this.state
const newId = rows.reduce((acc, row) => acc > row.id ? acc : row.id, 0) + 1
position = position + 1;
const newRows = [
...rows.filter(row => row.position < position),
{
id: newId,
position,
title: `id ${newId} position ${position}`
},
...rows.filter(row => row.position >= position).map(row => ({ ...row,
position: row.position + 1,
title: `id ${row.id} position ${row.position + 1}`
}))
]
newRows.sort((prev, next) => prev.position - next.position)
this.setState({
rows: newRows
})
}
render() {
const items = this.state.rows.map(item =>
createElement(Row, {
key: item.id,
title: item.title,
position: item.position,
onAdd: this.addLine
})
)
return createElement('form', null, items);
}
}
var rootElement = createElement(App, {}, )
ReactDOM.render(rootElement, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root">
</div>
Make sure every Row has key prop with unique id

How do I use the props in order to create a condition in ReactJS where I am able to change the state of my app

I have two components, the parent "App" component and the Hero component which contains an image and left and right arrow.
I want to use the right arrow to move the imageIndex + 1 until it reaches the images.length, and I want to have the left arrow to have a condition that I can't subtract if imageIndex = 0.
So something like: ( This part of the code is not added in my code yet because I keep getting undefined)
if (this.props.imageIndex > 0) {
this.setState({
// decrease the imageIndex by 1
})
}
if (this.props.imageIndex < this.props.images.length - 1){
this.setState({
// increase the imageIndex by 1
})
}
will be the condition or something like it.
App.jS (Parent Component)
export default class App extends Component {
constructor() {
super();
this.state = {
language: "english",
render: 'overview',
imageIndex: 0,
}
}
render() {
// to make sure the state changes
console.log(this.state.language)
const {render} = this.state
return <Fragment>
<Hero imageIndex = {this.state.imageIndex} />
</Fragment>;
}
}
How would I add that in my Hero Component which contains this code:
Hero.js
class Hero extends Component {
constructor(props) {
super(props);
this._ToggleNext = this._ToggleNext.bind(this);
}
_ToggleNext(props) {
console.log(this.props.listing.images.length)
console.log(this.props.imageIndex)
}
_TogglePrev(props) {
console.log(this.props.listing.images.length)
console.log(this.props.imageIndex)
}
render() {
const { listing: { images = [], name, location = {} } = {} } = this.props;
return <div className="hero">
<img src={images[0]} alt="listing" />
<a onClick={this._TogglePrev}) className="hero__arrow hero__arrow--left">◀</a>
<a onClick={this._ToggleNext} className="hero__arrow hero__arrow--right">▶</a>
<div className="hero__info">
<p>{location.city}, {location.state}, {location.country}</p>
<h1>{name}</h1>
</div>
</div>;
}
}
const getHero = gql`
query getHero {
listing {
name
images
location {
address,
city,
state,
country
}
}
}
`;
export default function HeroHOC(props) {
return <Query
query={getHero}
>
{({ data }) => (
<Hero
{...props}
listing={data && data.listing || {}} // eslint-disable-line no-mixed-operators
/>
)}
</Query>;
}
One solution is to define the data and functionality in the parent component, in this case App, and pass those down as props to the child which will focus on the rendering.
(code not tested but should give you the basic idea)
class App extends Component {
state = {
imageIndex: 0,
listing: {
images: ['foo.jpg', 'bar.jpg'],
name: 'foo',
location: {...}
}
}
_ToggleNext = () => {
const { imageIndex, listing } = this.state;
if (imageIndex === listing.images.length - 1) {
this.setState({imageIndex: 0});
}
}
_TogglePrev = () => {
const { imageIndex, listing } = this.state;
if (imageIndex === 0) {
this.setState({imageIndex: listing.images.length - 1});
}
}
render() {
return (
<Fragment>
<Hero
listing={this.state.listing}
imageIndex={this.state.imageIndex}
toggleNext={this._ToggleNext}
togglePrev={this._TogglePrev}
/>
</Fragment>
);
}
}
Hero component:
const Hero = props => {
const { listing, imageIndex, togglePrev, toggleNext } = props;
return (
<div className="hero">
<img src={listing.images[imageIndex]}/>
<a onClick={togglePrev})>◀</a>
<a onClick={toggleNext}>▶</a>
<div className="hero__info">
...
</div>
</div>
);
};

reactjs components communicating

I have created two separate components and a parent component. I am trying to see how I can connect them so that I can have the dropdown for the children vanish when their checkbox is unchecked. I think I may have created this so the 2 components can't communicate, but I wanted to see if there was a way to get them to. Been trying different ways, but cannot seem to figure it out.
This is the parent component. It builds sections from some data and renders a checkbox treeview with the first (parent) checkbox having a dropdown. When the third option is selected in this dropdown, it renders in a dropdown for each child checkbox. I am trying to see if I can have the child dropdowns vanish when the checkbox is unchecked, but I can't seem to get the 2 components to communicate.
export default class CheckboxGroup extends PureComponent {
static propTypes = {
data: PropTypes.any.isRequired,
onChange: PropTypes.func.isRequired,
counter: PropTypes.number,
};
mapParents = (counter, child) => (
<li key={child.get('name')} className='field'>
<SegmentHeader style={segmentStyle} title={child.get('label')} icon={child.get('icon')}>
<div className='fields' style={zeroMargin}>
<div className='four wide field'>
<TreeCheckbox
label={`Grant ${child.get('label')} Permissions`}
counter={counter}
onChange={this.props.onChange}
/>
{child.get('items') && this.buildTree(child.get('items'), counter + child.get('name'))}
</div>
<div className='twelve wide field'>
<GrantDropdown label={child.get('label')} childItems={child.get('items')}/>
</div>
</div>
</SegmentHeader>
</li>
)
mapDataArr = (counter) => (child) => (
(counter === 0 || counter === 1000) ?
this.mapParents(counter, child)
:
<li key={child.get('name')}>
<TreeCheckbox label={child.get('label')} onChange={this.props.onChange}/>
{child.get('items') && this.buildTree(child.get('items'), counter + child.get('name'))}
</li>
)
buildTree = (dataArr, counter) => (
<ul key={counter} style={listStyle}>
{dataArr.map(this.mapDataArr(counter))}
</ul>
)
render() {
return (
<div className='tree-view'>
{this.buildTree(this.props.data, this.props.counter)}
</div>
);
}
}
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const pointer = { cursor: 'pointer' };
class TreeCheckbox extends PureComponent {
static propTypes = {
onChange: PropTypes.func,
label: PropTypes.string,
currentPerson: PropTypes.any,
};
componentDidMount() {
if (this.props.currentPerson.get('permissions').includes(this.props.label)) {
this.checkInput.checked = true;
this.changeInput(this.checkInput);
}
}
getLiParents = (el, parentSelector) => {
if (!parentSelector) parentSelector = document; // eslint-disable-line
const parents = [];
let parent = el.parentNode;
let o;
while (parent !== parentSelector) {
o = parent;
if (parent.tagName === 'LI') parents.push(o);
parent = o.parentNode;
}
return parents;
}
traverseDOMUpwards = (startingEl, steps) => {
let elem = startingEl;
for (let i = 0; i < steps; i++) {
elem = elem.parentNode;
}
return elem;
}
markIt = (nodeElem, checkIt, indeter) => {
const node = nodeElem;
const up = this.traverseDOMUpwards(node, 1);
node.checked = checkIt;
node.indeterminate = indeter;
this.props.onChange(up.children[1].innerText, checkIt);
}
changeInput = (event) => {
const e = event === this.checkInput ? event : event.target;
const selector = 'input[type="checkbox"]';
const querySelector = (el) => el.querySelectorAll(selector);
const container = this.traverseDOMUpwards(e, 2);
const markAllChildren = querySelector(container.parentNode);
const checked = e.tagName === 'LABEL' ? !markAllChildren[0].checked : e.checked;
const siblingsCheck = (element) => {
let onesNotRight = false;
const sibling = [].slice.call(element.parentNode.children);
sibling.filter(child => child !== element).forEach(elem => {
if (querySelector(elem)[0].checked !== querySelector(element)[0].checked) {
onesNotRight = true;
}
});
return !onesNotRight;
};
const checkRelatives = (ele) => {
let el = ele;
if (el.tagName === 'DIV') el = el.parentNode;
if (el.tagName !== 'LI') return;
const parentContainer = this.traverseDOMUpwards(el, 2);
if (siblingsCheck(el) && checked) {
this.markIt(querySelector(parentContainer)[0], true, false);
checkRelatives(parentContainer);
} else if (siblingsCheck(el) && !checked) {
const parent = this.traverseDOMUpwards(el, 2);
const indeter = parent.querySelectorAll(`${selector}:checked`).length > 0;
this.markIt(querySelector(parent)[0], false, indeter);
checkRelatives(parent);
} else {
for (const child of this.getLiParents(el)) {
this.markIt(querySelector(child)[0], false, true);
}
}
};
for (const children of markAllChildren) {
this.markIt(children, checked, false);
}
checkRelatives(container);
};
getRef = (input) => { this.checkInput = input; }
render() {
const { label } = this.props;
return (
<div className='permission-item'>
<div className='ui checkbox'>
<input type='checkbox' onChange={this.changeInput} ref={this.getRef}/>
<label onClick={this.changeInput} style={pointer}>
{label}
</label>
</div>
</div>
);
}
}
const mapStatetoProps = (state) => ({
currentPerson: state.get('currentPerson'),
});
export default connect(mapStatetoProps)(TreeCheckbox);
class GrantDropdown extends AbstractSettingsComponent {
static propTypes = {
label: PropTypes.string,
currentPerson: PropTypes.any,
counter: PropTypes.number,
permissionOptions: PropTypes.any,
};
state = {
items: new List(),
}
componentDidMount() {
if (this.props.childItems) {
this.getAllChildLabels(this.props.childItems);
}
}
getAllChildLabels = (childItems) => {
let list = new List();
for (const item of childItems) {
list = list.push(item.get('label'));
if (item.get('items')) {
for (const childItem of item.get('items')) {
list = list.push(childItem.get('label'));
}
}
}
this.setState({ items: list });
}
handlePermissionChange = (label) => (e, { value }) => {
this.updatePerson(['locationsPermissionsMap', label], value);
}
mapItems = (val, i) => { // eslint-disable-line
const locationVal = this.props.currentPerson.getIn(['locationsPermissionsMap', val]);
return (
<div className={locationVal === 2 ? 'two fields' : 'field'} style={zeroMarginBottom} key={i}>
<OptionSelector
options={this.firstThreePermissionOpt()}
defaultValue={locationVal || 0}
onChange={this.handlePermissionChange(val)}
/>
{locationVal === 2 &&
<div className='field' style={zeroMarginBottom}>
<LocationMultiSelect name={val} {...this.props}/>
</div>
}
</div>
);
}
render() {
const { label, currentPerson } = this.props;
if (!currentPerson.get('permissions').includes(label)) {
return null;
}
const locationLabel = currentPerson.getIn(['locationsPermissionsMap', label]);
return (
<div className={ locationLabel === 2 ? 'two fields' : 'field'} style={zeroMarginBottom}>
<div className='field'>
<OptionSelector
options={this.getPermissionOptions()}
defaultValue={currentPerson.getIn(['locationsPermissionsMap', label]) || 0}
onChange={this.handlePermissionChange(label)}
/>
{locationLabel === 3 && this.state.items.map(this.mapItems)}
</div>
{locationLabel === 2 &&
<div className='field'>
<LocationMultiSelect name={label} {...this.props}/>
</div>
}
</div>
);
}
}
const mapStatetoProps = (state) => ({
currentPerson: state.get('currentPerson'),
locations: state.get('locations'),
});
export default connect(mapStatetoProps)(GrantDropdown);
What you can do is set couple of props to send to child component to re-render them.
Example
export default class CheckBoxComponent extends React.Component {
changeInput() {
this.props.onCheckedChanged();
}
render() {
return(
<div className='permission-item'>
<div className='ui checkbox'>
<input type='checkbox' onChange={this.changeInput} ref={this.getRef}/>
<label onClick={this.changeInput.bind(this)} style={pointer}>
{label}
</label>
</div>
</div>
)
}
}
export default class DropDownComponent extends React.Component {
renderSelect() {
// here render your select and options
}
render() {
return(
<div>
{this.props.checkboxChecked === false ? this.renderSelect : null}
</div>
)
}
}
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
checkboxChecked: false
};
}
onCheckedChanged() {
this.setState({ checkboxChecked: !this.state.checkboxChecked });
}
render() {
return(
<div>
<CheckBoxComponent onCheckedChanged={this.onCheckedChanged.bind(this)} />
<DropDownComponent checkboxChecked={this.state.checkboxChecked} />
</div>
)
}
}
You can set the <input/> name attribute into a corresponding property in your state so the handler can get the name of the list / input via the event parameter and set the state respectively.
Then you can conditionally render the Dropdown according to the state.
Here is a small example of such behavior:
const lists = [
[
{ value: "0", text: "im 0" },
{ value: "1", text: "im 1" },
{ value: "2", text: "im 2" }
],
[
{ value: "a", text: "im a" },
{ value: "b", text: "im b" },
{ value: "c", text: "im c" }
]
];
const DropDown = ({ options }) => {
return (
<select>
{options.map(opt => <option value={opt.value}>{opt.text}</option>)}
</select>
);
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
showList0: false,
showList1: true
};
this.toggleCheck = this.toggleCheck.bind(this);
}
toggleCheck(e) {
const listName = e.target.name;
this.setState({ [listName]: !this.state[listName] });
}
render() {
return (
<div>
{lists.map((o, i) => {
const listName = `showList${i}`;
const shouldShow = this.state[listName];
return (
<div>
<input
type="checkbox"
name={listName}
checked={shouldShow}
onChange={this.toggleCheck}
/>
{shouldShow && <DropDown options={o} />}
</div>
);
})}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Categories

Resources