How to change Antd form initialValues depends at url or id? - javascript

I got same component with Antd form for add/edit article. With pathes in router
<Route path="/add" component={ !currentUser ? Login : ArticleEditor } />
<Route path="/article/:id/edit" component={ !currentUser ? Login : ArticleEditor } />
When I click "edit" button I add initialValues to form, than if I click "Create new article" url changes to "/add", but form didn't update values. Values remains from edited article. How to update form values? Tried to set initialValues depends at path, or "id" but its not worked. How to update antd form values in that case?
const initialValues = this.props.location.pathname === '/add' ? {} : {
title: this.props?.title,
body: this.props?.body,
description: this.props?.description
};
Here you can see the component code - codesandbox link

The main issue with the code is form fields are not reset when url is changed, you can detect path change in shouldComponentUpdate and set isLoading to true and rest should work.
Updating initialValues will not work because, antd does shallow compare and once initialValues are set, you will not be able to change them.
There was an issue in the logic of componentDidUpdate which I corrected as well.
import React from "react";
import ErrorsList from "../ErrorsList/ErrorsList";
import userService from "../../services/userService";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import { Form, Input, Button } from "antd";
import { store } from "../../store";
import actionCreators from "../../actionCreators";
const formItemLayout = {
labelCol: { span: 24 },
wrapperCol: { span: 24 }
};
const formSingleItemLayout = {
wrapperCol: { span: 24, offset: 0 }
};
const mapStateToProps = (state) => ({
...state.editor
});
const mapDispatchToProps = (dispatch) => ({
onLoad: (payload) => dispatch(actionCreators.doEditorLoaded(payload)),
onUnload: () => dispatch(actionCreators.doEditorUnloaded()),
onUpdateField: (key, value) =>
dispatch(actionCreators.doUpdateFieldEditor(key, value)),
onSubmit: (payload, slug) => {
dispatch(actionCreators.doArticleSubmitted(payload));
store.dispatch(push(`/`)); //article/${slug}
},
onRedirect: () => dispatch(actionCreators.doRedirect())
});
class ArticleEditor extends React.Component {
constructor(props) {
super(props);
this.id = this.props.match.params.id;
const updateFieldEvent = (key) => (e) =>
this.props.onUpdateField(key, e.target.value);
this.changeTitle = updateFieldEvent("title");
this.changeDescription = updateFieldEvent("description");
this.changeBody = updateFieldEvent("body");
this.changeTagInput = updateFieldEvent("tagInput");
this.isLoading = true;
this.submitForm = () => {
const article = {
title: this.props.title,
description: this.props.description,
body: this.props.body,
tagList: this.props.tagInput.split(",")
};
const slug = { slug: this.props.articleSlug };
const promise = this.props.articleSlug
? userService.articles.update(Object.assign(article, slug))
: userService.articles.create(article);
this.props.onSubmit(promise, this.props.articleSlug);
};
}
componentDidUpdate(prevProps, prevState) {
if (this.props.match.params.id !== prevProps.match.params.id) {
if (prevProps.match.params.id) {
this.props.onUnload();
}
this.id = this.props.match.params.id;
if (this.id) {
return this.props.onLoad(userService.articles.get(this.id));
}
this.props.onLoad(null);
}
this.isLoading = false;
}
componentDidMount() {
if (this.id) {
this.isLoading = true;
return this.props.onLoad(userService.articles.get(this.id));
}
this.isLoading = false;
this.props.onLoad(null);
}
componentWillUnmount() {
this.props.onUnload();
}
shouldComponentUpdate(newProps, newState) {
if (this.props.match.params.id !== newProps.match.params.id) {
this.isLoading = true;
}
return true;
}
render() {
const { errors } = this.props;
const initialValues = {
title: this.props?.title,
body: this.props?.body,
description: this.props?.description,
tags: this.props?.tagList
};
return this.isLoading ? (
"loading..."
) : (
<div className="editor-page">
<div className="container page">
<div className="">
<div className="">
<ErrorsList errors={errors}></ErrorsList>
<Form
{...formItemLayout}
initialValues={initialValues}
onFinish={this.submitForm}
>
<Form.Item
label="Title"
name="title"
placeholder="Article Title"
rules={[
{
required: true,
message: "Please input article title"
}
]}
>
<Input onChange={this.changeTitle} />
</Form.Item>
<Form.Item
label="Description"
name="description"
placeholder="Short description"
rules={[
{
required: true,
message: "Please input article description"
}
]}
>
<Input onChange={this.changeDescription} />
</Form.Item>
<Form.Item
name="body"
label="Article Text"
placeholder="article text"
>
<Input.TextArea onChange={this.changeBody} />
</Form.Item>
<Form.Item name="tags" label="Tags" placeholder="Enter tags">
<Input onChange={this.changeTagInput} />
</Form.Item>
<Form.Item {...formSingleItemLayout}>
<Button
className="editor-form__btn"
type="primary"
htmlType="submit"
disabled={this.props.inProgress}
>
Submit Article
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ArticleEditor);
take a look at this forked codesandbox.

You have to clean the fields before you re-use the 'ArticleEditor' component. Here you are using the same component for two different route, hence it's not changing.
You have to check if you are editing or adding a new entry to the Editor. Your editor component may look like this then,
const ArticleEditor = props => {
const [form] = Form.useForm();
useEffect(() => {
if (props.match.params.id) form.setFieldsValue({value : 'Some values'})
else form.resetFields()
}, [props?.match?.params]);
return (
<Form form={form} onFinish={yourFinishMethod}>
//...your form fields
</Form>
)
}

Related

Add multiple input field dynamically in react

I have is a div that contains three input elements and a remove button. what is required is when the user clicks on the add button it will add this div dynamically and when the user clicks on the remove button it will remove this div. I was able to add one input element (without div container) dynamically with the following method.
create an array in the state variable.
assign a name to the dynamic input field with the help of array indexing like name0, name1
How can I do with these many input fields? The problem grows further when I create this whole div as a separate component. I am using a class-based component.
handleChange=(event) =>
{
this.setState({[event.target.name]:event.target.values});
}
render()
{
return(
<div className="row">
<button type="button" onClick={this.addElement}>Add</button>
<div className="col-md-12 form-group">
<input type="text" className="form-control" name="name" value={this.state.name} onChange={this.handleChange} />
<input type="text" className="form-control" name="email" value={this.state.email} onChange={this.handleChange} />
<input type="text" className="form-control" name="phone" value={this.state.phone} onChange={this.state.phone} />
<button type="button" onClick={this.removeElement}>Remove</button>
</div>
</div>
)
}
I would approach this from a configuration angle as it's a little more scalable. If you want to eventually change across to something like Formik or React Form, it makes the move a little easier.
Have an array of objects that you want to turn into input fields. Your main component should maintain state whether the <Form /> component is showing, and if it's visible pass in the config and any relevant handlers.
Your form component should maintain state for the inputs, and when you submit it, passes up the completed state to the parent.
const { Component } = React;
class Example extends Component {
constructor(props) {
super();
// The only state in the main component
// is whether the form is visible or not
this.state = { visible: false };
}
addForm = () => {
this.setState({ visible: true });
}
removeForm = () => {
this.setState({ visible: false });
}
handleSubmit = (form) => {
console.log(form);
}
render() {
const { visible } = this.state;
const { config } = this.props;
return (
<div>
<button
type="button"
onClick={this.addForm}
>Add form
</button>
{visible && (
<Form
config={config}
handleSubmit={this.handleSubmit}
handleClose={this.removeForm}
/>
)}
</div>
);
}
};
class Form extends Component {
constructor(props) {
super();
this.state = props.config.reduce((acc, c) => {
return { ...acc, [c.name]: '' };
}, {});
}
handleChange = (e) => {
const { name, value } = e.target;
this.setState({ [name]: value });
}
handleSubmit = () => {
this.props.handleSubmit(this.state);
}
render() {
const { name, email, phone } = this.state;
const { handleClose, config } = this.props;
return (
<div onChange={this.handleChange}>
{config.map(input => {
const { id, name, type, required } = input;
return (
<div>
<label>{name}</label>
<input key={id} name={name} type={type} required={required} />
</div>
)
})}
<button type="button" onClick={this.handleSubmit}>Submit form</button>
<button type="button" onClick={handleClose}>Remove form</button>
</div>
);
}
}
const config = [
{ id: 1, name: 'name', type: 'text', required: true },
{ id: 2, name: 'email', type: 'email', required: true },
{ id: 3, name: 'phone', type: 'phone', required: true }
];
ReactDOM.render(
<Example config={config} />,
document.getElementById('react')
);
input { display: block; }
label { text-transform: capitalize; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I hope this would be help for your question.
I made a child component which have three input tags.
// parent component
import React, { Component } from "react";
import TextField from "./TextField";
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: [
{
key: Date.now(),
name: "",
email: "",
phone: ""
}
]
};
}
onChange = (inputUser) => {
this.setState((prevState) => {
const newUsers = prevState.users.map((element) => {
if (element.key === inputUser.key) return inputUser;
return element;
});
return { users: newUsers };
});
};
addElement = () => {
const { name, email, phone } = this.state;
this.setState((prevState) => ({
users: prevState.users.concat({
key: Date.now(),
name,
email,
phone
})
}));
};
removeElement = (id) => {
this.setState((prevState) => ({
users: prevState.users.filter((user) => user.key !== id)
}));
};
render() {
const { users } = this.state;
return (
<div className="row">
<button type="button" onClick={this.addElement}>
Add
</button>
<div className="col-md-12 form-group">
{users.map((user) => (
<React.Fragment key={user.key}>
<TextField
value={user}
onChange={(inputUser) => this.onChange(inputUser)}
/>
<button
type="button"
onClick={() => this.removeElement(user.key)}
disabled={users.length <= 1}
>
Remove
</button>
</React.Fragment>
))}
</div>
</div>
);
}
}
export default App;
// child component
import { Component } from "react";
class TextField extends Component {
handleChange = (ev) => {
const { name, value } = ev.target;
this.props.onChange({
...this.props.value,
[name]: value
});
};
render() {
const { value: user } = this.props;
return (
<>
<input
className="form-control"
name="name"
value={user.name}
onChange={this.handleChange}
placeholder="name"
type="text"
/>
<input
className="form-control"
name="email"
value={user.email}
onChange={this.handleChange}
placeholder="email"
type="text"
/>
<input
className="form-control"
name="phone"
value={user.phone}
onChange={this.handleChange}
placeholder="phone"
type="text"
/>
</>
);
}
}
export default TextField;
You can also check the code in codesandbox link below.
https://codesandbox.io/s/suspicious-heisenberg-xzchm
It was a little difficult to write down every detail of how to generate what you want. So I find it much easier to ready a stackblitz link for you to see how is it going to do this and the link is ready below:
generating a dynamic div adding and removing by handling inputs state value
const { Component } = React;
class Example extends Component {
constructor(props) {
super();
// The only state in the main component
// is whether the form is visible or not
this.state = { visible: false };
}
addForm = () => {
this.setState({ visible: true });
}
removeForm = () => {
this.setState({ visible: false });
}
handleSubmit = (form) => {
console.log(form);
}
render() {
const { visible } = this.state;
const { config } = this.props;
return (
<div>
<button
type="button"
onClick={this.addForm}
>Add form
</button>
{visible && (
<Form
config={config}
handleSubmit={this.handleSubmit}
handleClose={this.removeForm}
/>
)}
</div>
);
}
};
class Form extends Component {
constructor(props) {
super();
this.state = props.config.reduce((acc, c) => {
return { ...acc, [c.name]: '' };
}, {});
}
handleChange = (e) => {
const { name, value } = e.target;
this.setState({ [name]: value });
}
handleSubmit = () => {
this.props.handleSubmit(this.state);
}
render() {
const { name, email, phone } = this.state;
const { handleClose, config } = this.props;
return (
<div onChange={this.handleChange}>
{config.map(input => {
const { id, name, type, required } = input;
return (
<div>
<label>{name}</label>
<input key={id} name={name} type={type} required={required} />
</div>
)
})}
<button type="button" onClick={this.handleSubmit}>Submit form</button>
<button type="button" onClick={handleClose}>Remove form</button>
</div>
);
}
}
const config = [
{ id: 1, name: 'name', type: 'text', required: true },
{ id: 2, name: 'email', type: 'email', required: true },
{ id: 3, name: 'phone', type: 'phone', required: true }
];
ReactDOM.render(
<Example config={config} />,
document.getElementById('react')
);
input { display: block; }
label { text-transform: capitalize; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

How to edit react content dynamically

Ok..first things first:
Please refer the image of webApp attached:
My Application displays loginIdCard's consisting(website,username,password) from mongoDb which
i can edit from react when clicking on edit button.
What i did is initially i maintained a editMode key in component state a set it as false.
when a user clicks on edit button the LoginIdCard becomes editable and on clicking save button new values are set in component state and then editLoginId function is dispatch which updates this new value in database.
Now,
following are the things i want:
Initially when edit button is clicked, the value inside the input field should be the original values,
but now it is show empty.
2.The new values should be displayed immediately without rerendering of component.
Note: Now,after cliciking on save button , the component rerenders and the endpoint api return res data which is not a array, so the LoginDisplay component is not able to map and gives this.map is not a function error.
Please Help me
Web app rendering LoginIdCard in LoginDisplay Component
"LoginDispaly Component:Here LoginIdCard Component Will Rendender"
import React, { Component } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import LoginIdCard from "./LoginIdCard";
import EditLoginIdComponent from "./EditLoginIdComponent";
import { fetchLoginIds } from "../actions/loginInIdsAction";
import "../css/displaySection.css";
class LoginsDisplay extends Component {
componentWillMount() {
this.props.fetchLoginIds();
}
render() {
const { logins } = this.props;
return (
<div className="display-section">
{logins.map((logins) => (
<LoginIdCard logins={logins} />
))}
</div>
);
}
}
function mapStateToProps(state) {
return {
logins: state.logins.logins,
};
}
LoginsDisplay.propTypes = {
logins: PropTypes.array.isRequired,
};
export default connect(mapStateToProps, { fetchLoginIds })(LoginsDisplay);
"LoginIdCard Component it will be render in LoginDispaly Component"
import React, { Component } from "react";
import { connect } from "react-redux";
import { editLoginId } from "../actions/loginInIdsAction";
import "../css/card.css";
class LoginIdCard extends Component {
constructor(props) {
super(props);
this.state = {
website: "",
username: "",
password: "",
editMode: false,
};
this.handleChange = this.handleChange.bind(this);
// this.handleSave = this.handleChange.bind(this);
}
handleChange = (fieldName, val) => {
console.log(val);
this.setState({
[fieldName]: val,
});
};
handleSave = () => {
const { website, username, password } = this.state;
const { logins } = this.props;
this.props.dispatch(editLoginId(website, username, password, logins._id));
console.log(this.state.website, username, password, logins._id);
};
render() {
const { editMode } = this.state;
const { logins } = this.props;
// const website = logins.website;
// const username = logins.username;
// const password = logins.password;
return (
<div className="card">
{editMode ? (
<input
type="text"
onChange={(e) => this.handleChange("website", e.target.value)}
value={this.state.website}
/>
) : (
<p>{this.state.website}</p>
)}
{editMode ? (
<input
type="text"
onChange={(e) => this.handleChange("username", e.target.value)}
value={this.state.username}
/>
) : (
<p>{logins.username}</p>
)}
{editMode ? (
<input
type="text"
onChange={(e) => this.handleChange("password", e.target.value)}
value={this.state.password}
/>
) : (
<p>{logins.password}</p>
)}
{editMode ? (
<button onClick={this.handleSave}>save</button>
) : (
<button onClick={() => this.handleChange("editMode", true)}>
edit
</button>
)}
</div>
);
}
}
// this.handleChange("editMode", false)
function mapStateToProps(state) {
return {
// user: state.user.users,
// cards: state.cards.cards,
logins: state.logins.logins,
};
}
// App.propTypes = {
// user: PropTypes.array.isRequired,
// };
export default connect()(LoginIdCard);
"redux action file for editing the LoginId in mongodb"
export function editLoginId(website, username, password, id) {
return function (dispatch) {
const req = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
website: website,
username: username,
password: password,
cardId: id,
}),
};
fetch("http://localhost:9000/editLoginId", req)
.then((res) => res.json())
.then((data) =>
dispatch({
type: EDIT_LOGIN_ID,
payload: data,
})
)
.catch((err) => {
console.log(err);
<!-- begin snippet: js hide: false console: true babel: false -->
});
};
}

