Need TextField to update text after state is changed - reactjs

I need to make a small change to a textfield. Basically there is a reset button that clears the search. The user can write the search string in the TextField. But the clear function only resets the actual search, but not the TextField which then still contains some kind of search string the user have written.
interface DrawerContainerProps {
dataProfile: DataProfile;
}
interface DrawerContainerComponentProps extends DrawerContainerProps, WithStibStyles<typeof styles>, WithNamespaces {
}
interface DrawerCantainerState {
attributeSearchText: string;
}
class ProfileDrawerContainerComponent extends React.Component<DrawerContainerComponentProps, DrawerCantainerState> {
readonly state: Readonly<DrawerCantainerState> = {
attributeSearchText: ""
};
setAttributeSearchText = debounce((searchText) => this.setState({ attributeSearchText: searchText }), 200);
onSearch = (event: React.ChangeEvent<HTMLInputElement>) => this.setAttributeSearchText(event.target.value);
clearSearch = () => this.setAttributeSearchText("");
render() {
const { classes, dataProfile, t } = this.props;
return (
<Drawer
variant="permanent"
classes={{ paper: classes.drawerPaper }}>
<div className={classes.toolbar} />
<div className={classes.menu}>
<Typography className={classes.drawerTitle}>{t("drawer.objectType", { defaultValue: "Object Type Filter" })}</Typography>
<div className={classes.objectTypes}>
{dataProfile && <ObjectTypeDrawer
objectTypes={dataProfile.profiledObjectTypes}
objectCount={dataProfile.objectCount}
businessConditionFiltering={dataProfile.businessConditionFilteringResult} />}
</div>
<div className={classes.attributeTitleSearch}>
<Typography className={classes.drawerTitle}>{t("drawer.attributes", { defaultValue: "Attributes" })}</Typography>
<TextField
id="attribute-search"
placeholder={t("drawer.search", { defaultValue: "Search" })}
type="search"
className={classes.searchField}
onChange={this.onSearch }
/>
</div>
<div className={classes.attributes}>
<AttributesDrawer attributeFilter={this.state.attributeSearchText} sendFunction={this.clearSearch} />
</div>
</div>
</Drawer>
);
}
}
My knowledge of web programming is very elementary. But I suspect that whenever the clearSearch function is called, it also has to update the value of the TextField. But that is where my knowledge of React and state handling comes short. Hope someone can give a hint.

You need to 'control' the value of your TextField. That is
<TextField
id="attribute-search"
placeholder={t("drawer.search", { defaultValue: "Search" })}
type="search"
className={classes.searchField}
onChange={this.onSearch }
value={this.state.attributeSearchText}
/>

Related

Is there a way to give a prop from a container to unknown children?

