How to render conditional form in reactJS? - javascript

Conditionally, when having in imported array attribute called 'entry', I want to render form 'write', but 'write' is not displayed in the browser (no errors in console). Should I use child component for this, or maybe you have ideas for other approaches?
The code:
render() {
var replyList = questions.map(reply => {
return (
reply.feedback.map(singleReply => {
return (
<div>
<button
key={singleReply.id}
value={singleReply.button}
goto={singleReply.goto}
onClick={this.onButtonClick}>
{singleReply.button}
</button>
</div>
);
})
);
});
var write = (evt) => {
//argument dla input
var toWrite = questions[this.state.currentDialog].entry;
//jeśli jest entry'
if (questions[this.state.currentDialog].entry)
return (
<form onSubmit={this.onInputSubmit}>
<label value={toWrite.label} />
<input
name={toWrite.name}
value={toWrite.value}
onChange={this.onInputChange}
/>
<input type='submit' />
</form>
);
};
return (
//questions - pytanie, replyList - lista odpowiedzi
<div className="App">
{questions[this.state.currentDialog].question}
<br /><br />
{replyList[this.state.currentDialog]}
{this.write}
<br /><br />
</div>)
}
Piece of my array:
{
//[0]
id: uuid.v4(),
question: 'dialog1',
feedback: [
{ button: 'reply1-1', goto: 1, id: uuid.v4() },
{ button: 'reply1-2', goto: 2, id: uuid.v4() },
{ button: 'reply1-3', goto: 3, id: uuid.v4() },
{ button: 'reply1-4', goto: 4, id: uuid.v4() }
],
entry: { label: 'input1-1', name: 'input1', value: '1', id: uuid.v4() }
},

Inorder to display the write you need to call it as
return (
<div className="App">
{questions[this.state.currentDialog].question}
<br /><br />
{replyList[this.state.currentDialog]}
{write()}
<br /><br />
</div>)
this is not required since the write is defined inside the render method.You should also keep in mind the problem with putting functions inside render method.
A function in the render method will be created each render which is a
slight performance hit. It's also messy if you put them in the render,
which is a much bigger reason, you shouldn't have to scroll through
code in render to see the html output. Always put them on the class
instead.

write is part of the local scope for render, no need to call this.write. simply call write. More on this you have to call the function as well: write()
To add to this, not really part of the question but you will get an error. Every component has to return a 'component-like' value. If the condition is not fulfilled the write function will return undefined which will throw an error. Returning null will not throw an error as it's 'component-like'

Related

ReactJS: update a controlled input field problem

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);
});
}

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.

Trying to wrap child elements on React

I'm doing a form helper to react-native, I also want to implement a feature called namespace to the forms to get a modular result.
An example:
return(
<Form
ref="configForm"
>
<Form.TextInput
name="firstName"
/>
<Form.TextInput
name="lastName"
/>
<Form.Namespace name="address">
<Form.TextInput
name="city"
/>
<Form.TextInput
name="address"
/>
<Form.TextInput
name="zip"
/>
</Form.Namespace>
</Form>
);
In this case the final result must be:
{
firstName: 'John',
lastName: 'Tompson',
address: {
city: 'New York',
address: '3th Av 231',
zip: '32132'
}
}
To get fields values I map recursivery the children of FormNamespace, and I clone the elements that own the property isFormValue (this property is owned by elements that provide values to Form, wich are FormNamespace and FormField):
wrapChildren(child) {
if (!child) {
return child;
}
if (Array.isArray(child)) {
return child.map((c) => this.wrapChildren(c));
}
if (child.type.isFormValue) {
let ref = child.ref || child.props.name;
child = React.cloneElement(child, {
ref: ref,
style: {borderWidth: 1, borderColor: 'red'}, // for debug
onUpdateValue: this.handleFieldChange.bind(this, ref),
});
}
if (child.props.children && !child.type.isFormStopableWrapper) {
child.props.children = this.wrapChildren(child.props.children);
}
return child;
}
render() {
let childs = this.wrapChildren(this.props.children);
debugger;
return(
<View style={this.props.style}>
{childs}
</View>
);
};
All logic is in FormNamespace component, in fact, the Form component only renders a FormNamespace:
render() {
return(
<Namespace
ref="formData"
name="mainNamespace"
style={[styles.form, this.props.style]}
onUpdateValue={(data) => this._handleUpdateData(data)}
>
{this.props.children}
</Namespace>
);
};
The Form component works correctly on the first depth, but when must to clone the FormNamespace element, by some issue it does not work as it should.
Debugging, I checked the React.cloneElement is applied to the FormNamespace but in the final render the FormNamespace never is referenced (nor the border style is applied), unlike the FormField that works well.
I don't know what is wrong here, the final FormNamespace rendered has not define the onUpdateValue prop. The objetive is the FormNamespace be taken like a simple value provider to Form (like fields components) and this manage theirs own childs.
The complete component code are here:
FormNamespace: https://paste.kde.org/pyz5gdsgb
Form: https://paste.kde.org/p4kmc2k9j

How to print out an array of user inputs in ReactJs

