I have a wizard that has many forms, at the end of the wizard I want to take them back to the first step. However every form is filled in with the previous values.
I just want to unmount and remount it to wipe everything clean. How do I do this in reactjs?
<StepWizard>
<Step>
<NewComponent/>
</Step>
<Step>
<NewComponent/>
</Step>
<Step>
<NewComponent/>
</Step>
<Step>
<NewComponent/>
</Step>
</StepWizard>
so how would I trigger something to just get "StepWizard" to render in a fresh state?
My components look something like this, I removed code that switches to the next step in the wizard.
export default class NewComponent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
}
render() {
return (
<Formik
initialValues={{
name: "",
website: "",
}}
validationSchema={Yup.object().shape({
name: Yup.string().required('Company Name is Required'),
website: Yup.string().url('Company Url is Invalid'),
})}
onSubmit={(
values,
{ setSubmitting, setErrors}
) => {
}}
render={({
values,
handleChange,
handleBlur,
handleSubmit,
setFieldValue,
setFieldTouched
}) => (
<form onSubmit={handleSubmit}>
<div className="field">
<label className="label">Name</label>
<div className="control">
<input
className="input"
type="text"
name="name"
maxLength="50"
onChange={handleChange}
onBlur={handleBlur}
value={values.name}
/>
<ErrorMessage name="name"/>
</div>
</div>
<div className="field">
<label className="label">Website</label>
<div className="control">
<Field className="input" name="website" type="text" />
<ErrorMessage name="website"/>
</div>
</div>
</form>
)}
/>
);
}
}
I am using Mbox State Tree, so I could store something in my store that could be used to trigger whatever needs to be triggered to cause a reset.
Edit
I should mention that I am using this plugin: https://github.com/jcmcneal/react-step-wizard
So I am not sure if stopping a step from rendering is an option, also then that would mean I would have to handle the previous step state everytime.
I am more looking for something that just blows away everything if possible as I spent already too much time on this area and don't want to rework tons.
Highlighting the above methods you can also do something like this. Lift the default state into an object that can be pre-filled by whatever, hydrate it into the state and then when you call a reset you can control how much you reset the state back to. This is a very generic example but it's one way to overcome your issue.
Click here to view a working example
import React from "react";
import ReactDOM from "react-dom";
// generic stage renderer
const Stage = ({ step, currentStep, children }) => {
return step === currentStep ? <div>{children}</div> : null;
};
// generic input controller
const Input = ({ stateKey, value, handleOnChange }) => (
<input
value={value}
onChange={evt => handleOnChange(stateKey, evt.target.value)}
/>
);
// default state that is used to reference
const defaultState = {
firstName: '',
lastName: '',
// default state can also be prefilled with data..
telephoneNumber: '0123456789',
}
class App extends React.Component {
state = {
step: 1,
...defaultState
};
handleStateUpdate = (key, value) => {
this.setState({
[key]: value
});
};
incrementStep = () => {
if (this.state.step < 3) {
this.setState({
step: this.state.step + 1
});
}
};
goBack = () => {
const { step, lastName, telephoneNumber } = this.state;
this.setState({
step: 1,
// always reset this one
firstName: defaultState.firstName,
// only reset this one if it's step 3
lastName: step > 2
? defaultState.lastName
: lastName,
// last step blargh, let's reset anyway
telephoneNumber: step === 3
? defaultState.telephoneNumber
: telephoneNumber,
});
}
render() {
const { step, firstName, lastName, telephoneNumber } = this.state;
return (
<div>
{JSON.stringify(this.state)}
<h1>Step Wizard - {step}</h1>
<Stage step={1} currentStep={step}>
<Input
stateKey="firstName"
value={firstName}
handleOnChange={this.handleStateUpdate}
/>
</Stage>
<Stage step={2} currentStep={step}>
<Input
stateKey="lastName"
value={lastName}
handleOnChange={this.handleStateUpdate}
/>
</Stage>
<Stage step={3} currentStep={step}>
<Input
stateKey="telephoneNumber"
value={telephoneNumber}
handleOnChange={this.handleStateUpdate}
/>
</Stage>
<button onClick={this.goBack}>Go Back to Step 1</button>
<button onClick={this.incrementStep}>Next</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can achieve that by storing the current state of the wizard in, you guessed it, state object. That state and actions to mutate it can be passed to children as props. After that, when you need to reset the wizard, you just reset the state.
Here's an oversimplified example:
class StepWizard extends React.Component {
constructor(props) {
super(props);
this.state = {
step1: {},
step2: {}
};
}
setStep(step, data) {
this.setState({ `step${ step }`: data });
}
resetWizard() {
this.setState({
step1: {},
step2: {}
});
}
render() {
return (
<React.Fragment>
<Step
data={ this.state.step1 }
setData={ (data)=> this.setStep(1, data) }
/>
<Step
data={ this.state.step2 }
setData={ (data)=> this.setStep(2, data) }
/>
</React.Fragment>
);
}
}
Now, call resetWizard whenever you'll need to reset the wizard.
How about creating a Step object that would have the render logic for each step? I understand your use case correctly, since you would want to render only one step at a time why not only render which is relevant at that particular step?
Something like below.
class Wizard {
constructor(props) {
super(props);
this.stepMap = {
first: <FirstStep />,
second: <SecondtStep />,
third: <ThirdStep />,
fourth: <FourthStep />
}
this.state = {
activeStep: "first"
}
}
changeStep = (stepId) => {
this.setState({activeStep: stepId});
}
render() {
const activeStepCmp = this.stepMap[this.state.activeStep];
return (
<StepWizard>
{activeStepCmp}
</StepWizard>
)
}
}
Related
I'm working on a CV Generator and I don't know how to properly append the school and field of study values to a new div inside React.
Using the onSubmit function I'm able to get the values after filling them out and clicking save, but I can't figure out where to go from here.
Update
What I want to do is take the values from the input and create a new div above the form that displays those values. For example, I want the School value to show
School: University of Whatever
And the same goes for Field of Study.
Field of Study: Whatever
I know how to do this in vanilla JS but taking the values and appending them to the DOM but it doesn't seem to work that way in React.
class Education extends Component {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit = (e) => {
e.preventDefault();
const schoolForm = document.getElementById("school-form").value;
const studyForm = document.getElementById("study-form").value;
};
render() {
return (
<>
<h1 className="title">Education</h1>
<div id="content">
<form>
<label for="school">School</label>
<input
id="school-form"
className="form-row"
type="text"
name="school"
/>
<label for="study">Field of Study</label>
<input
id="study-form"
className="form-row"
type="text"
name="study"
/>
<button onClick={this.onSubmit} className="save">
Save
</button>
<button className="cancel">Cancel</button>
</form>
)}
</div>
</>
);
}
}
export default Education;
You should use state in order to save the values then show it when the user submits.
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = { scool: "", study: "", showOutput: false };
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit = (e) => {
e.preventDefault();
this.setState({
showOutput: true
});
};
setStudy = (value) => {
this.setState({
study: value
});
};
setSchool = (value) => {
this.setState({
school: value
});
};
render() {
return (
<>
<h1 className="title">Education</h1>
<div id="content">
{this.state.showOutput && (
<>
<div>{`school: ${this.state.school}`}</div>
<div>{`study: ${this.state.study}`}</div>
</>
)}
<form>
<label for="school">School</label>
<input
id="school-form"
className="form-row"
type="text"
name="school"
onChange={(e) => this.setSchool(e.target.value)}
/>
<label for="study">Field of Study</label>
<input
id="study-form"
className="form-row"
type="text"
name="study"
onChange={(e) => this.setStudy(e.target.value)}
/>
<button onClick={this.onSubmit} className="save">
Save
</button>
<button className="cancel">Cancel</button>
</form>
)
</div>
</>
);
}
}
export default App;
I have also added 2 functions to set state and a condition render based on showOutput.
You don't append things to the DOM in react like you do in vanilla. You want to conditionally render elements.
Make a new element to display the data, and render it only if you have the data. (Conditional rendering is done with && operator)
{this.state.schoolForm && this.state.studyform && <div>
<p>School: {this.state.schoolForm}</p>
<p>Field of Study: {this.state.studyForm}</p>
</div>}
The schoolForm and studyForm should be component state variables. If you only have them as variables in your onSubmit, the data will be lost after the function call ends. Your onSubmit function should only set the state, and then you access your state variables to use the data.
Do not use document.getElementById. You don't want to use the 'document' object with react (Almost never).
You can access the element's value directly using the event object which is automatically passed by onSubmit.
handleSubmit = (event) => {
event.preventDefault();
console.log(event.target.school.value)
console.log(event.target.study.value)
}
I need to generate a certain number of fields according to a value that the user prompts in the first step of the form.
Since I'm doing a multi-step form with a "Wizard" class <Condition /> component doesn't work.
Basically I need to access to values (pull them from the class inside the functional component) in order to tell React the number of "pages" it needs to generate.
Something like this:
export default () => {(
<Wizard
initialValues={{ }}
onSubmit={onSubmit}
>
<Wizard.Page >
<FirstStep />
</Wizard.Page>
{values.items.map((item, index) => (
<Wizard.Page key="index">
<SecondStep stepName="item.name" key="index" />
</Wizard.Page>
) )}
</Wizard>
)}
In order to generate N pages according to the number of items the user created, one page for each item.
This approach does not work because it tells me that the values are not defined.
This is my Wizard React Component:
import React from 'react'
import PropTypes from 'prop-types'
import { Form } from 'react-final-form'
import arrayMutators from 'final-form-arrays'
export default class Wizard extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired
}
static Page = ({ children }) => children
constructor(props) {
super(props)
this.state = {
page: 0,
values: props.initialValues || {}
}
}
next = values =>
this.setState(state => ({
page: Math.min(state.page + 1, this.props.children.length - 1),
values
}))
previous = () =>
this.setState(state => ({
page: Math.max(state.page - 1, 0)
}))
validate = values => {
const activePage = React.Children.toArray(this.props.children)[
this.state.page
]
return activePage.props.validate ? activePage.props.validate(values) : {}
}
handleSubmit = values => {
const { children, onSubmit } = this.props
const { page } = this.state
const isLastPage = page === React.Children.count(children) - 1
if (isLastPage) {
return onSubmit(values)
} else {
this.next(values)
}
}
render() {
const { children } = this.props
const { page, values } = this.state
const activePage = React.Children.toArray(children)[page]
const isLastPage = page === React.Children.count(children) - 1
return (
<Form
initialValues={values}
validate={this.validate}
onSubmit={this.handleSubmit}
mutators={{...arrayMutators }}
>
{({ handleSubmit, submitting, values }) => (
<form onSubmit={handleSubmit}>
{activePage}
<div className="buttons centered">
{page > 0 && <button type="button" onClick={this.previous} >« Previous </button>}
{!isLastPage && <button type="submit">Next »</button>}
{isLastPage && <button type="submit" disabled={submitting}>Submit</button>}
</div>
<pre>{JSON.stringify(values, 0, 2)}</pre>
</form>
)}
</Form>
)
}
}
And this is the FirstStep component (where the user inserts its first values, it generates and array of items. All I need to do is pull the values of the state in order to generate a number of pages according to the length of that first array):
export default () => (
<FieldArray name="items">
{({ fields }) => (
<div className="centered-items">
{fields.map((name, index) => (
<div key={name} className="item-row">
<label htmlFor={`item-name-${index}`}>item: </label>
<Field
id={`item-name-${index}`}
name={`${name}.item`}
component="input"
type="text"
placeholder="Place Item Name"
/>
<button type="button" onClick={() => fields.remove(index)}>
Remove
</button>
</div>
))}
<button
className="centered"
type="button"
onClick={() => fields.push({ tipologia: '', numero_camere: '1' })}
>
Add Item
</button>
</div>
)}
</FieldArray>
)
I manage to solve this problem calling React.cloneElement() method.
This method needs to wrap either activePage variable and the static method "Page", in this way you can pass properties to children, so at least I manage to access the form values inside every page and so create some conditions.
You can see a working example here: Demo - Codesandbox
I still haven't figured out how to access this values outside the page (inside Wizard Class but before Wizard.Page method, this could be the ideal way because I can create conditions in order to generate/(or not generate) pages.
Codesandbox: https://codesandbox.io/s/github/adamschwarcz/react-firebase-app
I am really new to react and firebase and I followed this tutorial to come up with this app (full project – github link here) – it's an "Add your Wish app"
My problem is that I cannot store clap count on each post to my firebase – this component is called LikeButton.js.
I have been trying to add some similar firebase code (handleChange, handleSubmit, componentDidMount... etc.. etc..) as I learned in the tutorial to LikeButton.js to store the total amount of counts in firebase each time the button is clicked and the amount of claps incremented by +1.
Simply what I want – everytime the clap button is clicked and the initial ('0') state of count is incremented to +1 the current count is going to be updated into the database.
Just cannot come up with solution, can somebody please help?
My LikeButton.js code without any firebase:
import React, { Component } from 'react'
import firebase from '../../firebase.js';
import { makeStyles } from '#material-ui/core/styles';
import Button from '#material-ui/core/Button';
import './Like.css';
class LikeButton extends Component {
state = {
count: 0,
}
incrementLike = () => {
let newCount = this.state.count + 1
this.setState({
count: newCount
})
console.log(this.state.count);
}
render() {
return(
<div class="counter">
<Button type="submit" color="primary" onChange={this.handleCount} onClick={this.incrementLike}>{this.state.count} 👏</Button>
</div>
)
}
}
export default LikeButton
My Add.js code with firebase:
import React, { Component } from 'react';
import firebase from '../../firebase.js';
import Button from '#material-ui/core/Button';
import TextField from '#material-ui/core/TextField';
import FadeIn from "react-fade-in";
import Placeholder from '../Placeholder/Placeholder.js';
import LikeButton from '../Like/Like.js'
import './Add.css';
class Add extends Component {
constructor() {
super();
this.state = {
loading: true,
currentItem: '',
username: '',
items: []
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e) {
this.setState({
[e.target.name]: e.target.value
});
}
handleSubmit(e) {
e.preventDefault();
const itemsRef = firebase.database().ref('items');
const item = {
title: this.state.currentItem,
user: this.state.username
}
itemsRef.push(item);
this.setState({
currentItem: '',
username: ''
});
}
componentDidMount() {
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(json => {
setTimeout(() => this.setState({ loading: false }), 1500);
});
const itemsRef = firebase.database().ref('items');
itemsRef.on('value', (snapshot) => {
let items = snapshot.val();
let newState = [];
for (let item in items) {
newState.push({
id: item,
title: items[item].title,
user: items[item].user
});
}
this.setState({
items: newState
});
});
}
removeItem(itemId) {
const itemRef = firebase.database().ref(`/items/${itemId}`);
itemRef.remove();
}
render() {
return (
<div className="container">
<div className="wrap">
<section className="add-item">
<h1>Napíš svoj wish</h1>
<h3>Možno prilepíš sebe, možno posunieš firmu.</h3>
<form onSubmit={this.handleSubmit}>
<TextField
id="filled-required"
label="Meno"
name="username"
variant="filled"
value={this.state.username}
onChange={this.handleChange}
/>
<TextField
required
id="standard-multiline-flexible"
label="Tvoje prianie"
name="currentItem"
variant="filled"
multiline
rows="6"
rowsMax="8"
value={this.state.currentItem}
onChange={this.handleChange}
/>
<Button
type="submit"
variant="contained"
color="primary">
Poslať wish
</Button>
</form>
</section>
<section className='items-list'>
<div className="item">
<div>
{this.state.items.map((item) => {
return (
<div>
{this.state.loading ? (
<>
<FadeIn>
<Placeholder />
</FadeIn>
</>
) : (
<div className="wish" key={item.id}>
<FadeIn>
<h2>{item.title}</h2>
<div className="name">
<p>poslal <span>{item.user}</span></p>
<LikeButton />
</div>
</FadeIn>
</div>
)}
</div>
)
})}
</div>
</div>
</section>
</div>
</div>
);
}
}
export default Add
First of all, you need to tell the LikeComponent which Wish it will be updating, and you will also need to be able to access the clapCount of the wish from the LikeComponent. This can be done easily using props. You should re-configure LikeComponent to accept a prop similar to wish, which would be the wish that you are displaying and modifying.
So, this line in Add.js
<LikeButton />
would instead look like <LikeButton wish={item} />. This way, your LikeComponent can access the item/wish.
Next, in the LikeComponent, you need to remove the local state and instead use the clap count stored in Firebase. Luckily, since you're passing the wish via a prop, you can simply refactor the LikeComponent to look like this:
class LikeButton extends Component {
incrementLike = () => {
// TODO: Implement clap incrementation via Firebase updates
}
render() {
return(
<div class="counter">
<Button type="submit" color="primary" onClick={this.incrementLike}>{this.props.wish.clapCount} 👏</Button>
</div>
)
}
}
Next, we need to actually implement incrementLike. Luckily, since we are getting the wish item passed to us via the wish prop, we can easily update it like so:
incrementLike = () => {
// get a reference to the item we will be overwriting
const wishRef = firebase.database().ref(`/items/${this.props.wish.id}`);
// get the current value of the item in the database
wishRef.once('value')
.then(snapshot => {
// get the value of the item. NOTE: this is unsafe if the item
// does not exist
let updatedWish = snapshot.val();
// update the item's desired property to the desired value
updatedWish.clapCount = updatedWish.clapCount + 1;
// replace the item with `wish.id` with the `updatedWish`
wishRef.set(updatedWish);
});
}
While this should work with only a few tweaks, I'm sure there's a better way to do it. You might even be able to avoid the call to once('value') since you're passing wish as a prop to LikeComponent. You should play around with it.
However, I strongly encourage you to explore migrating to Firebase Cloud Firestore. It's API is way more straightforward (in my opinion) than Realtime Database.
I am trying to figure out how to pass the input variables to a graphql query function to run a query and display the results. Not sure if I am passing the variables correct when the button is clicked. The getObjectQuery takes two variables startTime and endTime, both will be selected on the frontend by the user.
Parent Component:
class Calendar extends React.Component {
constructor(props) {
super(props);
this.state = {
startTime: '',//This will keep track of the time
endTime:'',
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit = event => {
event.preventDefault();
console.log(this.state.startTime);
this.setState({
startTime: new Date(document.getElementById("startTime").value).valueOf(),//getElementById is a jQuery method
endTime: new Date(document.getElementById("endTime").value).valueOf()
}, () => {
this.props.data.refetch({//Assign the inputvalues, which is the current state, to the variables after pressing the submit button
startTime: this.state.startTime,
endTime:this.state.endTime
});
console.log(this.state.startTime);
console.log(this.state.endTime);
});
};
render() {
console.log(this.props.data);//This is where data is.
return (
<div className="Calendar">
<form onSubmit={this.handleSubmit.bind(this)}>
<label>Start Time</label>
<input type="datetime-local" id="startTime" step="1" />
<label>End Time</label>
<input type="datetime-local" id="endTime" step="1" onSubmit={this.handleSubmit.bind(this)}/>
<input type="submit" value="Submit" />
</form>
{<ElementList startTime={this.state.startTime} endTime={this.state.endTime}/>}
</div>
);
}
};
export default graphql(getObjectsQuery,
{ options: (ownProps) => {
console.log(ownProps.startTime);
return ({ variables: { startTime: ownProps.startTime,
endTime: ownProps.endTime
} })
} } )(Calendar);
Child Function
const ElementList = (props) => (
<Query
query={getObjectsQuery}
variables={props.startTime, props.endTime}
>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return (
<Item.Group divided>
{data.action.map(action =>
<div>
<ul>
<li>{action.action}</li>
<li>{action.timestamp}</li>
<ul>
{action.object.map( (obj) => {
return (<li>{obj.filename}</li>)
})}
</ul>
</ul>
</div>
)}
</Item.Group>
);
}}
</Query>
);
export default ElementList;
I believe your problem may be that you're just passing your props in as variables. They need to be set to specific property names in order for your graphql resolvers to accept them.
variables={{start: props.startTime, end: props.endTime}}
I have a component in which I'm trying to populate a <Select /> component with some options from my props. When the component mounts, I set the state of jobNumbers to an empty array.
I have two dropdowns in which one's values, depend on the other's selected value. When the value is selected, I run an onChange function to populate the second dropdown. The only problem is when I do this.setState({jobNumbers: [...array elements...]}), the state still shows the jobNumbers array to be empty. The function that actually does the state setting is getCustomerOptions().
Here is my component in it's entirety (it's not TOO terribly long)
import React from 'react';
import SelectInput from '../../components/SelectInput';
import LocationSelector from '../../components/LocationSelector';
import { Field } from 'redux-form/immutable';
import Select from 'react-select';
import 'react-select/dist/react-select.css';
class InputCurrentCustomerLocation extends React.Component {
constructor(props) {
super(props);
this.state = {
jobNumbers: [],
};
this.getCustomerOptions = this.getCustomerOptions.bind(this);
this.onChange = this.onChange.bind(this);
}
componentWillMount() {
if (this.props.active) {
this.props.input.onChange(this.props.active);
}
}
onChange(event) {
if (this.props.input.onChange) {
this.props.input.onChange(event.value); // <-- To be aligned with how redux-form publishes its CHANGE action payload. The event received is an object with 2 keys: "value" and "label"
// Fetch our Locations for this customer
this.props.handleCustomerLocationFetch(event.value);
this.getCustomerOptions(event);
}
}
getCustomerOptions(event) {
let options = [];
if(event) {
this.props.options.forEach((option, index) => {
if(option.value === event.value) {
console.log('props options', this.state);
this.setState({ jobNumbers: this.props.options[index] });
console.log('state options', this.state);
}
})
}
}
render() {
const { meta } = this.props;
return (
<div>
<Select
options={this.props.options} // <-- Receive options from the form
{...this.props}
value={this.props.input.value || ''}
// onBlur={() => this.props.input.onBlur(this.props.input.value)}
onChange={this.onChange.bind(this)}
clearable={false}
/>
{meta.error && <div className="form-error">{meta.error}</div>}
{this.props.activeLocations ? false : (
<div>
<div>
<p> Select a location </p>
<Field
name="locations"
component={props =>
<LocationSelector
{...props}
// active={this.props.activeLocations}
locations={this.props.locations}
/>
}
/>
</div>
<div>
<p> Select a job number</p>
<Field
name="jobNumber"
component={props =>
<Select
options={this.state.jobNumbers}
value={this.props.input.value || ''}
onChange={this.onChange.bind(this)}
clearable={false}
/>
}
/>
</div>
</div>
)}
</div>
);
}
}
export default InputCurrentCustomerLocation;
I'm relatively new to React and redux and I'm not entirely sure why this is happening. Shouldn't the state be populated?
this.setState({ jobNumbers: this.props.options[index] });
is async.
so when you do a console log on the state after setState, the state still won't change.
you should check the value on componentDidUpdate(prevProps, prevState) , and print it there.