Set parent's state from child - reactjs

My custom component should pass data to parent which is react-admin <Create>
I have came across some questions already and found that I can't simply set state from child to parent.
The problem is that this component should work like the default react-admin components (ex. ). It means when I submit the form It gets data from that component.
I have already tried addField()
This is my custom component (child):
import React from "react";
import MenuItem from "#material-ui/core/MenuItem";
import Select from "#material-ui/core/Select";
import { fetchUtils } from 'react-admin';
import FormControl from '#material-ui/core/FormControl';
import InputLabel from '#material-ui/core/InputLabel';
import { DataService } from '../routes/api';
import PropTypes from 'prop-types';
import { resources as rsrc } from '../resources';
const divStyle = {
marginTop: '16px',
marginBottom: '8px',
};
const inputStyle = {
width: '256px',
}
export default class MultipleSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
selectOptions: [],
selectedValues: [],
selectedValue: null,
};
}
getRoles() {
// get data from api
}
getAllOptions() {
// get some additional data from API
}
createRelationRecord(id) {
// create relation record (for ex. User's Role)
}
deleteRelationRecord(id) {
// delete relation record
}
componentDidMount() {
this.getRoles();
this.getAllOptions();
}
renderSelectOptions = () => {
return this.state.selectOptions.map((dt, i) => (
<MenuItem key={dt.id} value={dt.id}>
{dt.value}
</MenuItem>
));
};
handleChange = event => {
this.setState({ selectedValue: event.target.value });
// If record doesn't exist
if (this.state.selectedValue != event.nativeEvent.target.dataset.value) {
this.createRelationRecord(event.nativeEvent.target.dataset.value);
}
if (this.state.selectedValues.includes(Number(event.nativeEvent.target.dataset.value))) {
this.deleteRelationRecord(event.nativeEvent.target.dataset.value);
} else {
this.createRelationRecord(event.nativeEvent.target.dataset.value);
}
};
selectboxType() {
if (this.props.multiple) {
return true;
}
return false;
}
getSelected() {
if (this.selectboxType()) {
return this.state.selectedValues;
}
return this.state.selectedValue;
}
render() {
return (
<div style={divStyle}>
<FormControl>
<InputLabel htmlFor={this.props.label}>{this.props.label}</InputLabel>
<Select
multiple={this.selectboxType()}
style={inputStyle}
value={this.getSelected()}
onChange={this.handleChange}
>
{this.renderSelectOptions()}
</Select>
</FormControl>
</div>
);
}
}
Parent (create form):
export const ServerCreate = props => (
<Create {...props}>
<SimpleForm>
<TextInput source="Name" validate={required()} />
<ReferrenceSelectBox label="ServerType" multiple={false} source="ServerTypeId" reference="ServerType"></ReferrenceSelectBox>
<TextInput source="Barcode" validate={required()} />
</SimpleForm>
</Create>
);
It works with handleChange to achieve the data update. Now I need to save the selected data in Create form, but handleChange will not help me, because the object is not created yet and I cannot set attribute of non-existent record.
So my question is how can I pass value/values from my component to Create? How to set parent's state?

Your ServerCreate doesn't have any state, but if it did, you need to pass a function which can update its state to the child component as a prop.
Generally speaking, you should probably lift the state higher up the component tree

Related

Not sure if i'm using react context correcly

