I have a react class component which produces a product showcase using react-masonry. I now want to add filtering functionality with filter options displayed as buttons and on each click, the page elements should be filtered and the masonry display rerendered.
The filter functions work fine but I was not able to get the masonry to rerender.
import React from 'react'
import PropTypes from 'prop-types'
import Masonry from 'react-masonry-component'
import Product from 'components/Product'
const masonryOptions = {
transitionDuration: 0
};
class Gallery extends React.Component {
constructor (props){
super(props);
this.state ={
filter:props.filter,
elements:props.elements,
type:props.type,
}
this.handleFilterClick=this.handleFilterClick.bind(this)
}
handleFilterClick(filter){
console.log(filter)
this.setState({filter:filter})
console.log(this.state.filter)
}
filterProducts (elements,filter){
const filteredArray=elements.filter(function(el){
for (let i in el.tags) {
if (el.tags[i].slug===filter){
return true;
}
}
return false
})
return filteredArray;
}
renderGallery(){
const type=this.state.type
const elements=this.state.elements
var filter=this.state.filter
const filteredElements = elements
if (filter !=="*"){
const filteredElements = this.filterProducts(elements,filter)
}
const childElements = filteredElements.map(function(element,key){
if (element.mainPhoto!=null && element.isDogFood==type){
return (
<Product key={key} element={element}/>
);} else{
return null;
}
});
return (
<Masonry
className={'my-gallery-class'} // default ''
options={masonryOptions} // default {}
disableImagesLoaded={false} // default false
updateOnEachImageLoad={false} // default false and works only if disableImagesLoaded is false
>
<div className="col-md-12">
<ul className="filter text-center text-inline">
<li>
<button data-filter="*" className="selected">Tüm Ürünler</button>
</li>
<li>
<button filter="nograin">Tahılsız</button>
</li>
<li>
<button filter="seafood">Deniz Mahsülleri</button>
</li>
<li>
<button filter="poultry">Beyaz Et</button>
</li>
<li>
<button filter="redmeat">Kırmızı Et</button>
</li>
<li>
<button filter="pate" onClick={() =>{this.handleFilterClick("pate")}}>Püre</button>
</li>
</ul>
</div>
{childElements}
</Masonry>
);
}
render() {
return (
this.renderGallery()
)
}
}
Gallery.propTypes={
type: PropTypes.bool
}
export default Gallery
Use getDerivedStateFromProps instead of using props in constructor
import React from 'react'
import PropTypes from 'prop-types'
import Masonry from 'react-masonry-component'
import Product from 'components/Product'
const masonryOptions = {
transitionDuration: 0
};
class Gallery extends React.Component {
constructor (props){
super(props);
this.state = {
// Don't assign direct value in constructor as constructor function
// calls only at initialization
filter:props.filter,
elements:props.elements,
type:props.type,
}
this.handleFilterClick=this.handleFilterClick.bind(this)
}
getDerivedStateFromProps (props, prevState) {
if(//Put a valid condition) {
return {
filter: props.filter,
elements: props.elements,
type: props.type
}
}
return {}
}
handleFilterClick(filter){
console.log(filter)
this.setState({filter:filter})
console.log(this.state.filter)
}
filterProducts (elements,filter){
const filteredArray=elements.filter(function(el){
for (let i in el.tags) {
if (el.tags[i].slug===filter){
return true;
}
}
return false
})
return filteredArray;
}
renderGallery(){
const type=this.state.type
const elements=this.state.elements
var filter=this.state.filter
const filteredElements = elements
if (filter !=="*"){
const filteredElements = this.filterProducts(elements,filter)
}
const childElements = filteredElements.map(function(element,key){
if (element.mainPhoto!=null && element.isDogFood==type){
return (
<Product key={key} element={element}/>
);} else {
return null;
}
});
return (
<Masonry
className={'my-gallery-class'} // default ''
options={masonryOptions} // default {}
disableImagesLoaded={false} // default false
updateOnEachImageLoad={false} // default false and works only if disableImagesLoaded is false
>
<div className="col-md-12">
<ul className="filter text-center text-inline">
<li>
<button data-filter="*" className="selected">Tüm Ürünler</button>
</li>
<li>
<button filter="nograin">Tahılsız</button>
</li>
<li>
<button filter="seafood">Deniz Mahsülleri</button>
</li>
<li>
<button filter="poultry">Beyaz Et</button>
</li>
<li>
<button filter="redmeat">Kırmızı Et</button>
</li>
<li>
<button filter="pate" onClick={() =>{this.handleFilterClick("pate")}}>Püre</button>
</li>
</ul>
</div>
{childElements}
</Masonry>
);
}
render() {
return (
this.renderGallery()
)
}
}
Gallery.propTypes = {
type: PropTypes.bool
}
export default Gallery
Related
I'm doing a menu with a submenu with React. Clicking on the main menu (Example: CRM) creates the submenu (Example Hijo: Argumentarios) but when clicking on the following menu item (Example Padre: Permisos) "Argumentarios" is not deleted and the submenu of "Permisos" is also created.
I need that when pressing in the menu it shows its submenu and eliminates those of the other siblings.
This is a part of my json file
[
{
"Id":"114",
"Padre":"CRM",
"Hijo":"Argumentarios"
},
{
"Id":"115",
"Padre":"CRM",
"Hijo":"Argumentarios"
},
{
"Id":"116",
"Padre":"CRM",
"Hijo":"Argumentarios"
},
{
"Id":"44",
"Padre":"Permisos",
"Hijo":"root"
},
{
"Id":"45",
"Padre":"Permisos",
"Hijo":"root"
},
{
"Id":"47",
"Padre":"Permisos",
"Hijo":"root"
},
{
"Id":"50",
"Padre":"Telefonia",
"Hijo":"Audio"
},
{
"Id":"51",
"Padre":"Telefonia",
"Hijo":"Audio"
}
]
For confidential reasons I can not show the actual file.
In my next code I change the fetch for a fake json url
import React, { Component } from 'react';
import Icon from './Icon';
class Nav extends Component{
constructor(props){
super(props)
this.state = {
menuSubmenu:[],
}
}
componentWillMount(){
fetch('fake/json_menu.php')
.then(response => response.json())
.then(menuSubmenu =>{
menuSubmenu.forEach(datos => {
let data = {
menu:datos.Padre,
submenu:datos.Hijo,
id:datos.Id,
descripcion:datos.Description,
url:datos.URL
}
this.setState({
menuSubmenu:this.state.menuSubmenu.concat([data])
})
})
})
}
render() {
if (this.state.menuSubmenu.length > 0) {
return(
<nav className="nav">
<div className="menu">
<ul className="list">
{this.state.menuSubmenu.map(datos => <Menu key={datos.id} menu={datos.menu} submenu={datos.submenu} descripcion={datos.descripcion} url={datos.url}/>)}
</ul>
<div className="content-bnt">
<button id="desplegar" className='btn btn--rounded'>
<Icon icon="flecha" className='ico-flecha'/>
</button>
</div>
</div>
</nav>
);
}
return (<p>Cargando usuarios...</p>);
}
}
class Menu extends Component{
constructor(props){
super(props)
this.state = {
abrirSubmenu: false,
}
this.submenuClick = this.submenuClick.bind(this);
}
submenuClick() {
this.setState(state => ({
abrirSubmenu: !state.abrirSubmenu
}));
//alert('Click!')
}
render(){
return (
<>
<li className="list__item">
<button title={this.props.menu} id={"mn-" + this.props.menu} className="desplegable" onClick={this.submenuClick.bind(this)}><Icon icon="auriculares" className='ico-auriculares'/>{this.props.menu}</button>
{
this.state.abrirSubmenu
? (
<div id="sb-crm" className="submenu">
<h3 className="nav--title"><Icon icon="descargar" className='ico-descargar'/>{this.props.submenu}</h3>
<ul className="list">
<li className="list__item">
<a href={this.props.url} title={this.props.descripcion}>{this.props.descripcion}</a>
</li>
</ul>
</div>
)
: (
null
)
}
</li>
</>
)
}
}
export default Nav;
I hope to show only one of the menu items at the same time.
¡Thanks a lot!
While I'm trying to get in to React, I started a project and got stuck. Maybe some one can help me to find the issue. Bellow I explain what the app should do.
The user types a query in an input-box inside SearchBar.jsx
The SearchBar component passes the query to App.jsx and fires up fetchPhotos function, which starts an API request.
To sort out pagination, the App.jsx imports Pagination.jsx, which calculates the number of pictures in the response and displays pagination buttons.
The above works.
But now if you click on a pagination button, the value for page from Pagination component should be passed to App.jsx and so to fetchPhotos function (runs the API request).
I guess the problem is that the value of page never finds its way to App.jsx and so the API request is missing the value of page.
I spent hours but couldn't find a way to fix it, due to lack of knowledge. Could you please point me to the right direction and show me what is wrong with the code?
App.jsx
import React, { Component } from "react";
import axios from "axios";
import Pagination from "../Pagination";
import SearchBar from "../SearchBar";
import ListItem from "../ListItem";
import "./app.scss";
class App extends Component {
state = {
photos: [],
totalPhotos: 0,
perPage: 30,
currentPage: 1,
query: null
};
componentDidMount() {
this.fetchPhotos("gorilla", this.state.currentPage);
}
fetchPhotos = (inputValue, page) => {
const baseUrl = "https://api.unsplash.com/search/photos";
const options = {
headers: {
Authorization: `Client-ID ${process.env.REACT_APP_UNSPLASH_API_KEY}`
},
params: {
query: inputValue,
page: this.state.page,
per_page: this.state.perPage
}
};
axios
.get(baseUrl, options)
.then(response => {
this.setState({
photos: response.data.results,
totalPhotos: parseInt(response.headers["x-total"]),
currentPage: page,
query: inputValue
});
})
.catch(() => {
console.log("Error");
});
};
render() {
return (
<div className="app">
<SearchBar onSubmit={this.fetchPhotos} />
<Pagination
current={this.state.currentPage}
total={this.state.totalPhotos}
perPage={this.state.perPage}
query={this.state.query}
onPageChanged={query => this.fetchPhotos(this.state.query)}
/>
<List data={this.state.photos} />
</div>
);
}
}
const List = ({ data }) => {
var items = data.map(photo => <ListItem key={photo.id} photo={photo} />);
return <div className="grid">{items}</div>;
};
export default App;
SearchBar.jsx
import React, { Component } from "react";
class SearchBar extends Component {
state = {
inputValue: ""
};
handleFormSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state.inputValue);
};
render() {
return (
<div className="header">
<h1>Search for images on Unsplash</h1>
<form onSubmit={this.handleFormSubmit} className="ui form">
<input
type="text"
placeholder="Type here to search for images"
value={this.state.inputValue}
onChange={e => this.setState({ inputValue: e.target.value })}
/>
</form>
</div>
);
}
}
export default SearchBar;
Pagination.jsx
import React, { Component } from "react";
class Pagination extends Component {
pages() {
var pages = [];
for (var i = this.rangeStart(); i <= this.rangeEnd(); i++) {
pages.push(i);
}
return pages;
}
rangeStart() {
var start = this.props.current - this.props.pageRange;
return start > 0 ? start : 1;
}
rangeEnd() {
var end = this.props.current + this.props.pageRange;
var totalPages = this.totalPages();
return end < totalPages ? end : totalPages;
}
totalPages() {
return Math.ceil(this.props.total / this.props.perPage);
}
nextPage() {
return this.props.current + 1;
}
prevPage() {
return this.props.current - 1;
}
hasFirst() {
return this.rangeStart() !== 1;
}
hasLast() {
return this.rangeEnd() < this.totalPages();
}
hasPrev() {
return this.props.current > 1;
}
hasNext() {
return this.props.current < this.totalPages();
}
changePage(page) {
this.props.onPageChanged(page);
console.log("Page inside Pagination", page);
}
render() {
return (
<div className="pagination">
<div className="pagination__left">
<span
role="button"
className={!this.hasPrev() ? "hidden" : ""}
onClick={e => this.changePage(this.prevPage())}
>
Prev
</span>
</div>
<div className="pagination__mid">
<ul>
<li className={!this.hasFirst() ? "hidden" : ""}>
<span role="button" onClick={e => this.changePage(1)}>
1
</span>
</li>
<li className={!this.hasFirst() ? "hidden" : ""}>...</li>
{this.pages().map((page, index) => {
return (
<li key={index}>
<span
role="button"
onClick={e => this.changePage(page)}
className={this.props.current === page ? "current" : ""}
>
{page}
</span>
</li>
);
})}
<li className={!this.hasLast() ? "hidden" : ""}>...</li>
<li className={!this.hasLast() ? "hidden" : ""}>
<span
role="button"
onClick={e => this.changePage(this.totalPages())}
>
{this.totalPages()}
</span>
</li>
</ul>
</div>
<div className="pagination__right">
<span
className={!this.hasNext() ? "hidden" : ""}
onClick={e => this.changePage(this.nextPage())}
>
Next
</span>
</div>
</div>
);
}
}
Pagination.defaultProps = {
pageRange: 2
};
export default Pagination;
I think your error is at `onChange', because you are giving current state query to fetch instead of the new query:
onPageChanged={query => this.fetchPhotos(this.state.query)}
You should replace it for new query like:
onPageChanged={query => this.fetchPhotos(query)}
EDIT 1:
You can see working it on https://codesandbox.io/s/9ymjj8ko9p?fontsize=14.
The changes is just as I said, on App.jsx:
params fixed passing page from function params and not from
fix onPageChange prop to Pagination like:
onPageChanged={page => this.fetchPhotos(this.state.query, page)}
I need to add index value in my data list using React.js. My code is below.
Itemlist.js:
import React, { Component } from "react";
class TodoItems extends Component {
constructor(props, context) {
super(props, context);
this.createTasks = this.createTasks.bind(this);
}
edit(key){
this.props.edit(key);
}
delete(key){
this.props.delete(key);
}
createTasks(item) {
return <li key={item._id}>{item.name}<a href="#" className="button bg_green" onClick={()=>this.edit(item._id)}>Edit</a><a href="#" className="button bg_red" onClick={()=>this.delete(item._id)}>Delete</a></li>
}
render() {
var todoEntries = this.props.entries;
var listItems = todoEntries.map(this.createTasks);
return (
<ul className="theList">
{listItems}
</ul>
);
}
};
export default TodoItems;
Todolist.js:
import React, { Component } from "react";
import TodoItems from "./TodoItems";
import "./TodoList.css";
import ItemService from './ItemService';
import axios from 'axios';
class TodoList extends Component {
constructor(props, context){
super(props, context);
this.state={
items:[]
}
this.addItem=this.addItem.bind(this);
this.deleteItem = this.deleteItem.bind(this);
this.editItem = this.editItem.bind(this);
this.ItemService = new ItemService();
}
componentDidMount(){
axios.get('http://localhost:8888/item')
.then(response => {
this.setState({ items: response.data });
})
.catch(function (error) {
console.log(error);
})
}
addItem(e){
e.preventDefault();
if(this.state.editKey){
this.saveEditedText();
return;
}
var itemArray = this.state.items;
if (this.inputElement.value !== '') {
itemArray.unshift({
text:this.inputElement.value,
key:Date.now()
})
this.setState({
items:itemArray
})
//console.log('items',this.state);
this.ItemService.sendData(this.inputElement.value);
this.divRef.insertAdjacentHTML("beforeend", '<p className="textcolor">'+this.inputElement.value+' has added successfully</p>');
this.inputElement.value='';
setTimeout( () => {
this.divRef.querySelector(':last-child').remove();
window.location.reload();
}, 3000);
}
}
saveEditedText(){
let value = this.inputElement.value;
this.setState(prevState => ({
items: prevState.items.map(el => {
if(el.key == prevState.editKey)
return Object.assign({}, el, {text: value});
return el;
}),
editKey: ''
}));
this.divRef.insertAdjacentHTML("beforeend", '<p className="textcolor">'+this.inputElement.value+' has updated successfully</p>');
this.inputElement.value='';
setTimeout( () => {
this.divRef.querySelector(':last-child').remove();
}, 3000);
}
render() {
return (
<div className="todoListMain">
<div className="header" id="parentDiv">
<div className="pageHeading" dangerouslySetInnerHTML={{ __html: "Todo Demo Application" }}></div>
<div className="wrapper">
<div ref={divEl => {
this.divRef = divEl;
}}></div>
<form onSubmit={this.addItem}>
<input ref={(a)=>this.inputElement=a} placeholder="enter task">
</input>
<button type="submit">{this.state.editKey? "Update": "Add"}</button>
</form>
<TodoItems entries={this.state.items} delete={this.deleteItem} edit={this.editItem}/>
</div>
</div>
</div>
);
}
}
export default TodoList;
Here after adding the data into db, the added data are shown in the list. Here I need to display the index value for each row means 1 - item1 like this.
You can do this by :
class TodoItems extends Component {
constructor(props, context) {
super(props, context);
this.createTasks = this.createTasks.bind(this);
}
edit(key){
this.props.edit(key);
}
delete(key){
this.props.delete(key);
}
render() {
var todoEntries = this.props.entries;
return (
<ul className="theList">
{todoEntries.map(this.createTasks, this)}
</ul>
);
}
createTasks(item, index) {
return (
<li key={item._id}>
{index} - {item.name}
<a href="#" className="button bg_green" onClick={()=>this.edit(item._id)}>Edit</a><a href="#" className="button bg_red" onClick={()=>this.delete(item._id)}>Delete</a>
</li>
)
}
};
If you mean to display index in ItemList component map has an overload which has paramater index that represents current index of element in array being processed.
See more in docs
So make createTasks(item, index){ } and then you will have access to index of the element.
I'm making a collapsible list with React. So far it works but now I want to implement a button that expands/collapses everything. Therefore the button need to adjust the state of all elements. I'm not sure what's the best way to tackle this problem though. This is what I have:
import React, {Component} from 'react';
class CollapsibleList extends Component {
constructor(props) {
super(props);
this.state = {
collapsed: true
};
this.subLists = [];
this.papers = [];
if (this.props.subtitles) {
for (let subList of this.props.subtitles) {
this.subLists.push(
<CollapsibleList level={this.props.level + 1} subtitles={subList.subtitles} title={subList.title}/>
);
}
}
this.toggleCollapse = this.toggleCollapse.bind(this);
this.expandAll = this.expandAll.bind(this);
this.collapseAll = this.collapseAll.bind(this);
}
expandAll() {
this.setState({collapsed: false});
this.subLists.forEach(subList => subList.expandAll());
}
collapseAll() {
this.setState({collapsed: true});
this.subLists.forEach(subList => subList.collapseAll());
}
toggleCollapse() {
this.setState(prevState => {
return {collapsed: !prevState.collapsed};
});
}
render() {
return (this.state.collapsed ?
<li className={'collapsibleListItem'}>
<div onClick={this.toggleCollapse}>
{this.props.title}
</div>
<img title={'Expand all'} className={'icon'} alt={'Expand all'} src={require('../expand_all.png')} onClick={this.expandAll}/>
<img title={'Collapse all'} className={'icon'} alt={'Collapse all'} src={require('../collapse_all.png')} onClick={this.collapseAll}/>
</li> :
<li className={'collapsibleListItem'}>
<div onClick={this.toggleCollapse}>
{this.props.title}
</div>
<img title={'Expand all'} className={'icon'} alt={'Expand all'} src={require('../expand_all.png')} onClick={this.expandAll}/>
<img title={'Collapse all'} className={'icon'} alt={'Collapse all'} src={require('../collapse_all.png')} onClick={this.collapseAll}/>
<ul className={'collapsibleList'}>
{this.subLists}
</ul>
</li>
);
}
}
export default CollapsibleList;
Unfortunately, that doesn't seem to work though.
I can't understand what you are trying to do in your code but you should have 2 different components; one for the list and one for the list item. It should be something like this:
// Parent component
import React from 'react';
import ListItem from './ListItem';
class List extends React.Component {
constructor() {
super();
this.state = {
collapsed: false
}
}
render() {
const data = ['abc', 'def', 'ghi']; // whatever you want to have
return(
<div>
<button onClick={() => this.setState({collapsed: !this.state.collapsed})}>
Collapse
</button>
<ul>
{
this.state.collapsed &&
data.map((val, key) => {
return(
<li>
<ListItem value={val} key={key} />
</li>
)
})
}
</ul>
</div>
)
}
}
And this is the child component
// child component
import React from 'react';
class ListItem extends React.Component {
constructor() {
super();
}
render() {
return(
<div>
{/*// render anything you want*/}
<p>{this.props.value}</p>
</div>
)
}
}
export default ListItem;
This code is just to give you an insight.
I have a react element like this:
import React, { PropTypes, Component } from 'react'
class AlbumList extends Component {
constructor(props) {
super(props);
this.state = {'active': false, 'class': 'album'};
}
handleClick() {
if(this.state.active){
this.setState({'active': false,'class': 'album'})
}else{
this.setState({'active': true,'class': 'active'})
}
}
render() {
var album_list
const {user} = this.props
if(user.data){
list = user.data.filter(album => album.photos).map((album => {
return <div className={"col-sm-3"} key={album.id}>
<div className={this.state.class} key={album.id} onClick={this.handleClick.bind(this)}>
<div className={"panel-heading"}>{ album.name }</div>
<div className={"panel-body"}>
<img className={"img-responsive"} src={album.photo.source} />
</div>
</div>
</div>
}))
}
return (
<div className={"container"}>
<div className="row">
{list}
</div>
</div>
)
}
}
export default AlbumList
Here map gives the list of filter data as I wanted. Here what I am doing changes the class of all the list element if I click on one.
I am getting the class name from this.state.class
How can I change the class of only element that i have clicked..
Thanks in advance ...
I have considered it once.So you have so many divs and you want to know which is clicked.My way to solve this problem is to give a param to the function handleClick and you can get the dom of the div while you click the div.Like this:
array.map(function(album,index){
return <div onClick={this.handleClick}/>
})
handleClick(e){
console.log(e.target);
e.target.className = 'active';
...
}
Then you have a param for this function.While you can use the e.target to get the dom of your div which is clicked.
There are some mistake into your code about the state.class.
class AlbumList extends Component {
constructor(props) {
super(props);
this.state = {'active': false, 'class': 'album'};
}
handleClick(e) {
if(e.target.class === 'active'){
e.target.className = 'album'
}else{
e.target.className = 'active'
}
}
render() {
var album_list
const {user} = this.props
if(user.data){
list = user.data.filter(album => album.photos).map((album => {
return (
<div className={"col-sm-3"} key={album.id}>
<div className='active' key={album.id} onClick={this.handleClick.bind(this)}>
<div className={"panel-heading"}>{ album.name }</div>
<div className={"panel-body"}>
<img className={"img-responsive"} src={album.photo.source} />
</div>
</div>
</div>
)
}))
}
return (
<div className={"container"}>
<div className="row">
{list}
</div>
</div>
)
}
}
You can try this and tell me anything wrong.