ReactJS: update a controlled input field problem - javascript

I have been reading some threads on SO but I could not figure out how to solve this issue or why it's happening. Can someone explain it like I'm 5?
Warning: A component is changing a controlled input of type text to be
uncontrolled. Input elements should not switch from controlled to
uncontrolled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component
I am developing a lesson creator, and the user must be able to open an existing lesson, hence the input fields must be programmatically filled with the content of the existing lesson.
My constructor:
constructor(props) {
super(props);
this.state = {
lessonID: -1,
sectionsArray: [],
title: 'No title',
type: 'r',
language: 'gb',
book: 'booka',
level: '1',
loading: false,
saved: true,
messageBox: '',
lessonOpenModal: false,
}
this._state = this.state;
this.updateSectionsFromChild = this.updateSectionsFromChild.bind(this);
this.sectionAdd = this.sectionAdd.bind(this);
this.sectionRemove = this.sectionRemove.bind(this);
this.menuInput = this.menuInput.bind(this);
this.menuDropDown = this.menuDropDown.bind(this);
this.lessonCreate = this.lessonCreate.bind(this);
this.lessonSave = this.lessonSave.bind(this);
this.lessonDelete = this.lessonDelete.bind(this);
this.lessonOpen = this.lessonOpen.bind(this);
this.sections = [];
}
This are the functions that update the controlled components:
menuDropDown(event, data) {
this.setState({
[data.name]: data.value,
saved: false,
});
console.log(data.name);
console.log(data.value);
}
menuInput(event) {
this.setState({
[event.target.name]: event.target.value,
saved: false,
});
}
And then this is the part of code that retrieves the lesson and tries to update the state:
async openLesson(lessonID) {
await ARLessonOpen(lessonID).then((result) => {
this.setState(this._state);
this.setState({
id: result.lesson.id,
language: result.lesson.language,
book: result.lesson.book, // this is a drop down, and it's not causing errors
type: result.lesson.type, // this is a drop down, and it's not causing errors
level: result.lesson.level, // this is a drop down, and it's not causing errors
title: result.lesson.title, // this is an input, and IT'S THE ISSUE
sectionsArray: result.sections.map((section, i) => ({
key: i,
id: i,
title: section.title,
duration: section.duration,
content: section.content,
}))
})
}).catch(function(error) {
console.log(error);
});
}
The only field that is not working is the 'title' and I can't understand why. How can I update the input value programmatically?
JSX:
renderSections = () => {
if (this.state.sectionsArray.length > 0) {
return this.state.sectionsArray.map((section, i) =>
<LessonSection
key={section.id}
id={section.id}
title={section.title}
duration={section.duration}
content={section.content}
sectionRemove={this.sectionRemove}
sectionAdd={this.sectionAdd}
updateSectionsFromChild={this.updateSectionsFromChild}
/>
)
} else {
return (
<div style={{color: 'black'}}>
<Button
size='mini'
icon='plus square outline'
onClick={this.sectionAdd} />
Add a section to start creating your lesson.
</div>
)
}
}
render() {
return (
<div className='Lesson-editor'>
{this.state.messageBox}
<div style={{display: 'none'}}>
<DefaultLoader
active={this.state.loading}
message={this.state.message}
/>
</div>
<div className="Lesson-editor-menu Small-font">
<div className="Menu-buttons">
<Button
size='mini'
icon='plus square outline'
onClick={this.sectionAdd} />
<Button
size='mini'
icon='file outline'
onClick={this.lessonCreate} />
<DialoglessonOpen
open={this.state.lessonOpenModal}
actionOnLessonSelected={(lessonID) => this.openLesson(lessonID)}
onCancel={() => this.setState({lessonOpenModal: false})} />
<Button size='mini' icon='open folder outline' text='Open lesson' description='ctrl + o' onClick={this.lessonOpen} />
<Button
size='mini'
icon='save outline'
onClick={this.lessonSave} />
<Button
size='mini'
icon='delete'
onClick={this.lessonDelete} />
<Button
size='mini'
icon='delete'
color='red'
onClick={ARClearTables} />
</div>
<Input
className='title'
fluid
placeholder='Lesson title'
value={this.state.title}
name='title'
onChange={this.menuInput}
/>
<div>
<Dropdown
fluid
compact
placeholder='Language'
search
selection
options={lessonLanguages}
//defaultValue='gb'
value={this.state.language}
name='language'
onChange={this.menuDropDown}
/>
<Dropdown
fluid
compact
placeholder='Book'
search
selection
options={lessonBooks}
//defaultValue='booka'
value={this.state.book}
name='book'
onChange={this.menuDropDown}
/>
<Dropdown
fluid
compact
placeholder='Lesson type'
search
selection
options={lessonTypes}
defaultValue='r'
name='type'
onChange={this.menuDropDown}
/>
<Dropdown
fluid
compact
placeholder='Lesson level'
search
selection
options={lessonLevels}
defaultValue='1'
name='level'
onChange={this.menuDropDown}
/>
</div>
</div>
<div className='Sections'>
{ this.renderSections() }
</div>
</div>
);
}
}