I've created a form in react and after some research i think that if you don't want to use an external library to manage the form, the context could be the best choice, expecially in my case where i've many nested component that compose it.
But, i'm not sure that putting a function inside my state is a good thing.
But let me give you some code:
configuration-context.js
import React from 'react'
export const ConfigurationContext = React.createContext();
ConfigurationPanel.jsx
import React, { Component } from 'react'
import { Header, Menu, Grid } from 'semantic-ui-react'
import ConfigurationSection from './ConfigurationSection.jsx'
import {ConfigurationContext} from './configuration-context.js'
class ConfigurationPanel extends Component {
constructor(props) {
super(props)
this.state = {
activeItem: '',
configuration: {
/* the configuration values */
banana: (data) => /* set the configuration values with the passed data */
}
}
}
handleItemClick = (e, { name }) => this.setState({ activeItem: name })
render() {
return (
<ConfigurationContext.Provider value={this.state.configuration}>
<Grid.Row centered style={{marginTop:'10vh'}}>
<Grid.Column width={15} >
<div className='configuration-panel'>
/* SOME BUGGED CODE */
<div className='configuration-section-group'>
{this.props.data.map((section, i) => <ConfigurationSection key={i} {...section} />)}
</div>
</div>
</Grid.Column>
</Grid.Row>
</ConfigurationContext.Provider>
)
}
}
ConfigurationItem.jsx
import React, { Component } from 'react'
import { Input, Dropdown, Radio } from 'semantic-ui-react'
import {ConfigurationContext} from './configuration-context.js'
class ConfigurationItem extends Component {
static contextType = ConfigurationContext
constructor(props) {
super(props)
}
handleChange = (e, data) => this.context.banana(data)
itemFromType = (item) =>{
switch (item.type) {
case "toggle":
return <div className='device-configuration-toggle-container'>
<label>{item.label}</label>
<Radio name={item.name} toggle className='device-configuration-toggle'onChange={this.handleChange} />
</div>
/* MORE BUGGED CODE BUT NOT INTERESTING*/
}
}
render() {
return this.itemFromType(this.props.item)
}
}
So, at the end i've a ConfigurationContext that is just a declaration, everything is inside the parent state.
The thing that i don't like is putting the banana function inside the state (it will have more logic that just logging it)
What do you think about it?
Any suggestion is appreciated.
Thanks
banana is just a regular function and you do not have to put it in the state, just do:
class ConfigurationPanel extends Component {
banana = data => console.log(data)
...
render() {
return (
<ConfigurationContext.Provider value={{banana}}>
...
}
After that you can use this.context.banana(data) as normal.

How to create a React component in TypeScript that lets me handle an event?

I would like to create a custom component in React using TypeScript that's essentially a combobox which has auto-complete/search functionality, connecting to its own remote store. What I would like to do is send an "onSelect" event so that I can receive the selected item where ever I'm using that component in my app.
Doing the auto-complete/search stuff with the remote store is easy, but the React component stuff has me stumped. I'm still learning both, so perhaps I'm trying to walk before I can crawl, but I don't want to start out creating a mess when I know that it should be possible to achieve this outcome which would be more elegant. I just need to find some sort of guide, but so far I haven't found one.
Here's what I want to achieve:
<MyCombobox onSelect={handleSelect} />
The handleSelect function would be used throughout my app where ever I need to use the MyCombobox component. The function needs to accept an argument, of course (which is what has me stumped at the moment, in TS).
One possible solution is as following
import * as React from "react";
import { render } from "react-dom";
interface MyComboProps {
// Here props from parent should be defined
}
interface MyComboState {
ValuesToShow: string[];
SearchValue: string;
}
class StringSearchMenu extends React.Component<MyComboProps, MyComboState> {
constructor(p: MyComboProps) {
super(p);
this.state = {
ValuesToShow: [],
SearchValue: ""
};
}
protected selectString(event: React.ChangeEvent<HTMLInputElement>): void {
let value = event.target.value;
if (value === "") this.setState({ ValuesToShow: [] });
else {
/* here you can put fetch logic. I use static array as example */
let possibleValues = ["Green", "Red", "Blue", "Yellow", "Black"];
this.setState({
ValuesToShow: possibleValues.filter(f => f.indexOf(value) > -1)
});
}
}
render() {
return (
<div>
Enter value to search {" "}
<input onChange={this.selectString.bind(this)} />
<div>
{this.state.ValuesToShow.map(v => (
<div>{v}</div>
))}
</div>
</div>
);
}
}
And working example is here
From all the googling I've done this morning, I've managed to cobble together something that works (from a plethora of sources):
App.tsx:
import React from 'react';
import './App.css';
import MyCombobox from './MyCombobox';
class App extends React.Component {
receiveSomething(something: string) {
alert('Something: ' + something);
}
render() {
return (
<div>
<MyCombobox receiveSomething={this.receiveSomething} defaultValue="qwerty" />
</div>
);
}
}
export default App;
MyCombobox.tsx:
import React from 'react';
export interface IMyCombobox {
defaultValue: string,
receiveSomething:(name:string) => void
}
class MyCombobox extends React.PureComponent<IMyCombobox, any> {
state = {
something: this.props.defaultValue
}
sendSomething() {
this.props.receiveSomething(this.state.something);
}
handleChange = (event: any) : void => {
this.setState({
something: event.target.value
});
}
render() {
return (
<div>
<input
type='text'
maxLength={20}
value={this.state.something}
onChange={this.handleChange} />
<input
type='button'
value='Send Something'
onClick={this.sendSomething.bind(this)} />
</div>
)
}
}
export default MyCombobox;

Dispatch not a function

Objective
Setup a dynamic form controlled by the user using react-redux and revalidate to run validation checks on my form.
Problem:
Since my form is dynamic I need to make dynamic validations. In order to do this my form data needs to be passed in as props to my component, which can be used as a second argument in the validate function from revalidate
My approach
To do this I am waiting until the component is mounted, building the form, passing it to redux, and then will map state to props. As the user adds more rows I will update state and then the component will render. I will use shouldComponentUpdate() to avoid any render loops.
The Error
My error is regarding the dispatch. When I try to run the dispatch (which will pass the form into redux) I get Dispatch is not a function error.
I am not supper comfortable with the connect() as I have to wrap redux with it as well as firebase. This syntax really confuses me.
Question
I believe the issue with with how I am exporting the component where I am using HOC like withFirebase, Redux, and Connect. Somewhere along the way I am losing scope to the connect. Can someone shed light into what it is I am doing wrong?
Component
import React, { Component } from "react";
import { reduxForm, Field } from "redux-form";
import { Container, Form, Col, Button } from "react-bootstrap";
import MaterialIcon from '../../material-icon/materialIcon';
import { withFirestore } from "react-redux-firebase";
import { connect } from "react-redux";
import TextInput from "../../forms/textInput";
import { combineValidators, isRequired } from "revalidate";
import { setupStudentForm } from '../../../store/actions/students';
const validate = (values, ownprops) => {
// Will be passing in validation rules, once form is apssed in with props via mapStateToProps.
}
export class CreateStudentsForm extends Component {
// Using constrcutor so componentDidMount() can render() cann access variables
constructor(props) {
super(props);
this.state = {
rows: 2,
}
this.formArray = [];
this.form = null;
}
componentDidMount() {
// Once component is rendered, setup form and send to redux
for (let i = 1; i !== this.state.rows + 1; i++) {
let firstNameField = {
fieldName: `firstName${i}`,
label: 'First Name',
required: true,
type: "text",
}
let lastNameField = {
fieldName: `lastName${i}`,
label: 'Last Name',
required: true,
type: "text",
}
this.formArray.push([firstNameField, lastNameField]);
}
this.props.setupStudentFormHandler(this.formArray);
}
// Ensure we do not get stuck in render loop
shouldComponentUpdate(nextProps, nextState){
if(nextProps !== this.props){
return true
} else {
return false
}
}
render() {
// Allows user to add another row
const addRow = () => {
this.setState({
rows: this.state.rows + 1
})
}
// Map through form array and create template
if (this.formArray) {
let form = this.formArray.map((field, index) => {
return (
<Form.Row key={index} className="animated fadeIn">
<Col xs={5}>
<Form.Group className="mb-0 noValidate">
<Field
label={field[0].label}
attempt={this.props.attempt}
name={field[0].fieldName}
type={field[0].type}
component={TextInput}
/>
</Form.Group>
</Col>
<Col xs={5}>
<Form.Group className="mb-0 noValidate">
<Field
label={field[1].label}
attempt={this.props.attempt}
name={field[1].fieldName}
type={field[1].type}
component={TextInput}
/>
</Form.Group>
</Col>
<Col xs={2}>
<MaterialIcon icon="delete" className="mt-4" />
</Col>
</Form.Row>
)
})
}
return (
<Container>
{this.form}
<Button variant="outline-success" onClick={addRow}>Add Another Student</Button>
</Container>
)
}
}
const mapStateToProps = state => {
return {
// Get access to student state which will have form
studentForm: state.students
};
};
const mapDispatchToProps = dispatch => {
return {
//Send formArray to redux
setupStudentFormHandler: (form) => dispatch(setupStudentForm(form))
};
};
export default withFirestore(
reduxForm({
form: "createStudents", validate
})(connect(
mapDispatchToProps,
mapStateToProps
)(CreateStudentsForm)
)
);
mapStateToProps is the first argument of connect, mapDispatchToProps is the second. Try swapping the order:
connect(
mapStateToProps,
mapDispatchToProps
)(CreateStudentsForm)

How to change React component from outside?

The React component, which can be considered as third-party component, looks as following:
import * as React from 'react';
import classnames from 'classnames';
import { extractCommonClassNames } from '../../utils';
export const Tag = (props: React.ElementConfig): React$Node =>{
const{
classNames,
props:
{
children,
className,
...restProps
},
} = extractCommonClassNames(props);
const combinedClassNames = classnames(
'tag',
className,
...classNames,
);
return (
<span
className={combinedClassNames}
{...restProps}
>
{children}
<i className="sbicon-times txt-gray" />
</span>
);
};
The component where I use the component above looks as following:
import React from 'react';
import * as L from '#korus/leda';
import type { KendoEvent } from '../../../types/general';
type Props = {
visible: boolean
};
type State = {
dropDownSelectData: Array<string>,
dropDownSelectFilter: string
}
export class ApplicationSearch extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
dropDownSelectData: ['Имя', 'Фамилия', 'Машина'],
dropDownSelectFilter: '',
};
this.onDropDownSelectFilterChange = this.onDropDownSelectFilterChange.bind(this);
}
componentDidMount() {
document.querySelector('.sbicon-times.txt-gray').classList.remove('txt-gray');
}
onDropDownSelectFilterChange(event: KendoEvent) {
const data = this.state.dropDownSelectData;
const filter = event.filter.value;
this.setState({
dropDownSelectData: this.filterDropDownSelectData(data, filter),
dropDownSelectFilter: filter,
});
}
// eslint-disable-next-line class-methods-use-this
filterDropDownSelectData(data, filter) {
// eslint-disable-next-line func-names
return data.filter(element => element.toLowerCase().indexOf(filter.toLowerCase()) > -1);
}
render() {
const {
visible,
} = this.props;
const {
dropDownSelectData,
dropDownSelectFilter,
} = this.state;
return (
<React.Fragment>
{
visible && (
<React.Fragment>
<L.Block search active inner>
<L.Block inner>
<L.Block tags>
<L.Tag>
option 1
</L.Tag>
<L.Tag>
option 2
</L.Tag>
<L.Tag>
...
</L.Tag>
</L.Block>
</L.Block>
</React.Fragment>
)}
</React.Fragment>
);
}
}
Is it possible to remove "txt-gray" from the component from outside and if so, how?
Remove the class from where you're using the Tag component:
componentDidMount() {
document.querySelector('.sbicon-times.txt-gray').classList.remove('txt-gray')
}
Or more specific:
.querySelector('span i.sbicon-times.txt-gray')
As per your comment,
I have multiple components with "txt-gray", but when I use your code, "txt-gray" has been removed from first component only. How to remove it from all components?
I will suggest you to use the code to remove the class in the parent component of using the Tag component. And also use querySelectorAll as in this post.
Refactoring
A clean way is to modify the component to allow it to conditionally add txt-gray through a prop:
<i className={classnames('sbicon-times', { 'txt-gray': props.gray })} />
If the component cannot be modified because it belongs to third-party library, this involves forking a library or replacing third-party component with its modified copy.
Direct DOM access with findDOMNode
A workaround is to access DOM directly in parent component:
class TagWithoutGray extends React.Component {
componentDidMount() {
ReactDOM.findDOMNode(this).querySelector('i.sbicon-times.txt-gray')
.classList.remove('txt-gray');
}
// unnecessary for this particular component
componentDidUpdate = componentDidMount;
render() {
return <Tag {...this.props}/>;
}
}
The use of findDOMNode is generally discouraged because direct DOM access is not idiomatic to React, it has performance issues and isn't compatible with server-side rendering.
Component patching with cloneElement
Another workaround is to patch a component. Since Tag is function component, it can be called directly to access and modify its children:
const TagWithoutGray = props => {
const span = Tag(props);
const spanChildren = [...span.props.children];
const i = spanChildren.pop();
return React.cloneElement(span, {
...props,
children: [
...spanChildren,
React.cloneElement(i, {
...i.props,
className: i.props.className.replace('txt-gray', '')
})
]
});
}
This is considered a hack because wrapper component should be aware of patched component implementation, it may break if the implementation changes.
No, it is not possible
The only way is to change your Tag component
import * as React from 'react';
import classnames from 'classnames';
import { extractCommonClassNames } from '../../utils';
export const Tag = (props: React.ElementConfig): React$Node =>{
const{
classNames,
props:
{
children,
className,
...restProps
},
} = extractCommonClassNames(props);
const combinedClassNames = classnames(
'tag',
className,
...classNames,
);
const grayClass = this.props.disableGray ? 'sbicon-times' : 'sbicon-times txt-gray';
return (
<span
className={combinedClassNames}
{...restProps}
>
{children}
<i className={grayClass} />
</span>
);
};
Now, if you pass disableGray={true} it will suppress the gray class, otherwise of you pass false or avoid passing that prop at all it will use the gray class. It is a small change in the component, but it allows you not to change all the points in your code where you use this component (and you are happy with grey text)

