Dynamically creating a new component based off another component's state - reactjs

First timer here on StackOverflow! I'm trying to emulate a terminal interface for one of my portfolio projects. The way I have envisioned the terminal is that each terminal box has a state object with a few key/value pairs. Ideally, when someone enters text into the terminal box input form, the input becomes disabled and a new terminal box is rendered on the screen with a dynamic response based upon the userInput text which has been saved in the state. Where I'm stuck:
Once userInput state has been updated, how do I get a new terminal box to render beneath the prior box on the screen?
Prior to rendering, how do I set the initial state of the newly-rendered terminal box back to default with the exception of the "output" which would be re-valued to an appropriate response that I set?
How do I access the state in the prior terminal box so I can "read" the userInput stored there so I can determine what the appropriate response to that input would be?
I've included copies of each of the components below:
App.js
import React from "react";
import Terminal from "./components/Terminal";
import "./App.css";
class App extends React.Component {
render() {
return (
<div>
<Terminal />
</div>
);
}
}
export default App;
Terminal.js
import React, { Component } from "react";
import Form from "./Form";
import Falcon from "./Falcon";
import Messages from "./Alerts/Messages";
class Terminal extends Component {
state = {
output: Messages.intro,
userInput: "",
isComplete: false,
isDisabled: "",
};
markComplete = () => {
this.setState({
isComplete: true,
});
};
onSubmit = (event, userInput) => {
event.preventDefault();
this.setState({
userInput: userInput,
isDisabled: "disabled",
});
};
render() {
return (
<div>
<Falcon
output={this.state.output}
markComplete={this.markComplete}
isComplete={this.state.isComplete}
/>
<p />
<Form
input={this.state.userInput}
onSubmit={this.onSubmit}
isComplete={this.state.isComplete}
isDisabled={this.state.isDisabled}
/>
<p />
</div>
);
}
}
export default Terminal;
Falcon.js (Note: You'll see that there is a component "Typed" below - that is part of Matt Boldt's Typed.js (of which react-typed is an offshoot package) package which I'm using to simulate typing.)
import React, { Component } from 'react'
import Typed from 'react-typed'
class Falcon extends Component {
state = {
output: this.props.output,
};
render() {
return (
<div>
<Typed
strings={[this.state.output]}
typeSpeed={40}
onComplete={(self) => {
self.cursor.remove();
this.props.markComplete();
}}
/>
</div>
);
}
}
export default Falcon;
Form.js
import React from "react";
class Form extends React.Component {
state = {
input: this.props.input,
};
render() {
return (
<form
style={{
display: this.props.isComplete === false ? "none" : "",
}}
onSubmit={(event) => {
this.props.onSubmit(event, this.state.input);
}}
>
{"> "}
<input
ref={(input) => input && input.focus()}
type="text"
disabled={this.props.isDisabled}
style={{
border: "none",
outline: "none",
backgroundColor: "#FFF",
color: "#000",
}}
value={this.state.input}
onChange={(event) => this.setState({ input: event.target.value })}
/>
</form>
);
}
}
export default Form;
Any insight or guidance you can offer would be much appreciated! Thank you for helping this "first-timer" out!

Welcome to StackOverflow! I made a codesandbox demo with a few changes.
When developing React applications, it's a good practice to model the UI (the DOM elements) as a function of your internal state. You update the state and the UI changes automatically, it reacts to updates.
That said, you probably want to consider using the form only for the actual input element at the bottom of the terminal. The "past buffer" is just an array that only increases its content with user input and program output. Another good practice (actually a commandment!) is to never mutate the state. So, if you want to update the array, you create a new one from scratch, as in:
this.setState((state) => ({
conversation: [
...state.conversation, // we spread the previous state into the new one
{ text: state.userInput, id: faker.random.uuid(), type: "input" } // the last element is appended
]
}));}
Notice how setState (in class components) just schedules an update for the fields that you used. As your app scales, you will probably want to limit the length of this array.
The terminal component could be like:
class Terminal extends Component {
state = {
output: "Messages.intro",
userInput: "",
isComplete: false,
isDisabled: "",
conversation: [] // couldn't think of a nice name :(
};
markComplete = () => {
this.setState({
isComplete: true
});
};
onChange = (event) => {
this.setState({ userInput: event.target.value });
};
onSubmit = (event) => {
event.preventDefault();
this.setState((state) => ({
userInput: "",
conversation: [
...state.conversation,
{ text: state.userInput, id: faker.random.uuid(), type: "input" }
]
}));
};
render() {
const { conversation, userInput, output, isComplete } = this.state;
return (
<div>
<Falcon
output={output}
markComplete={this.markComplete}
isComplete={isComplete}
/>
<p />
// This is not really a form. Should be changed to another readonly element
{conversation.map((item, index) => (
<Form
key={item.id}
input={item.text}
onSubmit={this.onSubmit}
isComplete={isComplete}
isDisabled
/>
))}
<p />
<Form
input={userInput}
onSubmit={this.onSubmit}
isComplete={isComplete}
isDisabled={false}
onChange={this.onChange}
/>
<p />
</div>
);
}
}