The initial value of input forms fields cannot be undefined or null, if you want to control it later. It should be an empty string. If you provide an undefined or null it's uncontrolled component.
In you code, React doesn't see any value to the input fields, so React believe it is a un-controlled component on the first mount. Later, when you add a value to the component React warning you that you can't give a value (controlled component) after you didn't provided a value (uncontrolled component)

I figured it out: the problem is that there was an error in my code. I was assigning a null value to the input field value in state.
async openLesson(lessonID) {
await ARLessonOpen(lessonID).then((result) => {
this.setState(this._state);
this.setState({
/* HERE: I try to access result.lesson but it's null! I should
use result.lesson[0]. So the problem is that I was
assigning a null value to the input field resulting in the error */
id: result.lesson.id,
language: result.lesson.language,
book: result.lesson.book,
type: result.lesson.type,
level: result.lesson.level,
title: result.lesson.title,
sectionsArray: result.sections.map((section, i) => ({
key: i,
id: i,
title: section.title,
duration: section.duration,
content: section.content,
}))
})
}).catch(function(error) {
console.log(error);
});
}

Related

How to fix redux-form "validate" when rendering an input from a variable in react-admin?

So I'm using a custom-built dialog component in react-admin which has a SimpleForm inside it. When I try to use input-level validate attribute on a LongTextInput, it just doesn't work. There is no error in the console or anything like that, but the input just accepts the text as is without validation. I don't seem to understand where I'm going wrong.
In the code that you see below, the inputs in the form that I have to render are conditional based on the props I'm passing from the parent. Hence, creating the form in a variable and then rendering it in the return() method.
What I've tried is rendering the inputs directly in the return() method without using the variable, but that does not seem to fix the validation problem as well.
Here's the array that defines the validation parameters I want to use -
const validateReason = [required(), minLength(5), maxLength(100)];
And I'm using this in the below render() method -
render() {
const props = this.props;
let renderForm = null;
if(props.formType === 'reject') {
renderForm =
(<SimpleForm
form="form"
toolbar={null}>
<RadioButtonGroupInput onChange={this.handleChoiceChange} source="reason" choices={[
{ id: '1', name: 'Inappropriate' },
{ id: '2', name: 'Abusive' },
{ id: '3', name: 'Not Product Related' },
{ id: '4', name: 'Others' },
]} />
<FormDataConsumer label="Select L2 Category" alwaysOn>
{({ formData }) => formData.reason === '4' &&
<LongTextInput
defaultValue={this.state.reasontext}
onChange={this.handleReasonChange}
source="reason_text"
label={props.title}
validate={validateReason}
/>
}
</FormDataConsumer>
</SimpleForm>);
}
else if(props.formType === 'skip') {
renderForm =
(<SimpleForm
form="form"
toolbar={null}>
<LongTextInput
defaultValue={this.state.reasontext}
onChange={this.handleReasonChange}
source="reason_text"
label={props.title}
validate={validateReason}
/>
</SimpleForm>);
}
return (
<Fragment>
<Dialog
fullWidth
open={props.showDialog}
aria-label={props.title}
>
<DialogTitle>{props.title}</DialogTitle>
<DialogContent>
{renderForm}
</DialogContent>
<DialogActions>
<SaveButton
onClick={() => {
props.handleClose();
this.handleSaveClick(props.formType);
}}
label={props.positiveButtonText}
/>
<Button onClick={() => {
this.clearReason();
props.handleClose();
}}>
Cancel
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
I expect that the validate attribute validates my input correctly according to my validation parameters.

ReactJS: array of compoents issue

I have a Main.js page that has one button: when you click it it adds a Block component to an array and to the page. You can add as many Block components as you want. Each Block component has a "delete" button, that will remove the Block from the array and from the page.
Menu.js:
import React from 'react';
import './Menu.css';
import Block from './Block.js';
import './Block.css';
export default class Menu extends React.Component {
constructor(props) {
super(props);
this.state = { value: '', blocksArray: [] };
this.addBlock = this.addBlock.bind(this);
this.removeBlock = this.removeBlock.bind(this);
this.blocks = [];
}
addBlock() {
this.blocks.push({ title: 'Section title' + this.blocks.length, content: 'Content' + this.blocks.length });
this.setState({ value: '', blocksArray: this.blocks });
}
removeBlock(index) {
this.blocks.splice(index, 1);
this.setState({ value: '', blocksArray: this.blocks })
}
renderBlocks = () => {
return (
this.state.blocksArray.map((block, index) =>
<Block
remove={() => this.removeBlock(index)}
key={index}
title={block.title}
content={block.content}
/>
)
)
}
render() {
return (
<div>
<div className="Menu">
<header className="Menu-header">
<button className="Menu-button" onClick={ () => this.addBlock() }>Add block</button>
</header>
</div>
<div>
{ this.renderBlocks() }
</div>
</div>
);
}
}
Block.js (version 1)
import React from 'react';
import './Block.css';
class Block extends React.Component {
constructor(props) {
super(props);
this.state = {
title: props.title,
content: props.content,
remove: props.remove
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handleSubmit(event) {
//alert('A name was submitted: ' + this.state.title);
event.preventDefault();
}
render() {
return (
<div className="Block-container">
<form onSubmit={this.handleSubmit}>
<div className="Block-title">
<label>
Title:
<input type="text" name="title" value={this.props.title} onChange={this.handleChange} />
</label>
</div>
<div className="Block-content">
<label>
Content:
<input type="text" name="content" value={this.props.content} onChange={this.handleChange} />
</label>
</div>
<input type="submit" value="Save" />
<input type="button" value="Delete" onClick= { () => this.state.remove() } />
</form>
</div>
);
}
}
export default Block;
The issue: I found myself stuck with 2 situations and neither works properly.
First non working solution for Block.js:
<input type="text" name="title" value={this.props.title} onChange={this.handleChange} />
<input type="text" name="content" value={this.props.content} onChange={this.handleChange} />
If I use value={this.props.content} and value={this.props.title} when I push the delete button on the Block it works but I can't edit the text in the field since its value is always retrieved from the props.
Second non working solution for Block.js:
<input type="text" name="title" value={this.state.title} onChange={this.handleChange} />
<input type="text" name="content" value={this.state.content} onChange={this.handleChange} />
If I use value={this.state.content} and value={this.state.title} I can edit the text fields and when I push the delete button on the Block it removes properly the component from the array, but the text displayed in the fields is wrong (it's as if it's always popping only the last component from the array). Let me explain with a few screenshots.
Let's say I added 4 Block components, as follow:
Then I click on the delete button of the Block with "Section title1" / "Content1", as this screenshot:
It apparently removes the right element in the array, but for some reason I get the wrong text in the component:
Array console.log:
0: Object { title: "Section title0", content: "Content0" }
1: Object { title: "Section title2", content: "Content2" }
2: Object { title: "Section title3", content: "Content3" }
Displayed text:
I'm obviously missing something and I have been stuck for a while. Can someone explain what is wrong?
I think the problem is you are setting index as the key for each Block.
Origin keys are [0, 1, 2, 3]. When you remove Section title1, new render will produce keys [0, 1, 2]. So React assumes that element with keys [0, 1, 2] are not changed and key 3 is removed. So it removed the last one.
Try to use an unique property for the key.
You can read more here: https://reactjs.org/docs/reconciliation.html#keys
Your change handler needs to be operating on the state in the parent component where the title/content are coming from. The values shown in the Blocks are being read from the Menu's state, so while editing the block data changes its own internal state, the values coming from the menu to the block as props are staying the same because the internal state is not being fed back.
You could write a function to edit the arrays in the Menu state in place:
this.editBlock = this.editBlock.bind(this);
...
editBlock(index, newBlock) {
let blocks = Array.from(this.state.blocksArray);
blocks[index] = newBlock;
this.setState({
blocksArray: blocks
})
}
and then pass that to the Block as props and call it when the change event fires:
<Block
remove={() => this.removeBlock(index)}
key={index}
title={block.title}
content={block.content}
index={index}
editBlock={this.editBlock}
/>
handleChange(event) {
this.setState({[event.target.name]: event.target.value}, () => {
this.props.editBlock(this.props.index, { title: this.state.title, content: this.state.content})
});
}
Working demo here.

Identifying what item have been deleted (created and modifed) in a Formik FieldArray

Was wondering if Formik has a native solution for identifying the addition and deletion (and update) of FieldArray in the form ?
I have the code on sandbox here https://codesandbox.io/s/jn7x2m75o9 ( based on the original Formik Array example # https://github.com/jaredpalmer/formik/blob/master/examples/Arrays.js )
but also the relevant part here :
With an Initial state of 3 friend defined, how can I know in my onSubmithandler which one were modified,deleted,updated.
import React from "react";
import { Formik, Field, Form, ErrorMessage, FieldArray } from "formik";
const initialValues = {
friends: [
{
name: "Friend_A",
email: "email_A#somewhere.com"
},
{
name: "Friend_B",
email: "email_B#somewhere.com"
},
{
name: "Friend_C",
email: "email_C#somewhere.com"
}
]
};
const mySubmit = values => console.log();
const SignIn = () => (
<div>
<h1>Invite friends</h1>
<Formik
initialValues={initialValues}
onSubmit={values => {
var itemRemoved = values.GetItemRemoveFromArray; // This is what I'm looking for
console.log(itemRemoved);
// Would print Friend_A
var itemAdded = values.GetItemAddedFromArray; // This is what I'm looking for
console.log(itemAdded);
// Would print New_Friend
var itemUpdated = values.GetItemUpdatedInArray; // This is what I'm looking for
console.log(itemUpdated);
// Would print Friend_C
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
}, 500);
}}
render={({ values }) => (
<Form>
<FieldArray
name="friends"
render={({ insert, remove, push }) => (
<div>
{values.friends.length > 0 &&
values.friends.map((friend, index) => (
<div className="row" key={index}>
<div className="col">
<label htmlFor={`friends.${index}.name`}>Name</label>
<Field
name={`friends.${index}.name`}
placeholder="Jane Doe"
type="text"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<label htmlFor={`friends.${index}.email`}>Email</label>
<Field
name={`friends.${index}.email`}
placeholder="jane#acme.com"
type="email"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<button
type="button"
className="secondary"
onClick={() => remove(index)}
>
X
</button>
</div>
</div>
))}
<button
type="button"
className="secondary"
onClick={() => push({ name: "", email: "" })}
>
Add Friend
</button>
</div>
)}
/>
<button type="submit">Invite</button>
</Form>
)}
/>
</div>
);
export default SignIn;
So if with the above a user where to :
Click on the X below Friend_A
Modify Friend_C email to email_C#nothere.com
Click "Add Friend"
Enter value Name: New_Friend_X and email: XX#YY.com
Click "Add Friend"
Enter value Name: New_Friend_Z and email: Friend_Z#coolplace.com
Click "X" button below newly entered "New_Friend_X"
Click "Invite"
in my mySubmit I'm looking for a way to easily get :
Friend_A was Removed
Friend_C was Modified
New_Friend_Z was added (was not in the original initialValues to formik)
(I Don't care about New_Friend_X. No need to know it was added/removed )
Point of this is to minimize rest call to the back end to create/update entity/link and also I really dont want to write my own "secondary state" in the onClick handler of the remove button before calling the remove(index) handler provided by Formik to track what need to be deleted from the DB.
Its not built into Formik, but it is not hard to do in javascript.
First, understand that Formik clones the object you give to initialValues. So in onSubmit, you will compare the final value to your original object.
The incoming data:
const initialFriends = [
{
name: "Friend_A",
email: "email_A#somewhere.com"
},
{
name: "Friend_B",
email: "email_B#somewhere.com"
},
{
name: "Friend_C",
email: "email_C#somewhere.com"
}
];
const initialValues = { friends: initialFriends };
Modified Formik declaration:
<Formik initialValues={initialValues}
...
onSubmit={values => {
const { added, deleted, changed } = addDeleteChange(
initialFriends,
values.friends
);
setTimeout(() => {
alert(
"Added: " + JSON.stringify(Object.fromEntries(added.entries()))
);
alert(
"Deleted: " + JSON.stringify(Object.fromEntries(deleted.entries()))
);
alert(
"Changed:" + JSON.stringify(Object.fromEntries(changed.entries()))
);
alert(JSON.stringify(values, null, 2));
}, 500);
}}
...
Helper functions:
function partition(array, filter) {
let pass = [],
fail = [];
array.forEach(e => (filter(e) ? pass : fail).push(e));
return [pass, fail];
}
const addDeleteChange = (in1, out1) => {
let inMap = new Map(in1.map(f => [f.name, f]));
let outMap = new Map(out1.map(f => [f.name, f]));
let inNames = new Set(inMap.keys());
let outNames = new Set(outMap.keys());
let [kept, added] = partition(out1, f => inNames.has(f.name));
let deleted = in1.filter(f => !outNames.has(f.name));
//alert(JSON.stringify(Object.fromEntries(deleted.entries())));
let changed = kept.filter(f => f.email !== inMap.get(f.name).email);
//alert(JSON.stringify(Object.fromEntries(changed.entries())));
return { added: added, deleted: deleted, changed: changed };
};
Code in codesandbox
NOTE: If you change the name of a friend, that will appear as a delete of original friend and an add of a new friend.
A more robust solution would be to add a (hidden) "id" field to each friend. Then instead of comparing name, would compare id.
That requires generating a new id as add each friend.

getting a controlled input warning but i am not sure what is wrong

I am writing a form in react (which I am new to), and that form opens after I click a menu item that will pass the selected item id. The first loading is fine, but when I click on one of the input and type something, I get:
A component is changing a controlled input of type text to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
I am not sure how to fix that, as the places I read were telling me that my component would give me that message if I am initializing it with undefined, which I don't think I am in this case.
class EditMenu extends React.Component {
constructor(props) {
super(props);
console.log('props constructor:', props);
this.state = {
item: {}
};
this.itemTitleName = 'name';
this.itemTitleDescription = 'description';
this.itemTitletag = 'tag';
}
componentWillMount() {
console.log('will mount');
let itemId = this.props.selectedItem;
let item = this.getitemItem(itemId);
this.setState({ item: item });
}
getitemItem(itemId) {
const itemsInfo = [
{
id: 44,
title: 'title1',
description: 'desc1',
tag:''
},
{
id: 11,
title: 'title2',
description: 'desc2',
tag:''
},
{
id: 222,
title: 'tiotle3',
description: 'desc3',
tag:''
},
];
let item = _.find(itemsInfo, { id: itemId });
return item;
}
componentWillReceiveProps(nextProps) {
console.log('received props!')
const nextId = nextProps.selectedItem;
if (nextId !== this.state.item.id) {
this.setState({ item: this.getitemItem(nextId) });
}
}
handleInputChange = (event) => {
console.log('input change ');
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
console.log(name);
this.setState({
item: {
[name]: value
}
});
}
render() {
return (
<div className="form-container">
<form onSubmit={this.handleSubmit} >
<TextField
id="item-name"
name={this.itemTitleName}
label="item Name"
margin="normal"
onChange={this.handleInputChange}
value={this.state.item.title}
/>
<br />
<TextField
id="item-desc"
name={this.itemTitleDescription}
label="item Description"
margin="normal"
onChange={this.handleInputChange}
value={this.state.item.description}
/>
<br />
<TextField
className="tag-field-container"
name={this.itemTitletag}
label="tag"
type="number"
hinttext="item tag" />
<br /><br />
Photos:
<br /><br />
<Button variant="raised" onClick={this.handleSaveButtonClick} className="button-margin">
Save
</Button>
</form>
</div>
);
}
}
the places I read were telling me that my component would give me that
message if I am initializing it with undefined, which I don't think I
am in this case.
Actually you are :)))
your state is an empty object at the beginning:
this.state = {
item: {}
};
Which means:
this.state.item.description
this.state.item.title
...are undefined. And that's what you pass to your controls as a value - undefined.
<TextField
...
value={this.state.item.title}
/>
<br />
<TextField
...
value={this.state.item.description}
/>
Try to set initial value:
this.state = {
item: {
description: '',
title: '',
}
};
Forms work differently in React, because forms keep some internal state. The documentation provides a good run down