How to create bootstrap toggle buttons with React and Redux Form?

I am trying to create a toggable button group using React, Redux Form and Bootstrap (reactstrap).
What i have done is already correctly updating the redux form data.
The problem is with the button color attribute wich should be toggled between "success" and "secondary". Right now it does set the color on the first toggle, but does not update when i click another button afterwards.
This is my render component:
import React from 'react';
import classNames from 'classnames';
import { Label, FormGroup, ButtonGroup, Button } from 'reactstrap';
import FontAwesome from 'react-fontawesome';
export default class buttonOptions extends React.PureComponent {
static propTypes = {
input: React.PropTypes.object,
buttons: React.PropTypes.any,
label: React.PropTypes.string,
meta: React.PropTypes.shape({
touched: React.PropTypes.bool,
error: React.PropTypes.any,
})
};
constructor(props) {
super(props);
this.toggleOption = this.toggleOption.bind(this);
}
toggleOption(val) {
if (!this.props.input.value.length) this.props.input.value = [];
// option per buttongroud is always limited to 1
// remove previously selected options
for (let b of this.props.buttons) {
if (b.value !== val && this.props.input.value.indexOf(b.value) > -1) {
this.props.input.value.splice(this.props.input.value.indexOf(b.value), 1)
}
}
// push the new option and update state
this.props.input.value.push(val);
this.props.input.onChange(this.props.input.value)
}
render() {
const { input, buttons, label, meta: { touched, error }} = this.props;
const labelStyles = {width: '100%', marginBottom: '0'};
return (
<FormGroup>
<Label style={labelStyles}>{label}</Label>
<ButtonGroup>
{
buttons.map((b) => {
return (
<Button
key={b.title}
color={classNames({
success: input.value.indexOf(b.value) > -1,
secondary: input.value.indexOf(b.value) === -1,
})}
role="button"
onClick={() => { this.toggleOption(b.value) }}
>
{b.title}
</Button>
)
})
}
</ButtonGroup>
</FormGroup>
);
}
}
And this is how its implemented:
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AdWizard.css';
import cx from 'classnames';
import FontAwesome from 'react-fontawesome';
import { Field, reduxForm } from 'redux-form'
import { Row, Col, FormGroup, Label, Button } from 'reactstrap';
import buttonOptions from '../FormComponents/buttonOptions';
class Step2 extends React.Component {
constructor(props) {
super(props);
this.workingtimes = [
{
title: "Vollzeit",
value: "Vollzeit",
selected: true
},
{
title: "Teilzeit",
value: "Teilzeit",
selected: false
}
]
}
render() {
const { handleSubmit, previousPage } = this.props;
return (
<form onSubmit={handleSubmit}>
<Row className="justify-content-center">
<Col xs="12" sm="6">
<Field
label="Arbeitszeit"
name="arbeitszeit"
buttons={this.workingtimes}
component={buttonOptions}
/>
</Col>
</Row>
</form>
)
}
}
Step2 = reduxForm({
form: 'posting',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true
})(Step2);
export default withStyles(s)(Step2);
Would be great if someone could help out!
Cheers
Stefan
The problem you have is that your toggleOption function is impure*.
This means it is mutating this.props.input.value, instead of creating a new array whose value is based on it - putting it simple, always create a new reference!
Since most React code out there is very sensitive to pure function calls,
you must convert that function to a pure one, so that redux-form actually sees you changed the array:
toggleOption (b) {
let newValue;
const currValue = this.props.input.value || [];
if (currValue.includes(b.value)) {
// value already exists in array, let's remove it from there
newValue = currValue.filter(val => val !== b.value);
} else {
// value doesn't exist in array, let's add it there
newValue = currValue.concat([b.value]);
}
this.props.input.onChange(newValue);
}
Array methods such as .filter() and .concat() are your frinds here: they return new array instances, instead of mutating the existing array.
Your code was using .push() and .splice(), methods that are bad, because they mutate the existing array.
You can see a small demo here.
* You can read more about this subject here.

Resources