I am trying to create a collapsable container component that could be reused for different parts of a form in order to show the fields or a summary of that section. In my case, each container would contain a Form object passed as a child to that container. The base of my code goes as follows :
function CollapsableContainer({ title, children }: CollapsableContainerProps) {
const [isOpen, setIsOpen] = useState(false)
return (
<div className="comp-collapsable-container">
{isOpen ? (
{children}
) : (
<>
<div className="header-section">
<Typography variant="h6">
{title}
</Typography>
<Button onClick={() => setIsOpen(true)} startIcon={<EditIcon />}>
Modify
</Button>
</div>
<div className="summary-section">
/* Summary here */
</div>
</>
)}
</div>
)
}
I am wondering if it's possible to pass setIsOpen to the children in order to allow the container to collapse once the user pressed a button that that child would contain instead of having to replicate that behaviour multiple times. (Basically without copy/pasting my code)
To give more context, a child would look something like this, where the setIsOpen would be added to the onSubmit function :
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
<Form>
<FormikTextField
id="name"
name="name"
label="name"
fullWidth
required
/>
<FormikTextField
id="description"
name="description"
label="description"
fullWidth
required
/>
</Form>
</Formik>
Thank you for any help or suggestion!
I was able to create a CodeSandbox to you.
You have to type your CollapsableContainerProps as follows. You are stating that children will require props that extend the interface RequiresSetIsOpenProps. This is from this answer.
interface RequiresSetIsOpenProps {
setIsOpen?: Dispatch<SetStateAction<boolean>>;
}
interface CollapsableContainerProps {
title: string;
children:
| React.ReactElement<RequiresSetIsOpenProps>
| React.ReactElement<RequiresSetIsOpenProps>[];
}
Then, you can create any sort of child components you want that satisfy this condition:
interface SomeButtonProps {
// add your other child props here.
title: string;
}
function SomeButton(props: RequiresSetIsOpenProps & SomeButtonProps) {
const { setIsOpen, title } = props;
function handleClick() {
if (setIsOpen !== undefined) setIsOpen(false);
}
return <button onClick={handleClick}>{title}</button>;
}
Finally, inside your CollapsableContainer you have to apply the setIsOpen function to each of the children. I got this from another answer. You are telling React to copy whatever children were passed in and slap the setIsOpen function on top of the existing props.
function CollapsableContainer({ title, children }: CollapsableContainerProps) {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="comp-collapsable-container">
{isOpen ? (
React.Children.map(children, (child) => {
return React.cloneElement(child, {
setIsOpen: setIsOpen
});
})
) : (
<>
<div className="header-section">
<h6>{title}</h6>
<button onClick={() => setIsOpen(true)}>Modify</button>
</div>
<div className="summary-section">/* Summary here */</div>
</>
)}
</div>
);
}
Here is the CodeSandbox to check it out! I hope this helps!

Disabled options in react dropdown depending on specific conditions

I am currently building a dropdown component in react. It already works fine - I now would like to have some options disabled only when a specific condition occurs. In this case I have two dropdowns which work as a filter system. The first is a "from"-filter and the second a "to"-filter. If you select an option in the "from"-dropdown, I'd like the "to"-dropdown to disable the selected and all previous options.
This is my code:
import {
Dropdown,
IDropdownStyles,
IDropdownOption,
} from "#fluentui/react";
interface DropdownProps {
options: IDropdownOption[],
placeholder: string,
setDropdown: Dispatch<SetStateAction<string>>
}
const dropwdownStyles: Partial<IDropdownStyles> = {
dropdown: { width: 300 },
};
export const DropdownId = (props: DropdownProps) => {
return (
<div className={styles.root}>
<Dropdown
placeholder={props.placeholder}
options={props.options}
styles={dropwdownStyles}
onChange={(e, options) => {
props.setDropdown(`${options?.key}`)
}}
/>
</div>
);
};
Implementing the dropdown in index.tsx:
<Container>
<DropdownId options={apiTagsResponse.map(tag => ({ text: tag, key: tag }))} setDropdown={setStartObjectId} placeholder="Set Start Tag" />
</Container>
<Container>to</Container>
<Container>
<DropdownId options={apiTagsResponse.map(tag => ({ text: tag, key: tag }))} setDropdown={setEndObjectId} placeholder="Set End Tag" />
</Container>
<Container>
<DropdownApplyFilterButton onClick={() => setButtonClicked(true)} />
</Container>
The result:

Material ui autocomplete with react-select to fetch db on every search