Uncaught TypeError: Cannot read property 'icon' of null

i have a form for editing the tab. When a edit icon is clicked to edit that tab a form in dialog box appears where the input box has current data in it. But when i hit save without touching the icon field i get an error of Uncaught TypeError: Cannot read property 'icon' of null. If i did not touch the name field and only touch on icon field and hit save button then the tab gets edited. How can i make icon field work too like name field is working ? I mean if i want to only edit name, i can edit the name from name field and save without touching icon field which will save the tab name with edited name and current icon.
How can it be possible?
class EditForm extends Component {
render() {
const { tab } = this.props;
console.log('tab object is', this.props.tab);
const listOfIcon = _.map(this.props.fetchIcon.icons, (singleIcon) => ({
text: singleIcon.name,
id: singleIcon.id,
value: <MenuItem primaryText={singleIcon.name} />
}));
return (
<div>
<form
onSubmit={(e) => {
console.log('auto', e.target.auto);
e.preventDefault();
this.props.editTab(
tab.id,
e.target.text.value,
this.state.icon
);
this.props.closeTabIcon();
}
}
>
<div className="tab-name">
<TextField
hintText={tab.name}
name="text"
defaultValue={tab.name}
hintStyle={{ display: 'none' }}
floatingLabelStyle={{ color: '#1ab394' }}
floatingLabelFocusStyle={{ color: '#1db4c2' }}
underlineStyle={{ borderColor: '#1ab394' }}
/>
</div>
<div className="icon">
<AutoComplete
floatingLabelText={tab.icon}
name="auto"
filter={AutoComplete.noFilter}
openOnFocus
dataSource={listOfIcon}
textFieldStyle={{ borderColor: '#1ab394' }}
className="autocomplete"
onNewRequest={(e) => { this.setState({ icon: e.id }); }}
/>
</div>
<button className="btn">Save</button>
</form>
</div>
);
}
}
const mapStateToProps = state => {
console.log(state);
return {
fetchIcon: state.fetchIcon,
tabs: state.tabs.tabs.map(tab => {
const icons = state.fetchIcon.icons.find(icon => Number(icon.id) === tab.icon);
return {
...tab,
icon: icons && icons.name
};
})
};
};
function mapDispatchToProps(dispatch) {
return bindActionCreators({
editTab,
closeTabIcon
}, dispatch);
}
The state of a componnet is intitated with the null. YOu can set the intital value of state in constrocutor of the class
class EditForm extends Component {
constructor(props) {
super(props)
this.state ={}
}
render() {
const { tab } = this.props;
console.log('tab object is', this.props.tab);
const listOfIcon = _.map(this.props.fetchIcon.icons, (singleIcon) => ({
text: singleIcon.name,
id: singleIcon.id,
value: <MenuItem primaryText={singleIcon.name} />
}));..........
initialize 'input box' with empty value from code behind.

Categories

Resources