I want to use image uploader from AntDesign library. Here is a snapshot
import { Upload, Icon, Modal } from 'antd'
class PicturesWall extends React.Component {
state = {
previewVisible: false,
previewImage: '',
fileList: [],
}
handleCancel = () => this.setState({ previewVisible: false })
handlePreview = file => {
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
})
}
handleChange = ({ fileList }) => this.setState({ fileList })
render() {
const { previewVisible, previewImage, fileList } = this.state
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
)
return (
<div className="clearfix">
<Upload
action="//jsonplaceholder.typicode.com/posts/"
listType="picture-card"
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
>
{fileList.length >= 3 ? null : uploadButton}
</Upload>
<Modal
visible={previewVisible}
footer={null}
onCancel={this.handleCancel}
>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
)
}
}
ReactDOM.render(<PicturesWall />, mountNode)
It's so hard to understand what's going on here for me .
Can I get images from this component using something like
const img=event.target.files[0];?
All I want it's put that uploaded images into array and using FormData send axios.post request to the backend.
I'm newbie in React .If it's something obvious pardon me.Thank you in advance
antd's Upload component is doing the upload for you under the hood. But if you don't want to do that, and upload the file later, you can also achieve that with the help of beforeUpload prop.
From the docs:
beforeUpload: Hook function which will be executed before uploading. Uploading will be stopped with false or a rejected Promise returned. Warning:this function is not supported in IE9
I have written an example and added comments where necessary:
class PicturesWall extends React.Component {
state = {
previewVisible: false,
previewImage: "",
fileList: []
};
handleCancel = () => this.setState({ previewVisible: false });
handlePreview = file => {
this.setState({
previewImage: file.thumbUrl,
previewVisible: true
});
};
handleUpload = ({ fileList }) => {
//---------------^^^^^----------------
// this is equivalent to your "const img = event.target.files[0]"
// here, antd is giving you an array of files, just like event.target.files
// but the structure is a bit different that the original file
// the original file is located at the `originFileObj` key of each of this files
// so `event.target.files[0]` is actually fileList[0].originFileObj
console.log('fileList', fileList);
// you store them in state, so that you can make a http req with them later
this.setState({ fileList });
};
handleSubmit = event => {
event.preventDefault();
let formData = new FormData();
// add one or more of your files in FormData
// again, the original file is located at the `originFileObj` key
formData.append("file", this.state.fileList[0].originFileObj);
axios
.post("http://api.foo.com/bar", formData)
.then(res => {
console.log("res", res);
})
.catch(err => {
console.log("err", err);
});
};
render() {
const { previewVisible, previewImage, fileList } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div>
<Upload
listType="picture-card"
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleUpload}
beforeUpload={() => false} // return false so that antd doesn't upload the picture right away
>
{uploadButton}
</Upload>
<Button onClick={this.handleSubmit} // this button click will trigger the manual upload
>
Submit
</Button>
<Modal
visible={previewVisible}
footer={null}
onCancel={this.handleCancel}
>
<img alt="example" style={{ width: "100%" }} src={previewImage} />
</Modal>
</div>
);
}
}
ReactDOM.render(<PicturesWall />, document.getElementById("container"));
Related
I'm using draft.js to make a text editor and I have two components: CreatePost.js which gets the post fields from the back end and populates the state with the user input and TextEditor.js which contains a text editor which I am using in CreatePost.js. The text editor should populate the body field in the state of CreatePost.js onChange.
My question is how can I get the text editor to populate the state in the other component? Would I need to use props instead?
Before, I had a text area in CreatePost.js which populated the body. I want the text editor in the other component to populate it instead. I've tried using
<TextEditor onChange={this.changeHandler} value={body} /> in CreatePost.js but it didn't work.
console.log(body):
posts.js (controller)
exports.create = (req, res) => {
const { title, body, date } = req.body;
const post = new Post({
title,
body,
date,
"author.id": req.profile._id,
"author.name": req.profile.name,
});
post
.save()
.then((response) => {
res.send(response);
})
.catch((err) => {
return res.status(400).json({
error: errorHandler(err),
});
});
};
CreatePost.js
class CreatePost extends React.Component {
constructor(props) {
super(props);
this.state = {
title: "",
body: "",
createdPost: "",
error: "",
};
}
changeHandler = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
submitHandler = (e) => {
e.preventDefault();
const {
user: { _id },
} = isAuthenticated();
axios({
url: `${API}/post/new-post/${_id}`,
method: "POST",
data: this.state,
})
.then((response) => {
this.setState({ createdPost: this.state.title });
return response;
})
.catch((error) => {
if (!this.state.title || !this.state.body) {
this.setState({
error: "This post must contain a title and a body.",
});
}
console.log(error);
});
};
...
render() {
const { title, body } = this.state;
return (
<>
<Navbar />
<Tabs>
<TabList className="tabs">
<Tab className="tab">Draft</Tab>
<Tab className="tab">Preview</Tab>
</TabList>
<TabPanel>
<div className="newpost_container">
<form className="newpost_form" onSubmit={this.submitHandler}>
<div className="form-group">
<input
type="text"
placeholder="Title"
name="title"
className="newpost_field newpost_title"
onChange={this.changeHandler}
value={title}
/>
</div>
<div className="form-group newpost_body">
<TextEditor />
</div>
<button className="btn publish-post-btn" type="submit">
Publish
</button>
{this.showSuccess()}
{this.showError()}
</form>
</div>
</TabPanel>
<TabPanel>
<div>
<h1>{title}</h1>
<div>{body}</div>
</div>
</TabPanel>
</Tabs>
</>
);
}
}
export default CreatePost;
TextEditor.js
class TextEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
this.plugins = [addLinkPlugin];
}
toggleBlockType = (blockType) => {
this.onChange(RichUtils.toggleBlockType(this.state.editorState, blockType));
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
handleKeyCommand = (command) => {
const newState = RichUtils.handleKeyCommand(
this.state.editorState,
command
);
if (newState) {
this.onChange(newState);
return "handled";
}
return "not-handled";
};
// onClick for format options
onAddLink = () => {
const editorState = this.state.editorState;
const selection = editorState.getSelection();
const link = window.prompt("Paste the link -");
if (!link) {
this.onChange(RichUtils.toggleLink(editorState, selection, null));
return "handled";
}
const content = editorState.getCurrentContent();
const contentWithEntity = content.createEntity("LINK", "MUTABLE", {
url: link,
});
const newEditorState = EditorState.push(
editorState,
contentWithEntity,
"create-entity"
);
const entityKey = contentWithEntity.getLastCreatedEntityKey();
this.onChange(RichUtils.toggleLink(newEditorState, selection, entityKey));
};
toggleBlockType = (blockType) => {
this.onChange(RichUtils.toggleBlockType(this.state.editorState, blockType));
};
render() {
return (
<div className="editorContainer">
<div className="toolbar">
<BlockStyleToolbar
editorState={this.state.editorState}
onToggle={this.toggleBlockType}
/>
// format buttons
</div>
<div>
<Editor
placeholder="Post Content"
blockStyleFn={getBlockStyle}
editorState={this.state.editorState}
handleKeyCommand={this.handleKeyCommand}
onChange={this.onChange}
plugins={this.plugins}
placeholder="Post Content"
/>
</div>
</div>
);
}
}
export default TextEditor;
It looks like you've been quite close to solving this actually. You were on the right path when sending down the change handler using props to the TextEditor. One solution to your problem is to move up the editorState to your CreatePost component and then pass the value and the change handler downwards. If you are doing this you should remove the editorState and the change handler for it from the TextEditor file. Just by continuing on your example something like this should work, I haven't tried the code out but it should help you in the right direction.
In CreatePost.js
constructor(props) {
super(props);
this.state = {
title: "",
body: EditorState.createEmpty(),
createdPost: "",
error: "",
};
}
....
<TextEditor onChange={(value) => this.setState({ body: value })} editorState={body} />
In TextEditor.js
<Editor
placeholder="Post Content"
blockStyleFn={getBlockStyle}
editorState={this.props.editorState}
handleKeyCommand={this.handleKeyCommand}
onChange={this.props.onChange}
plugins={this.plugins}
placeholder="Post Content"
/>
When posting the data we need to access the content of the editor instead of the EditorState. We can do this through draft.js API (see more here: https://draftjs.org/docs/api-reference-editor-state/#getcurrentcontent). And that's not enough unfortunately. We also need to to convert the content to a format that's easier to handle. We can do this with draft.js convertToRaw which you also need to import from the library (https://draftjs.org/docs/api-reference-data-conversion/#converttoraw). Convert to raw returns a JS object so we also need to convert that to a string before being able to send it to the server by using JSON.stringify().
axios({
url: `${API}/post/new-post/${_id}`,
method: "POST",
data: {
...this.state,
body: JSON.stringify(convertToRaw(this.state.body.getCurrentContent()))
}
})
Ok so i have an MERN app (CRA) and photo upload. Everything works great but when i will do npm run build i cant send photo (over 1MB) to backend. I've used postman to test and again worked great :). In development mode its working. Something is blocking requests from frontend when i want to upload photos. I've checked logs with morgan and it shows that this request with large photo upload didnt happen. Im using axios to communicate with backend and express file upload but backend works well. I dont know what is blocking my photos. Also express file upload in debuging mode is telling that "Request is not eligible for upload!"
action in redux:
export const addGalleryPhoto = (file) => async (dispatch) => {
const config = {
headers: {
"Content-Type": "multipart/form-data",
},
};
try {
await axios.put(`/api/v1/config/uploadGalleryPhoto`, file, config);
dispatch(getPhotos());
} catch (err) {
const errors = err.response.data.error.split(",");
if (errors) {
errors.forEach((error) => dispatch(setAlert(error, "error")));
}
dispatch({ LOAD_PHOTOS_FAILED });
}
};
controller in node:
exports.uploadGalleryPhoto = asyncHandler(async (req, res, next) => {
if (!req.files) {
return next(new ErrorResponse(`Dodaj plik`, 400));
}
const file = req.files.file;
// Make sure the image is a photo
if (!file.mimetype.startsWith("image")) {
return next(new ErrorResponse(`Możesz wysłać tylko zdjęcia`, 400));
}
if (file.size > process.env.MAX_FILE_UPLOAD) {
return next(
new ErrorResponse(
`Zbyt duże zdjęcie. Maksymalny rozmiar pliku: ${Math.round(
process.env.MAX_FILE_UPLOAD / 1024
)} MB`,
400
)
);
}
file.name = `photo_${uuid()}${path.parse(file.name).ext}`;
file.mv(`${process.env.FILE_UPLOAD_PATH}/${file.name}`, async (err) => {
if (err) {
console.error(err);
return next(new ErrorResponse(`Problem with file upload`, 500));
}
const config = await Config.find();
await Config.findByIdAndUpdate(config[0].id, {
galleryPhotos: [...config[0].galleryPhotos, file.name],
});
res.status(200).json({
success: true,
data: file.name,
});
});
});
Gallery component:
import React, { useEffect, useState, useRef, Fragment } from "react";
import PropTypes from "prop-types";
import { Card, Modal, Button } from "antd";
import { DeleteOutlined } from "#ant-design/icons";
import Spinner from "./../layout/Spinner";
import { connect } from "react-redux";
import {
addGalleryPhoto,
getPhotos,
deletePhoto
} from "./../../store/actions/gallery";
const AdminGallery = ({ photos, addGalleryPhoto, getPhotos, deletePhoto }) => {
useEffect(() => {
getPhotos();
}, [getPhotos]);
const fileInput = useRef();
const [value, setValue] = useState("");
const [formData, setFormData] = useState({
deleteVisible: false,
photo: null
});
const onSubmit = e => {
e.preventDefault();
const formData = new FormData();
formData.append("file", value);
addGalleryPhoto(formData);
setValue("");
};
if (photos.loading || photos.photos.null) {
return <Spinner />;
} else {
return (
<Fragment>
<div className="admin-gallery">
<div>
<input
type="file"
style={{ display: "none" }}
ref={fileInput}
onChange={e => setValue(e.target.files[0])}
/>
{value === "" ? (
<button
type="button"
className="btn btn--primary"
onClick={e => fileInput.current.click()}
>
Dodaj plik
</button>
) : (
<button
type="button"
className="btn btn--primary"
onClick={e => onSubmit(e)}
>
Wyślij zdjęcie
</button>
)}
<div>
{photos.photos.length === 0 ? (
<div className="no-content">Brak Zdjęć</div>
) : (
<div className="admin-gallery__photo-wrapper">
{photos.photos.map(item => {
return (
<Card
style={{ padding: "0 !important" }}
key={item}
actions={[
<DeleteOutlined
onClick={() =>
setFormData({
...formData,
deleteVisible: true,
photo: item
})
}
/>
]}
>
<img
className="admin-gallery__photo"
src={`${process.env.PUBLIC_URL}/uploads/${item}`}
alt="photo"
/>
</Card>
);
})}
</div>
)}
</div>
</div>
</div>
{formData.deleteVisible && formData.photo !== null ? (
<Modal
visible={formData.deleteVisible}
title="Kasowanie zdjęcia"
onOk={() => {
deletePhoto(formData.photo);
setFormData({ ...formData, deleteVisible: false, photo: null });
}}
onCancel={() =>
setFormData({ ...formData, deleteVisible: false, photo: null })
}
footer={[
<Button
key="back"
onClick={() =>
setFormData({
...formData,
deleteVisible: false,
photo: null
})
}
>
Zamknij
</Button>,
<Button
key="accept"
onClick={() => {
deletePhoto(formData.photo);
setFormData({
...formData,
deleteVisible: false,
photo: null
});
}}
>
Skasuj
</Button>
]}
>
<p>Na pewno chcesz skasować to zdjęcie?</p>
</Modal>
) : (
""
)}
</Fragment>
);
}
};
AdminGallery.propTypes = {
photos: PropTypes.object.isRequired,
getPhotos: PropTypes.func.isRequired,
addGalleryPhoto: PropTypes.func.isRequired,
deletePhoto: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
photos: state.photos
});
export default connect(mapStateToProps, {
addGalleryPhoto,
getPhotos,
deletePhoto
})(AdminGallery);
Ok guys i've found an issue. I have nginx on my droplet and default file size to upload in nginx is set to 1mb. This line in nginx conf will do the job client_max_body_size 100M; Of course 100M is for example.
I am newbie in react and I have some trouble that I'd like to solve.
I would like to know how can I show and hide react components before and after to do a rest call.
I have the follow component:
class Loading {
render(){
return (
<div >
<Modal isOpen={true} centered >
<ModalHeader>Loading...</ModalHeader>
<ModalBody >
<div align='center' className="mt-2 mb-2">
<Spinner style={{ width: '4rem', height: '4rem' }} color="primary" />
</div>
</ModalBody>
</Modal>
</div>
)
}
}export default Loading;
And I would like to show this component in other module before to call a rest api and hide this component after the data come. The ideia is some like this:
class List extends Component {
constructor(props) {
super(props)
this.state = {
show : false
}
}
// HERE I WOULD LIKE TO SHOW THE LOADING COMPONENT
callRestApi = () => {
axiosAuth.get(url, getConfig())
.then(response => {
console.log(response.data)
this.setState({
eventos: response.data
})
}).catch(error => {
console.log(error);
return null
});
// HERE I WOULD LIKE TO HIDE THE LOADING COMPONENT
}
render() {
return(
<div>
<Button className="mr-2" color="primary" size="sm" onClick={this.callRestApi}>List All</Button>
</div>
How can I do it?
You can create state that dictates whether the loading spinner is visible or not. And append one last .then in the promise chain to modify it.
class List extends Component {
constructor(props) {
super(props)
this.state = {
show : false,
loaderVisible: true
}
}
// HERE I WOULD LIKE TO SHOW THE LOADING COMPONENT
callRestApi = () => {
axiosAuth.get(url, getConfig())
.then(response => {
console.log(response.data)
this.setState({
eventos: response.data
})
}).catch(error => {
console.log(error);
return null
}).then(() => {
this.setState({loaderVisible: false });
});
}
render() {
return(
<div>
{
this.state.loaderVisible? <Loading /> : ''
}
<Button className="mr-2" color="primary" size="sm" onClick={this.callRestApi}>List All</Button>
</div>
Then utilize ternary syntax on the spinner to determine visibility.
We use state to implement this. Here is the pseudo code.
class List extends Component {
state = { loading: false }
callRestApi = async () => {
this.setState({ loading: true });
await fetch(...);
this.setState({ loading: false });
}
render() {
<div>
{this.state.loading && <Loading />}
<button onClick={this.callRestApi}>List All</button>
</div>
}
}
I am having a problem related to redux.
I have 2 connected components which are:
avatar situated in the navbar which is always visible
profile which is responsible for changing the avatar image in the store
if I am right, when the store change, any connected component will re-render if needed.
In my case, when the action UPDATE_CURRENT_USER update the avatar image, the navbar avatar doesn't get the new image only after I change route or reload page.
I found a solution but many people say it's a hack,
I have put a listener on store changes in the main component and did forceUpdate()
componentDidMount() {
store.subscribe(res => this.forceUpdate());
}
and I don't want to use it since connected components are supposed to re-render on store changes.
user actions:
export const getCurrentUser = () => dispatch => {
axios.get("user").then(user => {
dispatch({
type: GET_CURRENT_USER,
payload: user.data
});
});
};
export const updateCurrentUser = user => dispatch => {
dispatch({
type: UPDATE_CURRENT_USER,
payload: user
})
}
user reducer
const initialState = {
user: {}
}
export default function (state = initialState, action) {
switch (action.type) {
case GET_CURRENT_USER:
return { ...state, user: action.payload };
case UPDATE_CURRENT_USER:
return { ...state, user: action.payload }
default:
return state;
}
}
profile component
class Profile extends Component {
render() {
const { currentUser, updateCurrentUser } = this.props;
return (
<div id="profile-container">
<ProfileSider
currentUser={currentUser}
updateCurrentUser={updateCurrentUser}
/>
<ProfileContent
currentUser={currentUser}
updateCurrentUser={updateCurrentUser}
/>
</div>
);
}
}
const mapStateToProps = state => ({
currentUser: state.userReducer.user
});
export default connect(
mapStateToProps,
{ updateCurrentUser }
)(Profile);
profile sidebar child of profile
class ProfileSider extends Component {
state = { uploading: false };
triggerAvatarInput() {
$("#avatarInput").click();
}
handleChange = async event => {
this.setState({ ...this.state, uploading: true });
const avatarFormData = new FormData();
avatarFormData.append("file", event.target.files[0]);
axios
.post("uploadFile", avatarFormData)
.then(res => {
const avatarURIFormData = new FormData();
avatarURIFormData.append("avatar", res.data.fileDownloadUri);
axios
.put("user/update", avatarURIFormData)
.then(res => {
const { currentUser } = this.props;
currentUser.avatar = res.data.avatar;
this.props.updateCurrentUser(currentUser);
this.setState({
...this.state,
uploading: false,
avatar: currentUser.avatar
});
message.success("Avatar updated successfully", 3);
})
.catch(error => {
this.setState({ ...this.state, uploading: false });
message.error("Updating avatar failed!", 3);
});
})
.catch(error => {
this.setState({ ...this.state, uploading: false });
message.error("Uploading avatar failed!", 3);
});
};
render() {
const { uploading } = this.state;
const { currentUser } = this.props;
return (
<div id="profile-sider">
<div id="profile-sider-info">
<div id="profile-sider-info-avatar">
<div className="container">
<div
className="overlay-uploading"
className={
uploading ? "overlay-uploading" : "overlay-uploading hidden"
}
>
<Icon type="loading" style={{ fontSize: 50, color: "#FFF" }} />
</div>
<div className="overlay" />
<div className="overlay-text" onClick={this.triggerAvatarInput}>
<Icon type="camera" style={{ fontSize: 20 }} />
<span>Update</span>
</div>
<div
className="avatar"
style={{
backgroundImage: "url(" + currentUser.avatar + ")"
}}
></div>
<input
onChange={this.handleChange}
type="file"
accept="image/png, image/jpeg, image/jpg"
id="avatarInput"
/>
</div>
</div>
<h2 style={{ marginTop: 20, textAlign: "center" }}>
{currentUser.fullName}
</h2>
<h4 style={{ textAlign: "center" }}>{currentUser.email}</h4>
</div>
<div id="profile-sider-actions">
<div className="profile-sider-actions-item">
<Link to="/profile/courses" style={{ transition: 0 }}>
<Button type="primary" id="courses-btn">
<Icon type="read" style={{ marginRight: 15 }} />
My Courses
</Button>
</Link>
</div>
<div className="profile-sider-actions-item">
<Link to="/profile/update">
<Button type="primary" id="update-infos-btn">
<Icon type="sync" style={{ marginRight: 15 }} />
Update Infos
</Button>
</Link>
</div>
</div>
</div>
);
}
}
export default ProfileSider;
avatar component situated in navbar
class ProfileAvatar extends Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
this.state = {
showProfileDropdown: false
};
}
componentDidMount() {
this.props.getCurrentUser();
}
handleLogout = async () => {
try {
await auth.logout();
this.props.onLogout();
notification["success"]({
message: "You have been successfully logged out!"
});
} catch (ex) {}
};
handleClick() {
if (!this.state.showProfileDropdown) {
// attach/remove event handler
document.addEventListener("click", this.handleOutsideClick, false);
} else {
document.removeEventListener("click", this.handleOutsideClick, false);
}
this.setState(prevState => ({
showProfileDropdown: !prevState.showProfileDropdown
}));
}
handleOutsideClick(e) {
// ignore clicks on the component itself
if (this.element && this.element.contains(e.target)) {
return;
}
this.handleClick();
}
render() {
const { currentUser } = this.props;
return (
<div
className="profile-avatar"
ref={element => {
this.element = element;
}}
>
<Avatar
onClick={this.handleClick}
size="large"
style={{ color: "#f56a00", backgroundColor: "#fde3cf" }}
src={currentUser.avatar}
>
{currentUser.fullName ? currentUser.fullName.charAt(0) : null}
</Avatar>
{this.state.showProfileDropdown && (
<div className="profile-dropdown-list">
<List
className="dropdown_list dropdown-shadow "
size="small"
style={{ width: "150px" }}
bordered
itemLayout="vertical"
dataSource={[
<Link to="/profile/update" className="profile-list-item">
<List.Item className="list-item">
<Icon className="profile-icons" type="user" /> My Profile
</List.Item>
</Link>,
<Link to="/profile/courses" className="profile-list-item">
<List.Item className="list-item">
<Icon className="profile-icons" type="container" /> My
Courses
</List.Item>
</Link>,
<List.Item className="list-item">
<Icon className="profile-icons" type="question-circle" /> Ask
for Help
</List.Item>,
<List.Item className="list-item" onClick={this.handleLogout}>
<Icon className="profile-icons" type="logout" /> Log out
</List.Item>
]}
renderItem={item => item}
/>
</div>
)}
</div>
);
}
}
const mapStateToProps = state => ({
currentUser: state.userReducer.user
});
export default connect(
mapStateToProps,
{ getCurrentUser }
)(ProfileAvatar);
image: https://imge.to/i/vywTNj
There are two problems here:
You are mutating the existing object from the store
You are sending that exact same user object back into the store when you dispatch the action.
Specifically, these lines are the cause:
const { currentUser } = this.props;
currentUser.avatar = res.data.avatar;
this.props.updateCurrentUser(currentUser);
currentUser is the user object that's already in the Redux store. This code mutates the object, and inserts it back into the store.
That results in the connected component thinking nothing has actually changed.
The shortest way to fix this is to create a new user object, and insert that:
const {currentUser} = this.props;
const updatedUser = {...currentUser, avatar: res.data.avatar};
this.props.updateCurrentUser(updatedUser);
To avoid this in the future, I strongly encourage you to use the configureStore function from our Redux Starter Kit package, which detects mutations and will throw errors if you mutate.
I'm doing a map project in React and using the google-maps-react api. I am able to type characters in the search box, but it doesn't filter my list or markers. How can I make that work?
Here's the code for my App.js. I have the updateQuery which should update with whatever is typed in the search box. filterItems is supposed to filter all the locations. addRealMarkers is supposed to replace with the filtered markers:
var foursquare = require("react-foursquare")({
clientID: "BTMAGTC2Y5G1IXAKA4VN4QN55R2DSN1105Y1XGHB0WZ5THHR",
clientSecret: "4HOKQ0ON1V1XEHKSUSEABQMNRFZGCGPIKIUIE5JMUMWVRG5W",
url: "https://api.foursquare.com/v2/venues/search?"
});
var params = {
ll: "31.462170,-97.195732",
query: "Hewitt"
};
class App extends Component {
/* Basic state object that must be kept at the highest "parent" level per
Doug Brown's training video */
constructor(props) {
super(props);
this.state = {
lat: 31.46217,
lon: -97.195732,
zoom: 13,
items: [],
filtered: null,
open: false,
selectedId: null,
activeMarker: null
};
}
realMarkers = [];
componentDidMount() {
foursquare.venues.getVenues(params).then(res => {
this.setState({ items: res.response.venues });
});
fetch("react-foursquare")
.then(response => response.json())
.then(response => {
const items = json.response.items;
this.setState({
items,
filtered: this.filterItems(items, "")
});
})
.catch(error => {
alert("Foursquare data could not be retrieved");
});
}
//Fetches the locations requested for this map.
/*fetchPlaces(mapProps, map) {
const { google } = mapProps;
const service = new google.maps.places.PlacesService(map);
}
//fetch Foursquare API data and use Axios to catch errors, instructed by
Yahya Elharony.
// Source: https://github.com/foursquare/react-foursquare
getPlaces = () => {
const endPoint = "https://api.foursquare.com/v2/venues/explore?";
const params = {
client_id: "BTMAGTC2Y5G1IXAKA4VN4QN55R2DSN1105Y1XGHB0WZ5THHR",
client_secret: "4HOKQ0ON1V1XEHKSUSEABQMNRFZGCGPIKIUIE5JMUMWVRG5W",
near: "Hewitt",
query: "query",
v: 20181117
};
// axios site: https://www.npmjs.com/package/axios
axios
.get(endPoint + new URLSearchParams(params))
.then(response => {
this.setState(
{
venues: response.data.response.groups[0].items
},
this.fetchPlaces()
);
})
.catch(error => {
console.log("ERROR! " + error);
});
};*/
// Creating the replacement markers that goes with the list. Based on my
1:1 training from Doug Brown
addRealMarker = marker => {
let checkList = this.realMarkers.filter(
m => m.marker.id === marker.marker.id
);
if (!checkList.length) this.realMarkers.push(marker);
};
updateQuery = query => {
this.setState({
selectedIndex: null,
filtered: this.filterItems(this.state.items, query)
});
};
filterItems = (items, query) => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
};
clickListItem = id => {
const marker = this.realMarkers.filter(
marker => marker.marker.id === id
)[0];
this.setState({
selectedId: id,
activeMarker: marker
});
};
/*Google Maps React Component courtesy of
https://www.npmjs.com/package/google-maps-react*/
render() {
const style = {
width: "100%",
height: "100%"
};
return (
<div className="App">
<HewittMap
lat={this.state.lat}
lng={this.state.lng}
zoom={this.state.zoom}
style={style}
items={this.state.items}
addRealMarker={this.addRealMarker}
activeMarker={this.state.activeMarker}
clickListItem={this.clickListItem}
/>
<Sidebar
items={this.state.items}
clickListItem={this.clickListItem}
filterItems={this.updateQuery}
/>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
export default App;
And here's the Sidebar Code. Added another updateQuery function that's supposed to call the props then you'll see some more code in the InputBase component:
class Sidebar extends Component {
state = {
mobileOpen: false,
query: ""
};
handleDrawerOpen = () => {
this.setState({ open: true });
};
handleDrawerClose = () => {
this.setState({ open: false });
};
updateQuery = newQuery => {
// Save the new query string in state and pass the string up the call
tree
this.setState({ query: newQuery });
this.props.filterItems(newQuery);
};
render() {
const { classes, theme } = this.props;
const { open } = this.state;
const items = this.props.items;
return (
<div className={classes.root}>
<CssBaseline />
<AppBar
position="fixed"
className={classNames(classes.appBar, {
[classes.appBarShift]: open
})}
>
<Toolbar disableGutters={!open}>
<IconButton
color="inherit"
aria-label="Open drawer"
onClick={this.handleDrawerOpen}
className={classNames(classes.menuButton, open && classes.hide)}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" color="inherit" noWrap>
City of Hewitt
</Typography>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon places={this.state.places} />
</div>
<InputBase
classes={{
root: classes.inputRoot,
input: classes.inputInput
}}
placeholder="Search…"
name="filter"
type="text"
value={this.state.query}
onChange={e => {
this.updateQuery(e.target.value);
}}
/>
</div>
</Toolbar>
</AppBar>
<Drawer
className={classes.drawer}
variant="persistent"
anchor="left"
open={open}
classes={{
paper: classes.drawerPaper
}}
>
<div className={classes.drawerHeader}>
<IconButton onClick={this.handleDrawerClose}>
{theme.direction === "ltr" ? (
<ChevronLeftIcon />
) : (
<ChevronRightIcon />
)}
</IconButton>
</div>
<Divider />
<List>
{this.props.items &&
this.props.items.map((item, index) => {
return (
<ListItem key={item.id}>
<button
key={index}
onClick={e => this.props.clickListItem(item.id)}
>
<ListItemText primary={item.name}> </ListItemText>
</button>
</ListItem>
);
})}
</List>
<Divider />
</Drawer>
<main
className={classNames(classes.content, {
[classes.contentShift]: open
})}
>
<div className={classes.drawerHeader} />
</main>
</div>
);
}
}
Sidebar.propTypes = {
classes: PropTypes.object.isRequired,
// Injected by the documentation to work in an iframe.
// You won't need it on your project.
container: PropTypes.object,
theme: PropTypes.object.isRequired
};
export default withStyles(styles, { withTheme: true })(Sidebar);
You can click in my CodeSandbox to see for yourself.
You are filtering your data and assigning it to filtered but you use items to drive your map, not filtered. It would need more refactoring, but what if you did this?
updateQuery = query => {
this.setState({
selectedIndex: null,
//filtered: this.filterItems(this.state.items, query) // -
items: this.filterItems(this.state.items, query) // +
});
};
You might want an indicator, say isFiltered, that is true when the search bar has a value in it. If true, use the filtered data, else, use the original items