Change the color of a button based on Formik/Yup Validation

There is a field where the user must introduce a text. If the text is the desired one, a button should change its color so the user knows the text is the correct one.
So far I've done the validation of the text, an error message is shown if the text is not the desired one but I don't know how to connect this to the button's styling.
Here is my code:
import React from 'react';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import * as Yup from 'yup';
import { Formik, Form, Field } from 'formik';
import { Button, Modal, Input } from 'semantic-ui-react';
import { Creators } from '../../../actions';
import './FridgeForm.scss';
class CreateFridgeForm extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
redirectCreate: false,
};
}
componentDidMount() {
const { edit, getFridge, fridge, getFridges } = this.props;
if (edit) {
getFridge(fridge._id);
getFridges();
}
}
onOpen = () => {
this.setState({ open: true });
};
closeModal = () => {
this.setState({ open: false });
};
handleDelete = values => {
const { deleteFridge, fridge, getFridges } = this.props;
deleteFridge(fridge._id);
this.setState({ open: false });
this.setState({ redirectCreate: true });
getFridges();
};
render() {
const { trigger } = this.props;
const title = 'Do you want to delete a fridge?';
const { open, redirectCreate } = this.state;
if (redirectCreate) {
return <Redirect to="/fridges" />;
}
const initialValues = {
deleteText: '',
};
const wrongTextMessage =
'You must write DELETE in order to delete the fridge';
const requiredErrorMessage = 'This field is required';
const deleteTextValidation = /DELETE\b/g;
const validationSchema = Yup.object({
deleteText: Yup.string()
.matches(deleteTextValidation, wrongTextMessage)
.required(requiredErrorMessage),
});
return (
<Modal
open={open}
trigger={trigger}
onOpen={this.onOpen}
onClose={this.closeModal}
size="small">
<Modal.Header >{title}</Modal.Header>
<Modal.Content >
<Modal.Description >
Are you sure you want to delete?
</Modal.Description>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={values => this.handleDelete(values)}>
{({ values, errors, touched, setFieldValue }) => (
<Form>
<Field
className="add-fridge-input"
name="deleteText"
as={Input}
placeholder="Write DELETE"
/>
<div className="add-fridge-error">
{touched.deleteText && errors.deleteText
? errors.deleteText
: null}
</div>
<Button className="delete-modal-button" type="submit"> // here is the button
Confirm
</Button>
</Form>
)}
</Formik>
</Modal.Content>
</Modal>
);
}
}
const mapStateToProps = state => ({
fridges: state.fridges.fridges,
fridge: state.fridges.selectedFridge,
});
const mapDispatchToProps = {
getFridges: Creators.getFridgesRequest,
getFridge: Creators.getFridgeRequest,
deleteFridge: Creators.deleteFridgeRequest,
};
export default connect(mapStateToProps, mapDispatchToProps)(CreateFridgeForm);
Is there a way to notify the button if validationSchema validates as correct the text?