Related

How do I automatically create a new element upon submission of an input in ReactJs

I am currently working on my first react js project as a task list. It has a text input value which's value is passed into a function on submit to update a list element. My question is this, I want to make it that on submit it creates a new element which holds the value of the state. I have first tried using an object when it gave me an error that objects cannot be used for a child element. Which I opted by changing the state into an array but it still does not create a new element like I intended. Here is my code
import React, { Component } from "react";
import ReactDOM from "react-dom";
class Tests extends Component {
constructor(props) {
super(props);
this.state = {
value: "",
object: [],
};
this.updateInputValue = this.updateInputValue.bind(this);
this.submit = this.submit.bind(this);
}
updateInputValue(evt) {
this.setState({
value: evt.target.value,
});
}
submit() {
this.setState({
object: this.state.value,
value: "",
});
}
render() {
return (
<div>
<label for="text">Text 1</label>
<input
type="text"
label="text"
onChange={(evt) => this.updateInputValue(evt)}
onSubmit={this.submit}
value={this.state.value}
/>
<br />
<button onClick={this.submit} style={{ height: 50, width: 60 }}>
Submit
</button>
<ul>
<li>{this.state.object}</li>
</ul>
</div>
);
}
}
export default Tests;
ReactDOM.render(<Tests />, document.getElementById("root"));
If there are any suggestions on how to improve this code I wrote I will read them and try to adjust my code to be better. Thanks
You are essentially overwriting the value of object on your setState. What you need to do, is keep the old array, and append the last item. Using spread syntax:
submit() {
this.setState({
object : [...this.state.object, this.state.value],
value : "",
});
}
This creates a new Array with your new item appended. Be careful not to mutate the state (in other words, change this.state):
// the above code is essentially doing this:
const newObject = [...this.state.object, this.state.value];
this.setState({ object: newObject });
// 👻 buggy version! This is mutating the state:
this.state.object.push(this.state.value);
On your render, you need to render a map of objects:
<ul>
{ this.state.object.map(item => <li>{ item }</li>) }
</ul>

ReactJS Error: Maximum update depth exceeded

I have a LessonEditor component that passes down to children components props with a function called setValues:
const [values, setValues] = React.useState({
title: 'This is a long lesson title lol lol',
desc: 'This is a description',
location: '1',
content: 'I\'m a poteto',
});
const handleChange = (name, value) => {
console.log(name + value);
setValues({ ...values, [name]: value });
};
The first component to which I pass props is:
<LessonContents
editing={editing}
title={values.title}
desc={values.desc}
content={values.content}
location={values.location}
setValues={handleChange} // right here
/>
Then inside LessonContents I have another component where I pass the setValues function:
<TextEditor
content={props.content}
setContent={props.setValues}
/>
Then finally inside the TextEditor the code is:
import React from 'react';
export default function TextEditor(props) {
return (
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
onKeyDown={(e) => props.setContent('content', e.target.innerHTML)}
>
{props.content}
</div>
)
}
The error is triggered when I try to type text in the contentEditable div. Why?
EDIT: now I just realized that also the input text fields are causing the error, but if I delete this code contained in LessonContent.js the input text are working again:
<div className='box-content justify-left'>
<LessonLocation
location={props.location}
setLocation={props.setValues}
/>
</div>
<div className='box-content justify-left'>
<TextEditor
content={props.content}
setContent={props.setValues}
/>
</div>
I don't know exactly why this is happening, but if I disable updating on your LessonLocation component, the error stops:
export default class LessonLocation extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
const data = {
label: 'hello',
value: 'hello'
}
return <Tree data={data}></Tree>
}
};
It has to do with the <Tree /> updating. I don't know why it even updates, it doesn't seem like it needs the data you use. You should try to implement a check into shouldComponentUpdate() yourself if you need it to update based on the data.

ReactJS Accessing Props of Rendered Component