I am currently using Material ui v1.4.3 autocomplete. Material UI stated that this autocomplete is integrated with react-select.
I have followed the code here which is working like a charm but in order to handle fetching larger data in the future, I would like to implement the search function to call the database whenever the input changes so that I am able to narrow down the data that is being fetched from the database.
Has anyone had experience on this? Because the static method from this code is blocking me to call any reducer function that is passed from my parent component.
What would be an appropriate way that allows me to catch the input from the user so that I am able to call my function.
function NoOptionsMessage(props) {
return (
<Typography
color="textSecondary"
className={props.selectProps.classes.noOptionsMessage}
{...props.innerProps}
>
{props.children}
</Typography>
);
}
function inputComponent({ inputRef, ...props }) {
return <div ref={inputRef} {...props} />;
}
function Control(props) {
////console.dir(props.selectProps.inputValue); i am able to get the user input here
// so i was thinking i can call my reducer somewhere here but no luck
// my function is passed from my parent component so i wish to call this.props.theFunction here
return (
<TextField
fullWidth
InputProps={{
inputComponent,
inputProps: {
className: props.selectProps.classes.input,
ref: props.innerRef,
children: props.children,
...props.innerProps,
},
}}
onChange={(e) => IntegrationReactSelect.testing(e)}
/>
);
}
function Option(props) {
return (
<MenuItem
buttonRef={props.innerRef}
selected={props.isFocused}
component="div"
style={{
fontWeight: props.isSelected ? 500 : 400,
}}
{...props.innerProps}
>
{props.children}
</MenuItem>
);
}
function Placeholder(props) {
return (
<Typography
color="textSecondary"
className={props.selectProps.classes.placeholder}
{...props.innerProps}
>
{props.children}
</Typography>
);
}
function SingleValue(props) {
return (
<Typography className={props.selectProps.classes.singleValue} {...props.innerProps}>
{props.children}
</Typography>
);
}
function ValueContainer(props) {
return <div className={props.selectProps.classes.valueContainer}>{props.children}</div>;
}
function MultiValue(props) {
return (
<Chip
tabIndex={-1}
label={props.children}
className={classNames(props.selectProps.classes.chip, {
[props.selectProps.classes.chipFocused]: props.isFocused,
})}
onDelete={event => {
props.removeProps.onClick();
props.removeProps.onMouseDown(event);
}}
/>
);
}
const components = {
Option,
Control,
NoOptionsMessage,
Placeholder,
SingleValue,
MultiValue,
ValueContainer
};
class IntegrationReactSelect extends React.Component {
state = {
single: null,
multi: null,
};
handleChange = name => value => {
this.setState({
[name]: value,
});
this.props.getSelectMultipleValue(value);
};
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<Select
classes={classes}
options={this.props.theUserFromParentComponent}
components={components}
value={this.state.multi}
onChange={this.handleChange('multi')}
placeholder={this.props.reactSelectName}
isMulti
/>
</div>
);
}
}

How to pass value from custom component class to redux-form?