Axios get method response in React cannot be displayed getting data from firebase as an array in my blog application

I wonder if someone could help me. I have read many StackOverflow's answers around this and other great articles like this one and I couldn't implement an answer yet.
I have got a simple blog app in React. I have a form to submit the data and I have separate post and posts component as well. I can actually send data to my firebase database. I also get the response in GET method but I cannot show the response as I need it to be. I need an array of posts which each post has a title and content so that I can send its data to my Post component. But I always get an error like( map cannot be used on the response) and I actually cannot get an array out of my database. I even wonder if I am sending data in the right format. Please check my code below and help me out. Thanks.
// The individual post component
const Post = props => (
<article className="post">
<h2 className="post-title">{props.title}</h2>
<hr />
<p className="post-content">{props.content}</p>
</article>
);
// The form component to be written later
class Forms extends React.Component {}
// The posts loop component
class Posts extends React.Component {
state = {
posts: null,
post: {
title: "",
content: ""
}
// error:false
};
componentDidMount() {
// const posts = this.state.posts;
axios
.get("firebaseURL/posts.json")
.then(response => {
const updatedPosts = response.data;
// const updatedPosts = Array.from(response.data).map(post => {
// return{
// ...post
// }
// });
this.setState({ posts: updatedPosts });
console.log(response.data);
console.log(updatedPosts);
});
}
handleChange = event => {
const name = event.target.name;
const value = event.target.value;
const { post } = this.state;
const newPost = {
...post,
[name]: value
};
this.setState({ post: newPost });
console.log(event.target.value);
console.log(this.state.post.title);
console.log(name);
};
handleSubmit = event => {
event.preventDefault();
const post = {
post: this.state.post
};
const posts = this.state.posts;
axios
.post("firebaseURL/posts.json", post)
.then(response => {
console.log(response);
this.setState({ post: response.data });
});
};
render() {
let posts = <p>No posts yet</p>;
if (this.state.posts) {
posts = this.state.posts.map(post => {
return <Post key={post.id} {...post} />;
});
}
return (
<React.Fragment>
<form className="new-post-form" onSubmit={this.handleSubmit}>
<label>
Post title
<input
className="title-input"
type="text"
name="title"
onChange={this.handleChange}
/>
</label>
<label>
Post content
<input
className="content-input"
type="text"
name="content"
onChange={this.handleChange}
/>
</label>
<input className="submit-button" type="submit" value="submit" />
</form>
</React.Fragment>
);
}
}
class App extends React.Component {
render() {
return (
<React.Fragment>
<Posts />
</React.Fragment>
);
}
}
// Render method to run the app
ReactDOM.render(<App />, document.getElementById("id"));
And this is a screenshot of my firebase database:
My Firebase database structure
It is interesting that what I found is rarely mentioned anywhere around it.
This is the entire Posts component:
class Posts extends React.Component {
state = {
posts: [],
post: {
title: "",
content: ""
}
};
componentWillMount() {
const { posts } = this.state;
axios
.get("firebaseURL/posts.json")
.then(response => {
const data = Object.values(response.data);
this.setState({ posts : data });
});
}
handleChange = event => {
const name = event.target.name;
const value = event.target.value;
const { post } = this.state;
const newPost = {
...post,
[name]: value
};
this.setState({ post: newPost });
console.log(event.target.value);
console.log(this.state.post.title);
console.log(name);
};
handleSubmit = event => {
event.preventDefault();
const {post} = this.state;
const {posts} = this.state;
axios
.post("firebaseURL/posts.json", post)
.then(response => {
console.log(response);
const newPost = response.data;
this.setState({ post: response.data });
});
};
render() {
let posts = <p>No posts yet</p>;
if (this.state.posts) {
posts = this.state.posts.map(post => {
return <Post key={post.id} {...post} />;
});
}
return (
<React.Fragment>
{posts}
<form className="new-post-form" onSubmit={this.handleSubmit}>
<label>
Post title
<input
className="title-input"
type="text"
name="title"
onChange={this.handleChange}
/>
</label>
<label>
Post content
<input
className="content-input"
type="text"
name="content"
onChange={this.handleChange}
/>
</label>
<input className="submit-button" type="submit" value="submit" />
</form>
</React.Fragment>
);
}
}
Actually as I first time read in this question you should not rely on console.log to see if your posts (or your response data) has been updated. Because in componentDidMount() when you immediately update state you will not see the change in console. So what I did was to display the data that I got from the response using map over the posts and it showed my items as I actually had an array although couldn't see in the console. This is my code for componentDidMount:
axios.get("firebaseURL/posts.json").then(response => {
const data = Object.values(response.data);
this.setState({
posts: data
});
And show the posts:
let posts = <p>No posts yet</p>;
if (this.state.posts) {
posts = this.state.posts.map(post => {
return <Post key={post.id} {...post} />;
});
}
And it shows all the posts as expected. Take away is to be careful once woking on componentDidMound and other lifecycle methods as you might not see the updated data in the console inside them but you actually need to use it as it is in the response. The state is updated but you are not able to see it inside that method.
Not a database expert, but I believe your database is structured a bit odd and will only cause problems further down the line, especially when it comes to editing/updating a single post. Ideally, it should structured like a JSON array:
posts: [
{
id: "LNO_qS0Y9PjIzGds5PW",
title: "Example title",
content: "This is just a test"
},
{
id: "LNOc1vnvA57AB4HkW_i",
title: "Example title",
content: "This is just a test"
},
...etc
]
instead its structured like a JSON object:
"posts": {
"LNO_qS0Y9PjIzGds5PW": {
"post": {
"title": "Example title",
"content": "This is just a test"
}
},
"LNOc1vnvA57AB4HkW_i": {
"post": {
"title": "Example title",
"content": "This is just a test"
}
},
...etc
}
Anyway, your project should have a parent Posts container-component that controls all your state and fetching of data, then it passes down its state and class methods to component children. Then the children can update or display the parent's state accordingly.
OR
You should separate your Posts container-component, so that it either displays found posts or a "No posts found" component. And then, have your Posts Form component be it's own/unshared component whose only function is to show a form and submit it to a DB.
Up to you and what you think fits your needs.
Working example: https://codesandbox.io/s/4x4kxn9qxw (the example below has one container-component that shares with many children)
Note: If you change posts to an empty array [], instead of data in fetchData()s this.setState() function, you can have the PostForm be displayed under the /posts route!
ex: .then(({ data }) => this.setState({ isLoading: false, posts: [] }))
index.js
import React from "react";
import { render } from "react-dom";
import App from "./routes";
import "uikit/dist/css/uikit.min.css";
import "./styles.css";
render(<App />, document.getElementById("root"));
routes/index.js
import React from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import Home from "../components/Home";
import Header from "../components/Header";
import Posts from "../containers/Posts";
export default () => (
<BrowserRouter>
<div>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/posts" component={Posts} />
<Route path="/postsform" component={Posts} />
</Switch>
</div>
</BrowserRouter>
);
containers/Posts.js
import isEmpty from "lodash/isEmpty";
import React, { Component } from "react";
import axios from "axios";
import PostsForm from "../components/postsForm";
import ServerError from "../components/serverError";
import ShowPosts from "../components/showPosts";
import Spinner from "../components/spinner";
export default class Posts extends Component {
state = {
content: "",
error: "",
isLoading: true,
posts: [],
title: ""
};
componentDidUpdate = (prevProps, prevState) => {
// check if URL has changed from "/posts" to "/postsform" or vice-versa
if (this.props.location.pathname !== prevProps.location.pathname) {
// if so, check the location
this.setState({ isLoading: true }, () => this.checkLocation());
}
};
componentDidMount = () => this.checkLocation();
checkLocation = () => {
// if the location is "/posts" ...
this.props.location.pathname === "/posts"
? this.fetchData() // then fetch data
: this.setState({ // otherwise, clear state
content: "",
error: "",
isLoading: false,
posts: [],
title: ""
});
};
// fetches posts from DB and stores it in React state
fetchData = () => {
axios
.get("firebaseURL/posts.json")
.then(({ data }) => this.setState({ isLoading: false, posts: data }))
.catch(err => this.setState({ error: err.toString() }));
};
// handles postsForm input changes { content: value , title: value }
handleChange = e => this.setState({ [e.target.name]: e.target.value });
// handles postsForm form submission
handleSubmit = event => {
event.preventDefault();
const { content, title } = this.state;
alert(`Sumbitted values: ${title} - ${content}`);
/* axios.post("firebaseURL/posts.json", { post: { title, content }})
.then(({data}) => this.setState({ content: "", posts: data, title: "" }))
.catch(err => this.setState({ error: err.toString() }))
*/
};
// the below simply returns an if/else chain using the ternary operator
render = () => (
this.state.isLoading // if isLoading is true...
? <Spinner /> // show a spinner
: this.state.error // otherwise if there's a server error...
? <ServerError {...this.state} /> // show the error
: isEmpty(this.state.posts) // otherwise, if posts array is still empty..
? <PostsForm // show the postForm
{...this.state}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
/>
: <ShowPosts {...this.state} /> // otherwise, display found posts!
);
}
components/postsForm.js
import React from "react";
export default ({ content, handleSubmit, handleChange, title }) => (
<form
style={{ padding: "0 30px", width: 500 }}
className="new-post-form"
onSubmit={handleSubmit}
>
<label>
Post title
<input
style={{ marginBottom: 20 }}
className="uk-input"
type="text"
name="title"
onChange={handleChange}
placeholder="Enter post title..."
value={title}
/>
</label>
<label>
Post content
<input
style={{ marginBottom: 20 }}
className="uk-input"
type="text"
name="content"
onChange={handleChange}
placeholder="Enter post..."
value={content}
/>
</label>
<button
disabled={!title || !content}
className="uk-button uk-button-primary"
type="submit"
>
Submit
</button>
</form>
);
components/showPosts.js
import map from "lodash/map";
import React from "react";
export default ({ posts }) => (
<div className="posts">
{map(posts, ({ post: { content, title } }, key) => (
<div key={key} className="post">
<h2 className="post-title">{title}</h2>
<hr />
<p className="post-content">{content}</p>
</div>
))}
</div>
);
components/serverError.js
import React from "react";
export default ({ err }) => (
<div style={{ color: "red", padding: 20 }}>
<i style={{ marginRight: 5 }} className="fas fa-exclamation-circle" /> {err}
</div>
);

How to update the state of array of objects from form fields?

I have created a component that can be used for creating a new company record. A modal is opened with a form and the values are linked to the state values. In my situation, it will be possible to create more than one record of a company if the user chooses to add another company. A new company object will be pushed to the company state and the new empty form will be rendered.
This is what I've tried based on this answer:
import { Component } from 'react';
import { Modal, Header, Form, Button, Icon, Tab, Segment } from 'semantic-ui-react';
export default class CompanyCreate extends Component {
constructor(props) {
super(props);
this.state = {
company: [
{
name: '',
segment: ''
}
]
};
this.initialState = this.state;
this.handleChange = this.handleChange.bind(this);
this.handleCompanyChange = this.handleCompanyChange.bind(this);
}
handleChange = (e, { name, value }) => this.setState({ [name]: value });
handleCompanyChange = (e, { name, value }) => {
const index = this.state.company.findIndex((x) => {
return x[name] === value;
});
if (index === -1) {
console.log('error');
} else {
this.setState({
company: [
...this.state.company.slice(0, index),
Object.assign({}, this.state.company[index], value),
...this.state.company.slice(index + 1)
]
});
}
};
render() {
const { company } = this.state;
return (
<Segment>
{company.map((e, index) => (
<Form size="large" key={index}>
<Form.Group>
<Form.Input
width={6}
onChange={this.handleCompanyChange}
label="Nome"
placeholder="Nome"
name="name"
value={e.name}
required
/>
<Form.Input
width={6}
onChange={this.handleCompanyChange}
label="Segmento"
placeholder="Segmento"
name="segment"
value={e.segment}
required
/>
</Form.Group>
</Form>
))}
</Segment>
);
}
}
My problem is that I can't set the company state properly. How can you update the state in relation to the changes in the form fields?
Looking for answers, I found the package: immutability-helper. Based on this answer, the problem was solved simply and elegantly.
The solution:
import update from 'immutability-helper';
//...
this.state = {
company: [
{
name: '',
segment: ''
}
]
};
//...
handleCompanyChange = (e, { name, value, id }) => {
let newState = update(this.state, {
company: {
[id]: {
[name]: { $set: value }
}
}
});
this.setState(newState);
};
//...
render() {
const { company } = this.state;
return (
<Segment>
{company.map((e, index) => (
<Form size="large" key={index}>
<Form.Group>
<Form.Input
width={6}
onChange={this.handleCompanyChange}
label="Nome"
placeholder="Nome"
name="name"
value={e.name}
id={index}
required
/>
<Form.Input
width={6}
onChange={this.handleCompanyChange}
label="Segmento"
placeholder="Segmento"
name="segment"
value={e.segment}
id={index}
required
/>
</Form.Group>
</Form>
))}
</Segment>
);
}

Categories