I am building a component that will be used for step-through processes such as :
This Workflow component takes an array of 'steps' as a prop and then it does the rest. Here is how it is being called in the image above :
let steps = [
{
display: "Sign Up Form",
component: SignupForm
},
{
display: "Verify Phone",
component: VerifyPhone
},
{
display: "Use Case Survey",
component: UseCase
},
{
display: "User Profile",
component: UserProfile
},
];
return (
<Workflow
steps={steps}
/>
);
The component field points to the component to be rendered in that step. For example the SignupForm component looks like this :
export default class SignupForm extends React.Component {
...
render() {
return (
<div>
<div className="page-header">
<h1>New User Sign Up Form</h1>
<p>Something here...</p>
</div>
<div className="form-group">
<input type="email" className="form-control" placeholder="Email address..." />
<small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
</div>
);
}
}
The issue I'm facing is that in each step there needs to be a Next button to validate the information in that step and move to the next. I was going to just put that button inside the component of each step, but that makes it less user-friendly. When a user clicks 'Next', and everything is valid, that step should be collapsed and the next step should open up. However this means that my Workflow component needs to render this button.
So, I need my Workflow component to call the method of each step component to validate the information in the step and return a promise letting it know if it passed or failed (with any error message). How do I need to call this method? Here is where the Workflow component renders all the steps
as <step.component {...this.props} />:
{
this.state.steps.map((step, key) => {
return (
...
<Collapse isOpen={!step.collapsed}>
<step.component {...this.props} />
<Button color="primary"
onClick={() => this.validate(key)}>Next</Button>
<div className="invalid-value">
{step.error}
</div>
</Collapse>
...
);
})
}
That renders the next button, as well as the onClick handler validate():
validate(i) {
let steps = _.cloneDeep(this.state.steps);
let step = steps[i];
step.component.handleNext().then(function () {
...
}).catch((err) => {
...
});
}
Ideally, step.component.validate() would call the validate method inside that component that has already been rendered:
export default class SignupForm extends React.Component {
....
validate() {
return new Promise((resolve, reject) => {
resolve();
})
}
render() {
...
}
}
.. which would have access to the state of that component. But, thats not how it works. How can I get this to work? I read a little about forwarding refs, but not exactly sure how that works. Any help is greatly appreciated!
One approach is to apply the Observer pattern by making your form a Context Provider and making it provide a "register" function for registering Consumers. Your consumers would be each of the XXXForm components. They would all implement the same validate API, so the wrapping form could assume it could call validate on any of its registered components.
It could look something like the following:
const WorkflowContext = React.createContext({
deregisterForm() {},
registerForm() {},
});
export default class Workflow extends React.Component {
constructor(props) {
super(props);
this.state = {
forms: [],
};
}
deregisterForm = (form) => {
this.setState({
forms: this.state.forms.slice().splice(
this.state.forms.indexOf(forms), 1)
});
};
registerForm = (form) => {
this.setState({ forms: [ ...this.state.forms, form ] })
};
validate = () => {
const validationPromises = this.state.forms.reduce(
(promises, formComponent) => [...promises, formComponent.validate()]);
Promise.all(validationPromises)
.then(() => {
// All validation Promises resolved, now do some work.
})
.catch(() => {
// Some validation Promises rejected. Handle error.
});
};
render() {
return (
<WorkflowContext.Provider
value={{
deregisterForm: this.deregisterForm,
registerForm: this.registerForm,
}}>
{/* Render all of the steps like in your pasted code */}
<button onClick={this.validate}>Next</button
</WorkflowContext.Provider>
);
}
}
// Higher-order component for giving access to the Workflow's context
export function withWorkflow(Component) {
return function ManagedForm(props) {
return (
<WorkflowContext.Consumer>
{options =>
<Component
{...props}
deregisterForm={options.deregisterForm}
registerForm={options.registerForm}
/>
}
</WorkflowContext.Consumer>
);
}
}
SignupForm and any other form that needs to implement validation:
import { withWorkflow } from './Workflow';
class SignupForm extends React.Component {
componentDidMount() {
this.props.registerForm(this);
}
componentWillUnmount() {
this.props.deregisterForm(this);
}
validate() {
return new Promise((resolve, reject) => {
resolve();
})
}
render() {
...
}
}
// Register each of your forms with the Workflow by using the
// higher-order component created above.
export default withWorkflow(SignupForm);
This pattern I originally found applied to React when reading react-form's source, and it works nicely.

react material-ui select not working