Currently my code will print out what ever the user types into the input box after they have submitted it. If I type 'Dave', that gets printed out. If I then type 'Paul'. Paul replaces where Dave has been outputted. I want a way where if I type out another name instead of replacing it, it will print it out underneath, unlimited times. I am thinking of using an array instead of the current string but not sure how to do this. here is my code so far:
var ToDoForm = React.createClass({
getInitialState: function() {
return {text: '', submittedValue: ''};
},
handleChange: function(event) {
this.setState({text: event.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
this.setState({submittedValue: this.state.text});
console.log("ToDO: " + this.state.text);
},
render: function() {
return (
<div>
<h1> todos </h1>
<form className="todoForm" onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Type out a task"
value={this.state.text}
onChange={this.handleChange}
/>
<input
type="submit"
value="Submit todo"
/>
</form>
<h2> Data should appear here </h2>
<p>{this.state.submittedValue}</p>
</div>
);
}
});
So far text gets assigned to submittedValue and then submitted value is what gets printed out.
I have started with this
getInitialState: function() {
return {text: '', submittedValue: []};
},
but now I am stuck as to what to do next
Once sumittedValue is an array in state. You should be able just to push to it:
handleSubmit: function(e) {
e.preventDefault();
this.state.subittedValue.push(this.state.text);
this.setState({submittedValue: this.state.subittedValue});
console.log("ToDO: ", this.state.submittedValue);
},
Of course then you have to loop through the array (map) in order to render:
<h2> Data should appear here </h2>
{this.state.submittedValue.map(
function(value, i) {
return (<p key={'val-' + i}>{value}</p>);
}
)}
key is required to uniquely identify the looped <p>. Otherwise, a warning would show on render.

How to get a reference to target element inside a callback

I'm building a component which displays a series of generic input fields. The backing store uses a simple array of key-value pairs to manage the data:
[
{fieldkey: 'Signs and Symptoms', value:'fever, rash'},
{fieldkey: 'NotFeelingWell', value:'false'},
{fieldkey: 'ReAdmission', value:'true'},
{fieldkey: 'DateOfEvent', value:'12/31/1999'}
]
In order to eliminate a lot of boilerplate code related to data binding, the component uses these same keys when generating the HTML markup (see 'data-fieldname' attribute).
var Fields = React.createClass({
handleOnChange:function(e){
Actions.updateField( {key:e.target.attributes['data-fieldname'].value, value:e.target.value})
},
setValue:function(){
var ref = //get a reference to the DOM element that triggered this call
ref.value = this.props.form.fields[ref.attributes['data-fieldname']]
},
render:function(){
return (<div className="row">
<Input data-fieldname="Signs and Symptoms" type="text" label='Notes' defaultValue="Enter text" onChange={this.handleOnChange} value={this.setValue()} />
<Input data-fieldname="NotFeelingWell" type="checkbox" label="Not Feeling Well" onChange={this.handleOnChange} value={this.setValue()} />
<Input data-fieldname="ReAdmission" type="checkbox" label="Not Feeling Great" onChange={this.handleOnChange} value={this.setValue()} />
<Input data-fieldname="DateOfEvent" type="text" label="Date Of Event" onChange={this.handleOnChange} value={this.setValue()} />
</div>)
}
})
My goal is to use the same two functions for writing/reading from the store for all inputs and without code duplication (i.e. I don't want to add a refs declaration to each input that duplicates the key already stored in 'data-fieldname') Things work swimmingly on the callback attached to the 'onChange' event. However, I'm unsure how to get a reference to the DOM node in question in the setValue function.
Thanks in advance
I'm not sure if I understand your question right, but to reduce boilerplate I would map your array to generate input fields:
render:function(){
var inputs = [];
this.props.form.fields.map(function(elem){
inputs.push(<Input data-fieldname={elem.fieldkey} type="text" label="Date Of Event" onChange={this.handleOnChange} value={elem.value} />);
});
return (<div className="row">
{inputs}
</div>)
}
This will always display your data in props. So when handleOnChange gets triggered the component will rerender with the new value. In my opinion this way is better than accessing a DOM node directly.
If you want to use dynamic information on the input, you need to pass it through the array, and make a loop.
Here is a little example based on Dustin code:
var fieldArray = [ //replace by this.props.form.fields
{
fieldkey: 'Signs and Symptoms',
value: 'fever, rash',
type: 'text',
label: 'Notes'
},
{
fieldkey: 'NotFeelingWell',
value: 'false',
type: 'checkbox',
label: 'Not Feeling Well'
},
];
var Fields = React.createClass({
handleOnChange:function(e){
var fieldKey = e.target.attributes['data-fieldname'].value;
Actions.updateField({
key: fieldKey,
value: e.target.value
})
},
render() {
var inputs = [];
fieldArray.map(function(field) { //replace by this.props.form.fields
inputs.push(
<Input
data-fieldname={field.fieldkey}
value={field.value}
type={field.type}
label={field.label}
onChange={this.handleOnChange} />
);
}.bind(this));
return (
<div className="row">
{inputs}
</div>
);
}
});

Categories

Resources