I'm calling a custom component in my redux-form.
<Field name="myField" component={SiteProjectSelect}/>
This component is a combination of two combo boxes. The second box is dependant on the value of the first on - i.e. depending on what site you select, you can choose from a list of projects. What I'd like to do is get the form to receive the selected site and the selected projects. However, I'm not sure how to pass the values to the redux-form.
class SiteProjectSelect extends Component {
constructor() {
super();
this.state = {
selectedSite: null,
selectedProject: null,
};
}
handleSiteSelection = selectedSite => {
console.log(selectedSite)
this.setState({ selectedSite, selectedProject: null });
};
handleProjectSelection = selectedProject => {
this.setState({ selectedProject });
this.props.input.onChange(selectedProject.value);
};
render() {
const selectedRow = this.state.selectedSite ? projects.find((node) => node.site === this.state.selectedSite.value) : "";
const filteredProjectOptions = selectedRow ? selectedRow.projects.map(project => ({ value: project, label: project })) : []
return (
<div {...this.props} >
<label>Site</label>
<div style={{ marginBottom: '20px' }} >
<Select
name="site"
value={this.state.selectedSite}
onChange={this.handleSiteSelection}
options={siteOptions}
isSearchable
/>
</div>
<div style={{ marginBottom: '20px' }} >
<label>Project</label>
<Select
name="project"
value={this.state.selectedProject}
onChange={this.handleProjectSelection}
options={filteredProjectOptions}
isMulti
isSearchable
closeMenuOnSelect={false}
/>
</div>
</div>
);
}
}
I did finally figure it out. For anyone else who stumbles across this, here's what I needed to know. To use a custom component,
Use the onChange prop to set the new value of the Field. You do this by calling the onChange function, this.props.input.onChange(your-components-new-value-here) when you need to change the value of the component and passing it the new value.
This new value will now be stored in the value prop: this.props.input.value. So, wherever in the render function for your component you need to pass/display the current value of your component, use the value prop. It has to be the value prop and not another variable such as what you passed to your onChange function. What this does is give control of what's displayed to the state of your redux-form which the value prop is tied to. Why is this useful? For example, you could take the user to a form review page when they're done and then back to the form if the user wants to make some more changes. How would redux-form know how to repopulate all of what's displayed without getting the user to fill in the form again? Because the display is dependant on the state, not user input! Took me a while to make sense of all this!!
In my example, where I was using two react-select components, one of which was dependant on the other, I ended up having to use the Fields component which allowed me to have two Fields in my component rather than just the one. Once I implemented this, it also became evident that I didn't need to have a separate state within my component as the value of both Fields is always accessible via the value prop for each of them. So, yes, I could have just used a stateless function after all!
I call my component with:
<Fields names={["site", "projects"]} component={SiteProjectSelect} />
My final working component:
class SiteProjectSelect extends Component {
handleSiteSelection = selectedSite => {
this.props.site.input.onChange(selectedSite);
this.props.projects.input.onChange(null);
};
handleProjectSelection = selectedProjects => {
this.props.projects.input.onChange(selectedProjects);
};
renderSite = () => {
const {
input: { value },
meta: { error, touched }
} = this.props.site;
return (
<div>
<label>Site</label>
<div style={{ marginBottom: '20px' }}>
<Select
name="site"
value={value}
onChange={this.handleSiteSelection}
options={siteOptions}
isSearchable
/>
</div>
<div className="red-text" style={{ marginBottom: '20px' }}>
{touched && error}
</div>
</div>
);
};
renderProjects = () => {
var {
input: { value },
meta: { error, touched }
} = this.props.projects;
const selectedSite = this.props.site.input.value;
const selectedRow = selectedSite
? projects.find(node => node.site === selectedSite.value)
: '';
const filteredProjectOptions = selectedRow
? selectedRow.projects.map(project => ({
value: project,
label: project
}))
: [];
return (
<div>
<div style={{ marginBottom: '20px' }}>
<label>Projects</label>
<Select
name="projects"
value={value}
onChange={this.handleProjectSelection}
options={filteredProjectOptions}
isMulti
isSearchable
closeMenuOnSelect={false}
/>
</div>
<div className="red-text" style={{ marginBottom: '20px' }}>
{touched && error}
</div>
</div>
);
};
render() {
return (
<div>
{this.renderSite()}
{this.renderProjects()}
</div>
);
}
}

Telling the difference between material-ui SelectFields and getting their values

