React: Add/Change text input base on a selected option - javascript

I am trying to display a new text input based on the selected option. I am able to do that as below but the old value entered is always present no matter what I change the new select option to.
What might be a better way to achieve this? Appreciate any suggestions.
class loadComponent extends React.Component {
static propTypes = {
......
};
static defaultProps = {
....
};
constructor() {
super();
this.state = {
value: ""
};
}
state = {
...
};
reset = (selected) => {
this.setState({
selectedInputName: selected.target[selected.target.selectedIndex].text,
selectedInputId: selected.target.value
});
};
makeTextInput = () => {
return (
<TextInput
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
value={this.state.value}
/>
);
};
render() {
let newInputText = '';
if (this.state.selectedInputId !== '') {
newInputText = this.makeTextInput();
}
return (
<Select
label="What would you like to search with?"
options={this.props.searchOptions}
onChange={selected => this.reset(selected)}
/>
<div className="search margin_bottom_large">
{newInputText}
);

makeTextInput function creates a new object, but from react's perspective it's the same component because react distinguishes them by looking at their type and key. To make react recreate an element, you have to change one of those values.
This code changes type of NewInputText element each time it renders (because NewInputText always refers to a new function):
reset = (selected) => {
this.setState({
selectedInputName: selected.target[selected.target.selectedIndex].text,
selectedInputId: selected.target.value
});
};
makeTextInput = () => {
return (
<TextInput
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
/>
);
};
render() {
let NewInputText = () => '';
if (this.state.selectedInputId !== '') {
NewInputText = () => this.makeTextInput();
}
return (
<Select
label="What would you like to search with?"
options={this.props.searchOptions}
onChange={selected => this.reset(selected)}
/>
<div className="search margin_bottom_large">
<NewInputText />
);
This code assigns different key to TextInput each time:
reset = (selected) => {
this.setState({
selectedInputName: selected.target[selected.target.selectedIndex].text,
selectedInputId: selected.target.value
});
};
makeTextInput = () => {
return (
<TextInput
key={Math.random()}
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
/>
);
};
render() {
let newInputText = '';
if (this.state.selectedInputId !== '') {
newInputText = this.makeTextInput();
}
return (
<Select
label="What would you like to search with?"
options={this.props.searchOptions}
onChange={selected => this.reset(selected)}
/>
<div className="search margin_bottom_large">
{newInputText}
);

Is there a better way to do this?
I think using the controlled component design pattern would be ideal in this situation.
class SomeInput extends Component {
constructor() {
super();
this.state = {
value: "" //Keep value state here
};
}
render() {
/* Doing something like the following will allow you to clear
the input value simply by doing the following..
this.setState({ value: '' });
*/
return (
<Input
type="text"
onChange={e => this.setState({ value: e.target.value })} // set value state to entered text
value={this.state.value} // set value of input to value piece of state
/>
);
}
}
This will give you full access to the current value of the input, thereby allowing you to set it to anything or clear it at anytime or for any event simply by doing the following this.setState({ value: '' }).

Don't know the rest of your code which could be handy but you can try:
makeTextInput = () => (
<TextInput
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
/>
);
change = (event) => {
this.setState({
selectedInputName: event.target.value
});
}
render() {
return (
<Select
label="What would you like to search with?"
options={this.props.searchOptions}
onChange={this.change}
/>
<div className="search margin_bottom_large">
{this.makeTextInput()}
);
What you need to do is only setState properly. Each time you change a state the component will be re-rendered which means that the makeTextInput method will be triggered.
EDIT:
by the way, it is good idea to use getter for returning component in render method, in this case:
get textInput() {
return (
<TextInput
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
/>
);
}
and then in render method, just use {this.textInput}

Related

clear search box after onclick

I have a search box in my header. i can clear my state after searching but the input doesn't get cleared.
I'm purely using the Searchbox to generate a dropdown that contains links to their respective field. So the input field is purely used to mimic a searc
I tried targeting it with refs but when i finally reach the value i can't use the search anymore.
There is a ref for SearchBarHeader, SearchBox and SearchField. But i'm not sure if that is the correct way to do it.
clearSearchBar = () => {
this.searchBarHeader.current.searchBox.current.searchField.current.value = '';
};
and the code for the searchbox.
class Search extends Component {
state = {
organisationNames: [],
errorMessage: null,
};
searchField = React.createRef();
async componentDidMount() {
const organisations = await getSearch();
this.setState({
organisationNames: organisations,
});
}
searchHandler = (e) => {
const searchValue = e.target.value.toLowerCase();
if (!searchValue) {
this.props.clearSearchResult();
} else {
const result = this.state.organisationNames.filter((organisationName) => {
return organisationName.toLowerCase().includes(searchValue);
});
this.props.setSearchResult(result, () => {
if (this.props.searchResult.length === 0) {
this.setState({
errorMessage: "No Results...",
});
} else {
this.setState({
errorMessage: null,
});
}
});
}
};
clearSearchInput = () => {
this.props.clearSearchResult();
};
render() {
return (
<div className="search">
<div className="form-group">
<input
ref={this.searchField}
type="search"
placeholder="Search for company"
onChange={this.searchHandler}
/>
</div>
<div className="search-result-wrapper">
<ul className="search-results">
{this.props.searchResult === undefined ? (
<Skeleton />
) : (
this.props.searchResult.map((res, id) => {
return (
<Link
key={id}
to={"/r/" + res}
onClick={this.clearSearchInput}
>
<li className="search-item">{res || <Skeleton />} </li>
</Link>
);
})
)}
{this.state.errorMessage === null ? (
""
) : (
<li>{this.state.errorMessage}</li>
)}
</ul>
</div>
</div>
);
}
}
export default Search;
It seems to me that you're missing the "value" attribute on your input that makes it reactive to changes in your state. Grabbing one example from react docs, here's the ideal setup:
this.state = {value: ''};
(...)
handleChange(event) {
this.setState({value: event.target.value});
}
(...)
<input type="text" value={this.state.value} onChange={this.handleChange} />
By following the method above, you won't need to use refs to manually clear the input value. Once the form is submitted, you can simply clear your state...
this.setState({value: ''});
... and your input should be cleared.
Here's the link for the docs: https://reactjs.org/docs/forms.html
You are clearing the ref, not the state. There is also not a value attached to your input, so even if the state was cleared, it will not reflect.
You will of course be able to make the form data more dynamic, without having to set and keep companyName constant.
Here is a simple working example is here: https://codesandbox.io/s/flamboyant-voice-oj85u?file=/src/App.js
export default function App() {
const [formData, setFormData] = useState({ companyName: "" });
const handleChange = (e) => {
setFormData({ companyName: e.target.value });
};
const handleClear = () => {
setFormData({ companyName: "" });
};
return (
<div className="search">
<div className="form-group">
<input
type="search"
name="companyName"
value={formData.companyName}
placeholder="Search for company"
onChange={handleChange}
/>
<button onClick={handleClear}>Clear</button>
</div>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</div>
);
}

Individual input values won't change

I'm attempting to loop through a set of inputs and have their color value be the value of a specific CSS variable using style.setProperty. Each CSS variable corresponds to part of syntax highlighting.
example: --function-color: #DD4A68 would be the color set for a function in a code block/markdown.
I've set it up to do this dynamically on input change, but the result is that the entire code preview container is colored with the selected color and not individual pieces of code.
Before changing any values
After changing the "keyword-color" value.
The keyword color in the code sample should have been the only thing that changed.
Here's the code sandbox to get a better sense.
Color Picker Component
function ColorPickerSection() {
const [colorVal, setColorVal] = useState(colors)
const handleInputChange = () => {
const inputs = document.querySelectorAll('input');
inputs.forEach(input => {
input.addEventListener('change', getInputVal)
})
}
const getInputVal = (e) => {
for (let option of Object.keys(colors)) {
const root = document.querySelector(':root')
root.style.setProperty(`--${option}`, e.target.value);
}
}
const onColorChange = (e, colorValKey) => {
setColorVal({
...colors,
[colorValKey]: e.target.value
})
handleInputChange()
}
return (
<div>
{Object.keys(colorSelect).map(groupName => {
return (<div key={groupName}>
<GroupName>{groupName}</GroupName>
{Object.keys(colorSelect[groupName]).map(color => {
return (
<ColorPicker
id={color}
key={color}
label={color}
value={colorVal[color]}
onChange={(e) => onColorChange(e, color)}
/>
)
})}
</div>)
})}
</div>
)
}
export default ColorPickerSection
Helper Object that stores all the colors and property names
const colorSelect = {
'Line Highlights': {
'highlight-background': '#F7EBC6',
'highlight-accent': '#F7D87C'
},
'Inline Code': {
'inline-code-color': '#DB4C69',
'inline-code-background': '#F9F2F4'
},
'Code Blocks': {
'block-background': '#F8F5EC',
'base-color': '#5C6E74',
'selected-color': '#b3d4fc'
},
'Tokens': {
'comment-color': '#93A1A1',
'punctuation-color': '#999999',
'property-color': '#990055',
'selector-color': '#669900',
'operator-color': '#a67f59',
'operator-background': '#FFFFFF',
'variable-color': '#ee9900',
'function-color': '#DD4A68',
'keyword-color': '#0077aa'
}
}
const colorNames = []
const colors = {}
Object.keys(colorSelect).map(key => {
const group = colorSelect[key]
Object.keys(group).map(color => {
colorNames.push(color)
colors[color] = group[color]
})
})
export { colorSelect, colorNames, colors }
In your ColorPicker component --> Change your onColorChange to onChange as you already assigned your function to onChange while passing as props.
return (
<ColorPickerContainer>
<p>{props.label}</p>
<ColorSwatch type="color" value={props.value} onChange={props.onChange} />
<HexInput
type="text"
value={props.value}
onChange={props.onChange}
/>
</ColorPickerContainer>
);
instead of
<ColorSwatch type="color" value={props.value} onChange={props.onColorChange} />
<HexInput
type="text"
value={props.value}
onChange={props.onColorChange}
/>

input target value from child component

I've managed to get my input component to render onto the dom, however I'm having a bit of trouble accessing the props.
Functional input component
const InputField = props => {
const { inputValue, updateInputValue } = props
return (
<Input>
<input
type="text"
placeholder="Please specify"
value={inputValue}
onChange={updateInputValue}
/>
<hr />
<label>Other</label>
</Input>
)
}
The component is only rendered to the dom based on an object property inside of an array
const MultiChoiceQuestions = props => {
const { multiChoiceArray, handleClick } = props
return (
<ButtonContainer>
{multiChoiceArray.map(questionChoice => {
if (questionChoice.type === 'input') {
return <InputField />
}
return (
<Button type="button" key={questionChoice.id} onClick={() => handleClick(questionChoice)}>
{questionChoice.text}
</Button>
)
})}
</ButtonContainer>
)
}
The multiChoice component is imported once again to create a top-level component that the app consumes
const Question = props => {
let responses
switch (props.data.type) {
case 'multiChoice':
responses = (
<MultiChoiceQuestions
multiChoiceArray={props.data.choices}
handleClick={props.handleClick}
inputValue={props.inputValue}
updateInputValue={props.updateInputValue}
/>
)
break
default:
responses = <div>Error: no question type: `{props.data.type}`</div>
}
const { data } = props
return (
<AnimatedDiv key={data.id}>
<QuestionText>{data.text}</QuestionText>
{responses}
</AnimatedDiv>
)
}
And the final component looks like this
class Survey extends Component {
constructor(props) {
super(props)
this.state = {
currentQuestionId: 1,
userAnswers: [],
isActive: false,
inputValue: '',
}
this.selectAnswer = this.selectAnswer.bind(this)
this.test = this.test.bind(this)
}
selectAnswer = answer => {
this.setState(state => ({
currentQuestionId: state.currentQuestionId + 1,
userAnswers: state.userAnswers.concat([answer]),
isActive: !state.isActive,
}))
}
checkInput = event => {
this.setState({
inputValue: event.target.value,
})
}
test = event => {
console.log(event.target.value)
}
render() {
const { currentQuestionId, isActive, inputValue } = this.state
const { questions } = this.props
const currentPercentage = (currentQuestionId * 100) / questions.length
return (
<SurveyContainer>
<Question
data={questions.find(q => q.id === currentQuestionId)}
className={isActive ? 'active' : ''}
handleClick={this.selectAnswer}
value={inputValue}
onChange={this.test}
/>
</SurveyContainer>
)
}
}
The InputField component renders out just fine, however, the function for my onChange event is not firing...There's a mistake somewhere in the pipeline, probably inside the question component?
It looks like you haven't passed any props to <InputField /> in your MultiChoiceQuestions component.
I can not see where you pass props from
<MultiChoiceQuestions>
...
<InputFiled props={props} />
...
</MultiChoiceQuestions>
Probably pass only the props which are needed in InputField component, such as inputValue, updateInputValue:
<InputFiled
inputValue={inputValue}
updateInputValue={updateInputValue}
/>
const InputField = (inputValue, updateInputValue) => {
...
<input
type="text"
placeholder="Please specify"
value={inputValue}
onChange={(e) => updateInputValue(e)}
/>
...
}
Hope that will help.

Using multiple filters re-renders component

I use filters in an application which limit the output of a list of users. See the example of the same concept in my codesandbox
The idea is that the list accepts multiple filter values to narrow down the search as much as needed. The first filter works fine when typing in any characters, but switching over and then also typing something inside the second filter input rerenders the list and overwrites the search findings of the first filter.
Main Component:
<div className="App">
<Filters onChange={this.handleFilter} />
<div className="list">
<List users={filteredUsers} />
</div>
</div>
Filter Inputs:
<div className="filters">
<input
name="name"
type="text"
placeholder="Search by name"
onChange={props.onChange}
/>
<input
name="email"
type="text"
placeholder="Search by email"
onChange={props.onChange}
/>
</div>
filter handler:
handleFilter = event => {
const target = event.target;
let updateUsers = this.state.users;
updateUsers = updateUsers.filter(user => {
let type;
if (target.name === "name") {
type = user.name;
} else if (target.name === "email") {
type = user.email;
}
return type.toLowerCase().search(target.value.toLowerCase()) !== -1;
});
this.setState({ filteredUsers: updateUsers });
};
I do plan on using several more filters and the list should not re-render.
What is a way to prevent this or work out a better solution?
How to big websites apply their filters?
You could set filter values of email and name both to state, then trigger a filter function that uses both of those values for the final result. You should also store the values of filter in state, either that of component or a parent. This way you shouldn't have your search values overwritten.
import React, { Component } from "react";
const Filters = ({ onChange, emailFilterValue, nameFilterValue }) => (
<div className="filters">
<input
name="name"
type="text"
placeholder="Search by name"
onChange={onChange}
value={nameFilterValue}
/>
<input
name="email"
type="text"
placeholder="Search by email"
onChange={onChange}
value={emailFilterValue}
/>
</div>
);
class Main extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
nameFilter: "",
emailFilter: ""
};
}
handleFilter = e => {
const { name, value } = e.target;
if (name === "email") {
this.setState({ emailFilter: value });
} else if (name === "name") {
this.setState({ nameFilter: value });
}
this.filterUsers();
};
filterUsers = () => {
const { users, nameFilter, emailFilter } = this.state;
let updateUsers = users.slice();
if (nameFilter.length > "") {
// do your search based on name
}
if (emailFilter.length > "") {
// then apply email filter
}
this.setState({ users: updateUsers });
};
render() {
return (
<div className="App">
<Filters
onChange={this.handleFilter}
emailFilterValue={this.state.emailFilter}
nameFilterValue={this.state.nameFilter}
/>
<div className="list">
<List users={filteredUsers} />
</div>
</div>
);
}
}
Here is a sample of my solution. Generally "big websites" reduce the amount of time filtering as much as possible if it's unnecessary. I would equally add a setTimeout() to not filter on EVERY input immediately, and clear said timeout if the user types within 400ms (for example).
In your filter component I would keep track of the filtered words:
class Filters extends React.Component {
constructor(props) {
super(props);
this.state = {
email: "",
name: ""
};
}
handleChange = event => {
const { onChange } = this.props;
let key = "email";
if (event.target.name === "name") {
key = "name";
}
this.setState({ [key]: event.target.value });
onChange({ email: this.state.email, name: this.state.name });
};
render() {
const { onChange } = this.props;
return (
<div className="filters">
<input
name="name"
type="text"
placeholder="Search by name"
onInput={this.handleChange}
/>
<input
name="email"
type="text"
placeholder="Search by email"
onInput={this.handleChange}
/>
</div>
);
}
}
And in your App component I would create two different filters, this way it puts a priority on your "name" property if it's filled out:
handleFilter = data => {
let updateUsers = this.state.users;
console.log(data);
if (data.name.length > 0) {
updateUsers = updateUsers.filter(user => {
return user.name.toLowerCase().includes(data.name);
});
}
if (data.email.length > 0) {
updateUsers = updateUsers.filter(user => {
return user.email.toLowerCase().includes(data.email);
});
}
this.setState({ filteredUsers: updateUsers });
};

React Redux Redux Form - moving a function from within a <Field> produces error

Novice.
I have a class Address which I ultimately want to split into a presentational component and container. It all works as is but when I move this particular function outside the render function from initially within the actual async.select form field -
onSuburbChange = (value) => {
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
...I find I am getting hit with a number of errors based on the the fact that they are unreferenced.
The error I get is
address.jsx:56 Uncaught ReferenceError: input is not defined
If I comment this line out I get the same type of error on updatePostcodeValue.
Here is the entire address file. As you can see it would be good to move the presentational section in render off to another file but I need to move all the functions to the outside of the render function.
NOTE: I have commented out where the function orginal sat so anybody who has a crack at this question knows where it was and also where I intended to move it...
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { Field, change } from 'redux-form'
import { Col, Panel, Row } from 'react-bootstrap'
import Select from 'react-select'
import { getSuburbs } from './actions'
import FormField from '../formComponents/formField'
import TextField from '../formComponents/textField'
import StaticText from '../formComponents/staticText'
import { CLIENT_FORM_NAME } from '../clients/client/client'
export class Address extends Component {
static contextTypes = {
_reduxForm: PropTypes.object.isRequired,
}
constructor(props, context) {
super(props, context)
this.state = {
selectedSuburb: null,
}
}
// Manage Select for new data request - for suburbs.
handleSuburbSearch = (query) => {
console.group('handleSuburbSearch')
console.log('query', query)
const { addressData } = this.props
console.log('addressData', addressData)
const companyStateId = addressData.companyStateId
console.log('companyStateId', companyStateId)
if (!query || query.trim().length < 2) {
console.log('no query; bailing!')
console.groupEnd()
return Promise.resolve({ options: [] })
}
const queryString = {
query: query,
companyStateId: companyStateId,
}
console.log('queryString', queryString)
return getSuburbs(queryString)
.then(data => {
console.log('Suburbs returned!', data)
console.groupEnd()
return { options: data }
})
}
//I HAVE MOVED IT TO HERE....
onSuburbChange = (value) => {
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
render() {
const { addressData, updatePostcodeValue } = this.props
const { value } = this.state
const sectionPrefix = this.context._reduxForm.sectionPrefix
return (
<Panel header={<h3>Client - Address Details</h3>}>
<Row>
<Field component={TextField}
name="address1"
id="address1"
type="text"
label="Address Line 1"
placeholder="Enter street 1st line..."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
<Field component={TextField}
name="address2"
id="address2"
type="text"
label="Address Line 2"
placeholder="Enter street 2nd line..."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
<Row>
<Field
component={props => {
const { input, id, placeholder, type } = props
const { fieldCols, labelCols, controlCols, label, inputClass } = props
// just the props we want the inner Typeahead textbox to have
const { name, onChange } = input
const onStateChange = (state) => {
console.log('onStateChange', state)
onChange(state)
}
return (
<FormField
id={id}
label={label}
fieldCols={fieldCols}
labelCols={labelCols}
controlCols={controlCols}
inputClass={inputClass}
>
<Select
name={name}
onChange={onStateChange}
placeholder="Select state"
valueKey="id"
options={addressData.states}
labelKey="stateLabel"
optionRenderer={option => `${option.stateShortName} (${option.stateName})`}
value={input.value}
selectValue={Array.isArray(input.value) ? input.value : undefined}
/>
</FormField>
)
}}
name="state"
id="state"
label="State."
fieldCols={6}
labelCols={3}
controlCols={6}
/>
</Row>
<Row>
<Field
component={props => {
const { input, id, placeholder, type } = props
const { fieldCols, labelCols, controlCols, label, inputClass } = props
const { name, value, onChange, onBlur, onFocus } = input
const inputProps = {
name,
value,
onChange,
onBlur,
onFocus,
}
{/*onSuburbChange = (value) => {
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}*/}
return (
<FormField
id={id}
label={label}
fieldCols={fieldCols}
labelCols={labelCols}
controlCols={controlCols}
inputClass={inputClass}
>
<Select.Async
{...inputProps}
onChange={this.onSuburbChange}
valueKey="id"
labelKey="suburbName"
loadOptions={this.handleSuburbSearch}
backspaceRemoves={true}
/>
</FormField>
)
}}
name="suburb"
id="AddressLocation"
label="Suburb."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
<Row>
<Field component={StaticText}
name="postcode"
id="postcode"
label="Postcode."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
</Panel>
)
}
}
const AddressContainer = connect(state => ({
addressData: state.addressData,
}), dispatch => ({
updatePostcodeValue: (postcode, sectionPrefix) => dispatch(change(CLIENT_FORM_NAME, `${sectionPrefix ? (sectionPrefix + '.') : ''}postcode`, postcode))
}))(Address)
export default AddressContainer
How do I structure the onSuburbChange so that it can sit outside the render function, update the onChange value and also update the Postcode etc?
well, if you look at the method, you'll see that... well, input is undefined in that scope.
onSuburbChange = (value) => { // <-- scope starts here
this.setState({ selectedSuburb: value }, () => {
input.onChange(value) // <-- input used here
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
assuming Select.Async is a "magic" blackbox Component that you don't have access to/are able to change, and the only parameter you get back from it in the callback is the new value, your best bet is a ref on the input.
<Field ref={(input) => this.input = input } ... />
and then change it to this.input instead of just input
I think you could also partially apply it (it's late any I'm not thinking straight) - it would look like
onSuburbChange = (input, value) => {
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
--
const mOnChange = onSuburbChange.bind(null, input) while input is in scope.
updatePostcodeValue can be referenced from props in the callback - and you've already taken care of ensuring it has the correct scope by using ES6 arrow function notation. Just destructure it out of props just like you did in render at the top of the callback.
also, unrelated, but you REALLY oughta break out those component props into another file or function...

Categories

Resources