Im' new to react from angularjs, using material-ui for a project and I can't get the select component to work like a select component. Basically I want to populate the dropdown with an array of objects and do something with the selected object once a selection is made by the user. I've been running into a bunch of problems, the most recent is that I can't figure out how to set a default starting value when the component loads and I can't see the selected option in the GUI. I'm able to set the state and log it out to the console you just can't see it in the select component. Also, what is the difference between #material-ui/core and material-ui. Are they different libraries, different versions of the same library?
class HomePage extends React.Component {
constructor(props) {
super();
this.reportSelected = this.reportSelected.bind(this);
this.state = {
report: "report1"
};
}
static propTypes = {
classes: PropTypes.object
};
reports = [
{
name: "report1"
},
{
name: "report2"
},
{
name: "report3"
}
];
reportSelected = event => {
this.setState((prevState) => {
return {
report: event.target.value
}
}, console.log(this.state))
};
render() {
const { classes, headerTitle } = this.props;
return (
<div className={classes.homePage}>
<HeaderTitle title="Home" />
<Helmet>
<title>{headerTitle}</title>
</Helmet>
<form>
<FormControl className={classes.reportsDropdown}>
<InputLabel htmlFor="reports">Reports</InputLabel>
<Select
value={this.state.report}
onChange={this.reportSelected}
>
{this.reports.map(report => (
<MenuItem value={report.name} key={report.name}>
{report.name}
</MenuItem>
))}
</Select>
</FormControl>
</form>
</div>
);
}
}
UPDATE:
The following code works as expected,
class HomePage extends React.Component {
constructor(props) {
super();
this.reportSelected = this.reportSelected.bind(this);
this.state = {
report: "report1"
};
}
static propTypes = {
classes: PropTypes.object
};
reports = [
{
name: "report1"
},
{
name: "report2"
},
{
name: "report3"
}
];
reportSelected = event => {
this.setState(() => {
return {
report: event.target.value
}
})
};
render() {
const { classes, headerTitle } = this.props;
return (
<div className={classes.homePage}>
<HeaderTitle title="Home" />
<Helmet>
<title>{headerTitle}</title>
</Helmet>
<form>
<FormControl className={classes.reportsDropdown}>
<InputLabel htmlFor="reports">Reports</InputLabel>
<Select
value={this.state.report}
onChange={this.reportSelected}
>
{this.reports.map(report => (
<MenuItem value={report.name} key={report.name}>
{report.name}
</MenuItem>
))}
</Select>
</FormControl>
</form>
</div>
);
}
}
I would imagine the problem is that the initial selected value must match a value of an item in the select.
In the code sample you are using the name property this.reports[0].name as the initial value, but your menu items use the object itself for the value, i.e. value={report}.
Either use the name property for the value of the menu items or use this.reports[0] as your initial value and see if that works.
As for your second question, material-ui is the previous version of the library (the 0.xx series). #material-ui is the latest and greatest 1.11 version.

Formsy-material-ui do not validate initial render

Is there any way, one can delay first validation of components in formsy-material-ui so that validations like isNotEmpty do not fire on first render of the form and mess the UX? I am using controlled components, therefore setting value from state on each render.
<FormsyText
name="name"
value={this.state.name}
floatingLabelText="Name"
onChange={partial(this._changeInputValue, ['name'])}
validations={{ isNotEmpty }}
validationError="Field shoud not be empty"
/>
I needed this solution too. I've been looking into the source code of formsy-material-ui, and it seems that the text field is setting its value right before it's mounted. That's why the field is marked changed (aka not pristine) when the rendering happens, so the validation error is shown.
Anyways, I wrote a hackish solution using a higher order component. I've been testing with text fields only, but should work with any fields having this problem. The core concept: if the formsy field doesn't have a "validationErrors" prop, it's not showing any errors.
import React, { Component, PropTypes } from 'react';
export const preventFirstValidation = (FormsyField) => {
return class extends Component {
static propTypes = { preventFirstValidation: PropTypes.bool };
static defaultProps = { preventFirstValidation: true };
constructor(props) {
super(props);
this.state = { isChanged: false };
}
render() {
const { preventFirstValidation, ...fieldProps } = this.props;
return (
<FormsyField
{...fieldProps}
onChange={(evt, val) => {
if (!this.state.isChanged) this.setState({ isChanged: true });
if (this.props.onChange) this.props.onChange(evt, val);
}}
validationErrors={(this.state.isChanged || !preventFirstValidation) ? this.props.validationErrors : undefined}
/>
);
}
};
};
How to use it:
import { Form } from 'formsy-react';
import FormsyTextField from 'formsy-material-ui/lib/FormsyText';
const TextField = preventFirstValidation(FormsyTextField);
const MyForm = () => (
<Form>
{/* You're using the transformed field, exactly like before */}
<TextField
name = "some_field"
validationErrors={{ isRequired: 'This is really required!' }}
required
preventFirstValidation={ /* you can enable or disable this feature */ }
/>
</Form>
);

Resources