I have a form that dynamically generates inputs, where one input is a material-ui TextField and SelectField with multiple options. I am having a problem with telling the select fields apart from each other though. In an ideal world I would like to be able to collect the data from both of these inputs and store them as an object (i.e. {name: Employee, type_id: 1}), which will become an array of objects depending on how many inputs are generated.
My current code looks like this:
import React from 'react';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import SelectField from 'material-ui/SelectField';
import MenuItem from 'material-ui/MenuItem';
import DatatypeStore from '../../stores/DatatypeStore';
const styles = {
customWidth: {
width: 100,
},
};
class MultipleEntry extends React.Component {
state={inputs: [], columnHeaders: [], value: 1};
addInputField(e) {
e.preventDefault();
let inputs = this.state.inputs;
inputs.push({name: null});
this.setState({inputs});
}
handleChange(e, index, value) {
const isSelectField = value !== undefined;
if (isSelectField) {
console.log(index, value);
} else {
console.log(e.target.value);
}
}
render() {
let {inputs, columnHeaders, value} = this.state;
return (
<div className="col-md-12">
{inputs.map((input, index) => {
let name = "header " + index;
return (
<div key={index}>
<br />
<TextField
hintText="Enter the name for the column"
floatingLabelText="Column Name"
type="text"
name={name}
onChange={e => this.handleChange(e)}
/>
<SelectField
value={this.state.value}
onChange={e => this.handleChange(e, index, value)}
style={styles.customWidth}
>
{DatatypeStore.getDatatypes().map(el => {
return <MenuItem key={el.id} value={el.id} primaryText={el.name} />;
})}
</SelectField>
<br />
</div>
);
})}
<br/>
<RaisedButton
style={{marginTop: 50}}
label="Add column input"
secondary={true}
onClick={e => this.addInputField(e)}
/>
<br />
</div>
);
}
}
export default MultipleEntry;
So yeah, examples doing what I would like to do would be much appreciated. If you can do it using material-ui components so much the better!
Thanks for your time
Update
Here is the parent component
import React from 'react';
import MultipleEntry from './MultipleEntry.jsx';
import Paper from 'material-ui/Paper';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import TokenStore from '../../stores/TokenStore';
const styles = {
paper: {
marginTop: 50,
paddingBottom: 50,
width: '100%',
textAlign: 'center',
display: 'inline-block',
},
};
class ColumnHeaderForm extends React.Component {
state = {title: '', input: null};
changeValue(e) {
const title = e.target.value;
this.setState({
title
});
}
handleInputChange(columnHeaderArray) {
let input = this.state.input;
input = columnHeaderArray;
this.setState({input});
}
handleFormSubmit(e) {
e.preventDefault();
let access_token = TokenStore.getToken();
let title = this.state.title;
let inputs = this.state.input;
this.props.handleFormSubmit(access_token, title, inputs);
}
render() {
let {title, input} = this.state;
return (
<div>
<Paper style={styles.paper}>
<form role="form" autoComplete="off">
<div className="text-center">
<h2 style={{padding: 10}}>Fill out the column names (you can add as many as you need)</h2>
<div className="col-md-12">
<TextField
hintText="Enter a title for the table"
floatingLabelText="Title"
type="text"
onChange={e => this.changeValue(e)}
/>
</div>
<div className="col-md-12">
<MultipleEntry handleInputChange={this.handleInputChange.bind(this)} />
</div>
<RaisedButton
style={{marginTop: 50}}
label="Submit"
primary={true}
onClick={e => this.handleFormSubmit(e)}
/>
</div>
</form>
</Paper>
</div>
);
}
}
export default ColumnHeaderForm;
well from my understanding you want to handle the TextField and SelectField onChange in the same method. They do have different signatures
TextField (event, value)
SelectField (event, index, value)
But you can achieve it easily by testing the third argument for example:
handleChange(event, index, value) {
const isSelectField = value !== undefined;
if(isSelectField) {
// do whatever you need to do with the SelectField value
} else {
// do whatever you need to do with the TextField value
}
}
Note:
You shouldn't mutate your state, that's wrong.
let columnHeaders = this.state.columnHeaders;
columnHeaders[e.target.name] = e.target.value;
To avoid it you can "clone" the state object and apply the changes there..
Object.assign({}, this.state.columnHeaders, {
[e.target.name]: event.target.value
})
Read more about Object.assign here: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
----------------------------------------------------
UPDATED EXAMPLE 26/04/2016
Now you can see I'm just changing the typeId inside the input object (that I found by its id) for SelectFields. And almost the same thing for TextField - just change the field name..
handleChange(inputId, event, index, value) {
const isSelectField = value !== undefined;
if(isSelectField) {
this.setState({
inputs: this.state.inputs.map((input) => {
return input.id === inputId ? Object.assign({}, input, {
typeId: value
}) : input
})
})
} else {
this.setState({
inputs: this.state.inputs.map((input) => {
return input.id === inputId ? Object.assign({}, input, {
name: event.target.value
}) : input
})
})
}
}
//Make sure the id is unique for each input
addInputField(e) {
e.preventDefault();
this.setState({
inputs: this.state.inputs.concat({ id: 1, name: null })
});
}
//Binding the ID in the call so we can use it in that method..
<SelectField
value={input.typeId}
onChange={this.handleChange.bind(this, input.id)}
style={styles.customWidth}
>

Resources