I have a little app that has an input and based on the search value, displays weather for a particular city. I'm stuck at a certain point though. The idea is that once you search a city, it hides the text input and search button and displays some weather info and another search button to search a new city. My issue is that I want to focus on the search box once I click to search again. I hope that makes sense. I read that the ideal way to do this is with refs. I wired it up like such:
class WeatherForm extends React.Component {
constructor(props) {
super(props);
this.city = React.createRef();
}
componentDidMount() {
this.props.passRefUpward(this.city);
this.city.current.focus();
}
render() {
if (this.props.isOpen) {
return (
<div className={style.weatherForm}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
)
} else {
return (
<div className={style.resetButton}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
);
}
}
}
With this I can pass that ref up to the parent to use in my search by using this.state.myRefs.current.value; It works great, but when I try to reference this.state.myRefs.current in a different function to use .focus(), it returns null.
resetSearch = () => {
console.log(this.state.myRefs.current); // <- returns null
this.setState({
isOpen: !this.state.isOpen,
details: [],
video: []
});
}
Is this because I'm hiding and showing different components based on the search click? I've read numerous posts on SO, but I still can't crack this. Any help is appreciated. I'll include the full code below. To see it in full here is the git repo: https://github.com/DanDeller/tinyWeather/blob/master/src/components/WeatherMain.js
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
recentCities: [],
details: [],
isOpen: true,
myRefs: '',
video: '',
city: ''
};
this.updateInputValue = this.updateInputValue.bind(this);
this.getRefsFromChild = this.getRefsFromChild.bind(this);
this.resetSearch = this.resetSearch.bind(this);
this.getWeather = this.getWeather.bind(this);
}
updateInputValue = (e) => {
...
}
resetSearch = () => {
console.log(this.state.myRefs.current);
this.setState({
isOpen: !this.state.isOpen,
details: [],
video: []
});
}
getWeather = (e) => {
...
}
getRefsFromChild = (childRefs) => {
...
}
render() {
return (
<section className={style.container}>
<div className={style.weatherMain + ' ' + style.bodyText}>
<video key={this.state.video} className={style.video} loop autoPlay muted>
<source src={this.state.video} type="video/mp4">
</source>
Your browser does not support the video tag.
</video>
<div className={style.hold}>
<div className={style.weatherLeft}>
<WeatherForm
updateInputValue={this.updateInputValue}
getWeather={this.getWeather}
passRefUpward={this.getRefsFromChild}
resetSearch={this.resetSearch}
isOpen={this.state.isOpen}
/>
<WeatherList
details={this.state.details}
city={this.state.city}
isOpen={this.state.isOpen}
/>
</div>
<div className={style.weatherRight}>
<Sidebar
recentCities={this.state.recentCities}
/>
</div>
<div className={style.clear}></div>
</div>
</div>
</section>
);
}
}
class WeatherForm extends React.Component {
constructor(props) {
super(props);
this.city = React.createRef();
}
componentDidMount() {
this.props.passRefUpward(this.city);
this.city.current.focus();
}
render() {
if (this.props.isOpen) {
return (
<div className={style.weatherForm}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
)
} else {
return (
<div className={style.resetButton}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
);
}
}
}
export default Weather;
You try to achieve unmounted component from DOM, because of this you can not catch the reference. If you put this code your instead of render function of WeatherForm component, you can catch the reference. Because i just hide it, not remove from DOM.
render() {
return (
<div>
<div className={style.weatherForm}
style={this.props.isOpen ? {visibility:"initial"} :{visibility:"hidden"}}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
<div className={style.resetButton} style={this.props.isOpen ? {visibility:"hidden"} :{visibility:"initial"}}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
</div>
)
}
console.log(this.state.myRefs.current) returns null , because it's a reference to an input dom element which does not exists as currently Weather form is displaying Search another city along with a reset button.
In reset function state changes, which results in change of prop isOpen for WeatherForm component. Now, screen would be displaying the input field along with search button.
After component is updated ComponentDidUpdate lifecycle method is called.
Please add ComponentDidUpdate lifecycle method in WeatherForm and add ,
this.city.current.focus() in the body of method.
There is no need to pass reference of a dom element to the parent element as it is not consider as a good practise.
Edit 1 :-
Need to set input field in focus only if prop ( isOpen ) is true as we will get reference to the input field only if its mounted.
ComponentDidUpdate(){
if(this props.isOpen)
this.city.current.focus
}
Link to Lifecycle method :-
https://reactjs.org/docs/react-component.html#componentdidupdate
Hope this helps,
Cheers !!
Related
thank you for reading this. I am attempting to learn React by making a dummy website, however I've run into a roadblock.
I want the "display-page" div to only show the Send element initially (which is easy) but when someone clicks one of the 4 options from the content_bar div I want remove the current element and only show the newly clicked element (in this case it is 'Transactions')
I've read about useState and routing but I'm not sure how to implement
Thanks! Please let me know if I didnt give enough details
import React, { Component } from 'react';
import './Data.css';
import Transactions from './Transactions';
import Send from './Send';
class Data extends Component {
constructor(props) {
super(props);
this.state = {
content: <Send />
}
}
transactionpage = () => {
this.setState({content: <Transactions/>});
}
render() {
return(
<div className="content">
<div className="content_bar">
<h5>Send</h5>
<h5 onClick={this.transactionpage}>Transactions</h5>
<h5>Friends</h5>
<h5>Professional</h5>
</div>
<div className="display-page">
{this.state.content}
</div>
</div>
);
}
}
export default Data;
Looking at You can't press an <h5> tag and React code without state feels strange.
You need to learn more to achieve your goal, these are the topics:
JSX expresssion
Conditional rendering
State management
Let me show you my solution, it is one of many ways.
class Data extends Component {
constructor(props) {
super(props);
this.state = {
toDisplay: ''
};
this.changeToDisplay = this.changeToDisplay.bind(this);
}
changeToDisplay(e) {
this.setState({ toDisplay: e.target.textContent.toString() });
}
render() {
return (
<div className="content">
<div className="content_bar">
<button onClick={e => changeToDisplay(e)}>Send</button> <br />
<button onClick={e => changeToDisplay(e)}>Transactions</button> <br />
<button>Friends</button> <br />
<button>Professional</button> <br />
</div>
<div className="display-page">
{this.state.toDisplay === 'Send' ? <Send /> : null}
{this.state.toDisplay === 'Transactions' ? <Transactions /> : null}
</div>
</div>
);
}
}
Trying to set up something simple.
Parent: app.js
class App extends React.Component {
constructor(props) {
super(props);
//This acts as our global state
this.state = {
username: "",
email: "",
bio: ""
};
}
componentDidMount() {
setTimeout(() => {
this.setState({
username: "jonny",
email: "jonny#mail.com",
bio: "My bio...."
});
}, 5000);
}
handleFormChange = data => {
this.setState(data);
};
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form data={this.state} onHandleFormChange={this.handleFormChange} />
<p>Name from App state: {this.state.username}</p>
<p>Email from App state: {this.state.email}</p>
<p>Bio from App state: {this.state.bio}</p>
</div>
);
}
}
Child: form.js
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
...this.props.data
};
}
handleSubmit = e => {
e.preventDefault();
};
handleChange = e => {
this.props.onHandleFormChange({ [e.target.name]: e.target.value });
};
// static getDerivedStateFromProps(nextProps, prevState) {
// console.log(nextProps.data)
// return {
// ...nextProps.data
// };
// }
componentDidUpdate(prevProps) {
if (prevProps.data !== this.props.data) {
this.setState({ ...this.props.data });
}
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input
type="text"
name="username"
defaultValue={this.state.username}
onChange={this.handleChange}
/>
<input
type="email"
name="email"
defaultValue={this.state.email}
onChange={this.handleChange}
/>
<textarea
name="bio"
defaultValue={this.state.bio}
onChange={this.handleChange}
/>
<input type="submit" value="submit" />
</form>
</div>
);
}
}
I created an artificial API call by using a setTimeout() in this example and I'm trying to set the state of the parent with the result of the API call. Then I wish to pass that as a prop to the child...
It's working except in the case of a textarea. I can see it if I inspect the DOM but it doesn't actually show in the browser...
Note the "my bio..." in the inspector, but the textarea being empty in the browser.
I've tried componentWillUpdate(), componentDidUpdate() and getDerivedStateFromProps() but nothing seems to work.
What am I missing?
Note: I am not using value="" because then it stops me typing and this form is supposed to allow you to update existing values
Sandbox... https://codesandbox.io/s/ancient-cloud-b5qkp?fontsize=14
It seems to work fine by using the value attribute instead of defaultValue. The defaultValue attribute should really only be used sparingly, since you almost always want your inputs to connect to component state. The optimal way to create a controlled input is by using value.
<textarea
name="bio"
value={this.state.bio}
onChange={this.handleChange}
/>
Change the defaultValue in textarea to value
I have a dynamically created from in React, and I'd like to be able to submit the values of all the input fields, but I can't add seperate on change handlers for each input elment, as they are created dynamically:
extract from the form js:
const FormElements = ({formFields}) => ( <div> {
formFields.map(formField => ( <FormElement name={formField.name} type={formField.fieldType} />)
)} </div> );
console.log(formFields);
return (
<div class="col-md-12">
<div class="panel panel-primary">
<div class="panel-heading">
<h4 class="panel-title">
{title} - {id}
</h4>
</div>
<div class="panel-body">
<form >
<FormElements formFields={formFields} />
<a
class="btn btn-primary"
onClick={this.handleSubmitButton}//what do I do with this function?
href="#">Submit</a>
</form>
</div>
</div>
</div>
);
form element js:
export default class FormElement extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div class="form-group">
<label for="{this.props.name}">{this.props.name}</label>
<input type="{this.props.type}}" class="form-control" id="{this.props.name}" placeholder="blah blah" />
</div>
);
}
}
Since they are controlled inputs there is not a react way to that, and even if there is I would not recommend it, React is all about declarative code.
There are two ways to solve this, one is to use make a property onChange on your FormElement and pass a function with ids, something like this:
<FormElements onChange={(key, value) => this.setState({ [key]: value })
The other way is to send give all the not defined props to the input:
export default class FormElement extends React.Component {
constructor(props) {
super(props);
}
render() {
const { name, type, ...other } = this.props
return (
<div class="form-group">
<label for="{name}">{name}</label>
<input type="{type}}" class="form-control" {...other} id="{this.props.name}" placeholder="blah blah" />
</div>
);
}
}
(the { [key]: value } and {...other} is ES6)
I actually managed this in a quite convoluted, and probably not recommended way, but it works! I've also never seen this done elsewhere...probably for good reason:
Form element:
export default class FormElement extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.props.handleChange(e.target.id, e.target.value);
}
render() {
return (
<div class="form-group">
<label for={this.props.id}>{this.props.name}</label>
<input type="{this.props.type}}" class="form-control" id={this.props.id} value={this.props.value} placeholder="blah blah" onChange={this.onChange}/>
</div>
);
}
}
form:
handleFormElementChange(id, value) {
console.log("changing: " + id + " = "+ value);
var frm = this.state.formData;
var index=-1;
for(var i=0;i<frm.length;i++) {
if(frm[i].id==id) {
index=i;
break;
}
}
frm[index].value = value;
this.setState({formData: frm});
}
const FormElements = ({formFields}) => ( <div> {
formFields.map(formField => ( <FormElement name={formField.name} key={formField.id} value={formField.value} id={formField.id} type={formField.fieldType} handleChange={this.handleFormElementChange.bind(this)}/>)
)} </div> );
What's happening is the actual full form data is being updated in the form component, and each time a change is made to one of the form elements, it passes it back to the parent form, update's the form's sate and then re-renders the whole form.
The complication here was actually finding the correct form element in the overall form status, by searching through the array for the key, and updating the value.
While I see this working with small forms, I can see how it would start to significantly slow down rendering on large form applications.
I have main component as follows :
export default class RegistrationFormStepFour extends React.Component{
constructor(props){
super(props);
this.state = {
terms: false,
emailNotifications: false,
smsNotifications: false,
errors: {}
}
}
handleTerms(event){
event.preventDefault();
this.setState({terms: !this.state.terms});
}
render(){
const language = this.props.currentLanguage;
return (
<div>
<Terms
nameYes="chcTerms"
text={language.termsText}
state={this.state.terms}
onChange={this.handleTerms.bind(this)}
currentLanguage={language}
error={this.state.errors.terms}/>
</div>
);
}
}
And component term is as follows :
import React from 'react';
const Terms = ({nameYes, text, state, onChange, error}) => {
let hasError = error ? "hasError" : "";
return (
<div className="col-lg-12 text-center" style={{marginBottom: 30}}>
<form>
<label className="radio-inline">
<input
type="checkbox"
name={nameYes}
checked={state}
onChange={onChange}
value=""/>
</label>
</form>
<p className={`questionsText ${hasError}`} style={{marginTop: 10}}>{text}</p>
</div>
);
};
export default Terms;
But when I click on the checkbox, nothing happens. If I console log the state in the terms component it show right value. First time is false, when I click on the checkbox than is true, but the checkbox isn't checked.
Any advice?
The event.preventDefault() is causing your problems in the controlled component checkbox.
http://www.matthiaslienau.de/blog/tags/checkbox
Checkboxes (and Radio Buttons): Manually updating state fails for checkboxes (and without having tested it: I think for radio controls as well). Why? The problem one will face is that the checkbox onChange event behaves in a special way since the era of HTML (how could I forget!): You may not toggle the state of a checkbox manually via the .checked property. Nor does React. The onChange (onClick) event is fired after the element state changed internally. This may just be reverted based on the return value of the event handler. See this post for a comprehensive examination of this fact.
const Terms = ({ nameYes, text, state, onChange, error }) => {
let hasError = error ? "hasError" : "";
return (
<div>
<form>
<label className="radio-inline">
<input
type="checkbox"
name={nameYes}
checked={state}
onChange={onChange}
/>
</label>
</form>
<p className={`questionsText ${hasError}`}>{text}</p>
</div>
);
};
class RegistrationFormStepFour extends React.Component {
constructor(props) {
super(props);
this.state = {
terms: false,
emailNotifications: false,
smsNotifications: false,
errors: {}
}
}
handleTerms(event) {
this.setState({ terms: event.target.checked });
}
render() {
const language = { termsText: 'Some Language' };
return (
<div>
<Terms
nameYes="chcTerms"
text={language.termsText}
state={this.state.terms}
onChange={this.handleTerms.bind(this)}
currentLanguage={language}
error={this.state.errors.terms}/>
{JSON.stringify(this.state)}
</div>
);
}
}
ReactDOM.render(<RegistrationFormStepFour />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
I think you should remove value attr from input tag. try this :
<input
type="checkbox"
name={nameYes}
checked={state}
onChange={onChange}
/>
I have a React component that has a few simple input fields where I am currently tracking the state to eventually be placed into an AJAX call. I also have a button that, on click, will create a new row of input fields (same as the initial inputs).
I am pretty new to React and initially built out a simple function that clones the entire div and appends it to the .ticket-section div. I ran into some problems about inputs having the same react-id and it honestly felt like I was fighting the framework a little.
Any recommendations of how to create these new inputs and be able to track the state individually of the new row of inputs? Greatly appreciated in advance.
Here is my component:
var AddItem = React.createClass({
getInitialState: function() {
return {item_name: '', quantity: '', price: ''}
},
itemNameChange: function(e) {
this.setState({item_name: e.target.value});
},
quantityChange: function(e) {
this.setState({quantity: e.target.value});
},
priceChange: function(e) {
this.setState({price: e.target.value});
},
render: function() {
return (
<div>
<div className="ticket-section">
<div className="add-ticket">
<ul>
<li>
<label>Name</label>
<input id="item-name" type="text" placeholder="xyz item" value={this.state.item_name} onChange={this.itemNameChange} />
</li>
<li>
<label>Quantity Available</label>
<input id="quantity" type="number" placeholder="100" value={this.state.quantity} onChange={this.quantityChange} />
</li>
<li>
<label>Price</label>
<input id="price" type="number" placeholder="25.00" value={this.state.price} onChange={this.priceChange} />
</li>
</ul>
</div>
</div>
<button className="add-another-item">+ Add another item</button>
</div>
);
}
});
Thanks again.
I'm not sure but let me quess, are you looking for something like this?
class InputComponent extends React.Component {
constructor(props){
super(props)
}
render(){
return <div>
<input type="text"
onChange={this.props.change}/>
</div>
}
}
class Widget extends React.Component {
constructor(){
this.state = {
values: ['']
};
this.handleClick = this.handleClick.bind(this);
}
handleChange(index, e){
const oldState = this.state.values;
oldState[index] = e.target.value
this.setState({values: oldState})
}
handleClick(){
const oldState = this.state.values
oldState.push('');
this.setState({values: oldState})
}
render(){
const itemList = this.state.values.map((item, index)=>{
return <InputComponent key={index} change={this.handleChange.bind(this, index)}/>
});
console.log(this.state.values)
return <div>
{itemList}
<hr/>
<button onClick={this.handleClick}>Click</button>
</div>
}
}
React.render(<Widget />, document.getElementById('container'));
Fiddle example. I hope it will help you.
Thanks